Skip to content

Add the quick start, Why pseudobulk and the guides to cubes - #66

Merged
parashardhapola merged 1 commit into
masterfrom
38b-start-here-and-cube-guides
Oct 26, 2026
Merged

parashardhapola merged 1 commit into
masterfrom
38b-start-here-and-cube-guides

Conversation

@parashardhapola

Copy link
Copy Markdown
Member

Summary

The first pages a new user meets, and the ones on cubes, as executed notebooks with
figures from cytepool.plotting. The quick start goes from cells to a cube, to
limma-voom in every cell type, to a result traced back to its cube; "Why pseudobulk"
tests a comparison in which nothing differs both per cell and per sample, and gives the
record of benchmarks and replication as the papers report it; three guides show how to
pool cells, save, share and trace cubes, and exchange them with AnnData and MuData. The
normalization and quality guide gains DESeq2's PCA plot beside the MDS plot.

Changes

Pages (all read the simulated_pbmc dataset, which docs/datasets.py simulates, unless
noted; "checked in CI" means the page is in the check-outputs list of
.github/workflows/docs.yml):

  • quickstart.ipynb, "Quick start" (rewritten): from_cells on the cells, limma_voom
    with a paired design, one cell type's table, the cube saved with write_cube, opened
    with read_cube and matched to the result's record by its fingerprint, and the
    profiles a cell type's fit left out, with the reason. Figures: the cells pooled into
    each sample and cell type (plot_cell_numbers), the genes up and down in every cell
    type (plot_significant_genes), the volcano plot of the monocytes (plot_volcano).
    Checked in CI.
  • why_pseudobulk.ipynb, "Why pseudobulk" (new): the 12 control samples split at random
    into two groups of six, every gene tested by Welch's t-test on the cells (computed
    from the cube's sums with Cube.collapse_samples, Cube.mean, Cube.variance and
    t_sf) and by limma_voom on the samples; the false calls of each in every cell
    type; why the test of cells fails, on the gene it ranks first; the benchmarks
    (Squair et al. 2021, Zimmerman et al. 2021, Murphy and Skene 2022, Junttila et al.
    2022, Crowell et al. 2020) and replication studies (Murphy et al. 2023, Nakatsuka et
    al. 2025, Garg et al. 2021); Choices. Figures: the p-value histograms of the CD4 T
    cells tested both ways (plot_p_values); the mean of each donor's cells against each
    group's mean and its interval from the test of cells. Checked in CI.
  • guides/pool_cells.ipynb, "Pool cells into pseudobulk profiles" (new): sample and
    group tables (a lineage column), the five layers, the dot plot of three markers of
    every cell type, the stimulation's effect on one response gene from the samples
    pooled by condition (Cube.collapse_samples, Cube.fraction_detected, Cube.mean,
    Cube.variance), the PCA of the monocyte samples, the rare dendritic cells merged
    into myeloid cells (Table.filter, Cube.regroup); Choices. Figures:
    plot_dotplot scaled per gene, plot_pca (PC1 and PC2 by condition, PC2 and PC3 by
    donor), plot_cell_numbers of the merged cube. Checked in CI.
  • guides/save_and_trace_cubes.ipynb, "Save, share and trace cubes" (new): the cube
    file opened lazily, its layers' sizes against the cells', and its fingerprint from
    the stored digests; a derived cube saved and its history(); a copy changed with
    zarr-python, caught by verify_fingerprint; a limma_voom result traced to the file
    by fingerprint, its T-cell profiles read back by id, the library sizes and the
    average log2 CPM of the table recomputed from them; what CytePool can and cannot
    resolve; Choices. Figure: the traced gene's log2 CPM in every sample, the two samples
    of each donor joined. Checked in CI.
  • guides/anndata_and_mudata.ipynb, "Work with AnnData and MuData" (new): an AnnData of
    cells pooled by from_cells; to_anndata and its obs, back through an .h5ad file
    with the same fingerprint; to_mudata and back; the calls for the layouts of
    scanpy, decoupler and LIANA, as validated, shown as code that is not run (the
    documentation environment has none of the three); Choices. No figure: its outputs are
    the AnnData and MuData summaries and the layout of obs. Checked in CI.
  • guides/export_cubes.ipynb, "Export a cube from another tool" (prose only): an
    opening for its reader, a link to the new guide for what the file and its records
    allow, and a Choices section (arrays for lazy layers, layers, the source record, the
    check against from_cells on part of the data). Its code and outputs are unchanged.
    Checked in CI, as before.
  • guides/normalization_and_quality.ipynb, "Normalization, filtering and sample
    quality": a cell after the MDS plot with principal_components and plot_pca of the
    CD14+ monocytes, the genes with the largest loadings on PC1 (interferon-stimulated
    genes) and PC2 (HBB), and how MDS and PCA differ. Executed again (the Kang data, a
    download, so not in the CI list); every output but the new one is unchanged.
  • guides/bulk_rna_seq.ipynb: the sentence that sent readers to the bulk quick start
    now points to a new last section, "The same steps in R", the table of CytePool and R
    functions that the bulk quick start ended with, with edgeR's and DESeq2's tests
    added. Prose only.
  • guides/genes_per_cell_type.ipynb: the link to "From cells to results" for a cell
    type that cannot be fitted now goes to "Designs that cannot answer the question".
    Prose only.
  • differential_expression.ipynb, "From cells to results": removed (below).
  • index.md: the line of links separated by centred dots is a sentence, with the quick
    start and Why pseudobulk in place of From cells to results.
  • statistics/cubes.md, statistics/choosing_a_method.md, reference/cube_files.md:
    one sentence each linking to the new pages.

