diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..77d9bf64 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this project is + +A Quarto-based community blog for the [Tidyomics ecosystem](https://github.com/tidyomics). Posts are authored in `.qmd` (Quarto Markdown) files with R or Python code, rendered locally with frozen outputs committed to `_freeze/`, and deployed to GitHub Pages. + +## Build commands + +```bash +quarto render # Build entire site → _site/ +quarto check # Validate configuration +``` + +In RStudio: open `tidyomicsBlog.Rproj`, then click **Build → Render Website**. + +## Writing a blog post + +Posts live under `posts/YYYY-MM-DD-slug/index.qmd`. Each post directory may also contain images and other static assets. + +**Required front matter:** +```yaml +--- +title: "Post Title" +author: "Author Name" +date: "YYYY-MM-DD" +description: "One-sentence summary shown in listings and RSS" +tags: + - tag1 +format: + html: + toc: true +execute: + freeze: true +--- +``` + +**Images:** +- Compress with `pngquant --ext .png --force my_figure.png` before committing. +- Every image needs alt text (use Quarto's `fig-alt` option for code-generated figures). +- You must have rights to publish any image included. + +**Code execution (freeze):** +- Posts set `freeze: true`, so code is run locally and outputs cached. +- After rendering, commit everything changed inside `_freeze/` along with the `.qmd`. +- Do **not** commit generated HTML files. + +**Licensing:** Text is CC-BY-4.0; code is BSD 3-Clause. + +## Contribution workflow + +1. Fork the repo and clone locally. +2. Create the post directory and `index.qmd`. +3. Render locally (`quarto render` or RStudio Build tab) to populate `_freeze/`. +4. Commit: the `.qmd`, any static files in the post directory, and `_freeze/` changes. +5. Open a PR — do not commit `_site/` or generated HTML. + +## Repository layout + +| Path | Purpose | +|------|---------| +| `posts/` | Blog posts (one subdirectory per post) | +| `posts/_metadata.yml` | Default metadata applied to all posts | +| `posts/bibliography.bib` | Shared bibliography | +| `posts-nolist/` | Posts excluded from the listing page | +| `_freeze/` | Cached computational outputs (committed) | +| `_site/` | Generated site (gitignored) | +| `_quarto.yml` | Site-wide Quarto configuration | +| `.github/workflows/` | CI: renders site, validates RSS, deploys to GitHub Pages | + +## CI/CD + +GitHub Actions (`.github/workflows/quarto-website.yml`) runs on every push and PR: +- Renders the site using the committed `_freeze/` cache (no code is re-executed in CI). +- Validates the RSS feed with `xmllint`. +- Deploys to GitHub Pages on push to `main`. diff --git a/_freeze/posts/2026-09-03-slicing-in-tidyomics/index/execute-results/html.json b/_freeze/posts/2026-09-03-slicing-in-tidyomics/index/execute-results/html.json new file mode 100644 index 00000000..c9495dd1 --- /dev/null +++ b/_freeze/posts/2026-09-03-slicing-in-tidyomics/index/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "88e9a7451cfa7beb1a084c0775b195bd", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Slicing in tidyomics\"\nauthor: \"Michael Love\"\ndate: \"2026-09-03\"\ntags:\n - tidyomics\n - tidyseurat\n - tidySingleCellExperiment\n - tidySummarizedExperiment\n - plyranges\n - slice\n - dplyr\ndescription: \"How to use dplyr-style slice operations on omics data objects in the tidyomics project.\"\nimage: slice.png\nformat:\n html:\n toc: true\n toc-float: true\nexecute:\n freeze: true\n---\n\n\n\n# Introduction\n\nThe tidyverse verb `slice()` lets you select observations by position, returning a subset of the data based on integer indices. There are also some related convenience functions, which return observations by:\n\n - `slice_min()` and `slice_max()`: the smallest or largest values of a variable.\n - `slice_head()` and `slice_tail()`: the first or last position(s).\n - `slice_sample()`: a random subset, either by count or proportion, with optional weighted sampling.\n\nAll of these support grouped operations, applying the selection within each group.\n\nIn 2023, this family of `slice_*` functions was added to three core _tidyomics_ packages,\n[`tidySummarizedExperiment`](https://github.com/tidyomics/tidySummarizedExperiment) ([commit](https://github.com/tidyomics/tidySummarizedExperiment/commit/5e9bb5f98a650b0d1b42fa32c4593e6e2077a9b7)),\n[`tidyseurat`](https://github.com/stemangiola/tidyseurat) ([commit](https://github.com/stemangiola/tidyseurat/commit/1ff674d7aaee58f99265c8fb1fb8ce3f08efa2ea)), and\n[`tidySingleCellExperiment`](https://github.com/tidyomics/tidySingleCellExperiment) ([commit](https://github.com/tidyomics/tidySingleCellExperiment/commit/e6fc0abd561d6a660286057ae78d579cc7e58412)).\nMore recently, in 2026, `slice()` was also introduced to\n[`plyranges`](https://github.com/tidyomics/plyranges) ([commit](https://github.com/tidyomics/plyranges/commit/41eb383b7643b5b03f1956f57c56119d645b3b9d))\nfor genomic ranges objects.\n\nThis post walks through how slicing works across _tidyomics_ packages.\n\n# Transcriptomics: tidyseurat, tidySCE and tidySE\n\nAll three transcriptomics packages — `tidyseurat`, `tidySingleCellExperiment`,\nand `tidySummarizedExperiment` — implement `slice_*` in the same way, operating\non cells or samples (i.e. columns of the underlying data matrix). We use\n`tidyseurat` here as a representative example; the same code patterns apply\ndirectly to the other two packages.\n\n## tidyseurat\n\n`tidyseurat` lets you work with Seurat objects using familiar tidyverse verbs.\nWe'll use `pbmc_small`, a small PBMC dataset bundled with Seurat (80 cells, 230 genes).\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(Seurat)\nlibrary(tidyseurat)\n\ndata(\"pbmc_small\")\nseurat_obj <- pbmc_small |>\n mutate(cell_id = seq_along(.cell)) |>\n select(-contains(\"ident\")) |>\n select(-starts_with(\"RNA\"))\n```\n:::\n\n\nWith `slice()` you can select cells by position, just as you would rows in a tibble.\nIn Bioconductor packages, and in _Seurat_, cells are represented as columns in the counts matrix.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# First 10 cells\nseurat_obj |> slice(1:5)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A Seurat-tibble abstraction: 5 × 12\n# Features=230 | Cells=5 | Active assay=RNA | Assays=RNA\n .cell nCount_RNA nFeature_RNA groups cell_id PC_1 PC_2 PC_3 PC_4 PC_5 tSNE_1 tSNE_2\n \n1 ATGCCAG… 70 47 g2 1 -0.774 -0.900 -0.249 0.559 0.465 0.868 -8.10 \n2 CATGGCC… 85 52 g1 2 -0.0260 -0.347 0.665 0.418 0.585 -7.39 -8.77 \n3 GAACCTG… 87 50 g2 3 -0.457 0.180 1.32 2.01 -0.482 -28.2 0.241\n4 TGACTGG… 127 56 g2 4 -0.812 -1.38 -1.00 0.139 -1.60 16.3 -11.2 \n5 AGTCAGA… 173 53 g2 5 -0.774 -0.900 -0.249 0.559 0.465 1.91 -11.2 \n```\n\n\n:::\n:::\n\n\nThe `slice_*` helpers work as expected:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# 20 randomly sampled cells\nseurat_obj |> slice_sample(n = 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A Seurat-tibble abstraction: 5 × 12\n# Features=230 | Cells=5 | Active assay=RNA | Assays=RNA\n .cell nCount_RNA nFeature_RNA groups cell_id PC_1 PC_2 PC_3 PC_4 PC_5 tSNE_1 tSNE_2\n \n1 ATGCCAG… 70 47 g2 1 -0.774 -0.900 -0.249 0.559 0.465 0.868 -8.10\n2 AGATATA… 187 61 g2 34 -1.12 -2.71 0.831 -0.738 0.745 21.3 -24.1 \n3 GGCATAT… 126 53 g1 39 -1.08 -3.68 0.122 -0.858 0.700 24.8 -21.9 \n4 TACAATG… 108 44 g2 43 -0.774 -0.900 -0.249 0.559 0.465 -0.795 -10.4 \n5 GATAGAG… 328 72 g1 68 -0.861 2.17 1.22 -0.797 -0.0620 -0.0649 23.1 \n```\n\n\n:::\n\n```{.r .cell-code}\n# Cells with the highest RNA count\nseurat_obj |> slice_max(nCount_RNA, n = 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A Seurat-tibble abstraction: 5 × 12\n# Features=230 | Cells=5 | Active assay=RNA | Assays=RNA\n .cell nCount_RNA nFeature_RNA groups cell_id PC_1 PC_2 PC_3 PC_4 PC_5 tSNE_1 tSNE_2\n \n1 TTGAGGAC… 787 88 g1 61 -0.918 1.61 -3.21 -2.07 -1.50 -18.4 22.0\n2 GACATTCT… 872 96 g1 65 -1.55 1.94 -2.52 -2.40 -0.299 -16.6 22.1\n3 ACGTGATG… 709 94 g2 66 -1.71 2.71 -0.742 -2.82 -0.313 -15.1 23.5\n4 ATTGTAGA… 745 84 g2 67 -1.47 1.59 -0.0495 -0.455 0.303 -4.29 22.8\n5 GCGTAAAC… 754 83 g1 70 -1.00 0.851 -2.56 -1.89 -0.0714 -17.1 20.7\n```\n\n\n:::\n\n```{.r .cell-code}\n# Cells with the lowest RNA count\nseurat_obj |> slice_min(nCount_RNA, n = 5)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A Seurat-tibble abstraction: 5 × 12\n# Features=230 | Cells=5 | Active assay=RNA | Assays=RNA\n .cell nCount_RNA nFeature_RNA groups cell_id PC_1 PC_2 PC_3 PC_4 PC_5 tSNE_1 tSNE_2\n \n1 TGGTATCT… 64 36 g1 7 -0.460 -1.19 -0.312 0.716 -1.65 17.9 -9.90\n2 GATATAAC… 52 36 g1 9 -0.774 -0.900 -0.249 0.559 0.465 1.33 -9.68\n3 AGGTCATG… 62 31 g2 11 -1.19 0.246 -1.86 2.33 1.43 2.45 7.33\n4 CATGAGAC… 51 26 g2 14 -1.30 0.813 -1.60 -0.111 0.334 -12.9 12.2 \n5 CTTCATGA… 41 32 g2 44 -0.940 -2.21 -0.163 0.0448 0.430 15.8 -25.3 \n```\n\n\n:::\n:::\n\n\nGrouping applies the slice within each group. Here, the top cell by count from\neach sample group:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nseurat_obj |>\n group_by(groups) |>\n slice_max(nCount_RNA, n=3) |>\n select(-starts_with(\"PC\"))\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\ntidyseurat says: A data frame is returned for independent data analysis.\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stdout}\n\n```\n# A tibble: 6 × 7\n# Groups: groups [2]\n .cell nCount_RNA nFeature_RNA groups cell_id tSNE_1 tSNE_2\n \n1 GACATTCTCCACCT 872 96 g1 65 -16.6 22.1 \n2 TTGAGGACTACGCA 787 88 g1 61 -18.4 22.0 \n3 GCGTAAACACGGTT 754 83 g1 70 -17.1 20.7 \n4 ATTGTAGATTCCCG 745 84 g2 67 -4.29 22.8 \n5 ACGTGATGCCATGA 709 94 g2 66 -15.1 23.5 \n6 TGAGCTGAATGCTG 387 83 g2 52 -35.5 1.71\n```\n\n\n:::\n:::\n\n\n## tidySE and tidySCE\n\n`tidySummarizedExperiment` (tidySE) and `tidySingleCellExperiment` (tidySCE)\nalso gained `slice_*` support in 2023.\nThe functions operate on samples (columns) in the same way as\n`tidyseurat` operates on cells, so the patterns above transfer directly.\n\n# Genomics: plyranges\n\n`plyranges` extends dplyr-style verbs to Bioconductor `GRanges` objects.\nWe construct a small ranges object to demonstrate slicing:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(plyranges)\n\nset.seed(123)\ndf <- data.frame(\n start = 1:50 * 1e6 + 1,\n width = 1e4,\n seqnames = \"chr5\",\n strand = \"*\",\n gc = runif(50),\n type = factor(sample(LETTERS[1:3], 50, replace = TRUE)),\n rng_id = 1:50\n)\nrng <- as_granges(df)\nrng\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 50 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 1000001-1010000 * | 0.287578 A 1\n [2] chr5 2000001-2010000 * | 0.788305 C 2\n [3] chr5 3000001-3010000 * | 0.408977 A 3\n [4] chr5 4000001-4010000 * | 0.883017 C 4\n [5] chr5 5000001-5010000 * | 0.940467 B 5\n ... ... ... ... . ... ... ...\n [46] chr5 46000001-46010000 * | 0.138806 C 46\n [47] chr5 47000001-47010000 * | 0.233034 C 47\n [48] chr5 48000001-48010000 * | 0.465962 B 48\n [49] chr5 49000001-49010000 * | 0.265973 C 49\n [50] chr5 50000001-50010000 * | 0.857828 A 50\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n:::\n\n\n`slice()` selects ranges by index; negative indices drop ranges:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# First two ranges\nrng |> dplyr::slice(c(1,3,5))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 3 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 1000001-1010000 * | 0.287578 A 1\n [2] chr5 3000001-3010000 * | 0.408977 A 3\n [3] chr5 5000001-5010000 * | 0.940467 B 5\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\n# Drop the last range\nrng |> dplyr::slice(-c(1:45))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 5 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 46000001-46010000 * | 0.138806 C 46\n [2] chr5 47000001-47010000 * | 0.233034 C 47\n [3] chr5 48000001-48010000 * | 0.465962 B 48\n [4] chr5 49000001-49010000 * | 0.265973 C 49\n [5] chr5 50000001-50010000 * | 0.857828 A 50\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n:::\n\n\nThe `slice_*` helpers work on metadata columns just as they do on data frames.\nThe sampling can be in terms of `n` or `prop` (the proportion). Additionally,\nit can accept `weight_by` and a column to be used for weighted sampling.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nrng |> slice_head(n = 2)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 2 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 1000001-1010000 * | 0.287578 A 1\n [2] chr5 2000001-2010000 * | 0.788305 C 2\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\nrng |> slice_tail(n = 2)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 2 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 49000001-49010000 * | 0.265973 C 49\n [2] chr5 50000001-50010000 * | 0.857828 A 50\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\nrng |> slice_max(gc)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 1 range and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 24000001-24010000 * | 0.99427 B 24\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\nrng |> slice_min(gc)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 1 range and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 35000001-35010000 * | 0.0246137 B 35\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\nrng |> slice_sample(prop = 0.3)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 15 ranges and 3 metadata columns:\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 46000001-46010000 * | 0.1388061 C 46\n [2] chr5 30000001-30010000 * | 0.1471136 C 30\n [3] chr5 35000001-35010000 * | 0.0246137 B 35\n [4] chr5 14000001-14010000 * | 0.5726334 A 14\n [5] chr5 29000001-29010000 * | 0.2891597 C 29\n ... ... ... ... . ... ... ...\n [11] chr5 21000001-21010000 * | 0.889539 C 21\n [12] chr5 37000001-37010000 * | 0.758460 A 37\n [13] chr5 8000001-8010000 * | 0.892419 A 8\n [14] chr5 10000001-10010000 * | 0.456615 C 10\n [15] chr5 34000001-34010000 * | 0.795467 A 34\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n:::\n\n\nGrouping by type and then slicing:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nby_type <- rng |> group_by(type)\n\n# Last range for each type\nby_type |> slice_tail()\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 3 ranges and 3 metadata columns:\nGroups: type [3]\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 48000001-48010000 * | 0.465962 B 48\n [2] chr5 49000001-49010000 * | 0.265973 C 49\n [3] chr5 50000001-50010000 * | 0.857828 A 50\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\n# Range with highest GC content for each type\nby_type |> slice_max(gc)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 3 ranges and 3 metadata columns:\nGroups: type [3]\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 21000001-21010000 * | 0.889539 C 21\n [2] chr5 24000001-24010000 * | 0.994270 B 24\n [3] chr5 31000001-31010000 * | 0.963024 A 31\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n\n```{.r .cell-code}\n# One range from each type\nby_type |> slice_sample(n = 1)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nGRanges object with 3 ranges and 3 metadata columns:\nGroups: type [3]\n seqnames ranges strand | gc type rng_id\n | \n [1] chr5 18000001-18010000 * | 0.0420595 A 18\n [2] chr5 21000001-21010000 * | 0.8895393 C 21\n [3] chr5 44000001-44010000 * | 0.3688455 B 44\n -------\n seqinfo: 1 sequence from an unspecified genome; no seqlengths\n```\n\n\n:::\n:::\n\n\n# Session information\n\n\n::: {.cell}\n\n```{.r .cell-code}\ninfo <- devtools::session_info()\npkgs <- info$packages |> as_tibble()\nattached_pkgs <- pkgs |> filter(attached) |> select(package, version=loadedversion)\n# 4. Print directly to your HTML report\nknitr::kable(attached_pkgs, format = \"html\", row.names = FALSE)\n```\n\n::: {.cell-output-display}\n`````{=html}\n\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n
package version
BiocGenerics 0.58.1
dplyr 1.2.1
generics 0.1.4
GenomicRanges 1.64.0
ggplot2 4.0.3
IRanges 2.46.0
plyranges 1.32.0
S4Vectors 0.50.1
Seqinfo 1.2.0
Seurat 5.5.1
SeuratObject 5.4.0
sp 2.2-3
tidyr 1.3.2
tidyseurat 0.8.10
ttservice 0.5.3
\n\n`````\n:::\n:::\n\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file diff --git a/posts/2026-09-03-slicing-in-tidyomics/index.qmd b/posts/2026-09-03-slicing-in-tidyomics/index.qmd new file mode 100644 index 00000000..9e898036 --- /dev/null +++ b/posts/2026-09-03-slicing-in-tidyomics/index.qmd @@ -0,0 +1,188 @@ +--- +title: "Slicing in tidyomics" +author: "Michael Love" +date: "2026-09-03" +tags: + - tidyomics + - tidyseurat + - tidySingleCellExperiment + - tidySummarizedExperiment + - plyranges + - slice + - dplyr +description: "How to use dplyr-style slice operations on omics data objects in the tidyomics project." +image: slice.png +format: + html: + toc: true + toc-float: true +execute: + freeze: true +--- + +```{r} +#| include: false +Sys.setenv(NO_COLOR = "1") +options(crayon.enabled = FALSE, cli.num_colors = 0) +local({ + hook <- knitr::knit_hooks$get("output") + knitr::knit_hooks$set(output = function(x, options) { + x <- gsub("\033\\[[0-9;]*m", "", x) + hook(x, options) + }) +}) +``` + +# Introduction + +The tidyverse verb `slice()` lets you select observations by position, returning a subset of the data based on integer indices. There are also some related convenience functions, which return observations by: + + - `slice_min()` and `slice_max()`: the smallest or largest values of a variable. + - `slice_head()` and `slice_tail()`: the first or last position(s). + - `slice_sample()`: a random subset, either by count or proportion, with optional weighted sampling. + +All of these support grouped operations, applying the selection within each group. + +In 2023, this family of `slice_*` functions was added to three core _tidyomics_ packages, +[`tidySummarizedExperiment`](https://github.com/tidyomics/tidySummarizedExperiment) ([commit](https://github.com/tidyomics/tidySummarizedExperiment/commit/5e9bb5f98a650b0d1b42fa32c4593e6e2077a9b7)), +[`tidyseurat`](https://github.com/stemangiola/tidyseurat) ([commit](https://github.com/stemangiola/tidyseurat/commit/1ff674d7aaee58f99265c8fb1fb8ce3f08efa2ea)), and +[`tidySingleCellExperiment`](https://github.com/tidyomics/tidySingleCellExperiment) ([commit](https://github.com/tidyomics/tidySingleCellExperiment/commit/e6fc0abd561d6a660286057ae78d579cc7e58412)). +More recently, in 2026, `slice()` was also introduced to +[`plyranges`](https://github.com/tidyomics/plyranges) ([commit](https://github.com/tidyomics/plyranges/commit/41eb383b7643b5b03f1956f57c56119d645b3b9d)) +for genomic ranges objects. + +This post walks through how slicing works across _tidyomics_ packages. + +# Transcriptomics: tidyseurat, tidySCE and tidySE + +All three transcriptomics packages — `tidyseurat`, `tidySingleCellExperiment`, +and `tidySummarizedExperiment` — implement `slice_*` in the same way, operating +on cells or samples (i.e. columns of the underlying data matrix). We use +`tidyseurat` here as a representative example; the same code patterns apply +directly to the other two packages. + +## tidyseurat + +`tidyseurat` lets you work with Seurat objects using familiar tidyverse verbs. +We'll use `pbmc_small`, a small PBMC dataset bundled with Seurat (80 cells, 230 genes). + +```{r} +#| message: false +#| warning: false +library(Seurat) +library(tidyseurat) + +data("pbmc_small") +seurat_obj <- pbmc_small |> + mutate(cell_id = seq_along(.cell)) |> + select(-contains("ident")) |> + select(-starts_with("RNA")) +``` + +With `slice()` you can select cells by position, just as you would rows in a tibble. +In Bioconductor packages, and in _Seurat_, cells are represented as columns in the counts matrix. + +```{r} +# First 10 cells +seurat_obj |> slice(1:5) +``` + +The `slice_*` helpers work as expected: + +```{r} +# 20 randomly sampled cells +seurat_obj |> slice_sample(n = 5) + +# Cells with the highest RNA count +seurat_obj |> slice_max(nCount_RNA, n = 5) + +# Cells with the lowest RNA count +seurat_obj |> slice_min(nCount_RNA, n = 5) +``` + +Grouping applies the slice within each group. Here, the top cell by count from +each sample group: + +```{r} +seurat_obj |> + group_by(groups) |> + slice_max(nCount_RNA, n=3) |> + select(-starts_with("PC")) +``` + +## tidySE and tidySCE + +`tidySummarizedExperiment` (tidySE) and `tidySingleCellExperiment` (tidySCE) +also gained `slice_*` support in 2023. +The functions operate on samples (columns) in the same way as +`tidyseurat` operates on cells, so the patterns above transfer directly. + +# Genomics: plyranges + +`plyranges` extends dplyr-style verbs to Bioconductor `GRanges` objects. +We construct a small ranges object to demonstrate slicing: + +```{r} +#| message: false +library(plyranges) + +set.seed(123) +df <- data.frame( + start = 1:50 * 1e6 + 1, + width = 1e4, + seqnames = "chr5", + strand = "*", + gc = runif(50), + type = factor(sample(LETTERS[1:3], 50, replace = TRUE)), + rng_id = 1:50 +) +rng <- as_granges(df) +rng +``` + +`slice()` selects ranges by index; negative indices drop ranges: + +```{r} +# First two ranges +rng |> dplyr::slice(c(1,3,5)) + +# Drop the last range +rng |> dplyr::slice(-c(1:45)) +``` + +The `slice_*` helpers work on metadata columns just as they do on data frames. +The sampling can be in terms of `n` or `prop` (the proportion). Additionally, +it can accept `weight_by` and a column to be used for weighted sampling. + +```{r} +rng |> slice_head(n = 2) +rng |> slice_tail(n = 2) +rng |> slice_max(gc) +rng |> slice_min(gc) +rng |> slice_sample(prop = 0.3) +``` + +Grouping by type and then slicing: + +```{r} +by_type <- rng |> group_by(type) + +# Last range for each type +by_type |> slice_tail() + +# Range with highest GC content for each type +by_type |> slice_max(gc) + +# One range from each type +by_type |> slice_sample(n = 1) +``` + +# Session information + +```{r} +info <- devtools::session_info() +pkgs <- info$packages |> as_tibble() +attached_pkgs <- pkgs |> filter(attached) |> select(package, version=loadedversion) +# 4. Print directly to your HTML report +knitr::kable(attached_pkgs, format = "html", row.names = FALSE) +``` diff --git a/posts/2026-09-03-slicing-in-tidyomics/slice.png b/posts/2026-09-03-slicing-in-tidyomics/slice.png new file mode 100644 index 00000000..506d6891 Binary files /dev/null and b/posts/2026-09-03-slicing-in-tidyomics/slice.png differ