Navigation (docs/source/myst.yml): Start here is Installation, Quick start, Why
pseudobulk; Build and share cubes is Pool cells into pseudobulk profiles; Save, share
and trace cubes; Export a cube from another tool; Work with AnnData and MuData.

Coverage (docs/coverage_exemptions.toml): plot_dotplot, plot_pca, Table.select,
Table.filter and Table.with_columns leave [planned]. normalization.library_sizes,
which only the bulk quick start used, is used by the trace guide. The report: 255
public names, 198 used or explained in a page, 53 exempt, 4 planned (the dispersion
plots of the bulk guide; plot_forest and CubeCollection.select of the guide of
several studies).

Workflow: check-outputs runs quickstart why_pseudobulk guides/pool_cells guides/save_and_trace_cubes guides/export_cubes guides/anndata_and_mudata guides/bulk_rna_seq guides/composition; differential_expression leaves it.

Design notes

  • The quick start absorbs "From cells to results". Both walked the same path,
    cells, cube, limma_voom in every cell type, provenance, on two simulations, and the
    agreed structure has one tutorial for it; two pages would say the same thing. The
    quick start now runs on the shared simulated PBMC study, as the cube guides do, in
    four cells. What the longer tutorial added has a home where a reader looks for it:
    tracing a result, and what CytePool can and cannot resolve, in "Save, share and trace
    cubes"; the options for pseudobulk data (min_counts, quality weights, weights from
    cell numbers, adjustment across cell types) in "Genes that change in each cell type"
    and "Normalization, filtering and sample quality"; cell types that cannot be fitted
    and contrasts that cannot be estimated in "Designs, linear models and contrasts". The
    bulk quick start's table of R functions moved to the bulk guide, which already ran
    its analysis. The page's URL, /differential-expression, goes; nothing is released.
  • "Export a cube from another tool" stays beside "Save, share and trace cubes". Its
    reader develops a tool that holds cells and its task is to build a cube from its own
    sums, record its source and check the export; the new guide's reader analyses a
    cube and checks and traces it. One page for both would be twice as long. The overlap
    is removed: the export page links to the new guide for what files, fingerprints and
    records allow, and gained a Choices section.
  • "Group patients by their profiles" is not written; it is proposed for 0.2. None of
    the documentation's datasets has patients with an outcome (the 8 donors of Kang et
    al. are all patients, with none recorded), and a simulation would show only what it
    plants. CytePool also lacks two of its steps: a table of features per patient across
    cell types (cell type proportions, scores of pathways and regulators per cell type,
    pooled expression), and an unsupervised grouping or a test of such features against
    a clinical outcome. A page for 0.2: a cohort with an outcome (Perez et al. 2022, 162
    lupus cases and 99 controls, as the Literature page lists it), the features built per
    patient from a cube (propeller's proportions, gsva or ulm per cell type,
    principal_components of the pooled profiles), mapped with plot_pca and tested
    against the outcome.
  • Datasets. Every new page reads the simulated PBMC study, so all run in CI. Its
    monocytes are a case for caution: the stimulation raises the 40 genes of the
    response program, and so lowers the share of every other gene. Library sizes alone
    leave the monocytes' unchanged genes a log2 fold change of -0.084 on average, and
    TMM -0.015, which, with 24 samples of up to 444 cells, still makes 18 of the 157
    monocyte calls of the quick start's design false (11%; 686 of 825 without TMM). The
    quick start therefore shows no table against the truth; a page that adds one should
    know it.
  • The test of cells in Why pseudobulk is Welch's t-test computed from the cube's
    pooled sums. On the CD4 T cells of the page's split it gives the t-statistics of
    scanpy 1.12.4's rank_genes_groups with its default method (t-test) to 6e-8
    relative, and its p-values to 6e-12, run on the cells as a check outside the page.
  • Literature numbers are those the papers report, checked against their abstracts
    and full texts in Europe PMC: eighteen datasets with matched bulk RNA-seq and
    hundreds of genes without biological differences (Squair et al. 2021); highly
    inflated type 1 error rates, pseudobulk conservative relative to mixed models
    (Zimmerman et al. 2021); the Matthews correlation coefficient, pseudobulk best
    (Murphy and Skene 2022); 18 methods, pseudobulk generally best (Junttila et al.
    2022); 26 genes against 14,274, 549 times fewer (Murphy et al. 2023); over 85% of the
    Alzheimer's disease genes of one dataset not reproduced in any of the 16 others, with
    pseudobulk DESeq2 (Nakatsuka et al. 2025); 8 of 20 results confirmed (Garg et al.
    2021). Every citation is a key of references.bib already.
  • Copy. The pages follow the copy rules: no em dashes or en dashes, no centred dots,
    no stock metaphors; figure titles and axis labels set in the pages follow them too.
  • Tests. tests/test_documentation.py named differential_expression.ipynb in
    three examples; they now name quickstart.ipynb and why_pseudobulk.ipynb. The check
    of the API pages' backlinks reads those of limma_voom, whose first use is the quick
    start, instead of those of voom, which the quick start no longer imports.

Bugs found

  • plot_dotplot's colour bar label ran into the key of the dot sizes whenever the
    label was longer than the colour bar. The PCA and dot plots pull request below this
    one now writes the label above the bar, as scanpy does, with a test that the label,
    the bar and the key do not overlap; the pooling guide draws the dot plot as it is.

Awkward APIs, for the API and language pull request

  • A cube's sample table cannot gain a column, and Cube.collapse_samples takes only the
    name of one: Why pseudobulk pools an arbitrary set of samples with
    cube.select(samples=mask).collapse_samples("condition"), which works only because
    those samples share a condition. Labels per sample, or a Cube method that adds
    sample columns, would do it directly.
  • No function returns what plot_dotplot draws, the fractions and means pooled over
    samples; users pool with collapse_samples and divide, or sum layers by hand.
  • plot_dotplot returns only the main Axes: the colour bar is ax.figure.axes[-1], and
    there is no vmin or vmax (scanpy has both) to give dot plots of two conditions one
    colour scale. Its standard_scale="gene" is scanpy's "var".
  • A cell type's record in a result is found with
    next(fit for fit in result.provenance["groups"] if fit["group_id"] == group), and
    its fitted samples with
    result.samples.filter((result.samples["group_id"] == group) & result.samples["used"]);
    accessors by cell type would read better.
  • A result keeps no voom log-CPM values, so the trace guide recomputes them,
    log2((counts + 0.5) / (library_size * norm_factor + 1) * 1e6), to draw a gene.
  • One cell type's cell numbers need its position:
    cube.n_cells[:, cube.groups["group_id"].tolist().index(group)].
  • plot_p_values and the other scatter plots put their legend outside the axes, so two
    panels side by side are narrow; an option to place it inside would help.
  • to_anndata leaves X empty, and every user of scanpy's tools then writes
    adata.X = adata.layers["counts"].copy().
  • PrincipalComponents.genes holds positions; gene names need
    cube.features["symbol"][pca.genes].
  • Cube.history() prints a "References" line of about 250 characters, which overflows
    the output boxes of the pages.

Validation

No library code changes, so no validation script changed; the checks of the pages:

  • make -C docs html (Node.js 24): the API reference with warnings as errors, and the
    site, built.
  • make -C docs check-outputs for every page of the CI list and for the other pages
    this change executed or edited, guides/normalization_and_quality and
    guides/genes_per_cell_type (with the Kang download): outputs current, 10 pages. The
    same with numba compiling for a generic processor, OpenBLAS's Sandybridge kernels and
    4 threads: outputs current, so what the pages print does not depend on the
    processor's rounding. The pages print at most three significant digits of fitted
    values.
  • uv run python docs/coverage_report.py: no name uncovered or listed wrongly.
  • uv run ruff check and ruff format --check on cytepool tests validation docs,
    notebooks included: clean.
  • The quick start runs in about 7 s, and in 35 s with an empty numba cache, which the
    page says.

Tests

tests/test_documentation.py: 85 passed, with the three examples that named the
removed page now naming existing ones. The full suite on top of the pull requests
before it: 4,931 passed, 18 skipped, also with bounds checks and with the rounding
changed; Python 3.14: 4,952 passed; Python 3.15: 4,846 passed, 107 skipped. The ten
pages that CI checks are current when their datasets are made from an empty cache, as
on CI, and the five guides that read downloaded data are current too.

The check of saved outputs now leaves out a version written after "CytePool " in
both texts (docs/execute_notebooks.py): the save guide prints a cube's history,
which names the version that built the dataset, and on CI every dataset is built
again by the version being checked, so the saved text could never match. A test
covers it, beside the replacement of the version that ran the page.

@parashardhapola
parashardhapola force-pushed the 55-threads-and-garbage-collection branch from 45b1bb4 to 9bb9150 Compare October 26, 2026 23:18
@parashardhapola
parashardhapola changed the base branch from 55-threads-and-garbage-collection to master October 26, 2026 23:23
The quick start goes from cells to a cube to differential expression per
cell type and a result traced back to its cube; it absorbs the longer
tutorial of the same path. "Why pseudobulk" shows, on a split of control
samples, how testing cells instead of samples makes false calls. New guides
pool cells into profiles (with the dot plot and the PCA), save, share and
trace cubes, and work with AnnData and MuData; the quality guide gains the
PCA plot.
@parashardhapola
parashardhapola marked this pull request as ready for review October 26, 2026 23:23
@parashardhapola
parashardhapola force-pushed the 38b-start-here-and-cube-guides branch from ac08aa1 to 35ebb8a Compare October 26, 2026 23:23
@parashardhapola
parashardhapola merged commit cd5dbe9 into master Oct 26, 2026
6 checks passed
@parashardhapola
parashardhapola deleted the 38b-start-here-and-cube-guides branch October 26, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant