diff --git a/AGENTS.md b/AGENTS.md
index 6014e54..6c30da5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@ These instructions apply to the whole repository.
## Layout
-- Package code lives in `cytepool/`, tests in `tests/`, reference comparisons in
+- Package code is in `cytepool/`, tests in `tests/`, reference comparisons in
`validation/`, and the test data those comparisons write in `tests/data/`.
- Name modules, functions and files after what they compute, in terms a statistician or
biologist would search for. Avoid project-internal labels.
@@ -16,10 +16,40 @@ These instructions apply to the whole repository.
and are imported where they are used.
- Use built-in generics and `X | None`. Do not add `typing.List`, `typing.Optional`, or
`from __future__ import annotations`.
-- Avoid em dashes in public prose, docstrings and comments.
- Validate arguments at the public boundary and raise errors that say what was wrong and
what is expected.
+## Writing
+
+These rules apply to everything people read: pages, the README, docstrings, error
+messages, figure labels, commit messages and pull-request descriptions.
+
+- Start with the problem, explain the solution, then show its value. Lead with the
+ reader's task: "Test which genes change..." rather than "A library for testing...".
+- Give numbers context, round them where more digits add nothing, and keep the
+ precision that statistics need. Keep caveats in supporting detail: an opening stays
+ accurate, but conditions, versions and limits do not dominate it.
+- Use plain words, short sentences and one term for one thing, and explain a
+ statistic where the reader meets it.
+- No em dashes, and no en dashes used as punctuation; hyphenated words and terms such
+ as p-value are fine. No all-caps or labelled headings, no decorative numbering, no
+ centred dots between words.
+- No stock metaphors, such as "X lives in Y", "the X behind it" or "under the hood",
+ and no promotional claims: say what something does or where it is stored.
+
+## Documentation
+
+- Each fact has one home; other places give it in a clause and link to it. The
+ chapters of "Understand the methods" (`docs/source/statistics/`) explain the methods;
+ the Validation page gives the agreement with the reference tools, with the numbers
+ the validation scripts print; docstrings define behaviour; the guides show tasks on
+ data; and "Choosing a method" lists the questions, their defaults and what is not
+ implemented yet.
+- A change that adds, removes or limits a capability updates the summary of the
+ Validation page and "Choosing a method".
+- Link a section by its label, `(name)=`, rather than by its heading, and give a
+ docstring the address of the chapter section that explains it.
+
## Numerical routines
- Compile numerical loops with `cytepool._jit.jit`. It caches machine code in a directory
@@ -50,9 +80,10 @@ These instructions apply to the whole repository.
## Checks
```bash
-uv sync --extra test --group dev
+uv sync --extra test --group dev --group docs
uv run pytest
-uv run ruff check cytepool tests validation
-uv run ruff format --check cytepool tests validation
+uv run ruff check cytepool tests validation docs
+uv run ruff format --check cytepool tests validation docs
uv run mypy cytepool
+make -C docs api
```
diff --git a/README.md b/README.md
index da80405..4f9aa98 100644
--- a/README.md
+++ b/README.md
@@ -1,374 +1,109 @@
CytePool
-CytePool is a Python library for statistics on aggregated omics data: pseudobulk
-profiles, built by pooling the cells of each sample and cell type, and ordinary bulk
-RNA-seq. Its aim is to bring the sample-level methods that are standard in R into one
-package with one data model. The status table below separates what is implemented from
-what is planned.
+Test which genes, cell types and pathways change between conditions in studies of many
+samples, from single-cell or bulk RNA-seq data. Cells of one donor are not independent
+replicates, so CytePool pools the cells of each sample and cell type into a cube of
+pseudobulk profiles and tests the samples with the methods that are standard in R:
+limma, edgeR, DESeq2, dream, propeller and metafor. It runs them in Python, with numpy
+and numba as its only required dependencies, and every method is checked against an
+independent reference, usually the package that defined it.
**Status:** early development. Nothing is released yet.
-## What is implemented
+## Install
-| Area | Implemented | Checked against |
-| --- | --- | --- |
-| Pseudobulk cubes | `Cube` (additive layers per sample, group and feature; merging groups; pooling samples by one or more columns, or all of them; columns added to its tables; lazy layers, read as a selection needs them), `from_cells` (dense or CSR counts), `from_bulk` | scanpy, decoupler |
-| Provenance and fingerprints | A record of each cube's source (the caller's record, kept unchanged), of the settings that built it and of what each operation kept, merged or pooled, with the fingerprint of the cube it was applied to; `Cube.history()` reads it back. A content fingerprint (SHA-256, documented encoding) of the tables, cell numbers, normalization and layers | a reimplementation of the documented encoding, zarr-python |
-| Cube files | `write_cube`, `read_cube` (a subset of Zarr v3, with orthogonal selections; a read opens only the chunks that hold the selected features; gzip, and zstd on Python 3.14 or newer; stored digests, checked when a file is opened and when a read covers a whole group) | zarr-python |
-| AnnData and MuData | `to_anndata`, `from_anndata`, `to_mudata`, `from_mudata` (optional extras `anndata` and `mudata`): cubes to and from the long AnnData (with a layer copied into `X` on request) of `scanpy.get.aggregate` and `decoupler.pp.pseudobulk` and the MuData of LIANA's `adata_to_views`, whose layouts are recognised; an import takes sums and counts of cells, refuses means, and gives a cube exported and read back with the same fingerprint | scanpy, decoupler, LIANA, anndata, mudata |
-| Several studies | `CubeCollection`: the cubes of several studies aligned, each kept as it is: samples named `study::sample_id`, with donor ids alike; genes aligned by id, with which study measured which, so that a gene a study did not measure is missing there, not zero; cell types aligned by Cell Ontology id, group id or a mapping for each study, merging those that a study splits, and named as given; each study's cube on the collection's axes, for analyses study by study; and one cube of the genes and cell types every study has, for a joint model. `analyse_each_study` runs one analysis per study, and `StudyResults` keeps the results with the collection, lined up gene by gene, with the reason a study has none for a gene, or as a studies x genes matrix | anndata, pandas, limma |
-| Normalization and filtering | TMM, upper-quartile and RLE factors, DESeq2 size factors, CPM, log-CPM, `filter_by_expression` | edgeR, DESeq2 |
-| Designs | Model matrices from R formulas, named contrasts, design checks | R, limma, lme4 |
-| Linear models | Ordinary, weighted and generalised least squares for every gene (in parallel when each gene needs its own fit), contrasts (named contrasts are matched to the coefficients by name) | limma |
-| Mixed models | `fit_mixed_model`: linear mixed models with random intercepts of one or more grouping factors (nested or crossed), with observation weights, fitted to every gene by REML or maximum likelihood, with exact zeros for variance components at the boundary, fitted values with the predicted random effects, and approximate residual degrees of freedom; `fixed_effect_tests`: t-tests of contrasts with Satterthwaite's degrees of freedom, as lmerTest computes them, or Kenward-Roger's adjusted standard errors and degrees of freedom, as pbkrtest computes them, with precision weights taken as dream takes them (every observation at its gene's average weight) or as the weighted model has them; `fixed_effect_f_test`: F-tests of several contrasts at once by either method (lmerTest's `contestMD`, pbkrtest's `KRmodcomp`) | lme4, lmerTest, pbkrtest, variancePartition |
-| Precision weights for counts | `voom`: log-CPM values and observation weights from the mean-variance trend, whose LOWESS span is chosen from the number of genes as in limma, with prior weights for samples, observations or genes, for a weighted linear model (limma-voom); `voom_with_quality_weights`, which combines them with sample quality weights | limma |
-| dream | `voom_with_dream_weights`: voom's precision weights from the mean-variance trend of every gene's mixed model, fitted by maximum likelihood, as variancePartition's `voomWithDreamWeights`; `moderated_fixed_effect_tests`: Satterthwaite or Kenward-Roger t-tests of a mixed model moderated by empirical Bayes as variancePartition's `eBayes` moderates `dream` fits, with `topTable`'s z-statistic, and the moderated F-test of several contrasts | variancePartition |
-| Sample quality weights | `sample_quality_weights`: a weight for each sample, or each group of samples, from how closely its values follow the linear model, by REML or the gene-by-gene update, as limma's `arrayWeights` | limma |
-| Negative binomial models | `fit_negative_binomial_model`: a negative binomial log-linear model for every gene, with known dispersions, as edgeR's `glmFit` (one-way layouts group by group, other designs by Newton's method with Levenberg damping, in parallel), with log fold changes shrunk by prior counts; exact unit deviances and deviances | edgeR, mpmath |
-| Quasi-likelihood F-tests | `fit_quasi_likelihood` and `quasi_likelihood_f_test`: edgeR 4's `glmQLFit` and `glmQLFTest` with their defaults: the common negative binomial dispersion of the most highly expressed genes by Cox-Reid adjusted profile likelihood (`common_dispersion`), deviances adjusted for small counts with exact moments of the unit deviance (`negative_binomial_unit_deviance_moments`), the average quasi-dispersion, limma's prior for unequal degrees of freedom with a trend in the average log CPM (`average_log2_cpm`) or robust, prior weights and offsets, and F-tests of coefficients and named contrasts with edgeR's columns (logFC, logCPM, F, PValue, FDR) | edgeR, limma, mpmath |
-| DESeq2 | `fit_deseq2`, `deseq2_wald_test`, `deseq2_likelihood_ratio_test` and `deseq2_dispersions`: DESeq2's analysis with its defaults, from gene-wise dispersions (Cox-Reid adjusted profile likelihood), the parametric trend and the maximum a posteriori dispersions to its fits of every gene, Wald tests of coefficients and contrasts, and likelihood ratio tests of a reduced design (all the levels of a factor at once, a time course), with the columns of `results()`, its independent filtering and its Cook's distance flags | DESeq2, PyDESeq2 |
-| Empirical Bayes moderation | Moderated t and F statistics, B-statistics, TREAT, with constant, trend and robust priors, estimated as limma estimates them by default: by the method of moments, or, when the residual degrees of freedom differ between genes, by weighted moments and profile likelihood (limma's `fitFDistUnequalDF1`); either can be chosen | limma |
-| Gene-set tests | `camera` (competitive, allowing for the correlation between the genes of a set, given or estimated per set; parametric or rank-based) and `fry` (self-contained, directional and mixed p-values), for one contrast of a cell type of a `limma_voom` or `limma_trend` result, or of a linear model of values with a design, with voom or other precision weights and trend or robust moderation; the rotation tests `roast` and `mroast` (self-contained, with the mean, floor-mean, mean-50 and mean-square set statistics, contribution weights per gene or per set, and the proportions of active genes) and `romer` (competitive, by the ranks of the genes), with rotations from a seeded generator; gene sets named by gene id or position. Gene sets read from and written to GMT files (MSigDB, Reactome, WikiPathways, the Gene Ontology), with the name, version and licence of the collection, matched to an analysis and filtered by size; over-representation analysis of the genes up, down or either way in a differential-expression result, a table or a list of genes (limma's `kegga` and `goana`), with its dot plot; gene set enrichment analysis (`gsea`) of genes ranked by a statistic, as fgsea computes it: enrichment scores, leading edges, normalized scores, and p-values by fgsea's multilevel method or by permutations, from seeds that a numpy generator draws, with its enrichment plot and table; scores of gene sets in every sample, as the GSVA package computes them: `gsva` (with the Gaussian, Poisson or no kernel, chosen as GSVA chooses it, and its three scores), `ssgsea`, `combined_z_scores` and `plage`, from a matrix or a cell type of a cube, with their heatmap, and tested for a contrast of a design by limma (`limma`, as for activities) | limma, R, decoupler, fgsea, GSVA |
-| Activities of regulators and pathways | `ulm`, `mlm` and `weighted_aggregate`, as decoupler computes them: the activity of every source of a network (transcription factors, pathways) in every sample, cell type of a cube or group and contrast of a differential-expression result, by a linear model of each source's weights, one model of all of them, or the weighted mean or sum of its targets with permutations; the networks CollecTRI (`collectri`, at a licence level for commercial or academic use) and PROGENy (`progeny`, human and mouse), downloaded from their releases on first use and checked against pinned SHA-256 digests, or any network given as a table; their heatmap and bar plot | decoupler, progeny |
-| Multiple testing | Bonferroni, Holm, Hochberg, Hommel, Benjamini-Hochberg, Benjamini-Yekutieli | R |
-| Differential expression | `limma_voom` and `limma_trend`: limma-voom and limma-trend on every group (cell type) of a cube, and `dream_voom`: dream, with the design's random intercepts, such as a donor's, in every group's fits, dream's choice of Kenward-Roger's or Satterthwaite's degrees of freedom by the number of samples in each group, and F-tests of several contrasts, with the design matched to the cube by sample id, samples with too few cells left out, rank-deficient groups fitted on their column space with the contrasts they can estimate, and a provenance record for every result, which keeps the cube and design it analysed and computes a cell type's fit again on request. Options for pseudobulk data, off by default: sample quality weights, prior weights from the number of cells, a minimum total count per sample, and p-values adjusted across cell types | limma, edgeR, variancePartition; on simulated data and on published single-cell data |
-| Variance partitioning | `variance_fractions`: the fraction of every gene's variance that each variable of a design explains, as variancePartition's `fitExtractVarPartModel`: a mixed model of every gene by maximum likelihood (or REML), with the categorical variables as random intercepts, the continuous ones as fixed effects and precision weights such as voom's, or a linear model without random effects; `variance_partitioning`: the same on every cell type of a cube, with dream's precision weights, the samples each fit used and a provenance record | variancePartition, lme4 |
-| Meta-analysis | `meta_analysis`: for every gene, the fixed-effect (inverse-variance) or random-effects estimate of a common effect from the `limma_voom` or `limma_trend` results of several cubes (`StudyResults`), matched by cell type, contrast and gene id, with the between-study variance by DerSimonian-Laird, REML or Paule-Mandel, Cochran's Q, I² and H², z-tests or the Knapp-Hartung adjustment, genes measured in some studies only, p-values adjusted over the genes and a provenance record; `fit_meta_analysis`: the same from arrays of effects and standard errors | metafor |
-| Combined p-values and replication | `combine_p_values`: every gene's p-values combined over the studies that tested it, by Fisher's, Stouffer's (weighted), Tippett's, Wilkinson's, Pearson's, Mudholkar and George's or Simes's method, one-sided or two-sided with the directions of the effects; `partial_conjunction`: the p-value that at least u studies have an effect, in one direction (Benjamini and Heller); `proportion_of_true_nulls`: pi0 and pi1 as qvalue's `pi0est`, with its smoother and bootstrap; `replication`: for the studies' results of a `CubeCollection` (`StudyResults`), every gene's combined and replication p-values, the studies that find it, its sign agreement and heterogeneity, pi1 (estimated at lambda = 0.5 by default) and sign agreement of every pair of studies, and the genes found in at least k studies, with their figures | scipy, metap, qvalue, metafor, mpmath |
-| Replication across studies | `sumrank`: SumRank (Nakatsuka et al. 2025), which ranks every gene within each study by its signed -log10 p-value and sums its best normalized ranks across studies, for genes up and down, for one cell type and contrast of the studies' results (`StudyResults`), with p-values from the Irwin-Hall distribution and from a null pooled over permutations that re-run each study's limma analysis from a seeded generator; `fit_sumrank`: the same from a matrix of statistics; `held_out_prediction`: each study left out in turn, genes chosen from the others by SumRank and by meta-analysis, its cases scored by UCell's rank score, and the AUC by study and method; with SumRank's Manhattan plot and the held-out AUC of every study | the authors' code, mpmath, scikit-learn |
-| Composition tests | `propeller`: differences in cell-type proportions between conditions, from the cell numbers of a cube or a table of them, with the logit or arcsine square-root transform, a design matched by sample id, t-tests of its contrasts and the F-test of a factor, robust empirical Bayes moderation, speckle's columns (baseline and mean proportions, proportion ratio, statistic, p-value, FDR) and a provenance record | speckle, limma |
-| Figures | `cytepool.plotting` (optional extra `plot`, matplotlib): the plots of limma, edgeR, DESeq2, speckle and metafor from CytePool's results, in one call each: MDS plots of pseudobulk samples (`plotMDS`), the cells pooled into each sample and cell type, cell-type composition and `propeller`'s results, voom's mean-variance trend, residual variances and their prior (`plotSA`), sample quality weights, MD and volcano plots, p-value histograms, the genes up and down in each cell type, barcode plots and summaries of gene-set tests, DESeq2's dispersions (`plotDispEsts`), edgeR's quasi-dispersions (`plotQLDisp`), forest plots of meta-analyses, the fractions of variance of every variable over the genes (`plotVarPart`) and of chosen genes (`plotPercentBars`), and heatmaps of pathway scores | limma, edgeR, DESeq2, metafor, speckle, variancePartition, GSVA |
-| Samples and cells at a glance | `principal_components` and `plot_pca`: samples on the principal components of their most variable genes, from a matrix or a cell type of a cube, as DESeq2's `plotPCA` computes and draws them; `plot_dotplot`: the fraction of cells that express each gene and their mean expression in each cell type, from a cube's sums and so without the cells, as scanpy's `dotplot` draws them | DESeq2, edgeR, scanpy |
-| Distributions and smoothers | Tails of the normal, t, F, chi-squared, gamma, beta and hypergeometric distributions, and quantiles of the first four; LOWESS, and limma's rule for its span; natural spline bases | mpmath, R, limma |
+CytePool needs Python 3.13 or newer:
-## Planned
-
-These are not implemented yet:
-
-- Likelihood ratio tests and gene-wise dispersions for negative binomial models
- (edgeR's `glmLRT` and `estimateDisp`), edgeR's quasi-likelihood F-tests on every
- group of a cube, and their `legacy = TRUE` methods
-- DESeq2's shrunken log fold changes (`lfcShrink` with apeglm, ashr or normal priors),
- its replacement of outlier counts, and its local and glmGamPoi dispersion trends
-- Random slopes in mixed models, with which variancePartition also partitions the
- variance within subsets of the samples. dream with random intercepts is implemented
- (see the table above); `fit_linear_model` refuses a design whose random effects it
- would drop, and its message points to `fit_mixed_model`.
-- Composition tests with precision weights or random effects (crumblr)
-- Ligand-receptor scoring and differential interactions
-- Dose-response fits, benchmark doses and synergy
-- Meta-regression (meta-analysis with study-level covariates) and multivariate
- meta-analysis of correlated effects
-- Cell types aligned up the Cell Ontology
-
-## Known limits
-
-- `from_cells` needs the cell-level matrix in memory and returns dense layers:
- 32 bytes per group x sample x feature with all five layers, 4 with counts only. For
- 30 groups, 100 samples and 30,000 genes that is 2.9 GB, or 360 MB for counts only.
- Ask for the layers you need.
-- A linear model fitted with observation weights or missing values stores a covariance
- matrix for every gene: 8 x genes x coefficients² bytes, so 16 MB for 20,000 genes and
- 10 coefficients, and 1.6 GB at 100 coefficients.
-- Such fits, in which each gene needs its own fit, run on numba's threads, as
- `from_cells` does. With numba's OpenMP threading layer, a process that has run a
- parallel routine cannot fork workers that run one again: start them with the
- "spawn" or "forkserver" method of `multiprocessing`. With one thread
- (`n_threads=1`), these fits run without a parallel region.
-- A mixed model stores, for every gene, the covariance matrix of the coefficients and
- its derivative with respect to each variance parameter, for Satterthwaite's degrees
- of freedom: about 8 x genes x (components + 2) x coefficients² bytes, so 23 MB for
- 20,000 genes, two random intercepts and 6 coefficients, and its weights, as many
- floats as the values. Kenward-Roger's tests invert each gene's n x n covariance
- matrix of the observations, as pbkrtest does: O(n³) operations per gene for n
- samples, 0.7 s for 20,000 genes of 20 samples and 13 s for 120 samples on one
- thread, where Satterthwaite's take 0.1 s. `fit_mixed_model` fits random
- intercepts only, by REML or maximum likelihood, to complete data with positive
- weights: missing values or a zero weight make it raise an error, as no gene is
- fitted to fewer samples. Its fitted values take as much memory as the values. It also
- refuses a design whose fixed and random effects together span every sample, which
- lme4 fits. A gene whose values the fixed and random effects reproduce, such as one
- constant within every donor, has no REML estimate and is returned unfitted (NaN).
- Its genes run on Python threads, in compiled code that releases the GIL, so the
- forking limit above does not apply to it.
-- A chunk of a cube file holds up to 4,096 features of every sample of one group, and a
- read decompresses every chunk that holds a selected feature. A few neighbouring
- features are therefore quick to read, but features scattered over the file, or a few
- samples of every feature, decompress most chunks of each selected group: 50 genes
- drawn at random from 30,000 usually fall in all eight chunks of a group. A read holds
- about what it returns plus one chunk, and with zstd also the decoder's window (2 MiB
- at the default level).
-- Every read that covers a whole group of a cube file, in any order, is hashed and
- checked against the digest stored with the file. Reading every group of a gzip
- file of 20 groups x 100 samples x 30,000 genes took 23% longer (SHA-256 ran at
- 2.3 GB/s with the processor's SHA instructions; without them it runs at about
- 0.5 GB/s). A selection of some samples or features reads part of a group, which is
- checked only by `Cube.verify_fingerprint()`. The fingerprint of a lazy cube derived
- from a cube file reads the groups whose values the operation changed and that were
- not read whole before, and an operation applied to such a cube records the
- fingerprint of its input only if it was computed. The first operation applied to a
- cube in memory hashes its layers to record its fingerprint (about 1 s per GB).
-- A lazy layer other than those of `read_cube` is read with `oindex` when it has one, as
- zarr-python arrays do, and otherwise one whole group at a time, as HDF5 datasets
- are. The cube copies what such a layer returns, so a read holds it twice for a moment.
-- Integer layers and cell numbers hold whole numbers up to 2**63 - 1, the largest int64.
- A library size, merged group or pooled sample whose sum would exceed it raises
- `OverflowError` instead of wrapping around. Normalization works in float64, which is
- exact for counts up to 2**53.
-- With residual degrees of freedom that differ between genes, robust moderation
- (`robust=True`) uses limma's likelihood estimator, as limma does, but leaves the
- genes without residual degrees of freedom out of its test for outliers. limma 3.62.1
- counts them in that test as zero variances, which become outliers and change which
- other genes count as outliers: in a simulation where 2% of the genes had no residual
- degrees of freedom, its robust prior degrees of freedom were 11.5% larger with a
- constant prior and 6% to 10% larger with a trend. Where every gene has residual
- degrees of freedom, the two agree. `fit_f_distribution_likelihood` also keeps prior
- weights in its second robust fit, where limma's drops them.
-- Contrast standard errors and moderated F statistics are exact by default. limma's
- approximations are available as an option and can differ when weights vary between
- genes.
-- `voom` normalizes by library-size factors only, and fits its trend without a block
- correlation; `voomLmFit` is not implemented. `voom` and `voom_with_quality_weights`
- need a design of full column rank (limma drops aliased columns; `limma_voom` fits a
- group's column space), and raise an error where limma returns values without
- meaning: for a sample whose library size is zero, or a trend that is not positive
- where weights are read. With prior weights, `voom` returns the trend's weights
- times the prior weights, which a fit needs; limma returns the trend's alone.
-- In `limma_voom` and `limma_trend`, sample quality weights give each sample a weight
- of its own; there is no variance model for groups of samples (limma's `var.group`
- and `var.design`). Prior weights are the number of cells of each sample and cannot
- be combined with quality weights, which edgeR's `voomLmFit` refuses too. In a
- design that pairs two samples per donor, both samples of a donor get the same
- quality weight from limma-trend and similar ones from limma-voom, as their
- residuals are equal and opposite.
-- `sample_quality_weights` stops REML where limma does, so that its weights are
- limma's: within about half a percent of the REML estimate without prior weights, and
- up to about 15% from it with prior weights (as for the voom weights of
- `voom_with_quality_weights` with `method="reml"`), where limma stops after one or
- two iterations, and further where the iterations oscillate and limma stops early. A
- smaller `tolerance` converges further where the iterations converge. The
- gene-by-gene algorithm, limma's default for quality weights, depends on the order of
- the genes, as limma's does, and its time grows with the cube of the number of
- samples: 0.2 to 0.3 s for 20,000 genes and 40 samples, 2.5 to 3 s for 100 samples.
-- `voom` chooses the span of its trend from the number of genes, as limma does by
- default since limma 3.66, and limma 3.60 to 3.65 do with `adaptive.span = TRUE`.
- Their default, and that of earlier versions, is a span of 0.5: pass `span=0.5` to
- reproduce it.
-- `camera` and `fry` need every value observed, with positive weights, and fit fixed
- effects only (no block correlation); `fry` has no gene contribution weights (limma's
- `gene.weights`). `fry`'s mixed p-value of a set of 2 to d genes (d the residual
- degrees of freedom) differs from limma's on purpose: it uses all d + 1 dimensions of
- the rotation, where limma's uses only the set's and can be far from the rotation
- p-value. For larger sets the two agree, and their beta approximation can be much too
- small far in the tail. `camera` also offers robust moderation, which limma's does
- not.
-- `fit_negative_binomial_model` takes the dispersions as given; CytePool estimates
- only a common dispersion (`common_dispersion`). Where a gene's maximum likelihood
- estimate is infinite, as when every count of a group is zero, its fitted values tend
- to zero and its coefficients are large but finite: -1e8 for such a group of a
- one-way layout, as in edgeR. Its deviances are exact, and differ from edgeR's, which
- adds 1e-8 to counts and means and approximates the deviance for dispersions below
- 1e-4 and where the dispersion times the mean reaches 1e6. Where weights differ
- between samples by a factor of more than about 1e10, the coefficients that only the
- samples of small weight determine lose accuracy (about 1e-16 times that factor), and
- beyond about 1e14 they are not determined.
-- `roast`, `mroast` and `romer` draw their rotations from a numpy generator, so their
- p-values agree with limma's within Monte Carlo error, and exactly when given the
- rotations limma draws; `mroast` tests every set with the same rotations. Like `camera`
- and `fry` they need every value observed and fit fixed effects only (no block
- correlation), and `romer` takes sample weights only, as limma's does. A rotated
- statistic equal to the observed one counts as at least as extreme, as Wu et al.
- (2010) define the p-value; limma's `roast` counts only larger ones, which gives about
- 1/2 instead of 1 where a set has no gene on one side of zero (`"floormean"`,
- `"msq"`). With contribution weights, a gene of weight zero leaves the set, and
- `"msq"` and the mixed `"floormean"` statistic weigh each gene by the size of its
- weight, as the paper does; limma's `"msq"` p-values differ for weights of both signs
- (and change with the scale of the weights beyond 1,000 rotations), and its
- `"floormean"` takes a negative weight as evidence against the set. `romer` shrinks
- genes whose log-odds exceed 709.8, which limma's shrinkage turns into NaN. Exact
- z-scores (limma's `approx.zscore = FALSE`) and given prior parameters (`var.prior`,
- `df.prior`) are not offered.
-- `fit_quasi_likelihood` follows edgeR 4.4's default method only (`legacy = FALSE`),
- as edgeR runs it on a matrix of counts; a given dispersion should come from a
- global model (common or trended), as edgeR's help page says. edgeR's method for a
- DGEList agrees, except when the DGEList holds a trended dispersion from
- `estimateDisp`: it then takes one dispersion from that trend in place of the
- Cox-Reid estimate. The moments of the unit deviance are exact, where edgeR's
- approximations are off by up to about 1e-3 at dispersions up to 0.2 and 1e-2 at a
- dispersion of 1, so that adjusted deviances, quasi-dispersions and F-statistics
- differ from edgeR's by up to about 3e-3. The robust prior leaves genes without
- residual degrees of freedom out of the outlier tests, where limma counts them as
- zero variances. With offsets per observation, edgeR's `aveLogCPM` ignores the
- offsets of the first gene, and so differs from CytePool's there.
-- `fit_negative_binomial_model` takes the dispersions as given; CytePool does not
- estimate them yet. Where a gene's maximum likelihood estimate is infinite, as when
- every count of a group is zero, its fitted values tend to zero and its coefficients
- are large but finite: -1e8 for such a group of a one-way layout, as in edgeR. Its
- deviances are exact, and differ from edgeR's, which adds 1e-8 to counts and means
- and approximates the deviance for dispersions below 1e-4 and where the dispersion
- times the mean reaches 1e6. Where weights differ between samples by a factor of more
- than about 1e10, the coefficients that only the samples of small weight determine
- lose accuracy (about 1e-16 times that factor), and beyond about 1e14 they are not
- determined.
-- The DESeq2 functions give the results of `DESeq(..., minReplicatesForReplace =
- Inf)`: outlier counts are not replaced, which DESeq2 does by default for samples
- whose design row occurs seven or more times. Each dispersion is the maximum of its
- objective, where DESeq2's line searches stop when an iteration gains less than 1e-6
- and can stall where they start, but for genes whose rough dispersion is at the lower
- bound, which keep it as in DESeq2; and the fits converge, where DESeq2's stop when
- the deviance changes by less than 1e-8 of it. On the validation's data DESeq2's
- final dispersions differ by medians of 1e-3 to 4e-2 on the log scale, and its
- p-values by medians of 3e-4 to 8e-3, relative, with 7 of 1,297 genes called
- differently at a false discovery rate of 0.1. Where DESeq2's parametric trend fails
- it substitutes a local regression; CytePool uses the mean of the gene-wise
- estimates, with a warning. With three or fewer residual degrees of freedom the
- prior's variance is the maximum likelihood estimate, where DESeq2 compares densities
- of simulated residuals: 10% apart in the validation. For Cook's distance a design
- matrix given without its `Design` compares two groups when its rows take two
- distinct values of zeros and ones, as a factor does in DESeq2 and a numeric
- covariate does not. Observation weights, normalization factors that differ between
- genes, thresholds on the fold change (`lfcThreshold`) and t-distributed Wald tests
- (`useT`) are not implemented.
-- `limma_voom` and `limma_trend` fit fixed effects only: a design with random effects
- is refused (`dream_voom` fits random intercepts), and there is no
- `duplicateCorrelation`. Normalization factors are TMM or
- none. In a group whose design is rank deficient they test every contrast the
- group's samples can estimate, including those that weigh an aliased column, which
- limma's `contrasts.fit` refuses, and they report the contrasts that cannot be
- estimated, which limma either refuses or, when they weigh no aliased column,
- computes with a changed meaning.
-- `dream_voom` chooses the degrees of freedom as `dream(ddf = "adaptive")` does:
- Kenward-Roger's for fewer than 20 samples in a group (with 20, dream uses
- Satterthwaite's, although its help page says "<= 20"), computed as dream computes
- them with pbkrtest, with every observation at its gene's average weight, which can
- make standard errors smaller than the unadjusted ones
- (`kenward_roger_weights="precision"` uses each observation's weight), and
- Satterthwaite's for a gene whose adjusted covariance
- matrix is ill conditioned, which dream decides from R's estimate of the condition
- number and CytePool from the exact one. dream's `topTable` gives the F-test of every
- gene the degrees of freedom of the first gene; CytePool gives each gene its own. No
- sample quality weights for dream. As variancePartition, they moderate the residual
- variance of an observation of each gene's average weight, which keeps the
- mean-variance trend that voom's weights remove; `trend=True` lets the prior follow
- it. `voom_with_dream_weights` chooses its span from the number of genes, as `voom`
- does; `voomWithDreamWeights` uses 0.5 (`span=0.5`). A gene whose values the fixed
- and random effects reproduce is left out of the trend and untested, where
- variancePartition keeps it with a residual standard deviation near zero.
-- `variance_fractions` and `variance_partitioning` fit random intercepts only:
- variancePartition's models of the variance within subsets of the samples, such as
- `(tissue + 0 | donor)`, need random slopes, and generalized linear mixed models and
- bootstrap intervals of the fractions (`varPartConfInf`) are not available. As
- variancePartition requires, a design with random effects gives its categorical
- variables as random effects. A gene whose values the fixed and random effects
- reproduce has no fractions (NaN), where variancePartition reports those of lme4's
- fit with a residual variance near zero; with `method="reml"` and no random effects
- the residual variance is REML's, where variancePartition takes the maximum
- likelihood one.
-- `from_anndata` and `from_mudata` take only sums and counts of cells: means,
- variances and medians, which `scanpy.get.aggregate` and `decoupler.pp.pseudobulk`
- can give, are refused, and counts are checked against the cells that detect each
- feature. A combination of sample and cell type without a row needs cell numbers to
- become an entry without cells, and a row removed after pooling (decoupler's
- `filter_samples`, LIANA's `min_cells`) becomes one too, so its cells are missing
- when groups are merged or samples pooled: import before filtering. LIANA's
- `adata_to_views` keeps no cell numbers with decoupler 2, so its views need them
- given, for example as a crosstab of the cells' labels.
-- `to_anndata` and `to_mudata` leave `X` empty and write every layer under its own
- name. anndata cannot write datetime or timedelta columns, text columns without any
- value, or column names with `/`, so the exports write them as ISO 8601 text,
- integers, NaN and names with `_`; the record in `uns` restores them on import. A
- MuData file cannot store a modality whose name holds `/` or is `.` or `..`. The
- tests of these
- conversions are skipped on Python 3.15, for which h5py and numcodecs, which
- anndata needs, have no wheels yet.
-- `propeller` fits fixed effects only: a design with random effects is refused, and
- there is no `duplicateCorrelation`. It reads whole numbers of cells, not
- proportions with assumed totals (speckle's `convertDataToList`), and its F-test
- compares all the levels of a factor; to compare some of them, test their contrasts,
- whose p-values use limma's degrees of freedom for t-tests, capped at the residual
- degrees of freedom of all cell types, where those of F-tests are not. It leaves out
- a cell type or a sample without cells, which speckle keeps when it is an unused
- level of a factor of labels; it tests contrasts that speckle refuses, such as the
- slope of a covariate, and gives no mean proportions or ratio for a contrast that
- compares no levels, where speckle reports regression coefficients; and it refuses
- data that the design fits exactly, such as copies of samples.
-- `meta_analysis` fits models without moderators and takes the studies as independent,
- with known sampling variances, and it takes limma's moderated standard errors as such,
- so a gene that one study tested, kept with `min_studies=1`, gets a z-test whose
- p-value is smaller than the study's own t-test's. Its REML estimate is the largest
- maximum of the restricted likelihood: where that likelihood has two maxima, as when an
- imprecise study lies far from precise ones that agree, metafor's Fisher scoring from
- the Hedges estimate can stop at the lower one or at zero (in 2 of the 9 such features
- of one validation case), and on some features it does not converge without `stepadj`.
- Its REML and Paule-Mandel estimates converge to rounding, beyond metafor's default
- stopping rules, and it reports I² as a proportion, where metafor reports a percentage.
- It refuses standard errors that are zero or infinite; NaN marks a study that did not
- measure a gene.
-- `gsva` estimates each gene's distribution from every pair of its samples, O(n²)
- operations per gene for n samples: 0.8 s for 15,000 genes of 50 samples on two
- threads with the Gaussian kernel, and 7 s with the Poisson kernel, whose
- probabilities cost more; from 200 samples on, `kernel="auto"` uses no kernel, as
- GSVA does. Genes whose kernel estimates are equal in exact arithmetic, as genes with
- the same low counts in different samples can be, are ordered by the last bit of
- their probabilities, which R computes differently: on counts of 0 to 6, the scores
- of their sets differed from GSVA's by up to 2.2e-3 of the largest score.
-- `combine_p_values` and `partial_conjunction` take the studies as independent, with
- p-values uniform without effects. With directions, a two-sided p-value below the
- smallest normal double (2.2e-308) loses digits when halved, and one of 5e-324 halves
- to 0. They keep p-values of 0 and 1 and a single study, which metap leaves out or
- refuses. `proportion_of_true_nulls` gives an estimate where qvalue stops: where no
- p-value reaches the largest lambda, and where the estimate is at most 0 (it is 0);
- with more than 49 values of lambda it keeps a knot at every one, where R's
- `smooth.spline` keeps some. `replication` reads limma's two-sided p-values with the
- signs of the log2 fold changes, so it needs a contrast, not an F-test.
-- `mlm` builds the design of the genes and sources of each set of profiles that observe
- the same genes, and its QR factor: 16 x genes x sources bytes, 190 MB for 15,000
- genes and 800 CollecTRI sources. `collectri` and `progeny` download 4.3 MB and
- 10.6 MB once, into `~/.cache/cytepool/networks`; PROGENy names genes by the symbols
- of its 2020 release, so data named by current symbols miss the genes renamed since,
- which decoupler's `op.progeny` renames through OmniPath.
-- numpy masked arrays, and array-likes that convert to one such as netCDF4 variables,
- are refused with a `TypeError` that names the argument, because numpy drops the
- mask when it converts one and the masked entries would be read as data. Where an
- argument accepts missing values, mark them with NaN in numbers
- (`numpy.ma.filled(values.astype(float), numpy.nan)`) and None in text
- (`values.tolist()`); otherwise remove them first. pandas objects work as before.
+```bash
+pip install "git+https://github.com/NygenAnalytics/CytePool.git"
+```
-## Principles
+The extras `anndata`, `mudata` and `plot` add conversions to AnnData and MuData and the
+figures; [Installation](https://docs.nygen.io/CytePool/installation) gives the details.
+
+## Example
+
+Pool the cells into a cube, then test a stimulation in every cell type with
+limma-voom, with a design that pairs the samples of each donor:
+
+```python
+from cytepool import Design, camera, from_cells, limma_voom, read_gmt
+
+# counts: cells x genes; cell_sample and cell_type: the labels of each cell;
+# samples: a table with sample_id, donor and condition; genes: with feature_id
+# and symbol.
+cube = from_cells(
+ counts,
+ sample_labels=cell_sample,
+ group_labels=cell_type,
+ samples=samples,
+ features=genes,
+)
+design = Design(
+ cube.samples,
+ "~ donor + condition",
+ levels={"condition": ["ctrl", "stim"]},
+ contrasts={"stim_vs_ctrl": "conditionstim"},
+)
+result = limma_voom(cube, design)
+table = result.table("Monocytes", "stim_vs_ctrl") # topTable's columns
+
+# Which pathways respond in the monocytes, from a GMT file of gene sets.
+hallmark = read_gmt("h.all.v2026.2.Hs.symbols.gmt")
+pathways = camera(result, hallmark, group="Monocytes", id_column="symbol")
+```
-- **Samples are the unit of replication.** Cells are pooled into samples before any
- test, and a cube records how many cells each pooled profile holds.
-- **Two required dependencies:** numpy and numba.
-- **Checked against the reference implementations.** Every method is compared with
- the package that defined it. The comparisons live in [`validation/`](validation/),
- and their results are reported with each change.
-- **Implemented from the literature.** Methods whose reference packages carry
- copyleft licenses are written from their papers and documentation, never from their
- source code, so CytePool can stay BSD-licensed.
+The result keeps the cube's fingerprint, so that it can be traced to the profiles it
+was computed from. The [Quick start](https://docs.nygen.io/CytePool/quickstart) pools
+and tests a simulated study this way, and [Gene sets and
+pathways](https://docs.nygen.io/CytePool/guides/gene-sets) tests pathways.
+
+## What CytePool answers
+
+| Question | Start with |
+| --- | --- |
+| Which genes change in each cell type? | `limma_voom` |
+| Which genes change in bulk RNA-seq? | `limma_voom` on a cube from `from_bulk` |
+| Which genes change, with several samples of each donor? | `dream_voom` |
+| What drives the variation between samples? | `variance_partitioning` |
+| Which cell types expand or contract? | `propeller` |
+| Do the genes of a pathway respond to a contrast? | `camera` |
+| Do the genes of a pathway gather along a ranking? | `gsea` |
+| How active is a pathway in each sample? | `gsva` |
+| Which regulators and pathways are active? | `ulm` |
+| What do several studies say together? | `meta_analysis` |
+| Which changes replicate across studies? | `sumrank` |
+
+[Choosing a method](https://docs.nygen.io/CytePool/statistics/choosing-a-method) gives
+the alternatives for each question and what is not implemented yet, and
+[Validation](https://docs.nygen.io/CytePool/validation) how closely each method agrees
+with its reference.
## Documentation
-The documentation is published at . It has a quick
-start and guides, pages on how the statistics work, an API reference, a
-[capability matrix](docs/source/reference/capabilities.md) and a
-[Validation](docs/source/validation.md) page that reports, for every validation
-script, the reference, the measured differences, the tolerances and the outcome. Its
-sources are in [`docs/`](docs/); build it with
-`uv sync --extra test --group dev --group docs` and `make -C docs html` with Node.js
-20 or newer (CI uses Node.js 24), as
-[the installation page](docs/source/installation.md#build-this-documentation)
-describes.
+The documentation is at : a quick start, guides to
+building cubes, comparing conditions and combining studies, chapters on how each method
+works, the API reference and the Validation page. Its sources are in
+[`docs/`](https://github.com/NygenAnalytics/CytePool/tree/master/docs), and
+[Installation](https://docs.nygen.io/CytePool/installation#build-the-documentation)
+shows how to build it.
## Development
```bash
-uv sync --extra test --group dev
+uv sync --extra test --group dev --group docs
uv run pytest
-uv run ruff check cytepool tests validation
-uv run ruff format --check cytepool tests validation
+uv run ruff check cytepool tests validation docs
+uv run ruff format --check cytepool tests validation docs
uv run mypy cytepool
+make -C docs api
```
+[`AGENTS.md`](https://github.com/NygenAnalytics/CytePool/blob/master/AGENTS.md) gives
+the conventions of the code, its tests and its documentation.
+
## License
-BSD 3-Clause. See [LICENSE](LICENSE) and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+BSD 3-Clause. See
+[LICENSE](https://github.com/NygenAnalytics/CytePool/blob/master/LICENSE) and
+[THIRD_PARTY_NOTICES.md](https://github.com/NygenAnalytics/CytePool/blob/master/THIRD_PARTY_NOTICES.md).
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index c863c7f..185f002 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -57,7 +57,7 @@ SOFTWARE.
### fgsea
-`cytepool/gsea.py` ports the algorithms of fgsea 1.32.2
+`cytepool/gene_set_enrichment.py` ports the algorithms of fgsea 1.32.2
(https://bioconductor.org/packages/fgsea, https://github.com/ctlab/fgsea), keeping their
structure, constants and order of operations, so that given fgsea's seeds it gives
fgsea's results: from `src/fastGSEA.cpp`, the block prefix sums (`SegmentTree`), the
@@ -169,8 +169,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### SumRank
-`cytepool/sumrank.py` and `cytepool/held_out_prediction.py` implement SumRank and its
-held-out prediction from Nakatsuka et al. (2025), following conventions of the authors'
+`cytepool/stats/sumrank.py`, `cytepool/sumrank_across_studies.py` and
+`cytepool/held_out_studies.py` implement SumRank and its held-out prediction from
+Nakatsuka et al. (2025), following conventions of the authors'
code (https://github.com/nathan-nakatsuka/scRNA_Reproducibility, commit 46392f66;
`SumRank.R`, `CalibratePValueswithPermutations.R`, `PermutingCaseControlStatus.R`,
`UCell_AUC_code.R`): ties ranked by their average rank, floor(p k) of the studies summed
diff --git a/cytepool/_analysis_records.py b/cytepool/_analysis_records.py
index 2602982..320a615 100644
--- a/cytepool/_analysis_records.py
+++ b/cytepool/_analysis_records.py
@@ -2,7 +2,7 @@
The result of a workflow on a cube, such as ``limma_voom`` or ``propeller``, holds a
provenance record. These functions build the parts that every workflow records alike:
-the cube it read, identified by its fingerprint and the digests behind it; the design
+the cube it read, identified by its fingerprint and the digests it is made of; the design
it fitted, with the contrasts it tested; and the versions of the software.
"""
diff --git a/cytepool/_expression_values.py b/cytepool/_expression_values.py
index f25c941..4ef141f 100644
--- a/cytepool/_expression_values.py
+++ b/cytepool/_expression_values.py
@@ -238,7 +238,7 @@ def _cube_values(
kind = checked_choice(values, "values", VALUES)
fewest = integral_number(min_cells, "min_cells")
if fewest < 0:
- raise ValueError(f"min_cells must be at least 0, got {fewest}")
+ raise ValueError(f"min_cells must be non-negative, got {fewest}")
if isinstance(expression_filter, Mapping):
filter_settings = filter_setting(expression_filter)
check_sample_groups({"expression_filter": filter_settings}, cube.samples)
diff --git a/cytepool/_gene_sets.py b/cytepool/_gene_sets.py
index 404d151..a3b7307 100644
--- a/cytepool/_gene_sets.py
+++ b/cytepool/_gene_sets.py
@@ -46,7 +46,10 @@
IntArray = npt.NDArray[np.intp]
# A pivot of the QR decomposition this small relative to the norm of its column is
-# zero up to rounding; see _effects_of_last_coefficient.
+# zero up to rounding; see _effects_of_last_coefficient. limma computes most such
+# zeros exactly, but leaves rounding noise in a few percent of genes, a different few
+# in camera and in fry, so that its bases change with the last bits of the data there
+# (found by probing); counted as zero, they do not here.
_ZERO_PIVOT = 1e-12
@@ -88,12 +91,8 @@ def gene_set_data(
gives its values (and a VoomResult its weights), and ``design``, ``contrast`` and
``feature_ids`` are as given.
"""
- with_trend = (
- None if trend is None else checked_flag(trend, "trend", expected="a bool")
- )
- with_robust = (
- None if robust is None else checked_flag(robust, "robust", expected="a bool")
- )
+ with_trend = None if trend is None else checked_flag(trend, "trend")
+ with_robust = None if robust is None else checked_flag(robust, "robust")
if isinstance(data, DifferentialExpressionResult):
for name, value, owner in (
("design", design, "the design of its group's fit"),
diff --git a/cytepool/_group_fits.py b/cytepool/_group_fits.py
index 3611819..791e2fa 100644
--- a/cytepool/_group_fits.py
+++ b/cytepool/_group_fits.py
@@ -186,14 +186,17 @@ def prepare_group(
return fit.stop(
"fixed_effects_not_estimable",
"the samples of this group cannot separate the fixed effects: "
- + "; ".join(relations),
+ + listing(relations, separator="; "),
), None
else:
+ # limma's contrasts.fit refuses an estimable contrast that weighs an aliased
+ # column, and every contrast passed with it (found by probing); it is
+ # computed here, from the fit on the kept columns.
violations = _contrast_violations(model, aliasing, contrasts)
for position, relations in violations.items():
fit.not_estimable[contrasts.names[position]] = (
"not estimable from the samples of this group, which cannot separate "
- "the effects it depends on: " + "; ".join(relations)
+ "the effects it depends on: " + listing(relations, separator="; ")
)
estimable = [p for p in range(len(contrasts)) if p not in violations]
if not estimable:
@@ -255,6 +258,8 @@ def left_out(
counts: np.ndarray, cells: np.ndarray | None, min_cells: int, min_counts: float
) -> list[str]:
"""Why each sample is left out of a group's fit, or "" when it is used."""
+ # A sample with exactly min_cells cells or min_counts counts is kept, as
+ # decoupler's filter_samples keeps it (found by probing).
has_counts = counts.any(axis=1)
# Totals over every feature, exact for whole counts, as R's colSums gives them.
totals = library_sizes(counts) if min_counts > 0.0 and counts.size else None
@@ -319,10 +324,10 @@ def group_positions(cube: Cube, groups: str | Sequence[str] | None) -> list[int]
def min_cells_setting(value: Any) -> int:
"""The fewest cells a sample needs in a group, a whole number of at least 0."""
if isinstance(value, bool | np.bool_):
- raise TypeError("min_cells must be a whole number of cells, not bool")
+ raise TypeError("min_cells must be a whole number of cells, got bool")
number = index_integer(value, "min_cells", expected="a whole number of cells")
if number < 0:
- raise ValueError(f"min_cells must be at least 0, got {number}")
+ raise ValueError(f"min_cells must be non-negative, got {number}")
return number
@@ -369,8 +374,8 @@ def threads_setting(value: Any) -> int | None:
if value is None:
return None
if isinstance(value, bool | np.bool_):
- raise TypeError("n_threads must be a positive integer or None, not bool")
- number = index_integer(value, "n_threads", expected="a positive integer or None")
+ raise TypeError("n_threads must be an integer or None, got bool")
+ number = index_integer(value, "n_threads", expected="an integer or None")
if number < 1:
raise ValueError(f"n_threads must be at least 1, got {number}")
return number
diff --git a/cytepool/_jit.py b/cytepool/_jit.py
index 23630f5..c9b9f8e 100644
--- a/cytepool/_jit.py
+++ b/cytepool/_jit.py
@@ -100,8 +100,8 @@ def thread_limit(n_threads: int | None) -> Iterator[int]:
yield numba.get_num_threads()
return
if isinstance(n_threads, bool):
- raise TypeError("n_threads must be a positive integer or None, not bool")
- limit = index_integer(n_threads, "n_threads", expected="a positive integer or None")
+ raise TypeError("n_threads must be an integer or None, got bool")
+ limit = index_integer(n_threads, "n_threads", expected="an integer or None")
if limit < 1:
raise ValueError(f"n_threads must be at least 1, got {limit}")
previous = numba.get_num_threads()
diff --git a/cytepool/_messages.py b/cytepool/_messages.py
index 41d7919..1982178 100644
--- a/cytepool/_messages.py
+++ b/cytepool/_messages.py
@@ -29,15 +29,22 @@
so that a warning is attributed to the first caller outside the package."""
-def listing(items: Sequence[Any]) -> str:
- """Items as "a, b and c", or the first few and how many more."""
- shown = [str(item) for item in items[:LISTED]]
+def listing(items: Sequence[Any], separator: str = ", ", limit: int = LISTED) -> str:
+ """Items as "a, b and c", or the first ``limit`` and how many more, as "a, b, c
+ and 3 more".
+
+ Items that hold commas themselves, such as relations between columns, take
+ another separator, such as "; ", which then comes before the "and" of the last
+ item too: "a; b; and c".
+ """
+ shown = [str(item) for item in items[:limit]]
more = len(items) - len(shown)
+ last = " and " if separator == ", " else f"{separator}and "
if more:
- return ", ".join(shown) + f" and {more} more"
+ return separator.join(shown) + f"{last}{more:,} more"
if len(shown) <= 1:
return "".join(shown)
- return ", ".join(shown[:-1]) + " and " + shown[-1]
+ return separator.join(shown[:-1]) + last + shown[-1]
def quoted(items: Sequence[Any]) -> str:
diff --git a/cytepool/_options.py b/cytepool/_options.py
index 0ce2bf8..910baaf 100644
--- a/cytepool/_options.py
+++ b/cytepool/_options.py
@@ -4,8 +4,8 @@
Each check takes the option as the caller gave it and its name, and returns it as a
bool, str, float, int or numpy random generator, or raises TypeError for a value of
the wrong type and ValueError for one out of range. The messages name the option, say
-what it must be and what it was; where the methods word a message differently, the
-check takes the phrase as an argument, so that every message stays as it was.
+what it must be and what it was: a TypeError ends with the type it got, and a
+ValueError with the value.
"""
import math
@@ -18,20 +18,18 @@
from ._masked_arrays import reject_masked
-def checked_flag(value: Any, name: str, *, expected: str = "True or False") -> bool:
+def checked_flag(value: Any, name: str) -> bool:
"""An option that is True or False, as a bool.
Args:
value: The option as given; a numpy bool is accepted.
name: The option's name, for the message.
- expected: What the message says the option must be: "True or False", or
- "a bool" as the statistics' messages say.
Raises:
- TypeError: If ``value`` is not a bool.
+ TypeError: If ``value`` is not True or False.
"""
if not isinstance(value, bool | np.bool_):
- raise TypeError(f"{name} must be {expected}, got {type(value).__name__}")
+ raise TypeError(f"{name} must be True or False, got {type(value).__name__}")
return bool(value)
@@ -83,14 +81,6 @@ def non_negative_number(value: Any, name: str) -> float:
return number
-def number_at_least(value: Any, name: str, lower: float) -> float:
- """A finite real number of at least ``lower``."""
- number = real_number(value, name)
- if not (math.isfinite(number) and number >= lower):
- raise ValueError(f"{name} must be finite and at least {lower:g}, got {number}")
- return number
-
-
def positive_finite_number(value: Any, name: str) -> float:
"""A finite real number above 0."""
number = real_number(value, name)
diff --git a/cytepool/activity.py b/cytepool/activity.py
index 24f4b44..980e6fa 100644
--- a/cytepool/activity.py
+++ b/cytepool/activity.py
@@ -2,16 +2,17 @@
their target genes, as decoupler computes them: the univariate and multivariate
linear models (ULM, MLM) and the weighted aggregate.
-A network (:class:`~cytepool.Network`), such as CollecTRI's regulons or PROGENy's
-pathway signatures (:func:`~cytepool.collectri`, :func:`~cytepool.progeny`), gives each
-source, a transcription factor or a pathway, a signed weight on each of its target
-genes. A source is active in a profile, the expression of a sample or the statistics of
-a contrast, when its targets are high where their weights are positive and low where
-they are negative. Each function scores every source in every profile, as decoupler
-2.2.0's ``mt.ulm``, ``mt.mlm`` and ``mt.waggr`` do (Badia-i-Mompel et al. 2022), and
-returns an :class:`~cytepool.Activities`, profiles x sources, which the linear models
-take as they take pathway scores, and which :func:`~cytepool.plotting.plot_activities`
-draws as a heatmap and :func:`~cytepool.plotting.plot_top_activities` as bars.
+:func:`~cytepool.ulm`, :func:`~cytepool.mlm` and :func:`~cytepool.weighted_aggregate`
+score every source of a network (:class:`~cytepool.Network`, such as
+:func:`~cytepool.collectri` or :func:`~cytepool.progeny` returns) in every profile, as
+decoupler 2.2.0's ``mt.ulm``, ``mt.mlm`` and ``mt.waggr`` do (Badia-i-Mompel et al.
+2022). Each returns an :class:`~cytepool.Activities`, profiles x sources, which
+:func:`~cytepool.limma` tests with a design when the profiles are samples, and which
+:func:`~cytepool.plotting.plot_activities` draws as a heatmap and
+:func:`~cytepool.plotting.plot_top_activities` as bars.
+https://docs.nygen.io/CytePool/statistics/activities#ulm explains the methods, and
+https://docs.nygen.io/CytePool/statistics/activities#activities-limits how they differ
+from decoupler's.
The profiles:
- A samples x genes array, one profile per row, with one gene id per column.
@@ -27,7 +28,8 @@
Each profile's genes are those with an id and a value: NaN marks a missing value,
and a gene that a group did not test has none in that group's profiles. Genes
whose values are zero in every profile are left out, as decoupler leaves them
- out: in sparse data they are genes that were not detected.
+ out: in sparse data they are genes that were not detected. A profile without
+ genes, or whose values are all zero, has NaN scores.
The network's targets:
Targets are matched to the genes' ids exactly. An edge whose weight is zero is no
@@ -36,61 +38,10 @@
as decoupler's ``tmin`` leaves it out; ``n_targets`` gives each source's targets
among each profile's genes.
-ULM (:func:`~cytepool.ulm`):
- For each source and profile, a linear model of the profile's n genes on the
- source's weights, zero for the genes that are not its targets:
-
- y_g = b_0 + b_1 x_g + e_g,
-
- fitted by least squares. The activity score is the t-statistic of the slope,
- t = b_1 / se(b_1) = r sqrt((n - 2) / (1 - r^2)), with r the correlation of y and
- x, and its p-value is two-sided, from Student's t distribution on n - 2 degrees
- of freedom. ``estimates`` are the slopes b_1.
-
-MLM (:func:`~cytepool.mlm`):
- One linear model of each profile's genes on the weights of all its sources at
- once, with an intercept, y_g = b_0 + sum_s b_s x_gs + e_g, so that each source's
- score, the t-statistic of b_s on n - m - 1 degrees of freedom for m sources, is
- that of its targets beyond what the other sources explain. The model is fitted by
- a QR decomposition. Sources whose weights over the genes are linear combinations
- of the weights of other sources, such as two sources with the same targets and
- weights, cannot be told apart, and are refused: the message names them.
-
-Weighted aggregate (:func:`~cytepool.weighted_aggregate`):
- The weighted mean of the values of a source's targets, sum_g w_g y_g /
- sum_g |w_g|, or their weighted sum, sum_g w_g y_g, the score of the original
- PROGENy method (Schubert et al. 2018). With ``n_permutations`` random
- permutations of the genes, each permutation giving every gene the value of
- another, as decoupler draws them (its ``seed`` is ``rng``), the score is
- normalized by the scores of the permutations, (ES - mean) / sd, the standard
- deviation with n - 1 in its denominator, and the p-value doubles the smaller
- of the two one-sided permutation p-values, (1 + b) / (1 + n_permutations), with b
- the permutations whose score is at least, or at most, the observed one (Phipson
- and Smyth 2010). ``estimates`` keep the weighted means or sums.
-
Adjusted p-values:
``adjusted_p_value`` are Benjamini and Hochberg's (1995) over the sources of each
- profile, as decoupler adjusts them.
-
-Deliberate differences from decoupler 2.2.0:
- - decoupler reports MLM's p-values without adjusting them, under the name of its
- adjusted p-values; here they are ``p_value``, and ``adjusted_p_value`` are
- adjusted as for the other methods.
- - decoupler keeps adjusted p-values in single precision, and computes MLM's
- p-values as one minus the distribution function, which gives 0 below about
- 1e-16; here both are in double precision, from the upper tail.
- - decoupler counts the permutations of the weighted aggregate whose absolute
- score exceeds the absolute observed score, and then folds that share as it
- would fold a one-sided one, so that a score near zero, which most permutations
- exceed, gets a p-value near 2 / n_permutations; here the p-value doubles the
- smaller one-sided p-value. When every permutation scores alike, decoupler's
- normalized score is infinite, or 0 for a score of 0; here it is NaN.
- - decoupler adds 2.2e-16 to 1 - r and 1 + r in ULM's t-statistic; here it is
- exact, which changes the score only for |r| within about 1e-6 of 1.
- - decoupler refuses missing values and leaves out samples whose values are all
- zero; here each profile uses its observed genes, and a profile without genes
- has NaN scores. Where MLM cannot separate sources, decoupler stops with a
- singular matrix; here the message names them.
+ profile, for every method, as decoupler adjusts those of ULM; decoupler leaves
+ MLM's unadjusted.
References:
Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S,
@@ -168,9 +119,10 @@ class Activities:
"""Activities of the sources of a network in every profile.
Rows of the arrays are profiles, in the order of ``profiles``, and columns are
- sources, in the order of ``network.sources``, so that
- :func:`~cytepool.stats.linear_models.fit_linear_model` takes ``scores`` as its
- values, with a design built from ``profiles``. The arrays are read-only.
+ sources, in the order of ``network.sources``. The activities of samples, such as
+ those of a cube's group, are tested with a design by :func:`~cytepool.limma`,
+ with the design built for the cube, whose rows it matches to ``profiles`` by
+ ``sample_id``. The arrays are read-only.
Attributes:
scores: ``(n_profiles, n_sources)`` activity scores: the t-statistics of ULM
@@ -335,11 +287,19 @@ def ulm(
"""Score the sources of a network in every profile by univariate linear models
(ULM).
- As decoupler's ``mt.ulm`` (Badia-i-Mompel et al. 2022): for each source, the
- values of a profile's genes are regressed on the source's weights, zero for the
- genes that are not its targets, and the score is the t-statistic of the slope.
- The module docstring gives the profiles, the matching of targets and the
- deliberate differences from decoupler.
+ As decoupler's ``mt.ulm`` (Badia-i-Mompel et al. 2022): for each source and
+ profile, a linear model of the values of the profile's n genes on the source's
+ weights, zero for the genes that are not its targets,
+
+ y_g = b_0 + b_1 x_g + e_g,
+
+ fitted by least squares. The score is the t-statistic of the slope,
+ t = b_1 / se(b_1) = r sqrt((n - 2) / (1 - r^2)), with r the correlation of y and
+ x, and its p-value is two-sided, from Student's t distribution on n - 2 degrees
+ of freedom. https://docs.nygen.io/CytePool/statistics/activities#ulm explains the
+ method, the module documentation gives the profiles and the matching of targets,
+ and https://docs.nygen.io/CytePool/statistics/activities#activities-limits the
+ differences from decoupler.
Args:
data: ``(n_profiles, n_genes)`` values, such as log-expression values of
@@ -464,12 +424,22 @@ def mlm(
"""Score the sources of a network in every profile by one multivariate linear
model (MLM).
- As decoupler's ``mt.mlm`` (Badia-i-Mompel et al. 2022): the values of a
- profile's genes are regressed on the weights of all the sources at once, with an
- intercept, and each source's score is the t-statistic of its coefficient, which
- measures its targets beyond what the other sources explain. The module docstring
- gives the profiles, the matching of targets and the deliberate differences from
- decoupler.
+ As decoupler's ``mt.mlm`` (Badia-i-Mompel et al. 2022): one linear model of each
+ profile's genes on the weights of all its m sources at once, with an intercept,
+ y_g = b_0 + sum_s b_s x_gs + e_g, fitted by a QR decomposition, so that each
+ source's score, the t-statistic of b_s on n - m - 1 degrees of freedom, is that of
+ its targets beyond what the other sources explain. Sources whose weights over the
+ genes are linear combinations of the weights of other sources, such as two
+ sources with the same targets and weights, cannot be told apart, and are refused:
+ the message names them. https://docs.nygen.io/CytePool/statistics/activities#mlm
+ explains the method, the module documentation gives the profiles and the
+ matching of targets, and
+ https://docs.nygen.io/CytePool/statistics/activities#activities-limits the
+ differences from decoupler.
+
+ Memory: the design of the genes and sources of each set of profiles that observe
+ the same genes, and its QR factor, take 16 x genes x sources bytes, about 190 MB
+ for 15,000 genes and 800 sources.
Args:
data: The profiles, as for :func:`ulm`.
@@ -497,9 +467,9 @@ def mlm(
ValueError: If the values are not 2-D or hold an infinite value, the ids do
not match the values, an id names two genes, an option is out of range or
applies to another kind of data, a table lacks a column, no source has
- ``min_targets`` targets among the genes, a profile has no more genes than
- sources, or the weights of some sources are linear combinations of
- those of others.
+ ``min_targets`` targets among the genes, a profile has fewer than two more
+ genes than sources, which leaves no residual degrees of freedom, or the
+ weights of some sources are linear combinations of those of others.
KeyError: If the cube has no such group or features column, or the result
has no such group or contrast.
"""
@@ -568,11 +538,22 @@ def weighted_aggregate(
their targets' values.
As decoupler's ``mt.waggr`` (Badia-i-Mompel et al. 2022): the weighted mean of
- the values of a source's targets, sum w y / sum |w|, or their weighted sum,
- sum w y, which is how PROGENy scores its pathways (Schubert et al. 2018); with
- permutations of the genes, normalized by the scores of the permutations, with a
- permutation p-value. The module docstring gives the permutations, the p-values
- and the deliberate differences from decoupler.
+ the values of a source's targets, sum_g w_g y_g / sum_g |w_g|, or their weighted
+ sum, sum_g w_g y_g, which is how PROGENy scores its pathways (Schubert et al.
+ 2018). With ``n_permutations`` random permutations of the genes, each giving every
+ gene the value of another, as decoupler draws them, the score is normalized by
+ the scores of the permutations, (ES - mean) / sd, the standard deviation with
+ n - 1 in its denominator, and the p-value doubles the smaller of the two
+ one-sided permutation p-values, (1 + b) / (1 + n_permutations), with b the
+ permutations whose score is at least, or at most, the observed one (Phipson and
+ Smyth 2010). If the permutations all score alike, so that their standard deviation
+ is zero, the normalized score is NaN, where decoupler gives infinity, or 0 for an
+ observed score of 0.
+ https://docs.nygen.io/CytePool/statistics/activities#weighted-aggregate
+ explains the method, the module documentation gives the profiles and the
+ matching of targets, and
+ https://docs.nygen.io/CytePool/statistics/activities#activities-limits the
+ differences from decoupler.
Args:
data: The profiles, as for :func:`ulm`.
@@ -1163,7 +1144,8 @@ def _fit_pattern(
raise ValueError(
f"MLM fits {m} sources and an intercept to the {n} genes of profile(s) "
f"{quoted(ids)}, which leaves no residual degrees of freedom; it needs "
- "more genes than sources: use ulm, or a network with fewer sources"
+ "at least two more genes than sources: use ulm, or a network with fewer "
+ "sources"
)
position = np.full(observed.shape[1], -1, dtype=np.int64)
position[genes] = np.arange(n)
diff --git a/cytepool/composition.py b/cytepool/composition.py
index 6aaffdf..ff59fe9 100644
--- a/cytepool/composition.py
+++ b/cytepool/composition.py
@@ -1,29 +1,12 @@
"""Tests of differences in cell-type composition between conditions, by propeller.
-:func:`~cytepool.propeller` tests, for every group (cell type) of a cube, whether the
-share of a sample's cells that belong to the group differs between conditions, as
-speckle's ``propeller`` does (Phipson et al. 2022). It reads only the number of cells of
-every sample in every group (``Cube.n_cells``), or a table of them, and fits one linear
-model to all groups at once:
-
-1. Proportions. Sample j, with x_ij cells of group i and N_j cells in all, has the
- proportion p_ij = x_ij / N_j of group i.
-2. Transform. The logit transform, log(q_ij / (1 - q_ij)), takes the proportions
- after half a cell is added to every count, q_ij = (x_ij + 0.5) / sum_i (x_ij +
- 0.5), so that a group absent from a sample has a finite value; the arcsine
- square-root transform, arcsin(sqrt(p_ij)), takes the proportions as they are
- (Phipson et al. 2022, supplementary section 1).
-3. Linear model. The transformed proportions of every group are fitted with the
- design by least squares (limma's ``lmFit``), and the contrasts are formed
- (``contrasts.fit``).
-4. Moderation. The residual variances of the groups are moderated by empirical
- Bayes, robustly by default, as speckle calls limma's ``eBayes(robust = TRUE)``
- (Smyth 2004; Phipson et al. 2016).
-5. Tests. Each contrast is tested by its moderated t-statistic, as
- ``propeller.ttest`` does. ``f_tests`` names factors whose levels are tested for
- equal means by the moderated F-statistic, as ``propeller.anova`` does, and
- ``propeller`` with more than two groups. The p-values of a test are adjusted by
- the method of Benjamini and Hochberg over the groups (speckle's ``FDR``).
+:func:`~cytepool.propeller` runs speckle's ``propeller`` (Phipson et al. 2022) on the
+number of cells of every sample in every group (cell type) of a cube
+(``Cube.n_cells``), or on a table of them, and returns a
+:class:`~cytepool.CompositionResult`.
+https://docs.nygen.io/CytePool/statistics/composition#propeller-steps gives the
+steps, and https://docs.nygen.io/CytePool/statistics/composition#composition-limits
+how the results differ from speckle's.
Samples and groups:
The design's sample table must have a ``sample_id`` column; its samples are
@@ -35,63 +18,15 @@
used is left out too: it is not observed in the experiment, and speckle, which
reads the label of every cell, never sees it. At least two groups must remain.
-Descriptive columns:
- Each table also gives speckle's descriptive columns, whose definitions were found
- by probing speckle 1.6.0. The baseline proportion of a group is its share of all
- the cells of the samples used (speckle's ``BaselineProp``). The mean proportion
- of a level of a factor is the mean of p_ij over the samples at that level
- (``PropMean``), taken over the samples as they are, whatever the other terms of
- the design. A contrast *compares the levels* of a factor when it is a
- combination, with weights d_l that sum to zero, of the differences between the
- levels of a factor that enters the formula as a main effect only; for example
- ``conditionstim`` in ``~ donor + condition``, ``conditionstim -
- conditionctrl`` in ``~ 0 + condition``, or ``(groupA + groupB)/2 - groupC``,
- but not ``groupA + groupB``. The proportion ratio of such a contrast is the
- product of the mean proportions m_l of its levels raised to their weights,
- prod_l m_l^d_l (``PropRatio``): m_stim / m_ctrl for stim against ctrl, and
- sqrt(m_A m_B) / m_C for ``(groupA + groupB)/2 - groupC``. The table of such a
- contrast gives the mean proportion of each level with a weight other than zero,
- and the ratio; the table of another contrast, such as a covariate's slope, gives
- neither.
-
-Deliberate differences from speckle:
- ``validation/composition_tests.py`` reports each of them.
-
- - speckle's ``PropMean`` are the coefficients of the proportions regressed on
- the columns of the design matrix that the contrast weighs, and its
- ``PropRatio`` is their product raised to the contrast's weights (found by
- probing). Where those columns indicate groups of samples, as in a design
- without an intercept whose first columns are the groups, the coefficients
- are the groups' mean proportions and the ratio is the one above. Otherwise
- they are not: for ``groupA + groupB``, which compares nothing, speckle
- reports the product m_A m_B as a ratio; for ``(Intercept) + groupB`` in ``~
- group + age``, the mean of the samples outside B and a difference of means;
- and for a contrast that weighs a single column, such as ``conditionstim`` in
- ``~ condition`` or a covariate, it stops with an error. Here the levels a
- contrast compares are found from the design's coding, so every
- parametrisation of the same comparison gives the same table, and a contrast
- that compares no levels is tested without descriptive columns.
- - A group without cells in any sample is left out. speckle keeps an unused level
- of a factor of cell labels, tests it, and with the logit transform its half
- cells change the transformed proportions of every other group (found by
- probing).
- - A sample without cells is left out. speckle keeps an unused level of a factor
- of sample labels: with the arcsine transform its proportions are NaN and limma
- leaves it out too, but with the logit transform every group of that sample has
- the proportion 1 / (number of groups), which enters the fit as data (found by
- probing).
- - Data that the design fits exactly in every group, up to rounding, as when the
- samples of each condition are copies of one another, raise an error: no
- variance can be estimated. speckle returns statistics of the rounding errors
- (t-statistics near 1e15, for example) or NaN.
-
-Accuracy:
- The transforms are computed as log((x_ij + 0.5) / (N_j - x_ij + 0.5 (I - 1)))
- for I groups and as arctan2(sqrt(x_ij), sqrt(N_j - x_ij)): the formulas above,
- without the cancellation in 1 - q_ij and the loss of digits of arcsin near 1.
- They agree with speckle's values to rounding, and stay within a few units in
- the last place where those lose digits, for a group that holds nearly all the
- cells of a sample. Cell numbers are summed exactly, in float64 up to 2**53.
+Matching speckle:
+ The results match speckle 1.6.0's ``propeller``, ``propeller.ttest`` and
+ ``propeller.anova``, with limma 3.62.1. Running speckle settled the conventions
+ that its documentation leaves open, such as the definitions of its descriptive
+ columns, which :class:`~cytepool.CompositionResult` gives. With two groups of
+ samples, ``propeller`` tests the first level against the second, and with more,
+ the F-test of their factor. The transforms equal speckle's formulas, computed
+ without the cancellation in 1 - q and the loss of digits of arcsin near 1, and
+ cell numbers are summed exactly.
References:
Phipson B, Sim CB, Porrello ER, Hewitt AW, Powell J, Oshlack A (2022). propeller:
@@ -175,16 +110,26 @@ class CompositionResult:
- ``group_id``;
- ``baseline_proportion``: the group's share of all the cells of the samples
used (speckle's ``BaselineProp``);
- - for a contrast that compares the levels of a factor (see "Descriptive columns" in
- the description of :func:`propeller`), ``mean_proportion_`` for each level
- it weighs, in level order: the mean proportion of the group over the samples at
- that level (``PropMean``), and ``proportion_ratio``: the product of these means
- raised to the weights of their levels, such as m_stim / m_ctrl (``PropRatio``);
+ - for a contrast that compares the levels of a factor, ``mean_proportion_``
+ for each level it weighs, in level order: the mean proportion of the group over
+ the samples at that level, taken over the samples as they are, whatever the
+ other terms of the design (``PropMean``); and ``proportion_ratio``: the product
+ of these means m_l raised to the weights d_l of their levels, prod_l m_l^d_l,
+ such as m_stim / m_ctrl, or sqrt(m_A m_B) / m_C for ``(groupA + groupB)/2 -
+ groupC`` (``PropRatio``);
- ``t`` and ``p_value``: the moderated t-statistic of the contrast on the
transformed proportions and its two-sided p-value (``Tstatistic``,
``P.Value``);
- ``adjusted_p_value``: Benjamini-Hochberg over the groups (``FDR``).
+ A contrast compares the levels of a factor when it is a combination, with
+ weights d_l that sum to zero, of the differences between the levels of a factor
+ that enters the formula as a main effect only: ``conditionstim`` in ``~ donor +
+ condition``, ``conditionstim - conditionctrl`` in ``~ 0 + condition``, or
+ ``(groupA + groupB)/2 - groupC``, but not ``groupA + groupB``. The table of
+ another contrast, such as the slope of a covariate, has neither mean proportions
+ nor a ratio.
+
The table of an F-test (``tables`` and ``table``, by the name of its factor) has
``group_id``, ``baseline_proportion``, ``mean_proportion_`` for every level
of the factor, ``f_statistic``, ``p_value`` and ``adjusted_p_value`` (speckle's
@@ -345,8 +290,8 @@ def provenance(self) -> dict[str, Any]:
levels and the contrasts between them), or null; the samples used, in the
order fitted, and those left out with the reason; and the groups tested
and those left out with the reason. The fingerprint, and the digest of
- ``n_cells``, say whether a cube at hand holds the cell numbers behind the
- tables (``Cube.verify_fingerprint``).
+ ``n_cells``, say whether a cube at hand holds the cell numbers that the
+ tables were computed from (``Cube.verify_fingerprint``).
"""
record: dict[str, Any] = json.loads(self._record)
return record
@@ -414,8 +359,11 @@ def propeller(
with the design, and its contrasts are tested by moderated t-statistics, and the
levels of a factor by a moderated F-statistic, after empirical Bayes moderation
of the residual variances across groups: speckle's ``propeller``,
- ``propeller.ttest`` and ``propeller.anova``. The module documentation describes
- the steps, the descriptive columns and the deliberate differences from speckle.
+ ``propeller.ttest`` and ``propeller.anova``.
+ https://docs.nygen.io/CytePool/statistics/composition#propeller-steps gives the
+ steps; :class:`CompositionResult` defines the descriptive columns, and
+ https://docs.nygen.io/CytePool/statistics/composition#composition-limits states
+ the differences from speckle.
As in speckle, ``propeller(clusters, sample, group)`` with two groups tests the
first level against the second: ``Design(samples, "~ 0 + group",
@@ -424,23 +372,28 @@ def propeller(
followed by covariates is a design such as ``~ 0 + group + age`` with a
contrast between groups, and ``propeller.anova`` the F-test of ``group`` in
it. The same comparisons in another parametrisation, such as ``~ group + age``
- with the contrast ``groupB``, give the same tables.
+ with the contrast ``groupB``, give the same tables. As in limma, the
+ t-statistics have ``df_residual + df_prior`` degrees of freedom, capped at the
+ residual degrees of freedom of all the groups together, and the F-statistics
+ ``df_residual + df_prior`` denominator degrees of freedom, not capped.
Provenance:
The result records the cube's fingerprint, the digests it is built from and its
own provenance, as :func:`~cytepool.limma_voom` does, although the test reads
only the cell numbers. For a cube opened with :func:`~cytepool.read_cube` this
reads nothing, as the file stores the digests; a cube in memory hashes its
- layers once, at about 1 s per GB of values where the processor has SHA
- instructions, and keeps the digests.
+ layers once (see "Cost" in :meth:`Cube.fingerprint
+ `), and keeps the digests.
Args:
- data: The number of cells of every sample in every group: a
- :class:`~cytepool.Cube` that records them (``n_cells``), or a table with one
- row per sample, a text column ``sample_id`` and one column of cell numbers
- (integers) per group, named by its group id: a ``Table``, a mapping of
- columns, or a pandas DataFrame. A DataFrame without a ``sample_id`` column
- is read with its index as the sample ids, as
+ data: The number of cells of every sample in every group, as whole numbers
+ (proportions with assumed totals, as speckle's ``convertDataToList``
+ takes them, are not read): a :class:`~cytepool.Cube` that records them
+ (``n_cells``), or a table with one row per sample, a text column
+ ``sample_id`` and one column of cell numbers (integers) per group, named
+ by its group id: a ``Table``, a mapping of columns, or a pandas
+ DataFrame. A DataFrame without a ``sample_id`` column is read with its
+ index as the sample ids, as
``pandas.crosstab(cells["sample_id"], cells["cell_type"])`` gives it.
design: A :class:`~cytepool.Design` whose sample table has a
``sample_id`` column with sample ids of the data; its rows may be some
@@ -459,7 +412,9 @@ def propeller(
every count, or ``"asin"`` for the arcsine square root.
robust: Whether to estimate the prior of the moderation robustly, as
speckle does by default (``eBayes(robust = TRUE)``), which protects
- against groups whose variance is unusually large.
+ against groups whose variance is unusually large. With fewer than 10
+ groups the robust estimate is the ordinary one, and two groups are not
+ moderated, as in limma.
trend: Whether the prior variance follows the mean transformed proportion
of the groups (``eBayes(trend = TRUE)``).
@@ -771,6 +726,11 @@ def _fit(cells: np.ndarray, matrix: np.ndarray, design: Design, transform: str)
"a sample has 2**53 cells or more, more than float64 counts exactly; "
"propeller needs exact proportions"
)
+ # Samples and groups without cells are left out. speckle keeps an unused level of
+ # a factor of labels: it tests an unused group, whose half cells change the logits
+ # of the others, and gives an unused sample the logit of 1 / (number of groups)
+ # in every group, or NaN with the arcsine, which limma leaves out (found by
+ # probing).
used = totals > 0.0
group_cells = cells[used].sum(axis=0)
tested = group_cells > 0
@@ -996,6 +956,11 @@ def _descriptive(
(only the levels it weighs are shown, with the ratio), the factor of an F-test
with weights None (every level is shown), or None.
"""
+ # speckle's BaselineProp, PropMean and PropRatio (found by probing speckle 1.6.0).
+ # Its PropMean are the coefficients of the proportions regressed on the columns
+ # that a contrast weighs, mean proportions only where those columns indicate
+ # groups of samples (found by probing); here they are always the means of the
+ # levels compared.
columns = {
"group_id": group_ids[fit.tested],
"baseline_proportion": fit.cells.sum(axis=0) / float(fit.cells.sum()),
diff --git a/cytepool/cube/__init__.py b/cytepool/cube/__init__.py
index 242f1ec..1aa3e88 100644
--- a/cytepool/cube/__init__.py
+++ b/cytepool/cube/__init__.py
@@ -2,8 +2,9 @@
:class:`cytepool.Cube`, :class:`cytepool.CubeCollection`, :func:`cytepool.from_cells`
and :func:`cytepool.from_bulk` are documented at the top level; the modules
-:mod:`cytepool.cube.fingerprints` and :mod:`cytepool.cube.provenance` specify the
-fingerprints and provenance records of cube files.
+:mod:`cytepool.cube.fingerprints` and :mod:`cytepool.cube.provenance` compute the
+fingerprints and provenance records of cubes, which
+https://docs.nygen.io/CytePool/reference/cube-files specifies.
"""
from .collection import CubeCollection as CubeCollection
diff --git a/cytepool/cube/collection.py b/cytepool/cube/collection.py
index f00d7ab..65cd475 100644
--- a/cytepool/cube/collection.py
+++ b/cytepool/cube/collection.py
@@ -1,9 +1,10 @@
"""Several studies' cubes, aligned by sample, gene and group: ``CubeCollection``.
-Studies of one question, such as cohorts of a disease, are compared or combined only
-once their samples, genes and cell types are aligned, and that is where errors enter:
-donor ids that repeat across studies, genes that a study did not measure read as
-zeros. ``CubeCollection`` aligns them and says what each study measured and has.
+:class:`~cytepool.CubeCollection` aligns the cubes of several studies by sample, gene
+and group (cell type), says what each study measured and has, and keeps each cube as
+it was given, for an analysis of each study on the collection's axes or a joint model
+of the studies stacked into one cube.
+https://docs.nygen.io/CytePool/statistics/aligning-studies explains why and how.
References:
Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
@@ -58,12 +59,12 @@
class CubeCollection:
"""Several studies' cubes, aligned by sample, gene and group.
- Most genes found in one study are not found in the next (Nakatsuka et al.
- 2025), and the studies are compared or combined only once their samples, genes
- and groups (cell types) are aligned. A collection aligns the cubes of several
- named studies and keeps each cube as it was given (``cubes``): it reads no layer,
- and each analysis of a study reads that study's cube. It is immutable:
- ``select`` returns a new collection.
+ Studies are compared or combined only once their samples, genes and groups (cell
+ types) are aligned. A collection aligns the cubes of several named studies and
+ keeps each cube as it was given (``cubes``): it reads no layer, and each analysis
+ of a study reads that study's cube. It is immutable: ``select`` returns a new
+ collection. https://docs.nygen.io/CytePool/statistics/aligning-studies explains
+ the alignment.
Samples:
Sample ids mean something only inside their study, so the collection names
@@ -83,7 +84,9 @@ class CubeCollection:
Genes:
Features are aligned by ``feature_id``, which must hold ids of one kind in
- every study, such as Ensembl gene ids. A study measured a feature when its
+ every study, such as Ensembl gene ids, compared as they are: ids are not
+ converted, so Ensembl ids with versions (``ENSG00000141510.17``) must have
+ them removed first. A study measured a feature when its
cube has it and does not mark it False in the feature column ``measured``:
tools that give every study the same genes fill those a study did not
measure with zeros, and ``measured`` tells them apart. The collection's
@@ -104,6 +107,8 @@ class CubeCollection:
``{"CL:0001054": "CD14 monocytes"}`` for groups aligned by their Cell Ontology
ids, and groups of one study with the same aligned id are merged: their
layers and cell numbers are summed, exactly, as ``Cube.regroup`` sums them.
+ Groups are aligned by equal values only: give merges up the Cell Ontology,
+ such as of CD4 T cell subsets into CD4 T cells, in ``group_mapping``.
``group_members`` lists the groups of each study under their aligned ids.
``groups`` has ``group_id``, the aligned id or its name, and every other
column of any study's group table: a group keeps a column's value where the
@@ -136,13 +141,6 @@ class CubeCollection:
their aligned ids, and each selection since. A stacked cube records the
studies and the alignment in its provenance (``cytepool.cube.provenance``).
- Not done:
- Groups are aligned by equal values only: rolling cell types up to a common
- ancestor in the Cell Ontology needs the ontology as a resource, so give such
- merges in ``group_mapping``. Feature ids are not converted, so Ensembl ids
- with versions (``ENSG00000141510.17``) must have them removed first.
- ``stack`` gives a cube in memory.
-
Args:
cubes: Study name to cube, at least one, in the order the collection keeps.
Study names are non-empty and hold no ``::``.
@@ -416,11 +414,10 @@ def provenance(self) -> dict[str, Any]:
CytePool that aligned them, ``align_groups_by``, ``group_mapping``,
``group_names`` (when given), ``within_study`` and ``groups``, each study's
groups under their aligned ids when the collection was made; and
- ``operations``: each selection since,
- with the studies, groups and features it kept (null for an axis given as
- None). The fingerprints cost what ``Cube.fingerprint`` costs: nothing for a
- cube file, which stores them, and a pass over the values of a cube in memory
- the first time, at about 1 s per GB.
+ ``operations``: each selection since, with the studies, groups and features
+ it kept (null for an axis given as None). The fingerprints cost what
+ ``Cube.fingerprint`` costs: nothing for a cube file, which stores them, and a
+ pass over the values of a cube in memory the first time.
"""
alignment = json.loads(self._settings)
return {
diff --git a/cytepool/cube/cube.py b/cytepool/cube/cube.py
index d5bc43f..1d8b7ab 100644
--- a/cytepool/cube/cube.py
+++ b/cytepool/cube/cube.py
@@ -66,6 +66,7 @@ class Cube:
for every feature, sums over those cells. Every layer is a sum, so merging groups
or pooling samples is an exact sum and never needs the cells again. A cube is
immutable: methods that change something return a new cube.
+ https://docs.nygen.io/CytePool/statistics/cubes explains cubes.
Layout:
Layers have shape ``(n_groups, n_samples, n_features)``, so one group is a
@@ -104,10 +105,11 @@ class Cube:
"library_size", "target_sum": 10000.0, "log": "natural", "pseudocount":
1.0}``. Required with ``log_sum``, ``log_sum_sq`` or ``linear_sum``.
provenance: A JSON-serialisable mapping that says where the data came from
- and how the cube was made (``cytepool.cube.provenance`` documents the
- keys CytePool writes). Operations that derive a cube append a record of
- the operation, its input and what it kept or pooled to the list under
- ``"operations"``.
+ and how the cube was made
+ (https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys
+ specifies the keys CytePool writes). Operations that derive a cube append
+ a record of the operation, its input and what it kept or pooled to the
+ list under ``"operations"``.
copy: If False, a layer given as a C-contiguous numpy array with an
accepted native dtype is stored without a copy and made read-only in
place, so it must not be changed through other references afterwards.
@@ -175,12 +177,13 @@ class Cube:
reorders samples or features), also hashes the group, to check it against
its digest: stored with the cube file, or kept from the first such read. A
read of a whole group of a layer derived from one hashes that group too, to
- keep its digest for ``fingerprint()``. SHA-256 runs at about 2 GB/s on
- processors with SHA instructions and 0.5 GB/s without, which adds about a
- fifth to the time of reading a gzip-compressed group on the former. A
- selection of some samples or features reads part of a group, which cannot
- be checked on its own; ``verify_fingerprint()`` reads the groups it comes
- from whole.
+ keep its digest for ``fingerprint()``. A selection of some samples or
+ features reads part of a group, which cannot be checked on its own;
+ ``verify_fingerprint()`` reads the groups it comes from whole.
+
+ Hashing takes about 0.4 s per 100 million values, each hashed as 8
+ bytes, on a laptop processor with SHA instructions (an Intel Core Ultra 7
+ 265U), and about 1.3 s without them.
Fingerprint and provenance:
``fingerprint()`` identifies the cube's values, tables and cell numbers
@@ -188,10 +191,9 @@ class Cube:
source record, the function that built it with its settings, and each
operation that derived it, with the fingerprint of its input.
``history()`` reads the record back in plain terms;
- ``cytepool.cube.fingerprints`` and ``cytepool.cube.provenance`` document
- both. To record the fingerprint of its input, the first operation applied
- to a cube in memory hashes its layers, once, at about 1 s per GB of values
- on processors with SHA instructions.
+ https://docs.nygen.io/CytePool/reference/cube-files specifies both. To
+ record the fingerprint of its input, the first operation applied to a cube
+ in memory hashes its layers, once, at the cost given under "Lazy layers".
"""
__slots__ = (
@@ -240,7 +242,7 @@ def __init__(
reject_masked_table(value, argument, id_column)
# A flag read by its truth value would read a masked one through its mask.
if not isinstance(copy, bool | np.bool_):
- raise TypeError(f"copy must be a bool, got {type(copy).__name__}")
+ raise TypeError(f"copy must be True or False, got {type(copy).__name__}")
sample_table, sample_index = checked_axis_table(samples, "samples", "sample_id")
group_table, group_index = checked_axis_table(groups, "groups", "group_id")
feature_table, feature_index = checked_axis_table(
@@ -358,7 +360,11 @@ def normalization(self) -> dict[str, Any] | None:
@property
def provenance(self) -> dict[str, Any]:
- """A copy of the provenance mapping (see ``cytepool.cube.provenance``)."""
+ """A copy of the provenance mapping.
+
+ https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys specifies
+ the keys that CytePool writes.
+ """
return provenance_from_texts(self._provenance)
def __repr__(self) -> str:
@@ -565,8 +571,8 @@ def fingerprint(self) -> str:
layer. It leaves out the provenance, so cubes with the same values have the
same fingerprint however they were made, and a result can name the cube it
was computed from. It is ``"sha256:"`` followed by 64 hexadecimal digits;
- ``cytepool.cube.fingerprints`` documents how it is computed, so another
- program can compute it too.
+ https://docs.nygen.io/CytePool/reference/cube-files#fingerprint-encoding
+ specifies how it is computed, so another program can compute it too.
Cost:
The fingerprint is built from a digest of each table, of ``n_cells``,
@@ -590,6 +596,10 @@ def fingerprint(self) -> str:
- A lazy layer of another array-like, which can change, is read on every
call.
+ Hashing takes about 0.4 s per 100 million values, each hashed as 8
+ bytes, on a laptop processor with SHA instructions (an Intel Core Ultra 7
+ 265U), and about 1.3 s without them.
+
Raises:
ValueError: If a lazy layer gives a value that breaks a constraint, or a
group of a cube file does not match its digest.
@@ -609,11 +619,12 @@ def digests(self) -> dict[str, Any]:
at the same cost.
Returns:
- A new JSON-serialisable dict in the form cube files store, which
- ``cytepool.cube.fingerprints`` documents. It holds the fingerprint
- version and the cube's shape; the digest of each table, of ``n_cells``
- and of the normalization (None without them); for each layer, its type
- and the digest of each group, by position; and the fingerprint.
+ A new JSON-serialisable dict in the form cube files store
+ (https://docs.nygen.io/CytePool/reference/cube-files#digest-record). It
+ holds the fingerprint version and the cube's shape; the digest of each
+ table, of ``n_cells`` and of the normalization (None without them); for
+ each layer, its type and the digest of each group, by position; and the
+ fingerprint.
Raises:
ValueError, OverflowError: As ``fingerprint()``.
@@ -678,12 +689,12 @@ def verify_fingerprint(self) -> str:
def history(self) -> str:
"""Describe how the cube was made, in plain terms, for printing.
- The text is ``describe_provenance(cube.provenance)`` (see
- ``cytepool.cube.provenance``): the caller's source record, how the cube was
- built, and each operation in order, with the fingerprint of the cube it was
- applied to. A last line gives the cube's own fingerprint when it is known
- without reading lazy layers (see ``fingerprint()``). Lines are at most 80
- characters, a longer item continuing on the next line.
+ The text is ``describe_provenance(cube.provenance)``: the caller's source
+ record, how the cube was built, and each operation in order, with the
+ fingerprint of the cube it was applied to. A last line gives the cube's own
+ fingerprint when it is known without reading lazy layers (see
+ ``fingerprint()``). Lines are at most 80 characters, a longer item continuing
+ on the next line.
"""
lines = [describe_provenance(self.provenance)]
fingerprint = self._known_fingerprint()
@@ -1006,7 +1017,7 @@ def collapse_samples(self, by: str | Sequence[str] | None = None) -> "Cube":
each group's cells together, as :func:`~cytepool.plotting.plot_dotplot`
pools them. Sample columns that are constant within every pooled set are
kept and the others are dropped. The column ``n_pooled_samples`` holds the
- number of original samples behind each new sample and comes last; if it
+ number of original samples that each new sample pools, and comes last; if it
already exists, it keeps its place and is summed, so collapsing twice counts
the original samples. Integer layers of the result are int64 when samples
are pooled, and keep their dtype when every new sample is one sample. The
@@ -1432,7 +1443,7 @@ def _group_position(self, group: object) -> int:
return position
if isinstance(group, bool | np.bool_):
raise TypeError(
- "group must be a group id (str) or a position (int), not bool"
+ "group must be a group id (str) or a position (int), got bool"
)
try:
position = operator.index(group) # type: ignore[arg-type]
diff --git a/cytepool/cube/fingerprints.py b/cytepool/cube/fingerprints.py
index a244b6d..1014f46 100644
--- a/cytepool/cube/fingerprints.py
+++ b/cytepool/cube/fingerprints.py
@@ -1,107 +1,15 @@
"""Content fingerprints of cubes: what they cover and how they are encoded.
-A cube's fingerprint identifies the values an analysis reads: the sample, group and
-feature tables (ids included), the cells pooled into each entry (``n_cells``), the
-normalization that defines the normalized layers, and every layer. It leaves out the
-provenance, which records how the cube was made, so two cubes with the same values
-have the same fingerprint however they were made, and a result can name the cube it
-was computed from by its fingerprint. ``Cube.fingerprint()`` returns it and
-``Cube.digests()`` the digests it is built from.
-
-Parts and digests:
- The fingerprint is built from the digests of the cube's parts: each table, the
- cell numbers, the normalization, and each group of each layer. ``write_cube``
- computes the digests while it writes the cube and stores them with the cube file,
- so the fingerprint of a saved cube needs no pass over its data. ``read_cube``
- checks the tables, cell numbers and normalization against their digests when it
- opens the file, and every read that covers a whole group of a layer, in any
- order, against the group's digest. One group can thus be checked without
- reading the others, and an analysis that reads whole groups checks exactly what
- it read. (A read of some samples or features of a group cannot be checked on
- its own; ``Cube.verify_fingerprint()`` reads such groups whole.) A single digest
- of the whole cube would need every layer read to check any of it.
-
-Encoding, version 1 (``FINGERPRINT_VERSION``):
- A digest is the SHA-256 hash (National Institute of Standards and Technology
- 2015) of a byte string, written as ``sha256:`` and 64 lowercase hexadecimal
- digits. Numbers in byte strings are little endian: ``u64`` and ``i64`` are
- unsigned and signed 8-byte integers. Text is UTF-8 (Yergeau 2003), written as
- its length in bytes (``u64``) followed by its bytes; a lone surrogate, which a
- Python string can hold but a cube file cannot, is written in its 3-byte form, as
- Python's ``surrogatepass`` error handler writes it. Strings are taken as they
- are, without Unicode normalization.
-
- Tables (samples, groups and features): the number of rows and of columns
- (``u64`` each), then for each column, in table order, its name (text), its type
- (text) and its values:
-
- - text, type ``text``: the length in bytes of each value (``i64``, -1 for a
- missing value), then the bytes of the values present, one after another;
- - ``bool``: one byte per value, 0 or 1;
- - ``int8`` to ``int64`` and ``uint8`` to ``uint64``: the values in that type;
- - ``float16``, ``float32`` and ``float64``: the values in that type, the binary
- formats of IEEE 754 (IEEE 2019), with -0.0 written as 0.0 and every NaN as the
- quiet NaN (``0x7E00``, ``0x7FC00000`` or ``0x7FF8000000000000``);
- - ``complex64`` and ``complex128``: the real and imaginary parts of each value,
- as floats of half the size written as above; a value with a NaN part is
- missing, and both its parts are written as the quiet NaN;
- - datetimes and timedeltas, of type ``datetime64[unit]`` or ``timedelta64[unit]``
- (numpy's name, such as ``datetime64[ms]``): the values as ``i64``, NaT being
- -2**63.
-
- The type is numpy's name for the column's dtype. These are the column types
- cube files store, and the only ones a cube's tables accept. Two tables have the
- same digest exactly when ``Table.equals`` finds them equal: the same names in the
- same order, the same types, and equal values, with missing values equal.
-
- Cell numbers: the number of samples and of groups (``u64`` each), then
- ``n_cells`` as ``i64``, sample by sample.
-
- Normalization: the mapping as a JSON value (Bray 2017), each value written as a
- tag byte and a payload: ``n`` for null; ``f`` and ``t`` for false and true;
- ``i`` and the decimal digits of the number as text (with a leading ``-`` when
- negative) for a number with a whole value, such as 10000 or 10000.0; ``d`` and
- its 8 bytes as a float64 for any other number; ``s`` and the text for a string;
- ``a``, the number of items (``u64``) and the items for an array; ``o``, the
- number of members (``u64``) and, in the order of the UTF-8 bytes of their keys,
- each key (text) and value for an object. A number is thus taken by its value,
- however a JSON writer spells it.
-
- Layers: one digest for each group of each layer, of the group's samples x
- features block: the number of samples and of features (``u64`` each), then the
- values sample by sample, as ``i64`` for an integer layer and as float64, with
- -0.0 written as 0.0, for a float layer. An integer layer holds the same numbers
- whatever the width of its dtype (``Cube`` accepts integers from 0 to 2**63 - 1,
- and merging or pooling makes them int64), so a uint32 group and an int64 group
- with the same values have the same digest.
-
- Manifest: UTF-8 text with one line per part, each ending with a line feed
- (``\\n``)::
-
- cytepool cube fingerprint 1
- shape
- samples
- groups
- features
- n_cells
- normalization
- layer
-
- with a ``layer`` line for each group of each layer: layers in the order of
- ``LAYER_NAMES`` (counts, detected, log_sum, log_sum_sq, linear_sum), and groups in
- the cube's order, counted from 0. The fingerprint is the digest of the manifest.
-
-Digest records:
- ``Cube.digests()`` returns, and a cube file stores as the root attribute
- ``digests``, a JSON object with everything the manifest lists (the order of its
- members does not matter)::
-
- {"version": 1, "shape": [, , ],
- "samples": , "groups": , "features": ,
- "n_cells": , "normalization": ,
- "layers": {: {"type": "int64" or "float64",
- "groups": []}},
- "fingerprint": }
+``Cube.fingerprint()`` returns a cube's fingerprint, a SHA-256 digest (National
+Institute of Standards and Technology 2015) of the values an analysis reads, and
+``Cube.digests()`` the digests it is built from: those of each table, of the cell
+numbers, of the normalization and of each group of each layer.
+https://docs.nygen.io/CytePool/statistics/cubes#what-a-cube-records explains what a
+fingerprint covers and why, and
+https://docs.nygen.io/CytePool/reference/cube-files#fingerprint-encoding specifies the
+encoding, version ``FINGERPRINT_VERSION``, for other programs: text as UTF-8 (Yergeau
+2003), the normalization as a JSON value (Bray 2017) and floats in the binary formats
+of IEEE 754 (IEEE 2019).
References:
National Institute of Standards and Technology (2015). Secure Hash Standard (SHS).
@@ -158,7 +66,9 @@
def _table_digest(table: Table) -> str:
- """Return the digest of a table, as the module documentation describes."""
+ """Return the digest of a table, encoded as
+ https://docs.nygen.io/CytePool/reference/cube-files#fingerprint-encoding specifies.
+ """
hasher = hashlib.sha256(_u64(len(table)) + _u64(len(table.columns)))
for name in table.columns:
column = table[name]
diff --git a/cytepool/cube/pool.py b/cytepool/cube/pool.py
index 5e0338f..c5cd523 100644
--- a/cytepool/cube/pool.py
+++ b/cytepool/cube/pool.py
@@ -1,13 +1,17 @@
"""Building cubes from a cell-level count matrix or from bulk counts.
+:func:`~cytepool.from_cells` pools a cells x features count matrix into a cube, and
+:func:`~cytepool.from_bulk` wraps a samples x features count matrix as a cube with a
+single group; https://docs.nygen.io/CytePool/statistics/cubes#cube-layers defines the
+layers they compute.
+
``from_cells`` pools the cells of every sample and group in one pass over the
stored counts. Cells are sorted by sample and group, and each thread pools whole
sample x group entries into rows of the result that no other thread writes, so no
atomic updates are needed. The entries are shared among the threads by the
longest-processing-time rule (Graham 1969); repeated entries of a row of a sparse
matrix are summed through a sparse-set index (Briggs and Torczon 1993), and log1p is
-computed as Goldberg (1991, Theorem 4) shows. ``from_bulk`` wraps a samples x features
-count matrix as a cube with a single group.
+computed as Goldberg (1991, Theorem 4) shows.
References:
Graham RL (1969). Bounds on multiprocessing timing anomalies. SIAM Journal on
@@ -154,7 +158,8 @@ def from_cells(
without a normalized layer. Its provenance holds ``source`` and the
settings: the number of cells, the layers, ``target_sum``, where library
sizes came from, how samples and groups were ordered and where feature ids
- came from (``cytepool.cube.provenance`` documents the record).
+ came from (https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys
+ specifies the record).
Raises:
TypeError: If ``counts`` is neither a numeric array nor a CSR matrix (a CSC
@@ -346,7 +351,9 @@ def from_bulk(
to 2**32 - 1, uint64 for larger whole numbers (up to 2**63 - 1), and
float64 when a count is not a whole number (or, for float input, is at
least 2**53). Its provenance holds ``source`` and where the sample and
- feature ids came from (``cytepool.cube.provenance`` documents the record).
+ feature ids came from
+ (https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys
+ specifies the record).
Raises:
TypeError: If ``counts`` is not numeric, ``source`` is not a mapping or
diff --git a/cytepool/cube/provenance.py b/cytepool/cube/provenance.py
index 90dedcf..9b69269 100644
--- a/cytepool/cube/provenance.py
+++ b/cytepool/cube/provenance.py
@@ -1,117 +1,24 @@
"""Provenance records: how a cube was built and what it pooled.
-A result traced back to its data passes through the cube it was computed from. The
-cube's fingerprint (``cytepool.cube.fingerprints``) names its values, and its
-provenance, ``Cube.provenance``, says how it was made: a JSON object whose keys
-below are written by the functions and methods that build and derive cubes. Any
-other key that a caller gives (``Cube(provenance=...)``) is kept as it is.
-``Cube.history()`` and ``describe_provenance`` read a record back in plain terms.
-
-``source``:
- The caller's record of where the data came from, given to ``from_cells``,
- ``from_bulk``, ``from_anndata`` or ``from_mudata`` (``cytepool.io.anndata``) as
- ``source``: for example a dataset identity, references to the
- exporter's artifacts (such as frozen cell memberships, which stay in the
- exporter's files) and the exporter's settings. CytePool stores it as JSON, as
- it was given, does not interpret it, and reports its references as unresolved:
- following them needs the tool that wrote them. Null when no record was given.
-
-``construction``:
- How the cube was built: ``function`` (``"from_cells"``, ``"from_bulk"``,
- ``"from_anndata"``, ``"from_mudata"`` or ``"CubeCollection.stack"``),
- ``cytepool_version``, and the function's settings. ``from_cells`` records
-
- - ``n_cells``: the number of cells pooled (the cells of each sample and group
- are the cube's ``n_cells``);
- - ``layers``: the layers computed;
- - ``target_sum``: the total that normalization scales every cell to;
- - ``library_sizes``: ``"cell_totals"`` (the total count of each cell) or
- ``"given"`` (the ``library_sizes`` argument), as in the normalization;
- - ``samples`` and ``groups``: ``{"order": ..., "labels": ...}``, where
- ``order`` is ``"sample_table"`` or ``"group_table"`` (the rows of the sample
- or group table given) or ``"sorted_labels"`` (the distinct labels, text
- sorted by code point and integers numerically), and ``labels`` is ``"text"``
- or ``"integers"`` (integer labels became their decimal text);
- - ``features``: where the feature ids came from: ``"table"``, ``"ids"`` (a
- sequence), ``"data_frame_columns"`` or ``"positions"`` (``feature_0``,
- ``feature_1``, ...).
-
- ``from_bulk`` records ``samples`` and ``features``, where their ids came from:
- ``"table"``, ``"ids"``, ``"data_frame_index"`` (samples) or
- ``"data_frame_columns"`` (features), or ``"positions"``.
-
- ``from_anndata`` and ``from_mudata``, reading data that CytePool did not
- export, record
-
- - ``layout``: the layout that CytePool recognised in the data, which chose the
- sources that ``layers`` did not name: ``"scanpy.get.aggregate"``,
- ``"decoupler.pp.pseudobulk"`` or ``"liana.multi.adata_to_views"``, or
- ``"given"`` for data of no layout it recognises;
- - ``rows``: the number of profiles read, and for ``from_mudata``
- ``modalities``, the modality names;
- - ``layers``: each cube layer with its source, ``"X"`` or a layer name;
- - ``n_cells``: where the cell numbers came from: an ``obs`` column, ``"table"``
- (a table of samples x groups), ``"uns['psbulk_stats']"`` (LIANA's
- statistics) or null (none);
- - ``samples`` and ``groups``, as ``from_cells`` records them;
- - ``obs_columns``: the metadata read into the tables, null when the tables were
- given. ``from_anndata`` gives the ``obs`` columns placed in the sample table
- and in the group table, and those left out, as ``{"samples": [...],
- "groups": [...], "left_out": [...]}``, and records the label columns as
- ``sample_column`` and ``group_column``; ``from_mudata`` lists the columns of
- the MuData's ``obs`` placed in the sample table.
-
- ``CubeCollection.stack`` (``cytepool.cube.collection``), which stacks the cubes
- of several studies, records
-
- - ``studies``: for each study, in the order stacked, the cube that the
- collection was given, by its ``fingerprint``, with that cube's own
- ``provenance``, and ``groups``: each group stacked, by its aligned id, with
- the ids of the study's groups that it sums;
- - ``align_groups_by``: the group column whose values aligned the groups;
- - ``within_study``: the sample columns whose values were written
- ``study::value``, as the sample ids are;
- - ``layers``: the layers stacked.
-
-``operations``:
- The operations that derived the cube, oldest first. Each is an object with
- ``operation``, the method, and ``input``, the fingerprint of the cube it was
- applied to, so that a chain of operations can be checked link by link: apply
- each operation to the cube with its input fingerprint, and compare the result's
- fingerprint with the next record's input, or with the cube's own fingerprint
- after the last.
-
- ``input`` is recorded when it is known without reading lazy layer values. That
- is so for a cube in memory: the first operation applied to it hashes its layers,
- once, at about 1 s per GB of values where the processor has SHA instructions.
- It is so for a cube read from a cube file, which stores its digests, and for a
- lazy cube derived from one when every digest it needs is known: kept unchanged
- from the file, or kept from whole reads of its groups, as ``fingerprint()``
- makes them. It is null for a lazy cube derived from another whose groups were
- not read whole, such as a cube file regrouped and then collapsed: call
- ``fingerprint()`` on the intermediate cube before deriving from it to record
- its fingerprint, or check the operations after the last recorded fingerprint
- together. It is always null for a cube whose lazy layers are other array-likes
- (a memmap or an HDF5 dataset), which can change: their fingerprint is computed
- anew on every call and never kept.
-
- - ``select``: ``samples``, ``groups`` and ``features``, the ids kept, in the
- order of the result, or null for an axis that was given as None.
- - ``regroup``: ``membership``, the id of each merged or renamed group with the
- ids of the groups it sums, in the input's order. The groups not listed are
- kept as they are.
- - ``collapse_samples``: ``by``, the sample column, a list of them, or null when
- every sample was pooled into one, and ``membership``, the id of each new
- sample with the ids of the samples it sums, in the input's order.
- - ``with_columns``: ``samples``, ``groups`` and ``features``, each with the
- names of the columns ``added`` and those ``replaced`` on that axis, or null
- for an axis that was given as None. The values are those of the cube's
- tables, which its fingerprint covers.
- - ``from_anndata`` and ``from_mudata``: the import of an AnnData or MuData that
- CytePool exported, whose provenance the cube continues. ``input`` is the
- fingerprint that the export stored, which the cube's own fingerprint equals
- unless the data were changed in between, and ``source`` the caller's record,
- or null.
+A cube's provenance, ``Cube.provenance``, is a JSON object with ``source``, the
+caller's own record of the data; ``construction``, how the cube was built; and
+``operations``, the operations that derived it, oldest first, each with ``input``, the
+fingerprint of the cube it was applied to where that is known without reading lazy
+layers, and null otherwise. Any other key that a caller gives
+(``Cube(provenance=...)``) is kept as it is. ``Cube.history()`` and
+``describe_provenance`` read a record back in plain terms.
+https://docs.nygen.io/CytePool/statistics/cubes#what-a-cube-records explains the
+record, and https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys
+specifies the keys that CytePool writes.
+
+``input`` is recorded when it is known without reading lazy layer values: for a cube
+in memory, whose first operation hashes its layers once; for a cube read from a cube
+file, which stores its digests; and for a lazy cube derived from one when every digest
+it needs is known. It is null for a lazy cube derived from another whose groups were
+not read whole, such as a cube file regrouped and then collapsed: call
+``fingerprint()`` on the intermediate cube before deriving from it to record its
+fingerprint. It is always null for a cube whose lazy layers are other array-likes (a
+memmap or an HDF5 dataset), which can change.
"""
import json
@@ -155,11 +62,12 @@ def describe_provenance(provenance: Mapping[str, Any]) -> str:
The source record comes first, then how the cube was built, then each operation
in order with what it kept, merged or pooled; long lists are shortened. Entries
- the schema in this module does not describe, such as those of cubes saved by
- earlier versions, are shown as JSON. The references that CytePool cannot follow
- on its own are reported as unresolved: those of the source record, and the
- fingerprints of the cubes that operations were applied to, which name those
- cubes without saying where they are.
+ that the specification of the record does not describe
+ (https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys), such as
+ those of cubes saved by earlier versions, are shown as JSON. The references that
+ CytePool cannot follow on its own are reported as unresolved: those of the source
+ record, and the fingerprints of the cubes that operations were applied to, which
+ name those cubes without saying where they are.
Args:
provenance: A provenance record, such as ``Cube.provenance`` or the record of
diff --git a/cytepool/design/__init__.py b/cytepool/design/__init__.py
index c4d6c33..cd3904a 100644
--- a/cytepool/design/__init__.py
+++ b/cytepool/design/__init__.py
@@ -1,4 +1,14 @@
-"""Design matrices, contrasts and design checks."""
+"""Design matrices, contrasts and design checks.
+
+:class:`cytepool.Design` joins a sample table, an R formula of the fixed effects,
+random effects in lme4's notation and named contrasts, and its parts are here:
+:func:`model_matrix` codes a formula as a :class:`ModelMatrix`, :func:`parse_formula`
+and :func:`parse_random_effects` read formulas into :class:`FormulaTerms` and
+:class:`RandomEffect` terms, :func:`make_contrasts` turns expressions into
+:class:`Contrasts`, and ``Design.check`` returns a :class:`DesignReport` of
+:class:`DesignIssue` entries. https://docs.nygen.io/CytePool/statistics/linear-models
+explains designs, their contrasts and their checks.
+"""
from .checks import DesignIssue, DesignReport
from .contrasts import Contrasts, make_contrasts
diff --git a/cytepool/design/checks.py b/cytepool/design/checks.py
index 8365cc9..69a9452 100644
--- a/cytepool/design/checks.py
+++ b/cytepool/design/checks.py
@@ -3,65 +3,65 @@
``Design.check()`` returns a ``DesignReport``. Errors are problems that make some
coefficient, contrast or variance impossible to estimate; warnings point at designs
that can be fitted but deserve a second look. Each message says what is wrong and
-how to fix it.
+how to fix it. https://docs.nygen.io/CytePool/statistics/linear-models#design-checks
+explains why each matters.
The leverage of a sample is its diagonal element of the hat matrix (Hoaglin and
Welsch 1978). A contrast is estimable when it is a linear combination of the rows of
the design matrix, that is, when it is orthogonal to the matrix's null space (Searle
1971). Random effects are those of lme4's notation (Bates et al. 2015).
-Errors:
- empty_design: the formula has no columns.
- duplicate_columns: two columns have the same name.
- rank_deficient: some columns are linear combinations of earlier ones (aliased),
- so their coefficients cannot be estimated. A column is aliased when its part
- orthogonal to the earlier columns has a norm at most 1e-7 times its own
- norm, the rule of the pivoted QR decomposition that R's ``lm.fit`` and
- limma's ``lmFit`` use, so the same columns are named.
- no_residual_df: as many independent columns as samples, so no variance can be
- estimated.
- constant_variable: a variable has the same value in every sample and so makes
- a column aliased (a warning when it does not, as in ``~ 0 + x``).
- empty_level: a factor level has no samples, which gives a column of zeros.
- confounded: two factors (main effects) group the samples so that one is
- constant within every level of the other, or a covariate is constant within
- every level of a factor, so their effects cannot be separated.
- one_sample_per_level: a factor gives every sample its own level.
- contrast_not_estimable: a contrast depends on effects the design cannot
- separate.
- random_single_group: a random-effect grouping with one group.
- random_unreplicated: a random effect with at least as many effects (groups
- times effect columns) as samples, lme4's rule, which includes one sample
- per group.
- random_confounded: the fixed effects already separate the groups of a random
- effect.
- random_duplicate: two random effects with the same effects and groupings that
- split the samples identically.
-
-Warnings:
- single_sample_level: a factor level with a single sample, so no replicate.
- unreplicated_sample: a sample that the model fits exactly (leverage 1), so it
- tells nothing about the residual variance and the effects it determines
- rest on it alone.
- high_leverage: a sample with leverage above 0.99, nearly fitted exactly,
- because of a covariate value far beyond the others (beyond them by more
- than their range) or because the design has few residual degrees of
- freedom.
- numeric_codes: a numeric covariate whose values are a few consecutive whole
- numbers, each shared by several samples, which is how codes for batches or
- plates look; as a number it enters the model as one slope.
- numeric_ids: a numeric covariate whose name says ids and whose values are at
- least three whole numbers that are not consecutive, each shared by several
- samples, as donor ids such as 101, 107 and 1015 are; as a number it enters
- the model as one slope, where ids need a group each. A name says ids when
- its last word is donor, patient, subject, individual, participant, sample,
- batch, plate, lane, run or id (or their plurals), as in donor_id or
- subjectID, or is number, no, nr or code after one of them, as in
- run_number; donor_age names an age, and n_donors a count.
- missing_label: a factor level such as "" or "NA" that looks like a missing
- value, which then forms a group of its own.
- similar_levels: factor levels that differ only in case or surrounding spaces,
- which usually split one group in two.
+Errors, in ``DesignReport.errors``:
+ - ``empty_design``: the formula has no columns.
+ - ``duplicate_columns``: two columns have the same name.
+ - ``rank_deficient``: some columns are linear combinations of earlier ones
+ (aliased), so their coefficients cannot be estimated. A column is aliased when its
+ part orthogonal to the earlier columns has a norm at most 1e-7 times its own norm,
+ the rule of the pivoted QR decomposition that R's ``lm.fit`` and limma's ``lmFit``
+ use, so the same columns are named.
+ - ``no_residual_df``: as many independent columns as samples, so no variance can be
+ estimated.
+ - ``constant_variable``: a variable has the same value in every sample and so makes
+ a column aliased (a warning when it does not, as in ``~ 0 + x``).
+ - ``empty_level``: a factor level has no samples, which gives a column of zeros.
+ - ``confounded``: two factors (main effects) group the samples so that one is
+ constant within every level of the other, or a covariate is constant within every
+ level of a factor, so their effects cannot be separated.
+ - ``one_sample_per_level``: a factor gives every sample its own level.
+ - ``contrast_not_estimable``: a contrast depends on effects the design cannot
+ separate.
+ - ``random_single_group``: a random-effect grouping with one group.
+ - ``random_unreplicated``: a random effect with at least as many effects (groups
+ times effect columns) as samples, lme4's rule, which includes one sample per
+ group.
+ - ``random_confounded``: the fixed effects already separate the groups of a random
+ effect.
+ - ``random_duplicate``: two random effects with the same effects and groupings that
+ split the samples identically.
+
+Warnings, in ``DesignReport.warnings``:
+ - ``single_sample_level``: a factor level with a single sample, so no replicate.
+ - ``unreplicated_sample``: a sample that the model fits exactly (leverage 1), so it
+ tells nothing about the residual variance and the effects it determines rest on it
+ alone.
+ - ``high_leverage``: a sample with leverage above 0.99, nearly fitted exactly,
+ because of a covariate value far beyond the others (beyond them by more than their
+ range) or because the design has few residual degrees of freedom.
+ - ``numeric_codes``: a numeric covariate whose values are a few consecutive whole
+ numbers, each shared by several samples, which is how codes for batches or plates
+ look; as a number it enters the model as one slope.
+ - ``numeric_ids``: a numeric covariate whose name says ids and whose values are at
+ least three whole numbers that are not consecutive, each shared by several
+ samples, as donor ids such as 101, 107 and 1015 are; as a number it enters the
+ model as one slope, where ids need a group each. A name says ids when its last
+ word is donor, patient, subject, individual, participant, sample, batch, plate,
+ lane, run or id (or their plurals), as in donor_id or subjectID, or is number, no,
+ nr or code after one of them, as in run_number; donor_age names an age, and
+ n_donors a count.
+ - ``missing_label``: a factor level such as "" or "NA" that looks like a missing
+ value, which then forms a group of its own.
+ - ``similar_levels``: factor levels that differ only in case or surrounding spaces,
+ which usually split one group in two.
References:
Hoaglin DC, Welsch RE (1978). The hat matrix in regression and ANOVA. The American
@@ -75,14 +75,14 @@
import re
import textwrap
-from collections.abc import Iterable, Sequence
+from collections.abc import Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING
import numpy as np
from .._linear_algebra import RANK_TOLERANCE, column_space
-from .._messages import WIDTH, fitted_lines
+from .._messages import WIDTH, fitted_lines, listing
from ..table import Table
from .contrasts import Contrasts
from .formula import RandomEffect
@@ -113,7 +113,6 @@
_ROUNDING = (
1e-10 # relation coefficients this small relative to the largest are rounding
)
-_LISTED = 6 # items listed in a message before "and n more"
_MISSING_LABELS = frozenset({"", "na", "n/a", "nan", "null", "", "nat", "?"})
# Words that name a column of ids, words that do after one of them (run_number),
# and the words of a name: runs of letters, split where a capital starts a word
@@ -260,7 +259,7 @@ def check_design(design: "Design") -> DesignReport:
DesignIssue(
"duplicate_columns",
"more than one column is named "
- + _join(repr(name) for name in duplicates)
+ + listing([repr(name) for name in duplicates])
+ "; coefficients and contrasts refer to columns by name, so rename "
"a variable or a level so that every column name is unique",
)
@@ -345,7 +344,7 @@ def _relation(model: ModelMatrix, aliasing: _Aliasing, j: int) -> str:
covariates = [name for name, level in model.coding[j] if level is None]
if covariates and spread <= 1e-6 * float(np.max(np.abs(column))):
text += (
- f" up to {remainder:.2g} of its size: {_join(covariates)} hardly "
+ f" up to {remainder:.2g} of its size: {listing(covariates)} hardly "
"varies compared with its size, which R's QR also treats as aliased; "
"centering it (subtracting its mean) makes it estimable"
)
@@ -359,18 +358,16 @@ def _relation(model: ModelMatrix, aliasing: _Aliasing, j: int) -> str:
def _rank_message(model: ModelMatrix, aliasing: _Aliasing) -> DesignIssue:
- relations = [_relation(model, aliasing, j) for j in aliasing.aliased[:_LISTED]]
- more = len(aliasing.aliased) - len(relations)
+ relations = [_relation(model, aliasing, j) for j in aliasing.aliased]
count = len(aliasing.aliased)
text = (
f"the design matrix has {_count(model.shape[1], 'column')} but rank "
f"{len(aliasing.kept)}, so {_count(count, 'coefficient')} "
f"{'is' if count == 1 else 'are'} not estimable. Aliased columns: "
- + "; ".join(relations)
- + (f"; and {more} more" if more else "")
+ + listing(relations, separator="; ")
+ ". The columns in each relation cannot be estimated separately; remove "
- "or recode the variables behind them (see the other findings), or drop "
- "terms from the formula"
+ "or recode the variables that make up these columns (see the other "
+ "findings), or drop terms from the formula"
)
return DesignIssue("rank_deficient", text)
@@ -467,10 +464,10 @@ def _variable_checks(
DesignIssue(
"empty_level",
f"{name} has no samples at level{'' if len(empty) == 1 else 's'} "
- + _join(repr(level) for level in empty)
+ + listing([repr(level) for level in empty])
+ (
", so "
- + _join(zero_columns)
+ + listing(zero_columns)
+ " "
+ ("is" if len(zero_columns) == 1 else "are")
+ " zero in every sample"
@@ -503,16 +500,14 @@ def _variable_checks(
single_sample.update(samples)
described = [
f"{variable.levels[k]!r} ({names[sample]})"
- for k, sample in zip(lonely[:_LISTED], samples, strict=False)
+ for k, sample in zip(lonely, samples, strict=True)
]
- more = len(lonely) - len(described)
warnings.append(
DesignIssue(
"single_sample_level",
f"{name} has a single sample at level"
f"{'' if len(lonely) == 1 else 's'} "
- + _join(described)
- + (f" and {more} more" if more else "")
+ + listing(described)
+ "; effects of such a level rest on one sample with no "
"replicate. Add replicates, merge the level with a similar one, "
"or remove the sample",
@@ -563,7 +558,7 @@ def _label_checks(name: str, observed: list[str], warnings: list[DesignIssue]) -
DesignIssue(
"missing_label",
f"{name} has the level{'' if len(suspicious) == 1 else 's'} "
- + _join(repr(level) for level in suspicious)
+ + listing([repr(level) for level in suspicious])
+ ", which looks like a missing value and forms a group of its own; "
"if these samples lack a label, fill it in or remove them (a real "
"group with this name needs no change)",
@@ -574,7 +569,10 @@ def _label_checks(name: str, observed: list[str], warnings: list[DesignIssue]) -
groups.setdefault(level.strip().casefold(), []).append(level)
similar = [members for members in groups.values() if len(members) > 1]
if similar:
- listed = "; ".join(_join(repr(level) for level in group) for group in similar)
+ listed = listing(
+ [listing([repr(level) for level in group]) for group in similar],
+ separator="; ",
+ )
warnings.append(
DesignIssue(
"similar_levels",
@@ -593,20 +591,23 @@ def _confounded_factors(
set(zip(outer.codes.tolist(), inner.codes.tolist()))
):
pairs.setdefault(inner.levels[code_inner], []).append(outer.levels[code_outer])
- listing = "; ".join(
- f"{second} {level!r}: {first} " + _join(repr(value) for value in values)
- for level, values in list(pairs.items())[:_LISTED]
+ listed = listing(
+ [
+ f"{second} {level!r}: {first} " + listing([repr(value) for value in values])
+ for level, values in pairs.items()
+ ],
+ separator="; ",
)
if both:
return DesignIssue(
"confounded",
- f"{first} and {second} group the samples identically ({listing}), so "
+ f"{first} and {second} group the samples identically ({listed}), so "
"their effects cannot be separated by any analysis; keep one of them in "
"the formula, knowing that its effect is also the effect of the other",
)
return DesignIssue(
"confounded",
- f"every level of {first} holds a single level of {second} ({listing}), so "
+ f"every level of {first} holds a single level of {second} ({listed}), so "
f"the effect of {second} cannot be separated from the {first} effects. Remove "
f"{first} from the formula; if {first} groups repeated samples (donors, "
f"batches), the right model has it as a random effect, random='(1 | {first})'; "
@@ -718,7 +719,7 @@ def _leverage_check(
if exact:
factors = [k for k, value in variables.items() if isinstance(value, _Factor)]
described = []
- for sample in exact[:_LISTED]:
+ for sample in exact:
labels = []
for name in factors:
factor = variables[name]
@@ -727,14 +728,12 @@ def _leverage_check(
described.append(
names[sample] + (f" ({', '.join(labels)})" if labels else "")
)
- more = len(exact) - len(described)
plural = len(exact) > 1
warnings.append(
DesignIssue(
"unreplicated_sample",
f"the model fits sample{'s' if plural else ''} "
- + _join(described)
- + (f" and {more} more" if more else "")
+ + listing(described)
+ " exactly (leverage 1), so "
+ ("they tell" if plural else "it tells")
+ " nothing about the residual variance and the effects "
@@ -774,7 +773,7 @@ def _leverage_check(
DesignIssue(
"high_leverage",
"the model nearly fits "
- + _listing(extreme)
+ + listing(extreme)
+ " exactly because of an extreme covariate value, so such a sample "
"decides that covariate's slope almost alone and barely informs the "
"residual variance. Check the value, or transform the covariate (for "
@@ -786,7 +785,7 @@ def _leverage_check(
DesignIssue(
"high_leverage",
"the model nearly fits "
- + _listing(crowded)
+ + listing(crowded)
+ f" exactly: with {_count(df_residual, 'residual degree')} of "
"freedom, such a sample largely decides some coefficients and barely "
"informs the residual variance. Add samples or simplify the formula",
@@ -804,11 +803,6 @@ def _far_outside(values: np.ndarray, sample: int) -> bool:
return bool(values[sample] < low - spread or values[sample] > high + spread)
-def _listing(items: list[str]) -> str:
- more = len(items) - _LISTED
- return _join(items[:_LISTED]) + (f" and {more} more" if more > 0 else "")
-
-
def _contrast_checks(
design: "Design", model: ModelMatrix, aliasing: _Aliasing, errors: list[DesignIssue]
) -> None:
@@ -821,7 +815,7 @@ def _contrast_checks(
"contrast_not_estimable",
f"contrast {contrasts.names[position]!r} is not estimable: it depends "
"on effects that the design cannot separate ("
- + "; ".join(relations)
+ + listing(relations, separator="; ")
+ "). Fix the design first, or choose a contrast that does not "
"separate the columns of these relations",
)
@@ -845,7 +839,7 @@ def _contrast_violations(
Returns:
The position of each contrast that is not estimable, with the relations it
- violates as text (the first ``_LISTED`` of them).
+ violates as text.
"""
if not len(contrasts) or not aliasing.aliased:
return {}
@@ -862,7 +856,7 @@ def _contrast_violations(
if violated:
violations[position] = [
_relation(model, aliasing, aliasing.aliased[index])
- for index in violated[:_LISTED]
+ for index in violated
]
return violations
@@ -987,7 +981,7 @@ def _random_confounded(
what = f"every effect of the random effect {effect}"
fix = "drop the random effect"
else:
- what = f"the {_join(parts)} of the random effect {effect}"
+ what = f"the {listing(parts)} of the random effect {effect}"
fix = (
"drop " + ("it" if len(parts) == 1 else "them") + " from the random effect"
)
@@ -1062,11 +1056,3 @@ def _leverage_text(value: float) -> str:
def _count(number: int, noun: str) -> str:
"""Write a count with its noun, as in "1 sample" and "2 samples"."""
return f"{number} {noun}{'' if number == 1 else 's'}"
-
-
-def _join(items: Iterable[object]) -> str:
- """Join items as "a, b and c"."""
- values = [str(item) for item in items]
- if len(values) <= 2:
- return " and ".join(values)
- return ", ".join(values[:-1]) + " and " + values[-1]
diff --git a/cytepool/design/contrasts.py b/cytepool/design/contrasts.py
index 408f63c..14fd8b1 100644
--- a/cytepool/design/contrasts.py
+++ b/cytepool/design/contrasts.py
@@ -1,23 +1,21 @@
"""Named contrasts: linear combinations of design columns, written as expressions.
A contrast weighs the coefficients of the design columns, as the contrasts of limma's
-linear models do (Smyth 2004). ``make_contrasts({"b_vs_a": "groupB - groupA",
-"mean_bc": "(groupB + groupC)/2 - groupA"}, columns)`` gives the weights of each
-contrast on the design columns, as limma's ``makeContrasts`` does (Ritchie et al.
-2015). Expressions may use ``+``, ``-``, ``*`` and ``/`` with numbers, parentheses,
+linear models do (Smyth 2004). :func:`~cytepool.design.make_contrasts` turns named
+expressions into a :class:`~cytepool.design.Contrasts`, the weights of each contrast on
+the design columns, as limma's ``makeContrasts`` does (Ritchie et al. 2015):
+``make_contrasts({"b_vs_a": "groupB - groupA", "mean_bc": "(groupB + groupC)/2 -
+groupA"}, columns)``.
+https://docs.nygen.io/CytePool/statistics/linear-models#contrasts explains contrasts.
+
+Expressions may use ``+``, ``-``, ``*`` and ``/`` with numbers, parentheses,
and the design's column names written as they are, including names that are not
syntactic in R such as ``(Intercept)`` or ``conditionstim:sexM``; a name can also be
quoted with backticks. Where a name could be read more than one way, the longest
column name that matches is used. A column name that starts or ends with a space
(from a level label with stray spaces) must be quoted with backticks, so that
-``conditionctrl - conditionstim`` never reads the column ``conditionctrl `` by
-accident.
-
-Unlike ``makeContrasts``, which evaluates the expression in R and so returns a
-matrix for any expression, only linear combinations are accepted: an expression
-that multiplies or divides two columns, adds a constant (``groupB - 1``), uses a
-power or a function, or whose weights cancel to zero raises an error, and so does a
-name that is not a column.
+``conditionctrl - conditionstim`` never reads a column named ``"conditionctrl "``,
+with its trailing space, by accident.
Weights that name the columns they refer to are matched to columns by name wherever
they are used, never by position: the rows of a ``Contrasts`` are named by its
@@ -26,6 +24,13 @@
columns in another order, and how ``contrasts_fit`` matches contrasts to the
coefficients of a fit.
+Differences from limma:
+ ``makeContrasts`` evaluates an expression in R, and so returns weights for any
+ expression. Only linear combinations are accepted here: an expression that
+ multiplies or divides two columns, adds a constant (``groupB - 1``), uses a power
+ or a function, or whose weights cancel to zero raises an error, and so does a
+ name that is not a column.
+
References:
Smyth GK (2004). Linear models and empirical Bayes methods for assessing
differential expression in microarray experiments. Statistical Applications in
diff --git a/cytepool/design/design.py b/cytepool/design/design.py
index dc7a604..f3ede9a 100644
--- a/cytepool/design/design.py
+++ b/cytepool/design/design.py
@@ -56,7 +56,8 @@ class Design:
estimated by any function yet. ``fit_linear_model`` fits one random intercept
with a known correlation, given ``block=`` that groups the samples as the
intercept does, and refuses a design whose random effects it would otherwise
- drop.
+ drop. https://docs.nygen.io/CytePool/statistics/linear-models#designs-and-formulas
+ explains designs.
Args:
samples: The sample table: a ``Table``, a mapping of column name to 1-D
@@ -253,8 +254,11 @@ def check(self) -> DesignReport:
single sample, samples fitted exactly or nearly so, numeric columns that
look like batch or plate codes, labels that look like missing values or
differ only in case or spaces, contrasts that are not estimable, and random
- effects whose variances cannot be estimated. See ``cytepool.design.checks``
- for the list.
+ effects whose variances cannot be estimated. ``cytepool.design.checks``
+ lists every check, as the designs page of the API reference shows
+ (https://docs.nygen.io/CytePool/api/designs.html), and
+ https://docs.nygen.io/CytePool/statistics/linear-models#design-checks
+ explains them.
Returns:
The errors and warnings found, with the rank, the aliased columns and
diff --git a/cytepool/design/formula.py b/cytepool/design/formula.py
index ed4e3fd..fa6b308 100644
--- a/cytepool/design/formula.py
+++ b/cytepool/design/formula.py
@@ -1,5 +1,13 @@
"""Parse R-style model formulas into terms.
+:func:`~cytepool.design.parse_formula` expands the right-hand side of a formula into
+its terms, a :class:`~cytepool.design.FormulaTerms`;
+:func:`~cytepool.design.parse_random_effects` reads random-effect terms in lme4's
+notation (Bates et al. 2015) into :class:`~cytepool.design.RandomEffect` entries; and
+:func:`quote_name` writes a variable name as R writes it in term labels.
+https://docs.nygen.io/CytePool/statistics/linear-models#designs-and-formulas explains
+designs and their formulas.
+
A formula such as ``"~ condition * batch"`` describes the columns of a design matrix,
in the notation of Wilkinson and Rogers (1973) as S and R adapted it. It is expanded to
an intercept flag and a list of terms, each term a set of variables, by the rules of
@@ -22,15 +30,17 @@
which they first appear in the formula. Term labels quote names that are not
syntactic R names with backticks, as R does.
-Some formulas that R accepts are rejected because their R meaning is almost never
-what was intended: a ``-`` that removes nothing (``~ x - z`` does not subtract z; it
-is ``~ x`` in R), ``^`` applied to a single term (R reads ``~ age^2`` as ``~ age``),
-and a power that is not a whole number (R truncates ``^2.5`` to ``^2``). Function
-calls such as ``log(age)`` or ``I(age^2)`` are rejected with a suggestion to add a
-column with the computed values.
+Differences from R:
+ Some formulas that R accepts are rejected, because their R meaning is almost
+ never what was intended:
+
+ - a ``-`` that removes nothing: ``~ x - z`` does not subtract z, it is ``~ x``
+ in R;
+ - ``^`` applied to a single term: R reads ``~ age^2`` as ``~ age``;
+ - a power that is not a whole number: R truncates ``^2.5`` to ``^2``.
-Random-effect terms, such as ``(1 | donor)``, are written in lme4's notation (Bates et
-al. 2015) and read by ``parse_random_effects``.
+ Function calls such as ``log(age)`` or ``I(age^2)`` are rejected too, with a
+ suggestion to add a column with the computed values.
References:
Wilkinson GN, Rogers CE (1973). Symbolic description of factorial models for
@@ -212,8 +222,10 @@ def group(self) -> str:
A single grouping variable keeps its name as it is (``cell type``); a
combination of variables is written as in a formula, with backticks around
- names that need them (found by probing lme4).
+ names that need them, as lme4 writes it.
"""
+ # lme4 names a combination of grouping variables as a formula writes it
+ # (found by probing lme4).
if len(self.grouping) == 1:
return self.grouping[0]
return ":".join(quote_name(name) for name in self.grouping)
diff --git a/cytepool/design/model_matrix.py b/cytepool/design/model_matrix.py
index 0e894a2..082199d 100644
--- a/cytepool/design/model_matrix.py
+++ b/cytepool/design/model_matrix.py
@@ -1,5 +1,12 @@
"""Model matrices from a sample table and a formula, coded as R's ``model.matrix``.
+:func:`~cytepool.design.model_matrix` builds the design matrix of a formula, column for
+column as R's ``model.matrix`` builds it, and returns a
+:class:`~cytepool.design.ModelMatrix` with R's column names, the term of each column
+and the levels it codes. The formula's grammar is that of ``cytepool.design.formula``.
+https://docs.nygen.io/CytePool/statistics/linear-models#designs-and-formulas explains
+designs.
+
Variables:
Numeric columns (integer or float) are covariates and give one column each. Text,
boolean and pandas categorical columns are factors. A boolean column always has
@@ -60,7 +67,7 @@
from .._columns import is_data_frame, missing_mask
from .._masked_arrays import reject_masked
-from .._messages import close_matches, fitted_lines
+from .._messages import LISTED, close_matches, fitted_lines, listing
from ..table import Table
from .formula import FormulaTerms, parse_formula, quote_name
@@ -70,12 +77,11 @@
"""The name of the intercept column, as R names it."""
_SAMPLE_ID = "sample_id"
-_LISTED_ROWS = 8 # rows named in an error message before "and n more"
@dataclass(frozen=True, eq=False)
class ModelMatrix:
- """A design matrix with R's column names and the terms behind each column.
+ """A design matrix with R's column names and the term of each column.
Attributes:
matrix: float64 array of shape ``(n_samples, n_columns)``, read-only.
@@ -187,7 +193,9 @@ def model_matrix(
or a pandas DataFrame, with one row per sample.
formula: A right-hand-side formula such as ``"~ condition + batch"``,
``"~ 0 + group"``, ``"~ condition * sex"`` or ``"~ donor + condition"``.
- See ``parse_formula`` for the grammar.
+ The grammar, R's, is given in the documentation of
+ ``cytepool.design.formula``, which the designs page of the API reference
+ shows (https://docs.nygen.io/CytePool/api/designs.html).
levels: Level order of factors, for example
``{"condition": ["ctrl", "stim"]}``; the first level is the reference.
Each list must contain every value the column holds. For a boolean
@@ -395,15 +403,11 @@ def _read_variables(
def _describe_rows(table: Table, positions: np.ndarray | Sequence[int]) -> str:
"""Describe rows by position, and by sample_id when the table has one."""
positions = [int(position) for position in positions]
- shown = positions[:_LISTED_ROWS]
- more = len(positions) - len(shown)
noun = "row" if len(positions) == 1 else "rows"
- text = f"{noun} " + ", ".join(str(position) for position in shown)
+ text = f"{noun} " + listing(positions)
if _SAMPLE_ID in table and table[_SAMPLE_ID].dtype == object:
ids = table[_SAMPLE_ID]
- text += " (sample_id " + ", ".join(str(ids[p]) for p in shown) + ")"
- if more:
- text += f" and {more} more"
+ text += " (sample_id " + listing([ids[p] for p in positions[:LISTED]]) + ")"
return text
diff --git a/cytepool/differential_expression.py b/cytepool/differential_expression.py
index 8893d07..523257c 100644
--- a/cytepool/differential_expression.py
+++ b/cytepool/differential_expression.py
@@ -1,16 +1,18 @@
"""Differential expression between samples, one group (cell type) of a cube at a time.
-:func:`~cytepool.limma_voom` and :func:`~cytepool.limma_trend` run the limma workflows
-for RNA-seq counts (Ritchie et al. 2015) on the pseudobulk counts of each group of a
-cube: limma-voom, which gives every observation a precision weight from the
-mean-variance trend of the log counts per million (Law et al. 2014), and limma-trend,
-which lets the prior variance of the empirical Bayes moderation follow that trend
-instead. :func:`~cytepool.dream_voom` runs variancePartition's dream (Hoffman and
-Roussos 2021), limma-voom with a linear mixed model in place of the linear model, for
-designs with random effects such as several samples of each donor. Each tests the named
-contrasts of a :class:`~cytepool.Design` and returns, for every group and contrast, a
-table of the tested features with ``topTable``'s statistics, together with a summary,
-the samples behind every fit and a provenance record.
+:func:`~cytepool.limma_voom` and :func:`~cytepool.limma_trend` run limma-voom (Law et
+al. 2014) and limma-trend, the limma workflows for RNA-seq counts (Ritchie et al.
+2015), on the pseudobulk counts of each group of a cube, and
+:func:`~cytepool.dream_voom` runs variancePartition's dream (Hoffman and Roussos 2021),
+which adds random effects to limma-voom's models, for designs such as several samples
+of each donor. Each tests the named contrasts of a :class:`~cytepool.Design` and
+returns a :class:`~cytepool.DifferentialExpressionResult`: for every group and
+contrast, a table of the tested features with ``topTable``'s statistics, together
+with a summary, the samples of every fit and a provenance record. The functions list
+their steps;
+https://docs.nygen.io/CytePool/statistics/limma-voom explains limma-voom and
+limma-trend, and https://docs.nygen.io/CytePool/statistics/mixed-models#dream explains
+dream.
Samples:
The design's sample table must have a ``sample_id`` column. Its samples are matched
@@ -19,29 +21,21 @@
fitted with its samples in the cube's order. In each group, a sample is left out
of the fit when it has fewer than ``min_cells`` cells (when the cube records cell
numbers), no counts, or fewer than ``min_counts`` counts in all (its total over
- every feature of the cube), and the result records why. ``min_counts`` is 0 by
- default; the scverse single-cell best practices (Heumos et al. 2023) keep samples
- with at least 10 cells and 1,000 counts (``min_cells=10, min_counts=1000``), as
- decoupler's ``filter_samples`` does, which keeps a sample with exactly as many
- (found by probing).
+ every feature of the cube), and the result records why; a sample with exactly
+ ``min_cells`` cells or ``min_counts`` counts is kept, as decoupler's
+ ``filter_samples`` keeps it.
The design of a group:
The design matrix of a group is the rows of the full design matrix for the
samples left in it, so every column keeps its meaning: factor levels, reference
- levels and covariate values are those of the full design. With fewer samples that
- matrix can be rank deficient, for example when every sample of one donor is left
- out. Such a group is fitted on the column space of its design matrix, keeping the
- columns that R's ``qr()`` keeps, as limma's ``lmFit`` does; each other column
- (aliased) equals a combination of the kept columns before it. A contrast ``c`` is
- estimable from the group's samples when it is orthogonal to the null space of the
- group's design matrix (Searle 1971): when its weight on every aliased column is the
- sum of its weights on the kept columns, weighted by that column's combination.
- This is the rule of :meth:`Design.check `. ``c``
- applied to the coefficients then equals its weights on the kept columns applied to
- the fit on the kept columns, whatever the coefficients that the samples cannot
- separate, so its estimate and its exact standard error are those of any full-rank
- parametrisation of the same model. A contrast that is not estimable is reported,
- with the relations it violates, and not computed.
+ levels and covariate values are those of the full design. Where those rows are
+ rank deficient, as when every sample of one donor is left out, the group is
+ fitted on the columns that R's ``qr()`` keeps, as limma's ``lmFit`` does, and a
+ contrast is tested when the group's samples can estimate it (Searle 1971), which
+ :meth:`Design.check ` decides by the same rule; a contrast
+ that is not estimable is reported, with the relations it violates, and not
+ computed. https://docs.nygen.io/CytePool/statistics/linear-models#estimable-contrasts
+ explains the rule.
Groups that are not fitted:
A group is not fitted, and its status in the summary says why, when no sample is
@@ -51,90 +45,34 @@
no counts in the features that pass, or voom, the sample quality weights or the
moderation fail on its data. A run never stops for one group.
-Steps:
- For each group, with the R functions they match in the limma and edgeR User's
- Guides:
-
- 1. Leave out samples, as above; the design matrix is the rows of the rest.
- 2. Filter features by expression with the group's design, by edgeR's
- ``filterByExpr(y, design)`` (Chen, Lun and Smyth 2016) with its defaults
- (``expression_filter``), and recompute the library sizes over the features
- kept (``y[keep, , keep.lib.sizes = FALSE]``).
- 3. Normalization factors by TMM (Robinson and Oshlack 2010), edgeR's
- ``calcNormFactors`` (``normalization``).
- 4. limma-voom: ``voom`` on the library sizes times the normalization factors,
- with its LOWESS span chosen from the number of features tested, as limma's
- ``voom`` chooses it by default since limma 3.66 (``span``), then ``lmFit``
- with its weights. limma-trend: log2 counts per million with a prior count of
- 3, ``cpm(y, log = TRUE, prior.count = 3)`` as the limma User's Guide uses for
- limma-trend, then ``lmFit``. Sample weights, when asked, enter here (below).
- 5. ``contrasts.fit``, ``eBayes`` (Smyth 2004), with ``trend = TRUE`` for
- limma-trend and ``robust`` (Phipson et al. 2016) when asked, and
- ``topTable(sort.by = "none")``: the Benjamini-Hochberg adjustment (Benjamini
- and Hochberg 1995) runs over the tested features of one group and contrast,
- and with ``adjust_across_groups`` also over every group (below).
-
Sample weights:
- Both kinds are off by default, and they are alternatives: limma's
- ``voomWithQualityWeights`` takes no prior weights, and edgeR's ``voomLmFit``
- refuses prior weights together with estimated sample weights (found by probing),
- so CytePool refuses the two together too. The sample table and the record of
- each fitted group hold the sample weights of its fit. With limma-voom, the
- quality weights recorded are the final ones; ``voomWithQualityWeights`` estimates
- them twice, and its first estimate only weights the fit of voom's second trend.
- :func:`~cytepool.stats.voom.voom_with_quality_weights`, given the group's counts,
- design rows and normalization factors, reproduces both.
-
- - ``quality_weights``: sample quality weights, estimated in each group from how
- closely each sample's values follow the linear model (Ritchie et al. 2006; Liu
- et al. 2015), which a limma author recommends for pseudobulk data (Smyth 2025,
- on the Bioconductor support site). limma-voom runs limma's
- ``voomWithQualityWeights``
+ Both kinds are off by default, and only one of them may be given
+ (https://docs.nygen.io/CytePool/statistics/limma-voom#limma-voom-defaults says
+ why). The sample table and the record of each fitted group hold the weights of
+ its fit.
+
+ - ``quality_weights``: sample quality weights (Ritchie et al. 2006; Liu et al.
+ 2015). limma-voom runs limma's ``voomWithQualityWeights``
(:func:`~cytepool.stats.voom.voom_with_quality_weights`), gene by gene by
- default, as limma does. limma-trend estimates them from the log2 counts per
- million as limma's ``arrayWeights`` does
+ default, as limma does; it records the final weights, as
+ ``voomWithQualityWeights`` estimates them twice and its first estimate only
+ weights the fit of voom's second trend. limma-trend estimates them from the log2
+ counts per million as limma's ``arrayWeights`` does
(:func:`~cytepool.stats.sample_weights.sample_quality_weights`), by REML by
default, the method that ``arrayWeights`` chooses for such data, and passes
- them to ``lmFit`` as sample weights. Where a donor has two samples, as in
- ``~ donor + condition``, their residuals are equal and opposite (up to voom's
- weights), so both get the low weight of an outlying sample. Where the weights
- cannot be estimated, as with fewer than two residual degrees of freedom, they
- are one, as limma's are, with a warning that names the group and that its
- record keeps; where the estimate fails, as when a sample carries no
- information about its variance and ``prior_n`` is 0, the group is not fitted.
+ them to ``lmFit`` as sample weights. Where the weights cannot be estimated, as
+ with fewer than two residual degrees of freedom, they are one, as limma's are,
+ with a warning that names the group and that its record keeps; where the
+ estimate fails, as when a sample carries no information about its variance and
+ ``prior_n`` is 0, the group is not fitted.
- ``prior_weights="n_cells"``: each sample's number of cells in the group, as
- prior weights. A pseudobulk profile sums the counts of its cells, so its log2
- counts per million estimate the mean expression of those cells, whose
- variance falls as one over their number: the precision of a profile is
- proportional to its number of cells, as the dreamlet paper argues (Hoffman et
- al. 2026, Methods), and dreamlet's ``processAssays`` weights its fits by the
- number of cells of each sample, as its documentation describes.
- limma-voom passes them to ``voom`` as ``weights``: they weight the fits
- behind voom's trend, and the precision weights are the trend's times them,
- as limma's help page says voom's weights modify prior weights, and as edgeR's
- ``voomLmFit(prior.weights = ...)`` forms them (found by probing). limma-trend
+ prior weights, as dreamlet weights its fits (Hoffman et al. 2026). limma-voom
+ passes them to ``voom`` as ``weights``: they weight the least-squares fits from
+ which voom's trend is estimated, and the precision weights are the trend's times
+ them, as edgeR's ``voomLmFit(prior.weights = ...)`` forms them. limma-trend
passes them to ``lmFit`` as sample weights. Only their ratios within a group
matter: weights that are all ten times larger give the same results.
-dream:
- :func:`~cytepool.dream_voom` takes the steps of limma-voom up to the normalization,
- then those of variancePartition's ``voomWithDreamWeights``, ``dream``, ``eBayes``
- and ``topTable``: precision weights from the mean-variance trend of the design's
- mixed model, fitted to every feature by maximum likelihood
- (:func:`~cytepool.stats.dream.voom_with_dream_weights`); the mixed model of every
- feature fitted by REML with those weights
- (:func:`~cytepool.stats.mixed_models.fit_mixed_model`); t-tests of the estimable
- contrasts (:func:`~cytepool.stats.mixed_models.fixed_effect_tests`) with the degrees
- of freedom dream chooses by default (``ddf = "adaptive"``), Kenward and Roger's
- (1997) in a group of fewer than 20 samples and Satterthwaite's (1946) otherwise;
- F-tests of sets of contrasts; and their empirical Bayes moderation
- (:func:`~cytepool.stats.dream.moderated_fixed_effect_tests`). The random effects of
- a group are those of its samples: a grouping factor's levels without samples in the
- group drop out. :mod:`cytepool.stats.dream` documents the conventions found by
- probing variancePartition and the deliberate differences from it. Prior weights from
- the number of cells are divided by their mean in each group, as variancePartition
- divides prior weights, which changes no test.
-
P-values adjusted across groups:
``adjusted_p_value`` adjusts the p-values of one group and contrast, as limma's
``topTable`` does. With ``adjust_across_groups``, the column
@@ -142,29 +80,14 @@
Benjamini-Hochberg over every test of that contrast in the analysis: every
feature tested in every group that was fitted and can estimate the contrast,
stacked into one family. Each contrast is a family of its own, and groups left
- out of ``groups`` are not in it. muscat reports both adjustments (Crowell et
- al. 2020), whose comparison found the adjustment over all groups very
- conservative. It is not larger for every test: a family with more small
- p-values can lower an adjusted value.
-
-Deliberate differences from limma:
- ``validation/differential_expression.py`` reports each of them.
-
- - In a rank-deficient group, a contrast that is estimable but weighs an aliased
- column is computed. limma's ``contrasts.fit`` stops with an error for such a
- contrast, and with it for every contrast passed together with it (found by
- probing), so in limma it needs a full-rank parametrisation.
- - A contrast that is not estimable, but weighs no aliased column, is reported as
- not estimable. limma computes it from the kept columns, whose meaning has then
- changed: with every sample of the reference donor D1 left out of
- ``~ donor + condition``, limma reports for the coefficient ``donorD2`` (donor D2
- against D1) the difference between donor D2 and the last donor.
- - With ``contrast_standard_errors="exact"``, the default of
- :func:`~cytepool.limma_voom`, the standard errors of contrasts that combine
- several coefficients are exact; limma's ``contrasts.fit`` approximates them when
- weights differ between features, and ``"limma"`` reproduces it. limma's
- approximation, unlike the exact value, depends on the parametrisation, so only
- exact standard errors are the same in every parametrisation of a group's design.
+ out of ``groups`` are not in it.
+
+Differences from limma:
+ https://docs.nygen.io/CytePool/statistics/linear-models#estimable-contrasts says
+ what limma's ``contrasts.fit`` does with a group whose design lost rank, and
+ https://docs.nygen.io/CytePool/statistics/linear-models#contrasts why the standard
+ errors of contrasts are exact by default (``contrast_standard_errors``);
+ ``validation/differential_expression.py`` reports each difference.
Memory:
One group is in memory at a time: its counts layer, read from the cube when the
@@ -209,11 +132,6 @@
single-cell transcriptomics data using Dreamlet. Nature Communications 17, 9597.
doi:10.1038/s41467-026-75680-8
- Crowell HL, Soneson C, Germain PL, Calini D, Collin L, Raposo C, Malhotra D,
- Robinson MD (2020). muscat detects subpopulation-specific state transitions from
- multi-sample multi-condition single-cell transcriptomics data. Nature Communications
- 11, 6077. doi:10.1038/s41467-020-19894-4
-
Hoffman GE, Roussos P (2021). Dream: powerful differential expression analysis for
repeated measures designs. Bioinformatics 37, 192-201.
doi:10.1093/bioinformatics/btaa687
@@ -235,15 +153,6 @@
Kenward MG, Roger JH (1997). Small sample inference for fixed effects from
restricted maximum likelihood. Biometrics 53, 983-997. doi:10.2307/2533558
-
- Heumos L, Schaar AC, Lance C, Litinetskaya A, Drost F, Zappia L, Luecken MD,
- Strobl DC, Henao J, Curion F, Single-cell Best Practices Consortium, Schiller HB,
- Theis FJ (2023). Best practices for single-cell analysis across modalities. Nature
- Reviews Genetics 24, 550-572. doi:10.1038/s41576-023-00586-w
-
- Smyth GK (2025). Answer to "Guidance on limma for pseudobulk scRNA-seq with
- multi-factor design", 11 September 2025. Bioconductor support site,
- https://support.bioconductor.org/p/9162569/
"""
import json
@@ -288,7 +197,7 @@
from ._options import (
checked_choice,
checked_flag,
- number_at_least,
+ non_negative_number,
positive_finite_number,
positive_integer,
real_number,
@@ -348,11 +257,11 @@
# TRUE).
_DREAM_SETTINGS = {"weights_fit": "ml", "fit": "reml"}
_DF_METHODS = ("adaptive", "satterthwaite", "kenward_roger")
-# dream's ddf = "adaptive" uses Kenward and Roger's method for fewer samples than this.
-# variancePartition 1.36.2 documents "<= 20 samples", but gives Kenward and Roger's
+# dream's ddf = "adaptive" uses Kenward-Roger's method for fewer samples than this.
+# variancePartition 1.36.2 documents "<= 20 samples", but gives Kenward-Roger's
# degrees of freedom for 19 samples and Satterthwaite's for 20 (found by probing).
_KENWARD_ROGER_SAMPLES = 20
-# dream tests a feature by Satterthwaite's method when Kenward and Roger's adjusted
+# dream tests a feature by Satterthwaite's method when Kenward-Roger's adjusted
# covariance matrix of the coefficients is not positive definite or has a condition
# number above this (variancePartition's NEWS for 1.31.16; the bound found by probing).
_CONDITION_LIMIT = 1e6
@@ -367,7 +276,7 @@ class GroupFit:
:meth:`DifferentialExpressionResult.fit` computes it again from the cube and
design the result keeps, with the settings it records: the values, weights and
- moderated statistics behind the group's tables, which figures such as
+ moderated statistics from which the group's tables come, which figures such as
:func:`~cytepool.plotting.plot_mean_variance` and
:func:`~cytepool.plotting.plot_residual_variances` draw. Rows of the arrays are
the samples fitted, in the order of ``samples``, and columns the features
@@ -436,7 +345,7 @@ class DifferentialExpressionResult:
(limma's B-statistic, ``B``).
Tables from :func:`dream_voom` also give ``df``, the degrees of freedom of each
- test (Satterthwaite's or Kenward and Roger's plus the prior's, ``df.total``),
+ test (Satterthwaite's or Kenward-Roger's plus the prior's, ``df.total``),
after ``t``, and ``z_statistic``, the z-statistic with the test's p-value
(``topTable``'s ``z.std``), after ``log_odds``; a feature whose mixed model has
no minimum has NaN statistics. Its F-tests have tables of their own, whose
@@ -846,30 +755,40 @@ def limma_voom(
) -> DifferentialExpressionResult:
"""Test contrasts in every group of a cube by limma-voom.
- For each group, the samples with fewer than ``min_cells`` cells or no counts are
- left out, features are filtered by expression, the counts are normalized, voom
- gives every observation a precision weight from the mean-variance trend of the
- log2 counts per million, a weighted linear model is fitted to every feature, and
- its contrasts are moderated by empirical Bayes: the steps of the limma User's
- Guide for RNA-seq (``voom``, ``lmFit``, ``contrasts.fit``, ``eBayes``,
- ``topTable``) after edgeR's ``filterByExpr`` and ``calcNormFactors``. The module
- documentation describes the steps, rank-deficient groups and the deliberate
- differences from limma.
-
- voom smooths its mean-variance trend with a LOWESS span that, by default, it
- chooses in each group from the number of features tested there, by limma's
- ``chooseLowessSpan`` rule (:func:`~cytepool.stats.smoothing.choose_lowess_span`),
- as limma's ``voom`` does by default since limma 3.66 (``adaptive.span = TRUE``).
- ``span`` fixes it instead; limma's default before 3.66 was 0.5. The record of
- each group gives the span its trend used, as ``voom_span``.
-
- Options for pseudobulk data, each off by default so that the results are
- limma's standard workflow unless asked: a minimum total count per sample
- (``min_counts``), sample quality weights (``quality_weights``, limma's
- ``voomWithQualityWeights``), prior weights from the number of cells behind each
- profile (``prior_weights="n_cells"``, as dreamlet weights samples), and p-values
- adjusted across groups as well as within each (``adjust_across_groups``). The
- module documentation gives the rules and their sources.
+ The steps of the limma and edgeR User's Guides for RNA-seq, in each group (Law
+ et al. 2014; Ritchie et al. 2015), with the R functions they match:
+
+ 1. Leave out the samples with fewer than ``min_cells`` cells, no counts, or
+ fewer than ``min_counts`` counts; the design matrix is the rows of the rest
+ (the module documentation gives the rules for samples and for designs that
+ lose rank).
+ 2. Filter the features by expression with that design matrix, as edgeR's
+ ``filterByExpr(y, design)`` does with its defaults (Chen, Lun and Smyth
+ 2016), and take the library sizes over the features kept
+ (``y[keep, , keep.lib.sizes = FALSE]``).
+ 3. Normalization factors by TMM, edgeR's ``calcNormFactors`` (Robinson and
+ Oshlack 2010).
+ 4. ``voom`` on the library sizes times the normalization factors, whose LOWESS
+ span is chosen from the number of features tested by limma's
+ ``chooseLowessSpan`` rule (:func:`~cytepool.stats.smoothing.choose_lowess_span`),
+ as limma's ``voom`` chooses it by default since limma 3.66, then ``lmFit``
+ with its weights; sample weights, when asked, enter here (the module
+ documentation gives the rules).
+ 5. ``contrasts.fit``, ``eBayes`` (Smyth 2004), robustly when asked (Phipson et
+ al. 2016), and ``topTable(sort.by = "none")``, whose Benjamini-Hochberg
+ adjustment (Benjamini and Hochberg 1995) runs over the tested features of one
+ group and contrast.
+
+ The record of each group gives the span its trend used, as ``voom_span``. The
+ defaults are those of limma's workflow, but for ``min_cells``, which leaves out
+ samples of fewer than 10 cells, and for ``contrast_standard_errors``, which
+ computes the standard errors of contrasts exactly. The options for pseudobulk
+ data are off by default: a minimum total count per sample (``min_counts``),
+ sample quality weights (``quality_weights``), prior weights from the number of
+ cells of each profile (``prior_weights="n_cells"``), and p-values adjusted across
+ groups as well as within each (``adjust_across_groups``).
+ https://docs.nygen.io/CytePool/statistics/limma-voom explains the method and
+ when to choose each option.
Provenance:
The result records the cube's fingerprint, the digests it is built from and
@@ -877,12 +796,13 @@ def limma_voom(
``Cube.provenance``), computed after the fits. For a cube opened with
:func:`~cytepool.read_cube` this reads nothing: the file stores the
digests, and each group the fits read whole is checked against its digest.
- A cube in memory hashes its layers once, at about 1 s per GB of values
- where the processor has SHA instructions. A lazy cube derived from a cube
- file with ``select``, ``regroup`` or ``collapse_samples`` reads, once, the
- groups whose values its operations changed and that the fits did not read
- whole: those of its other layers, and those of groups not analysed. A lazy
- layer of another array-like, which can change, is read whole once more.
+ A cube in memory hashes its layers once (see "Cost" in
+ :meth:`Cube.fingerprint `). A lazy cube derived
+ from a cube file with ``select``, ``regroup`` or ``collapse_samples`` reads,
+ once, the groups whose values its operations changed and that the fits did
+ not read whole: those of its other layers, and those of groups not analysed.
+ A lazy layer of another array-like, which can change, is read whole once
+ more.
Args:
cube: A cube with a ``counts`` layer, such as one from
@@ -1021,17 +941,18 @@ def limma_trend(
) -> DifferentialExpressionResult:
"""Test contrasts in every group of a cube by limma-trend.
- As :func:`limma_voom`, but the counts become log2 counts per million with a
- prior count (edgeR's ``cpm(y, log = TRUE, prior.count = 3)``, the limma User's
- Guide's choice for limma-trend), every feature is fitted without observation
- weights, and the prior variance of the empirical Bayes moderation follows the
- mean log2 CPM of the features (``eBayes(trend = TRUE)``). The standard errors of
- contrasts are exact, as limma's are, also with sample weights. The result
- records the cube's fingerprint and digests, at the cost "Provenance" in
- :func:`limma_voom` gives.
-
- The options for pseudobulk data are those of :func:`limma_voom`; sample weights
- (prior or quality weights) enter ``lmFit`` as sample weights.
+ The steps of :func:`limma_voom`, but in step 4 the counts become log2 counts per
+ million with a prior count, as edgeR's ``cpm(y, log = TRUE, prior.count = 3)``
+ computes them for limma-trend in the limma User's Guide, and every feature is
+ fitted without observation weights; in step 5 the prior variance of the
+ empirical Bayes moderation follows the mean log2 CPM of the features
+ (``eBayes(trend = TRUE)``). The options for pseudobulk data are those of
+ :func:`limma_voom`; sample weights (prior or quality weights) enter ``lmFit`` as
+ sample weights, and the standard errors of contrasts are exact, as limma's are
+ then. The result records the cube's fingerprint and digests, at the cost
+ "Provenance" in :func:`limma_voom` gives.
+ https://docs.nygen.io/CytePool/statistics/limma-voom#limma-trend says when to
+ choose limma-trend over limma-voom.
Args:
cube: A cube with a ``counts`` layer.
@@ -1142,8 +1063,11 @@ def dream_voom(
are tested (:func:`~cytepool.stats.mixed_models.fixed_effect_tests`) with the
degrees of freedom of ``df_method``; and the tests are moderated by empirical
Bayes as variancePartition's ``eBayes`` moderates them
- (:func:`~cytepool.stats.dream.moderated_fixed_effect_tests`). The module
- documentation gives the steps and the differences from variancePartition.
+ (:func:`~cytepool.stats.dream.moderated_fixed_effect_tests`).
+ https://docs.nygen.io/CytePool/statistics/mixed-models#dream explains the steps,
+ :mod:`cytepool.stats.dream` gives how they match variancePartition, and
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-models-limits how they
+ differ from it.
Every group's mixed model uses the random effects of the samples left in it: the
levels of a grouping factor without samples there drop out, and a group whose
@@ -1153,11 +1077,10 @@ def dream_voom(
is tested with NaN statistics; its group's record counts them.
Degrees of freedom:
- As dream's ``ddf``, by default (``"adaptive"``) Kenward and Roger's method
- in a group with fewer than 20 samples and Satterthwaite's otherwise.
- variancePartition documents "20 samples or fewer"; with 20 its ``dream``
- uses Satterthwaite's (found by probing variancePartition 1.36.2). By
- default (``kenward_roger_weights="average"``), Kenward and Roger's
+ As dream's ``ddf``, by default (``"adaptive"``) Kenward-Roger's method
+ in a group with fewer than 20 samples and Satterthwaite's otherwise, as
+ dream chooses them (:mod:`cytepool.stats.dream` compares its help page).
+ By default (``kenward_roger_weights="average"``), Kenward-Roger's
adjustment is computed as dream computes it with pbkrtest, with every
observation at its feature's average weight; with voom's weights that
adjustment can make standard errors smaller than the unadjusted ones,
@@ -1165,18 +1088,20 @@ def dream_voom(
:func:`~cytepool.stats.mixed_models.fixed_effect_tests`). As dream does, a
feature whose adjusted covariance matrix is not positive definite or has a
condition number above 1e6 is tested by Satterthwaite's method instead;
- the group's record counts such features. That number depends on the units
- of continuous covariates: centring and scaling them avoids it.
+ the group's record counts such features. The condition number is computed
+ exactly, where dream uses R's estimate, so the two can choose differently
+ within about 2% of 1e6. It depends on the units of continuous covariates:
+ centring and scaling them avoids the fallback.
F-tests:
``f_tests`` names sets of contrasts to test at once, such as every
difference among three time points: each is tested by dream's moderated
F-statistic, which ``topTable`` gives for several coefficients (see
- :mod:`cytepool.stats.dream`), in every group that can estimate all its
- contrasts. Its table has, for every tested feature, ``feature_id`` (and
- ``symbol``), ``log2_fold_change_`` for each contrast,
- ``average_log2_cpm``, ``f_statistic``, ``df`` (the denominator degrees of
- freedom), ``p_value``, ``adjusted_p_value`` (and
+ :class:`~cytepool.stats.dream.ModeratedFixedEffectTests`), in every group
+ that can estimate all its contrasts. Its table has, for every tested
+ feature, ``feature_id`` (and ``symbol``), ``log2_fold_change_``
+ for each contrast, ``average_log2_cpm``, ``f_statistic``, ``df`` (the
+ denominator degrees of freedom), ``p_value``, ``adjusted_p_value`` (and
``adjusted_p_value_across_groups``), and ``standardized_f_statistic``, the
F-statistic on infinite denominator degrees of freedom with the same
p-value (``F.std``). The numerator degrees of freedom are the number of
@@ -1207,19 +1132,22 @@ def dream_voom(
0.5.
prior_weights: ``"n_cells"`` to give each sample its number of cells in
the group as a prior weight, as dreamlet weights samples, or None for
- none. Needs a cube that records cell numbers.
+ none. Needs a cube that records cell numbers. The weights are divided
+ by their mean in each group, as variancePartition divides prior
+ weights, which changes no test.
df_method: ``"adaptive"`` (dream's default, above), ``"satterthwaite"`` or
``"kenward_roger"`` (with dream's fallback, above) for every group.
- kenward_roger_weights: ``"average"``, as dream computes Kenward and Roger's
+ kenward_roger_weights: ``"average"``, as dream computes Kenward-Roger's
adjustment, or ``"precision"``, with each observation's own weight;
see above.
robust: Whether to estimate the prior of the moderation robustly, as
``eBayes(robust = TRUE)`` does.
trend: Whether the prior variance of the moderation follows the mean log2
- CPM of the features, as ``eBayes(trend = TRUE)`` does. dream moderates
- the residual variance of an observation of average weight, which keeps
- much of the mean-variance trend that voom's weights remove (see
- :mod:`cytepool.stats.dream`); variancePartition's default is False.
+ CPM of the features, as ``eBayes(trend = TRUE)`` does; False by
+ default, as in variancePartition. The variances moderated keep voom's
+ mean-variance trend, which True lets the prior follow
+ (https://docs.nygen.io/CytePool/statistics/mixed-models#dream says
+ why).
adjust_across_groups: Whether to add the column
``adjusted_p_value_across_groups``, as for :func:`limma_voom`.
n_threads: The most threads for the mixed model fits of the features (see
@@ -1717,7 +1645,7 @@ def _dream_parts(
) -> InferenceParts:
"""The parts of the group's tests, with dream's choice of degrees of freedom.
- With Kenward and Roger's method, a feature whose adjusted covariance matrix is
+ With Kenward-Roger's method, a feature whose adjusted covariance matrix is
not positive definite or has a condition number above 1e6 takes Satterthwaite's
parts, as dream does; ``fit`` records the method and how many features did.
Call it within ``thread_limit``.
@@ -1786,6 +1714,8 @@ def _voom_weights(
"""
quality = settings["quality_weights"]
if quality is None:
+ # voom's weights times the prior weights, as edgeR's
+ # voomLmFit(prior.weights = ...) forms them (found by probing).
return voom(y, x, norm_factors=factors, span=settings["span"], weights=prior)
counts, sizes, fraction = voom_inputs(
y, x, None, factors, settings["span"], "limma_voom"
@@ -1926,6 +1856,8 @@ def _sample_weight_settings(
"""The checked settings of the sample weights, which are alternatives."""
prior_weight_setting(prior_weights)
quality = _quality_settings(quality_weights, _QUALITY_METHODS[method])
+ # edgeR's voomLmFit refuses prior weights together with estimated sample weights
+ # (found by probing).
if prior_weights is not None and quality is not None:
raise ValueError(
"give prior_weights or quality_weights, not both: quality weights "
@@ -1962,8 +1894,8 @@ def _quality_settings(value: Any, method: str) -> dict[str, Any] | None:
"method": checked_choice(
settings["method"], "quality_weights['method']", ("gene_by_gene", "reml")
),
- "prior_n": number_at_least(
- settings["prior_n"], "quality_weights['prior_n']", 0.0
+ "prior_n": non_negative_number(
+ settings["prior_n"], "quality_weights['prior_n']"
),
"max_iterations": positive_integer(
settings["max_iterations"], "quality_weights['max_iterations']"
diff --git a/cytepool/gene_set_enrichment.py b/cytepool/gene_set_enrichment.py
index d473041..484bfe5 100644
--- a/cytepool/gene_set_enrichment.py
+++ b/cytepool/gene_set_enrichment.py
@@ -1,16 +1,18 @@
"""Gene set enrichment analysis (GSEA) of genes ranked by a statistic, as fgsea computes
it.
-:func:`~cytepool.gsea` asks of each gene set whether its genes gather at the top or at
-the bottom of a ranking of all genes by a statistic, such as the moderated t-statistic
-of a contrast: preranked GSEA (Subramanian et al. 2005) as fgsea 1.32.2 computes it
-(Korotkevich et al. 2021), with p-values by fgsea's default multilevel method
-(``fgseaMultilevel``) or by permutations of the genes (``fgseaSimple``). Results match
-fgsea's exactly when CytePool is given the random seeds that fgsea draws, and within
-Monte Carlo error otherwise, except where noted under "Deliberate differences". The
-routines port fgsea's (MIT licence), as ``THIRD_PARTY_NOTICES.md`` records.
-:func:`~cytepool.plotting.plot_enrichment` and
-:func:`~cytepool.plotting.plot_gsea_table` draw its figures.
+:func:`~cytepool.gsea` computes preranked GSEA (Subramanian et al. 2005) as fgsea 1.32.2
+computes it (Korotkevich et al. 2021), for genes ranked by a statistic such as the
+moderated t-statistic, with p-values by fgsea's default multilevel method
+(``fgseaMultilevel``) or by permutations of the genes (``fgseaSimple``), in a table with
+one row per set tested. :func:`~cytepool.plotting.plot_enrichment` and
+:func:`~cytepool.plotting.plot_gsea_table` draw its figures. The routines port fgsea's
+(MIT licence), as ``THIRD_PARTY_NOTICES.md`` records: the results are fgsea's exactly
+when CytePool is given the random seeds that fgsea draws, and within Monte Carlo error
+otherwise. https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#gsea
+explains the method, and
+https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation-and-gsea-limits
+how it differs from fgsea.
The genes and the sets:
The statistics come from a group and contrast of a
@@ -19,70 +21,63 @@
with equal statistics keep the order in which they are given, as in fgsea: the score
of a set whose genes tie with other genes depends on that order, and a warning says
how many statistics tie. Rows without a gene id or without a statistic (NaN) are
- left out, where fgsea refuses them; infinite statistics and ids on two rows are
- refused. Each set's genes are matched to the ranked genes, and a set is tested when
- ``min_size`` to ``max_size`` of its genes are ranked (fgsea's ``minSize`` and
- ``maxSize``); a set of every ranked gene is never tested, as its score is not
- defined. The table keeps the order of ``gene_sets``, where fgsea orders the sets by
- name.
+ left out; infinite statistics and ids on two rows are refused. Each set's genes are
+ matched to the ranked genes, and a set is tested when ``min_size`` to ``max_size``
+ of its genes are ranked (fgsea's ``minSize`` and ``maxSize``); a set of every ranked
+ gene is never tested, as its score is not defined.
The enrichment score:
- With weights |r|^p of the statistics r, p the ``exponent`` (fgsea's
- ``gseaParam``), the walk down the ranking rises at each gene of a set by its share
- of the set's weight and falls at each other gene by 1 / (N - m), for m of the N
- genes in the set. The enrichment score (ES) is the walk's largest deviation from
- zero: with ``score_type="std"``, its maximum where that exceeds the absolute
- minimum, its minimum where the absolute minimum is larger, and 0 where they are
- equal; ``"pos"`` and ``"neg"`` take the maximum and the minimum, for one-sided
- tests. An exponent of 0 gives the unweighted statistic of Mootha et al. (2003), 1
- the weighted one of Subramanian et al. (2005). The leading edge is the set's genes
- from the top of the ranking down to the maximum, for a positive score, or from
- the minimum to the bottom, for a negative one, listed from the most extreme
- statistic inwards: the genes that drive the score.
+ With weights |r|^p of the statistics r, p the ``exponent`` (fgsea's ``gseaParam``),
+ the walk down the ranking rises at each gene of a set by its share of the set's
+ weight and falls at each other gene by 1 / (N - m), for m of the N genes in the set.
+ The enrichment score (ES) is the walk's largest deviation from zero: with
+ ``score_type="std"``, its maximum where that exceeds the absolute minimum, its
+ minimum where the absolute minimum is larger, and 0 where they are equal; ``"pos"``
+ and ``"neg"`` take the maximum and the minimum, for one-sided tests. The leading
+ edge is the set's genes from the top of the ranking down to the maximum, for a
+ positive score, or from the minimum to the bottom, for a negative one, listed from
+ the most extreme statistic inwards.
Random gene sets:
- The null distribution of a set's score is that of random sets of the same size
- (gene permutation). As fgsea does, each of the ``n_permutations`` permutations
- draws one random sample of as many genes as the largest set, and the scores of all
- its first genes, one score for each size, are computed together in
- O(K^(3/2)) time for K genes, from the upper convex hulls of the walk in blocks
- of about sqrt(K) genes (fgsea's ``calcGseaStatCumulative``).
-
- The normalized enrichment score (NES) divides the ES by the mean of the random
- scores of its sign: of those at least 0 for a positive ES, of the absolute values
- of those at most 0 otherwise; it is NaN where that mean is 0 or there is no such
- score. The permutation p-value is (1 + b) / (1 + n), with b the random scores at
- least as extreme as the ES and n those of its sign, so that it is conditional on
- the sign of the score, as GSEA's is.
+ The null distribution of a set's score is that of random sets of the same size. As
+ fgsea does, each of the ``n_permutations`` permutations draws one random sample of
+ as many genes as the largest set, and the scores of all its first genes, one score
+ for each size, are computed together in O(K^(3/2)) time for K genes, from the upper
+ convex hulls of the walk in blocks of about sqrt(K) genes (fgsea's
+ ``calcGseaStatCumulative``). The normalized enrichment score (NES) divides the ES
+ by the mean of the random scores of its sign: of those at least 0 for a positive
+ ES, of the absolute values of those at most 0 otherwise; it is NaN where that mean
+ is 0 or there is no such score. The permutation p-value is (1 + b) / (1 + n), with
+ b the random scores at least as extreme as the ES and n those of its sign, so that
+ it is conditional on the sign of the score, as GSEA's is.
P-values by the multilevel method:
- ``method="multilevel"`` (fgsea's default) starts from the permutation p-values of
- ``n_permutations`` permutations. A set with fewer than 10 random scores of the sign
- of its ES gets NaN for its p-value and NES. A set goes to adaptive multilevel
- splitting (Korotkevich et al. 2021) when its permutation p-value would be less
- accurate than the multilevel estimate: with b' of the n random scores beyond its ES
- on its side, when half the larger distance from log2 of (b' + 1) / (n + 1) to the
- ends of a 95% interval of the proportion (from the 2.5% and 97.5% quantiles of beta
- distributions) exceeds the expected error of the multilevel estimate, as fgsea
- decides; with fgsea's defaults, when b' is at most 23. For each set size,
- ``sample_size`` random sets
- are drawn; at each level the half with the lower positive scores (the largest
- deviation of the walk above zero) is replaced by copies of the other half, each
- of which a Markov chain then moves, swapping one gene at a time and keeping its
- score above the level's median, about ``sample_size`` times the size moves in all.
- The levels continue until the median reaches the ES, the median stops rising, or
- the p-value would fall below ``eps``. The probability of each level, about 1/2, is
- estimated on the log scale by the expected logarithm of an order statistic of
- uniform variables, psi(k) - psi(n + 1) for the k-th of n with psi the digamma
- function, and the p-value is the product over the levels, times the share of the
- sampled sets beyond the ES whose signed score is positive, divided by the share of
- random scores of the ES's sign among the permutations. ``log2_error`` gives the
- expected standard deviation of log2 of the p-value: from the number of levels for
- the multilevel method, and from the trigamma function of the counts for the
- permutation p-values (fgsea's ``log2err``). It is NaN where fewer than half of the
- last level's sets have a positive signed score, which makes the p-value likely
- too large, and where the p-value is below ``eps``, which is then its value: when
- the multilevel method has run, p-values below ``eps`` are reported as ``eps``.
+ ``method="multilevel"`` starts from the permutation p-values of ``n_permutations``
+ permutations. A set with fewer than 10 random scores of the sign of its ES gets NaN
+ for its p-value and NES. A set goes to adaptive multilevel splitting (Korotkevich et
+ al. 2021) when its permutation p-value would be less accurate than the multilevel
+ estimate: with b' of the n random scores beyond its ES on its side, when half the
+ larger distance from log2 of (b' + 1) / (n + 1) to the ends of a 95% interval of the
+ proportion (from the 2.5% and 97.5% quantiles of beta distributions) exceeds the
+ expected error of the multilevel estimate, as fgsea decides; with fgsea's defaults,
+ when b' is at most 23. For each set size, ``sample_size`` random sets are drawn;
+ at each level the half with the lower positive scores (the largest deviation of the
+ walk above zero) is replaced by copies of the other half, which a Markov chain then
+ moves, swapping one gene at a time and keeping each score above the level's
+ median, about ``sample_size`` times the size moves in all. The levels continue
+ until the median reaches the ES, the median stops rising, or the p-value would fall
+ below ``eps``. The probability of each level, about 1/2, is estimated on the log
+ scale by the expected logarithm of an order statistic of uniform variables,
+ psi(k) - psi(n + 1) for the k-th of n with psi the digamma function, and the
+ p-value is the product over the levels, times the share of the sampled sets beyond
+ the ES whose signed score is positive, divided by the share of random scores of the
+ ES's sign among the permutations. ``log2_error`` gives the expected standard
+ deviation of log2 of the p-value: from the number of levels for the multilevel
+ method, and from the trigamma function of the counts for the permutation p-values
+ (fgsea's ``log2err``). It is NaN where fewer than half of the last level's sets
+ have a positive signed score, which makes the p-value likely too large, and where
+ the p-value is below ``eps``, which is then its value: when the multilevel method
+ has run, p-values below ``eps`` are reported as ``eps``.
Random numbers:
``rng`` draws the seeds of the random sets: two distinct integers from 1 to 10^9
@@ -97,20 +92,6 @@
them. A seeded ``rng`` gives the same results every time, with any number of
threads.
-Deliberate differences from fgsea:
- - Adjusted p-values are Benjamini and Hochberg's over the sets with a p-value.
- When no set goes to the multilevel method, fgsea adjusts them over the sets
- before it removes those with fewer than 10 random scores of their sign, as
- though those sets had been tested; its adjusted p-values are then smaller or
- larger than those of the sets it reports.
- - fgsea tests a single set with random sets from R's ``sample.int``, and several
- sets with its own sampler; CytePool uses fgsea's sampler for one set too, so
- that the two agree within Monte Carlo error.
- - Random samples of at least half of the genes are put in random order by the
- Fisher-Yates shuffle (Durstenfeld 1964) with fgsea's random integers, where
- fgsea calls C++'s ``std::shuffle``, whose algorithm the standard leaves to each
- library; the results agree within Monte Carlo error.
-
References:
Subramanian A, Tamayo P, Mootha VK, Mukherjee S, Ebert BL, Gillette MA, Paulovich
A, Pomeroy SL, Golub TR, Lander ES, Mesirov JP (2005). Gene set enrichment
@@ -156,7 +137,7 @@
from ._messages import PACKAGE_DIRECTORY, fitted_message, quoted
from ._options import (
checked_choice,
- number_at_least,
+ non_negative_number,
positive_integer,
random_generator,
real_number,
@@ -249,8 +230,12 @@ def gsea(
(Korotkevich et al. 2021): whether the genes of each set gather at the top or the
bottom of the ranking, with the enrichment score, its leading edge, the score
normalized by those of random sets of the same size, and p-values by fgsea's
- multilevel method or by permutations of the genes. The module docstring gives the
- method, the random numbers and the deliberate differences from fgsea.
+ multilevel method or by permutations of the genes.
+ https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#gsea
+ explains the method, the module documentation its rules and the random numbers,
+ and
+ https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation-and-gsea-limits
+ the differences from fgsea.
Args:
statistics: The statistics that rank the genes: a
@@ -1622,7 +1607,7 @@ def _settings(
f"max_size ({largest}) is smaller than min_size ({smallest}), so no "
"set could be tested"
)
- power = number_at_least(exponent, "exponent", 0.0)
+ power = non_negative_number(exponent, "exponent")
kind = _SCORE_TYPES.index(checked_choice(score_type, "score_type", _SCORE_TYPES))
how = checked_choice(method, "method", _METHODS)
count = positive_integer(n_permutations, "n_permutations")
diff --git a/cytepool/gene_set_tests.py b/cytepool/gene_set_tests.py
index ea7cf46..8db7bf4 100644
--- a/cytepool/gene_set_tests.py
+++ b/cytepool/gene_set_tests.py
@@ -1,117 +1,77 @@
"""Gene-set tests of one contrast of a linear model: limma's camera and fry.
-A gene set, such as a pathway, is tested as a unit for one contrast of the linear model
-fitted to every gene. :func:`~cytepool.camera` is competitive: it asks whether the genes
-of a set are more differentially expressed than the other genes, allowing for the
-correlation between the genes of the set (Wu and Smyth 2012). :func:`~cytepool.fry` is
-self-contained: it asks whether the genes of a set are differentially expressed at all,
-as the rotation test of Wu et al. (2010) with the mean statistic does, without drawing
-rotations (Giner and Smyth 2016). Results match limma 3.62.1 except where noted under
-"Deliberate differences". The rotation tests, which draw rotations (limma's roast,
-mroast and romer), are :func:`~cytepool.roast`, :func:`~cytepool.mroast` and
-:func:`~cytepool.romer`.
-
-Effects:
- Both tests work with the effects of each gene's least squares fit (Wu et al. 2010,
- section 4.1). The design X, n samples by p coefficients, is reparametrised so that
- the contrast is its last coefficient: a contrast given as a column of the design
- moves that column last; a contrast given as weights c becomes the last column, X c /
- (c'c), after the columns X h_2, ..., X h_p, where h_1, ..., h_p are the columns of
- the Householder reflection H of c, so that the last coefficient is c'alpha (limma's
- ``contrastAsCoef``, found by probing). With the Householder QR decomposition (Golub
- and Van Loan 2013) of this design in R's sign convention, a gene's data y give the
- effects Q'y; the p-th is the contrast effect u_0, which divided by the residual
- standard deviation is the gene's t-statistic, and the last d = n - p are its
- residual effects u_1, ..., u_d, independent with variance sigma^2 under the model.
- With precision weights W each gene's data and design are whitened by W^(1/2) first.
- Every sample must be observed, with a positive weight, in every gene, so that all
- genes have the same residual degrees of freedom d.
-
- The residual effects are coordinates in a basis of the residual space, which the
- QR decomposition chooses. With observation weights each gene has a basis of its
- own, and the inter-gene correlation of camera and the statistics of fry depend on
- them; fry's standardizations other than "none" depend on the basis even without
- weights, through the largest effect. The bases are limma's: they depend on how the
- contrast is given, and a pivot of the decomposition that is zero up to rounding
- (below 1e-12 of its column), as in indicator designs with the samples sorted by
- group, counts as zero, which R's ``qr()`` gives the positive sign. limma computes
- most such zeros exactly, but leaves rounding noise in a few percent of genes, a
- different few in camera and in fry, so that their bases, and limma's results, change
- with the last bits of the data there; CytePool's do not (found by probing).
-
-camera:
- Genes get moderated t-statistics, the contrast effect over the posterior standard
- deviation of :func:`~cytepool.stats.empirical_bayes.squeeze_variances` on
- min(d + d0, G d) degrees of freedom (as ``eBayes`` caps them, for G genes), turned
- into z-scores by Hill's (1970) approximation (Brophy 1987, equation 5), as limma's
- ``zscoreT(approx = TRUE, method = "hill")`` does (found by probing). With the mean
- z-score of a set of m genes and the mean and variance of all G z-scores, the test
- statistic is the two-sample t-statistic of the set against the other G - m genes
- with the variance of the set's mean inflated by VIF = 1 + (m - 1) rho (Wu and Smyth
- 2012). The inter-gene correlation rho is a value given for every set, or estimated
- for each set from the residual effects: each gene's are divided by its residual
- standard deviation (its variance bounded below by 1e-8, found by probing), the m x d
- matrix is averaged over genes, and m/d times the sum of squares of that mean
- estimates the VIF. Unless negative correlations are allowed, rho enters the VIF as
- max(rho, 0); a given negative correlation that makes the VIF of a set negative,
- which no correlation matrix allows, gives the set a NaN p-value. The p-value is
- two-sided, from the t distribution on G - 2 degrees of freedom for a given
- correlation and on min(d, G - 2) for an estimated one (found by probing). The rank
- version replaces the t-statistic by the Wilcoxon-Mann-Whitney rank sum of the set's
- z-scores among all genes, with the variance of the rank sum when the set's values
- share a correlation rho (Wu and Smyth 2012, from Barry et al. 2008, equation 4.5),
- times Zar's (1999, section 8.10) correction for ties, 1 - sum(t^3 - t) / (G^3 - G),
- and a continuity correction of 1/2; it is referred to the normal distribution for a
- given correlation and to the t distribution on min(d, G - 2) degrees of freedom for
- an estimated one (found by probing).
-
-fry:
- The effects of each gene are divided by a scale (see ``standardize``), and the set
- statistic is the mean b over the set's genes of these (d + 1)-vectors. Under the
- null hypothesis the d + 1 effects of a gene are independent with equal variances,
- so one rotation applied to the effects of every gene leaves the joint distribution
- of the data unchanged (Wu et al. 2010, section 3.4). Under a uniformly random
- rotation b turns uniformly over its sphere, so T = b_0 / (sum_k b_k^2 / d)^(1/2) has
- the t distribution on d degrees of freedom: the p-value the rotation test of the
- mean statistic would give with infinitely many rotations, exactly so when every
- gene has the same scale. The mixed (non-directional) statistic is the sum over
- genes of the squared standardized contrast effects. Under rotation it is r'(E'E)r
- for the set's m x (d + 1) matrix E of standardized effects and r uniform on the unit
- sphere, so it equals sum_k lambda_k v_k with lambda_k the d + 1 eigenvalues of E'E
- and (v_k) Dirichlet(1/2, ..., 1/2); scaled to [0, 1] by the smallest and largest
- eigenvalue, it is approximated by the beta distribution with its first two moments
- (found by probing to be limma's approximation, for sets of more than d genes). With
- one gene the approximation is exact: the mixed p-value is the two-sided p-value of
- the gene's t-statistic. For more genes it can be much too small in the tail, as
- limma's is: 6.3e-4 where a simulation of the rotation test gives 3.2e-3.
-
- The scales of ``standardize="residual_sd"`` and ``"posterior_sd"`` start from the
- mean square of a gene's d + 1 effects without the largest, which leaves out the
- contrast effect of a strongly differentially expressed gene (found by probing).
- ``"posterior_sd"`` divides it by its expected value under the null hypothesis,
- which makes it unbiased for sigma^2, and moderates it with the prior of the
- residual variances as a variance on 0.92 d degrees of freedom, fewer than d since
- the largest effect is left out (limma's rule, found by probing).
-
-Deliberate differences from limma:
- - fry's mixed p-value of a set of 2 to d genes. limma uses only the set's m nonzero
- eigenvalues and treats them as an m-dimensional problem (found by probing), which
- can be far from the rotation p-value; here all d + 1 dimensions are used, as for
- larger sets. ``validation/gene_set_tests.py`` compares both with the rotation
- p-value by simulation.
- - fry's ``standardize="posterior_sd"``. limma's expected value of the trimmed mean
- square is about 7.3e-5 (d + 1) / d above the exact one used here (found by
- probing), which moves p-values by about 1e-4 of their value.
- - camera's rank test with continuity correction can give both tails above 1/2;
- limma then reports twice the smaller, a p-value above 1, and here it is 1.
- - A gene listed twice in a set counts once; limma counts it twice.
- - A set without genes, or containing every gene in camera, has NaN p-values and no
- direction, and is left out of the adjusted p-values of the other sets. limma
- reports the direction "Up", in camera's rank test the p-value 2, which it adjusts
- with the others, and in fry a mixed p-value of 0 for an empty set.
- - camera takes ``robust``: the moderated t-statistics of the robust empirical Bayes
- estimator (Phipson et al. 2016), each turned into a z-score on its own degrees of
- freedom. limma's camera has no such option and ignores it.
+:func:`~cytepool.camera` asks whether the genes of each set are more differentially
+expressed for a contrast than the other genes, allowing for the correlation between the
+genes of the set (Wu and Smyth 2012), and :func:`~cytepool.fry` whether they are
+differentially expressed at all, in one direction or in either, by the limit of the
+rotation test of Wu et al. (2010) with the mean statistic over infinitely many rotations
+(Giner and Smyth 2016). Each returns a table with one row per set. The rotation tests,
+which draw rotations, are :func:`~cytepool.roast`, :func:`~cytepool.mroast` and
+:func:`~cytepool.romer`. https://docs.nygen.io/CytePool/statistics/gene-set-tests#camera
+explains the tests, and
+https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits how they
+differ from limma's.
+
+Both tests work with the effects of each gene's least squares fit (Wu et al. 2010,
+section 4.1): with the design reparametrised so that the contrast is its last
+coefficient, the QR decomposition of the design, whitened by the gene's precision
+weights, gives the gene's contrast effect u_0 and its d residual effects
+(https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-effects). Every sample
+must be observed, with a positive weight, in every gene, so that all genes have the same
+residual degrees of freedom d, and the design must be of fixed effects only, of full
+column rank, with d of at least 1.
+
+Matching limma:
+ The results match limma 3.62.1. limma's documentation gives some of the conventions
+ below, and running limma on chosen inputs gave the others.
+
+ - The design is reparametrised as limma's ``contrastAsCoef`` does: a contrast given
+ as a column of the design moves that column last; a contrast given as weights c
+ becomes the last column, X c / (c'c), after the columns X h_2, ..., X h_p, where
+ h_1, ..., h_p are the columns of the Householder reflection H of c, so that the
+ last coefficient is c'alpha.
+ - The QR decomposition is Householder's (Golub and Van Loan 2013) in R's sign
+ convention, so that the residual effects are in limma's basis. With observation
+ weights each gene has a basis of its own, and the correlation that camera
+ estimates and the statistics of fry depend on it; fry's standardizations other
+ than "none" depend on the basis even without weights, through the largest
+ effect. The basis depends on how the contrast is given, and a pivot that is zero
+ up to rounding (below 1e-12 of its column), as in indicator designs with the
+ samples sorted by group, counts as zero, which R's ``qr()`` gives the positive
+ sign.
+ - camera's moderated t-statistics are the contrast effects over the posterior
+ standard deviations of :func:`~cytepool.stats.empirical_bayes.squeeze_variances`,
+ on min(d + d0, G d) degrees of freedom (as ``eBayes`` caps them, for G genes),
+ turned into z-scores by Hill's (1970) approximation (Brophy 1987, equation 5), as
+ limma's ``zscoreT(approx = TRUE, method = "hill")`` does.
+ - camera's estimated correlation divides each gene's residual effects by its
+ residual standard deviation (its variance bounded below by 1e-8), averages the
+ m x d matrix over the set's m genes, and takes m/d times the sum of squares of
+ that mean as the variance inflation factor, VIF = 1 + (m - 1) rho (Wu and Smyth
+ 2012). Unless negative correlations are allowed, rho enters the VIF as
+ max(rho, 0); a given negative correlation that makes the VIF of a set negative,
+ which no correlation matrix allows, gives the set a NaN p-value.
+ - camera's p-value is two-sided, from the t distribution on G - 2 degrees of
+ freedom for a given correlation and on min(d, G - 2) for an estimated one, and a
+ set whose two tails are equal goes "up". The rank version uses the variance of
+ the rank sum when the set's values share a correlation rho (Wu and Smyth 2012,
+ from Barry et al. 2008, equation 4.5), times Zar's (1999, section 8.10)
+ correction for ties, 1 - sum(t^3 - t) / (G^3 - G), with a continuity correction
+ of 1/2, referred to the normal distribution for a given correlation and to the t
+ distribution on min(d, G - 2) degrees of freedom for an estimated one.
+ - fry's mixed p-value is limma's beta approximation for sets of more than d genes.
+ Under a rotation r, the sum of the set's squared standardized contrast effects
+ is r'(E'E)r for the set's m x (d + 1) matrix E of standardized effects, so it
+ equals sum_k lambda_k v_k, with lambda_k the d + 1 eigenvalues of E'E and (v_k)
+ Dirichlet(1/2, ..., 1/2); scaled to [0, 1] by the smallest and largest
+ eigenvalue, it is given the beta distribution with its first two moments.
+ - fry's scales for ``standardize="residual_sd"`` and ``"posterior_sd"`` start from
+ the mean square of a gene's d + 1 effects without the largest, which leaves out
+ the contrast effect of a strongly differentially expressed gene.
+ ``"posterior_sd"`` divides it by its expected value under the null hypothesis,
+ which makes it unbiased for sigma^2, and moderates it with the prior of the
+ residual variances as a variance on 0.92 d degrees of freedom, fewer than d since
+ the largest effect is left out.
References:
Wu D, Smyth GK (2012). Camera: a competitive gene set test accounting for inter-gene
@@ -222,10 +182,14 @@ def camera(
limma's ``camera`` (Wu and Smyth 2012): tests whether the genes of each set are more
differentially expressed for ``contrast`` than the other genes, by a two-sample test
of their moderated z-scores against those of the other genes, with the variance of
- the set's mean inflated by the correlation between its genes. The module docstring
- gives the method. The default correlation of 0.01 is limma's (since limma 3.29.6):
- it ranks co-regulated, interpretable sets higher than a correlation estimated for
- each set, whose error rate control is strict.
+ the mean of a set of m genes inflated by VIF = 1 + (m - 1) rho for a correlation
+ rho between them.
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#camera explains the test,
+ the module documentation the conventions that match limma, and
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits the
+ differences from it. The default correlation of 0.01 is limma's (since limma
+ 3.29.6): it ranks co-regulated, interpretable sets higher than a correlation
+ estimated for each set, whose error rate control is strict.
A result of :func:`~cytepool.limma_voom` or :func:`~cytepool.limma_trend` is tested
as limma's camera tests the fit of one group: its values and weights, the design
@@ -273,13 +237,16 @@ def camera(
estimated, may shrink the variance of a set's mean; otherwise it counts as
zero.
use_ranks: Whether to test the ranks of the z-scores (a correlation-adjusted
- Wilcoxon-Mann-Whitney test) rather than their values.
+ Wilcoxon-Mann-Whitney test) rather than their values. Where the
+ continuity correction leaves both tails above 1/2, the p-value is 1;
+ limma reports twice the smaller tail, above 1.
trend: Whether the prior variance of the moderation follows the mean
log-expression of the genes (limma's ``trend.var``); None takes the
moderation of a result (limma-trend's trend), and False otherwise.
- robust: Whether to moderate the variances with the robust estimator, which
- limma's camera does not offer; None takes the result's setting, and
- False otherwise.
+ robust: Whether to moderate the variances with the robust estimator
+ (Phipson et al. 2016), which limma's camera does not offer; each gene's
+ moderated t-statistic then becomes a z-score on its own degrees of
+ freedom. None takes the result's setting, and False otherwise.
Returns:
A table with one row per gene set, in the order of ``gene_sets``: ``gene_set``;
@@ -288,7 +255,9 @@ def camera(
one; ``direction``, ``"up"`` or ``"down"`` (None where the p-value is NaN);
``p_value``, two-sided; and ``adjusted_p_value``, Benjamini-Hochberg over the
sets (Benjamini and Hochberg 1995). A set with no genes, or with every gene,
- has NaN p-values.
+ has NaN p-values and no direction, and the adjustment leaves it out; limma
+ reports "Up" for it, and in the rank test the p-value 2, which it adjusts
+ with the others.
Raises:
TypeError: If an argument has the wrong type, or is or holds a numpy masked
@@ -304,9 +273,9 @@ def camera(
"""
rho = _correlation_option(inter_gene_correlation)
allow_negative = checked_flag(
- allow_negative_correlation, "allow_negative_correlation", expected="a bool"
+ allow_negative_correlation, "allow_negative_correlation"
)
- ranks = checked_flag(use_ranks, "use_ranks", expected="a bool")
+ ranks = checked_flag(use_ranks, "use_ranks")
read = gene_set_data(
data,
group=group,
@@ -399,12 +368,18 @@ def fry(
limma's ``fry`` (Giner and Smyth 2016): tests whether the genes of each set are
differentially expressed for ``contrast``, in the same direction (the directional
p-value) or in either (the mixed p-value), by the limit of the rotation test of Wu
- et al. (2010) with the mean statistic over infinitely many rotations. The module
- docstring gives the method and the deliberate differences from limma. limma's
- gene contribution weights (``gene.weights``, or weights inside a set), which give
- genes signs and sizes in the set statistic, are not implemented. A result of
- :func:`~cytepool.limma_voom` or :func:`~cytepool.limma_trend` is tested as for
- :func:`camera`.
+ et al. (2010) with the mean statistic over infinitely many rotations: the mean b of
+ the set's standardized effects gives T = b_0 / (sum_k b_k^2 / d)^(1/2) on d degrees
+ of freedom, and the mixed p-value is a beta approximation to the rotation p-value
+ of the sum of the set's squared standardized contrast effects, from all d + 1
+ dimensions of the rotation, also when the set has 2 to d genes, where limma uses
+ only the set's own m. https://docs.nygen.io/CytePool/statistics/gene-set-tests#fry
+ explains the test, the module documentation the conventions, and
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits the
+ differences from limma. limma's gene contribution weights (``gene.weights``, or
+ weights inside a set), which give genes signs and sizes in the set statistic, are
+ not implemented. A result of :func:`~cytepool.limma_voom` or
+ :func:`~cytepool.limma_trend` is tested as for :func:`camera`.
Args:
data: A differential-expression result, a VoomResult or log-expression
@@ -438,7 +413,9 @@ def fry(
Benjamini-Hochberg over the sets; ``mixed_p_value``, the non-directional
p-value; and ``mixed_adjusted_p_value``. A set with no genes has NaN p-values,
and so, with ``standardize="residual_sd"``, has a set containing a gene whose
- d + 1 effects are zero but for at most one, which cannot be scaled.
+ d + 1 effects are zero but for at most one, which cannot be scaled; such a set
+ has no direction, and the adjustment leaves it out. For a set with no genes,
+ limma reports "Up" and a mixed p-value of 0.
Raises:
TypeError: If an argument has the wrong type, or is or holds a numpy masked
@@ -453,12 +430,8 @@ def fry(
"standardize must be one of "
f"{', '.join(map(repr, _STANDARDIZATIONS))}, got {standardize!r}"
)
- given_trend = (
- None if trend is None else checked_flag(trend, "trend", expected="a bool")
- )
- given_robust = (
- None if robust is None else checked_flag(robust, "robust", expected="a bool")
- )
+ given_trend = None if trend is None else checked_flag(trend, "trend")
+ given_robust = None if robust is None else checked_flag(robust, "robust")
if (given_trend or given_robust) and standardize != "posterior_sd":
raise ValueError(
"trend and robust shape the prior of the posterior standard deviations, so "
@@ -575,6 +548,8 @@ def _hill_z_scores(t: FloatArray, df: FloatArray) -> FloatArray:
+ 100))) with w^2 = (df - 1/2) log(1 + t^2 / df) and b = 48 (df - 1/2)^2 (Brophy
1987, equation 5). Infinite t stay infinite.
"""
+ # camera's z-scores, as limma's zscoreT(approx = TRUE, method = "hill") gives them
+ # (found by probing).
with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
a = df - 0.5
b = 48.0 * a * a
@@ -706,6 +681,8 @@ def _trimmed_mean_square_expectation(d: int) -> float:
z > 0 of 2 z (1 - (1 - 2 Q(z))^k), Q the normal upper tail, by composite
Gauss-Legendre quadrature; 1 - 2/pi for d = 1.
"""
+ # The exact expectation; limma's is about 7.3e-5 (d + 1) / d above it (found by
+ # probing), a deliberate difference.
k = float(d + 1)
total = 0.0
for panel in range(_PANELS):
@@ -726,6 +703,9 @@ def _mixed_p_value(standardized: FloatArray, d: int) -> float:
to [0, 1] by the smallest and largest of the d + 1 eigenvalues lambda_k of E'E, it
is given the beta distribution with the same mean and variance.
"""
+ # limma's approximation for sets of more than d genes (found by probing). For
+ # sets of 2 to d genes limma uses only the set's m nonzero eigenvalues, as an
+ # m-dimensional problem (found by probing); all d + 1 are used here, on purpose.
n_effects = d + 1
singular = np.linalg.svd(standardized, compute_uv=False)
eigenvalues = np.zeros(n_effects)
diff --git a/cytepool/gene_sets.py b/cytepool/gene_sets.py
index 7591471..10c5899 100644
--- a/cytepool/gene_sets.py
+++ b/cytepool/gene_sets.py
@@ -4,55 +4,36 @@
id, and keeps the description of each set and where the collection comes from: its name,
version and licence, as the user states them from what the provider of the collection
documents, and, for a collection read from a file, the name of the file and its SHA-256
-digest. CytePool bundles no collection: MSigDB (Liberzon et al. 2011), Reactome,
-WikiPathways and the Gene Ontology distribute their sets under their own terms, and
-:func:`~cytepool.read_gmt` reads the files that users download from them.
+digest. :func:`~cytepool.read_gmt` reads one from a GMT file, and
+:func:`~cytepool.write_gmt` writes one. CytePool bundles no collection: MSigDB (Liberzon
+et al. 2011), Reactome and WikiPathways distribute their sets as GMT files under their
+own terms, as do the GMT exports of the Gene Ontology, and :func:`~cytepool.read_gmt`
+reads the files that users download from them.
+https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-collections
+explains how a collection is matched to an analysis.
A ``GeneSets`` is a mapping of set name to genes, so every gene-set function of CytePool
takes it as it is: as ``gene_sets`` of :func:`~cytepool.camera`, :func:`~cytepool.fry`,
-:func:`~cytepool.mroast`, :func:`~cytepool.romer` and
-:func:`~cytepool.over_representation`, and one of its sets, as ``gene_sets[name]``, as
-``gene_set`` of :func:`~cytepool.roast`. The tests match the genes of each set to the
-column ``id_column`` of a result's features, or to the ``feature_ids`` of an array.
-
-Genes and sets:
- A set's genes are a tuple of gene ids (non-empty strings, compared exactly, case
- included), each once, in the order given: a gene listed twice keeps its first
- place, and the genes of a Python ``set`` are taken in sorted order, so that every
- run lists them alike. Set names are non-empty strings. A description of ``"na"``,
- which GSEA writes for a set without one, or an empty description, is None.
+:func:`~cytepool.mroast`, :func:`~cytepool.romer`,
+:func:`~cytepool.over_representation`, :func:`~cytepool.gsea` and the pathway scores,
+and one of its sets, as ``gene_sets[name]``, as ``gene_set`` of :func:`~cytepool.roast`.
+The tests match the genes of each set to the column ``id_column`` of a result's
+features, or to the ``feature_ids`` of an array.
Matching and filtering:
- :meth:`~cytepool.GeneSets.restrict_to` keeps the genes of each set that an analysis
- measured and drops the sets left without genes, as limma's ``ids2indices`` does by
- default, and refuses ids that match no gene at all, as when the sets name genes by
- symbol and the analysis by Ensembl id. :meth:`~cytepool.GeneSets.filter_by_size`
- keeps the sets of a range of sizes; restricted first, the sizes are those within the
- analysis, as GSEA counts them before it leaves out sets of fewer than 15 or more
- than 500 genes, its defaults (GSEA team 2026). :meth:`~cytepool.GeneSets.select`
- takes sets by name. Each returns a new ``GeneSets`` with the descriptions and the
- record of the collection.
+ :meth:`~cytepool.GeneSets.restrict_to` matches the sets to the genes of an
+ analysis, :meth:`~cytepool.GeneSets.filter_by_size` keeps the sets whose sizes lie
+ in a range, and :meth:`~cytepool.GeneSets.select` takes sets by name. Each returns
+ a new ``GeneSets`` with the descriptions and the record of the collection.
GMT files:
The GMT (gene matrix transposed) format of GSEA (Subramanian et al. 2005; GSEA
team 2026) holds one gene set per line, in fields separated by tabs: the set's
- name, a description, and the set's genes. :func:`~cytepool.read_gmt` reads:
-
- - fields separated by tabs only, so that names with spaces, such as Reactome's
- pathway names, are read whole;
- - lines that end in a line feed, a carriage return and line feed, or a carriage
- return; blank lines are skipped;
- - empty fields among the genes, such as a tab at the end of a line, which are
- skipped, and spaces at either end of a field, which are removed;
- - a set without genes (a name and a description only), kept with no genes;
- - UTF-8 text, with or without a byte-order mark, compressed by gzip or not
- (recognised by the first bytes of the file, whatever its name).
-
- A line with one field, a set without a name, or a name used for two sets is refused,
- with the number of the line. :func:`~cytepool.write_gmt` writes the same format,
- with ``"na"`` for a set without a description, so that :func:`~cytepool.read_gmt`
- reads back the same sets and descriptions. A GMT file holds no name, version or
+ name, a description, and the set's genes. A GMT file holds no name, version or
licence of the collection; they are given again when the file is read.
+ :func:`~cytepool.read_gmt` says what it reads, and :func:`~cytepool.write_gmt`
+ writes the same format, so that :func:`~cytepool.read_gmt` reads back the same sets
+ and descriptions.
References:
Subramanian A, Tamayo P, Mootha VK, Mukherjee S, Ebert BL, Gillette MA, Paulovich
@@ -101,9 +82,14 @@ class GeneSets(Mapping[str, tuple[str, ...]]):
"""Named gene sets, with their descriptions and where the collection comes from.
A read-only mapping of set name to the set's genes, a tuple of gene ids, so that
- the gene-set functions of CytePool take it as their ``gene_sets``. The module
- docstring says how ids, names and descriptions are read. ``read_gmt`` builds one
- from a GMT file. Immutable.
+ the gene-set functions of CytePool take it as their ``gene_sets``. ``read_gmt``
+ builds one from a GMT file. Immutable.
+
+ A set's genes are gene ids (non-empty strings, compared exactly, case included),
+ each once, in the order given: a gene listed twice keeps its first place, and the
+ genes of a Python ``set`` are taken in sorted order, so that every run lists them
+ alike. Set names are non-empty strings. A description of ``"na"``, which GSEA
+ writes for a set without one, or an empty description, is None.
Args:
sets: Mapping of set name to the set's genes, as gene ids (strings): a list,
@@ -328,7 +314,9 @@ def restrict_to(self, genes: Any, *, keep_empty: bool = False) -> "GeneSets":
Ensembl id.
"""
if not isinstance(keep_empty, bool | np.bool_):
- raise TypeError(f"keep_empty must be True or False, got {keep_empty!r}")
+ raise TypeError(
+ f"keep_empty must be True or False, got {type(keep_empty).__name__}"
+ )
measured = set(gene_ids(genes, "genes")[0])
kept: dict[str, tuple[str, ...]] = {}
matched = 0
@@ -406,14 +394,11 @@ def __repr__(self) -> str:
digest = f", SHA-256 {self._sha256}" if self._sha256 else ""
lines.append(f" read from {self._file}{digest}")
if sizes:
- shown = [
+ described = [
f"{name} ({len(members):,})".replace(" ", KEEP_TOGETHER)
- for name, members in list(self._sets.items())[:_SETS_SHOWN]
+ for name, members in self._sets.items()
]
- more = count - len(shown)
- lines.append(
- " sets: " + ", ".join(shown) + (f" and {more:,} more" if more else "")
- )
+ lines.append(" sets: " + listing(described, limit=_SETS_SHOWN))
return fitted_lines("\n".join(lines))
def _record(self) -> dict[str, str | None]:
@@ -444,10 +429,22 @@ def read_gmt(
One set per line: its name, a description and its genes, separated by tabs, as
MSigDB, Reactome, WikiPathways and the GMT exports of the Gene Ontology write
- them; the module docstring says what is read and how. The collection records the
- name of the file and the SHA-256 digest of its bytes, and the name, version and
- licence given here, which the file cannot hold: state them as the provider
- documents them, so that results can say which sets they used.
+ them. It reads:
+
+ - fields separated by tabs only, so that names with spaces, such as Reactome's
+ pathway names, are read whole;
+ - lines that end in a line feed, a carriage return and line feed, or a carriage
+ return; blank lines are skipped;
+ - empty fields among the genes, such as a tab at the end of a line, which are
+ skipped, and spaces at either end of a field, which are removed;
+ - a set without genes (a name and a description only), kept with no genes;
+ - UTF-8 text, with or without a byte-order mark, compressed by gzip or not
+ (recognised by the first bytes of the file, whatever its name).
+
+ Names, genes and descriptions are then read as :class:`GeneSets` reads them. The
+ collection records the name of the file and the SHA-256 digest of its bytes, and
+ the name, version and licence given here, which the file cannot hold: state them
+ as the provider documents them, so that results can say which sets they used.
Args:
path: The GMT file, compressed by gzip or not.
@@ -556,7 +553,9 @@ def write_gmt(
reject_masked(gene_sets, "gene_sets")
collection = gene_sets if isinstance(gene_sets, GeneSets) else GeneSets(gene_sets)
if not isinstance(overwrite, bool | np.bool_):
- raise TypeError(f"overwrite must be True or False, got {overwrite!r}")
+ raise TypeError(
+ f"overwrite must be True or False, got {type(overwrite).__name__}"
+ )
location = _file_path(path)
lines = []
for name, genes in collection.items():
@@ -662,7 +661,7 @@ def _size(value: Any, name: str) -> int:
raise TypeError(f"{name} must be an integer, got {type(value).__name__}")
size = int(value)
if size < 0:
- raise ValueError(f"{name} must be at least 0, got {size}")
+ raise ValueError(f"{name} must be non-negative, got {size}")
return size
@@ -692,7 +691,7 @@ def _file_path(path: Any) -> Path:
raise TypeError(f"path must be a str or os.PathLike, got {type(path).__name__}")
text = os.fspath(path)
if not isinstance(text, str):
- raise TypeError("path must be text, not bytes")
+ raise TypeError("path must be text, got bytes")
return Path(text)
diff --git a/cytepool/held_out_studies.py b/cytepool/held_out_studies.py
index a23a9e7..6a0d9da 100644
--- a/cytepool/held_out_studies.py
+++ b/cytepool/held_out_studies.py
@@ -1,16 +1,17 @@
"""Prediction of case status in held-out studies, from genes chosen in the others.
-Genes found by combining studies should separate cases from controls in a study that had
-no part in finding them. :func:`~cytepool.held_out_prediction` measures this as the
-paper that introduced SumRank does (Nakatsuka et al. 2025): each study of a collection
-is left out in turn; genes up and genes down in each cell type are chosen from the other
-studies, by SumRank (:func:`~cytepool.stats.sumrank.fit_sumrank`) and, for comparison,
-by a random-effects meta-analysis
-(:func:`~cytepool.stats.meta_analysis.fit_meta_analysis`); every sample of the held-out
-study is scored by how high its profiles rank those genes; and the area under the ROC
-curve (AUC) of the score says how well it separates the study's cases from its controls.
-:func:`~cytepool.plotting.plot_held_out_auc` draws the AUC of every held-out study by
-method.
+:func:`~cytepool.held_out_prediction` leaves out the studies of a collection in turn,
+chooses genes up and down in each cell type from the other studies, by SumRank
+(:func:`~cytepool.stats.sumrank.fit_sumrank`) and by a random-effects meta-analysis
+(:func:`~cytepool.stats.meta_analysis.fit_meta_analysis`), scores each sample of the
+held-out study by how high its profiles rank those genes, and returns the area under the
+ROC curve (AUC) of the scores of the study's cases against its controls, in a
+:class:`~cytepool.HeldOutPrediction`, as the paper that introduced SumRank compares
+methods (Nakatsuka et al. 2025). :func:`~cytepool.plotting.plot_held_out_auc` draws the
+AUCs by study and method.
+https://docs.nygen.io/CytePool/statistics/replication#held-out-prediction explains the
+method, and https://docs.nygen.io/CytePool/statistics/replication#replication-limits how
+it differs from the paper's.
Choosing the genes:
In every group (cell type), from the studies other than the held-out one whose
@@ -63,26 +64,6 @@
direction; the AUC is NaN, with the reason, for a study without a case or
without a control.
-Deliberate differences from the paper:
- - The paper fits a logistic regression of case status on the score in the other
- studies and takes the AUC of its predictions in the held-out study from
- pROC's ``auc``, whose default ``direction = "auto"`` compares the medians of
- the held-out cases and controls and takes the direction in which the cases
- score higher (Robin et al. 2011). A logistic regression on one score is
- monotone in it, so that AUC is the score's in the direction pROC chooses,
- which puts a score that ranks controls above cases above 0.5 too. Here the
- cases are expected to score higher, as genes up minus genes down should make
- them, so the logistic regression is not needed.
- - The profiles are the summed counts, which rank the genes of a sample as their
- mean over its cells does; the paper ranked Seurat's average of the cells'
- normalized expression, which weighs each cell's counts by its depth.
-
-Not done:
- The number of studies summed is given, not chosen: the paper chose the proportion
- whose genes predict best, which a loop over the numbers gives. The paper's
- prediction of disease severity in cases (a linear regression on the score) is
- not done.
-
References:
Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
Improving reproducibility of differentially expressed genes in single-cell
@@ -275,8 +256,13 @@ def held_out_prediction(
chosen from the results of the other studies, by SumRank and by a
random-effects meta-analysis; each case and control of the held-out study is
scored by how high its profiles rank those genes, as UCell scores them; and the
- AUC says how well the scores separate cases from controls. The module
- description gives each step. The results are those that
+ AUC says how well the scores separate cases from controls. A sample's score is
+ UCell's rank score of the chosen genes in each cell type, scaled over the
+ held-out study's samples, the genes up counted for it and the genes down against
+ it, summed over the cell types; the AUC is the probability that a case scores
+ higher than a control, ties counting one half.
+ https://docs.nygen.io/CytePool/statistics/replication#held-out-prediction explains
+ the method. The results are those that
:func:`~cytepool.sumrank` takes: each study's analysis of its aligned cube.
Args:
@@ -513,7 +499,7 @@ def _groups(collection: CubeCollection, value: Any) -> list[str]:
def _n_features(value: Any) -> int:
if isinstance(value, bool | np.bool_):
- raise TypeError("n_features must be a whole number of genes, not bool")
+ raise TypeError("n_features must be a whole number of genes, got bool")
number = index_integer(value, "n_features", expected="a whole number of genes")
if number < 1:
raise ValueError(f"n_features must be at least 1, got {number}")
diff --git a/cytepool/io/_files.py b/cytepool/io/_files.py
index 21f499b..c8a02a7 100644
--- a/cytepool/io/_files.py
+++ b/cytepool/io/_files.py
@@ -18,7 +18,7 @@ def local_path(path: Any) -> Path:
raise TypeError(f"path must be a str or os.PathLike, got {type(path).__name__}")
text = os.fspath(path)
if not isinstance(text, str):
- raise TypeError("path must be text, not bytes")
+ raise TypeError("path must be text, got bytes")
if "://" in text:
raise ValueError(
f"{text!r} is a URL; only local directories are supported (zarr-python "
diff --git a/cytepool/io/anndata.py b/cytepool/io/anndata.py
index 867c9a8..e33bf32 100644
--- a/cytepool/io/anndata.py
+++ b/cytepool/io/anndata.py
@@ -12,55 +12,26 @@
samples as its observations, as LIANA's ``liana.multi.adata_to_views`` (Dimitrov et
al. 2024) makes it: ``to_mudata`` and ``from_mudata``.
+An export keeps a record in ``uns["cytepool"]`` from which an import gives back the
+same cube, fingerprint included, as
+https://docs.nygen.io/CytePool/reference/cube-files#anndata-record specifies. An
+import takes only sums and counts of cells, since a cube's layers are sums
+(https://docs.nygen.io/CytePool/statistics/cubes#profiles-from-other-tools explains
+why); ``from_anndata`` gives the rules, and ``to_anndata`` and ``to_mudata`` the
+layouts they write.
+
anndata and mudata are optional dependencies: install CytePool's ``anndata`` or
``mudata`` extra, or the packages themselves. Only ``to_anndata`` and ``to_mudata``
import them; ``from_anndata`` and ``from_mudata`` read the object they are given.
-What an import takes:
- A cube's layers are sums over the cells of each sample and group, so an import
- takes only what is such a sum or gives one exactly: summed counts, the number of
- cells detecting each feature, and sums of normalized or log-normalized values.
- The source of a cube layer is ``X`` or a layer of the AnnData (of every modality,
- for a MuData), read as sums, except where its name says what scanpy or decoupler
- stored in it:
-
- - ``count_nonzero`` (scanpy) counts the cells with a nonzero value, and gives
- ``detected`` only;
- - ``psbulk_props`` (decoupler) is the fraction of cells with a nonzero value. It
- gives ``detected`` with the cell numbers: each fraction times its number of
- cells must be a whole number, to within the precision the fractions are stored
- in, and rounding it recovers the number of cells exactly;
- - ``mean``, ``var`` and ``median`` hold means, variances and medians, which are
- refused. A variance or a median gives no sum, and a mean times its number of
- cells gives the sum exactly only where the sum is a whole number; every tool
- that gives means gives sums too: aggregate with ``func="sum"`` (scanpy) or
- ``mode="sum"`` (decoupler).
-
- Counts and ``detected`` become integers, as ``from_cells`` makes them: uint32
- when every value fits, uint64 otherwise, and float64 for counts that are not
- whole numbers; sums of whole numbers that a tool stored as floats become
- integers. Counts are checked against the cells that ``detected`` counts: a sum
- of counts is positive exactly where a cell has a positive count, and a sum of
- whole counts (counts that come as integers, or from a recognised layout) is at
- least the number of such cells, which means, medians and counts of other cells
- break.
-
- A combination of sample and group without a row is an entry without cells,
- which only cell numbers can record: an import with such a combination and no
- cell numbers is refused. A row removed after pooling, as by decoupler's
- ``filter_samples`` or LIANA's ``min_cells``, is such a combination too, so its
- cells are then missing from merged groups and pooled samples of the cube:
- import the profiles before filtering them, and leave out samples with few cells
- in the analysis instead (``limma_voom`` has ``min_cells``).
-
Recognised layouts:
The arguments left as None take their values from the layout of the data; a
long AnnData that CytePool did not make still needs ``sample_column=`` and
``group_column=``, the columns of its labels.
- - CytePool's own export, which ``uns["cytepool"]`` marks (see "The record in
- uns" below): every layer, the cell numbers, the tables, the normalization and
- the label columns, as they were exported.
+ - CytePool's own export, which ``uns["cytepool"]`` marks: every layer, the cell
+ numbers, the tables, the normalization and the label columns, as they were
+ exported.
- ``decoupler.pp.pseudobulk`` (an ``obs`` column ``psbulk_cells``, or
``psbulk_n_cells`` as older versions name it): counts from ``X``, cell numbers
from that column, and ``detected`` from ``psbulk_props``. Whatever ``layers``
@@ -86,65 +57,13 @@
import counts that are not, or to read a ``sum`` of normalized values. Data of
no recognised layout need ``layers``.
-Export:
- ``to_anndata`` writes a row for every entry with cells (every entry of a cube
- without cell numbers), the rows of each group together, groups and samples in
- the cube's order. Entries without cells have no row, as in
- ``scanpy.get.aggregate``. ``obs`` holds ``sample_id`` and ``group_id``, as
- categorical columns, the other columns of the sample and group tables, and
- ``n_cells``; ``obs_names`` are ``_``, or the row positions
- when those repeat. ``var`` holds the feature table, indexed by feature id, and
- each layer of the cube is a layer of the same name. ``X`` is left empty, so
- that no layer is stored twice, unless ``x_layer`` names a layer to copy into it:
- ``to_anndata(cube, x_layer="counts")`` gives tools that read ``X`` the counts.
-
- ``to_mudata`` writes a modality per group, named by the group id, whose
- observations are the samples with cells in that group (every sample, without
- cell numbers), named by sample id, with ``n_cells`` in its ``obs``, every layer
- under its own name, and ``X`` empty. Its ``var_names`` are ``:``, as LIANA writes them, so that they are unique across modalities; ``var``
- holds the other columns of the feature table. The columns of the sample table
- are on the MuData's ``obs``. A MuData file cannot store a modality whose name
- holds ``/`` or NUL, or is ``.`` or ``..``: rename such a group first with
- ``Cube.regroup``.
-
- anndata writes only some columns, so table columns are written in a form it
- stores and reads back as they were: datetimes as ISO 8601 text (ISO 2019), ``NaT``
- where missing, timedeltas as int64 counts of their unit, and a text column without
- any value as float64 NaN. Column names have ``_`` in place of ``/`` and NUL,
- and ``.``, ``..`` and ``_index``, which anndata reserves, end in ``_``. A sample
- or group column whose name is taken is prefixed with ``sample_`` or ``group_``.
-
-The record in uns:
- ``uns["cytepool"]`` (of the AnnData, or of the MuData) holds ``format``
- (``"cytepool.cube.anndata"`` or ``"cytepool.cube.mudata"``), ``format_version``
- (1), the cube's ``fingerprint``, and as JSON text its ``provenance`` and
- ``normalization``, its ``layers`` with the type of each, whether it has cell
- numbers (``n_cells``), its ``samples`` and ``groups`` tables, with the samples
- and groups that have no row, and the name, ``var`` column and type of each
- column of its ``features`` table. Only text and integers are stored, which
- every anndata writer keeps.
-
- Reading the record, an import gives back the cube: the same tables (column
- types included), layers of the same types and the same values, so the same
- fingerprint, unless the data were changed in between. A layer converted to
- another type is converted back when that keeps its values; a layer whose values
- its type no longer holds was changed, and is refused (give ``layers`` to read
- it as what it holds). Every argument overrides what the record gives. The
- tables of samples and groups come from the record, so ``obs`` columns added
- later are not read; features come from ``var``, so a subset of features gives a
- cube of those features. The provenance is the exported cube's, followed by a
- record of the import with the fingerprint that the export stored:
- ``cube.fingerprint() == adata.uns["cytepool"]["fingerprint"]`` says whether the
- AnnData still holds the exported cube.
-
Example:
>>> import numpy as np
- >>> from cytepool.cube import from_cells
+ >>> from cytepool import from_cells
>>> cube = from_cells(
... np.array([[1, 0], [3, 2], [0, 4]]),
- ... samples=["S1", "S1", "S2"],
- ... groups=["B", "T", "B"],
+ ... sample_labels=["S1", "S1", "S2"],
+ ... group_labels=["B", "T", "B"],
... layers=["counts", "detected"],
... )
>>> adata = to_anndata(cube) # doctest: +SKIP
@@ -239,16 +158,27 @@
def to_anndata(cube: Cube, *, x_layer: str | None = None) -> Any:
"""Return a cube as a long AnnData, with a row per sample and group with cells.
- The rows of each group come together, groups and samples in the cube's order; a
- cube without cell numbers gives a row for every entry. ``obs`` holds
- ``sample_id`` and ``group_id`` (categorical), the other columns of the sample and
- group tables, and ``n_cells``; ``var`` the feature table, indexed by feature id;
- each layer of the cube is a layer of the same name, and ``X`` is empty unless
- ``x_layer`` names a layer to copy into it, as tools that read ``X`` need.
- ``uns["cytepool"]`` holds the cube's fingerprint and provenance, and what
- ``from_anndata`` needs to give back the cube with the same fingerprint. The
- module documentation describes the layout ("Export") and the record ("The record
- in uns").
+ The rows of each group come together, groups and samples in the cube's order.
+ Entries without cells have no row, as in ``scanpy.get.aggregate``, and a cube
+ without cell numbers gives a row for every entry. ``obs`` holds ``sample_id`` and
+ ``group_id`` (categorical), the other columns of the sample and group tables, and
+ ``n_cells``; ``obs_names`` are ``_``, or the row positions
+ when those repeat. ``var`` holds the feature table, indexed by feature id, and each
+ layer of the cube is a layer of the same name. ``X`` is empty, so that no layer is
+ stored twice, unless ``x_layer`` names a layer to copy into it, as tools that read
+ ``X`` need.
+
+ anndata writes only some columns, so table columns are written in a form it
+ stores and reads back as they were: datetimes as ISO 8601 text (ISO 2019),
+ ``NaT`` where missing, timedeltas as int64 counts of their unit, and a text column
+ without any value as float64 NaN. Column names have ``_`` in place of ``/`` and
+ NUL, and ``.``, ``..`` and ``_index``, which anndata reserves, end in ``_``. A
+ sample or group column whose name is taken is prefixed with ``sample_`` or
+ ``group_``.
+
+ ``uns["cytepool"]`` holds the cube's fingerprint, its provenance and what
+ ``from_anndata`` needs to give back the cube with the same fingerprint, the record
+ that https://docs.nygen.io/CytePool/reference/cube-files#anndata-record specifies.
Args:
cube: The cube. A lazy cube is read one group at a time.
@@ -336,10 +266,61 @@ def from_anndata(
Each row is the profile of the cells of one sample and group, as
``scanpy.get.aggregate`` and ``decoupler.pp.pseudobulk`` make them; a combination
without a row is an entry without cells. With the defaults, the layout of the
- AnnData chooses the source of each layer and of the cell numbers, and an AnnData
- that ``to_anndata`` made gives back its cube with the same fingerprint. The
- module documentation says what each source may hold ("What an import takes") and
- which layouts are recognised.
+ AnnData chooses the source of each layer and of the cell numbers (the module
+ documentation lists the layouts it recognises), and an AnnData that
+ ``to_anndata`` made gives back its cube with the same fingerprint.
+
+ What an import takes:
+ A cube's layers are sums over the cells of each sample and group, so an import
+ takes only what is such a sum or gives one exactly
+ (https://docs.nygen.io/CytePool/statistics/cubes#profiles-from-other-tools):
+ summed counts, the number of cells detecting each feature, and sums of
+ normalized or log-normalized values. The source of a cube layer is ``X`` or a
+ layer of the AnnData, read as sums, except where its name says what scanpy or
+ decoupler stored in it:
+
+ - ``count_nonzero`` (scanpy) counts the cells with a nonzero value, and gives
+ ``detected`` only;
+ - ``psbulk_props`` (decoupler) is the fraction of cells with a nonzero value.
+ It gives ``detected`` with the cell numbers: each fraction times its number
+ of cells must be a whole number, to within the precision the fractions are
+ stored in, and rounding it recovers the number of cells exactly;
+ - ``mean``, ``var`` and ``median`` hold means, variances and medians, which
+ are refused, as the chapter linked above explains; every tool that gives
+ means gives sums too: aggregate with ``func="sum"`` (scanpy) or
+ ``mode="sum"`` (decoupler).
+
+ Counts and ``detected`` become integers: uint32 when every value fits, uint64
+ otherwise, and float64 for counts that are not whole numbers, as ``from_cells``
+ makes counts; sums of whole numbers that a tool stored as floats become
+ integers. Counts are checked against the cells that ``detected`` counts: a sum
+ of counts is positive exactly where a cell has a positive count, and a sum of
+ whole counts (counts that come as integers, or from a recognised layout) is at
+ least the number of such cells, which means, medians and counts of other cells
+ break.
+
+ A combination of sample and group without a row is an entry without cells,
+ which only cell numbers can record: an import with such a combination and no
+ cell numbers is refused. A row removed after pooling, as by decoupler's
+ ``filter_samples`` or LIANA's ``min_cells``, is such a combination too, so its
+ cells are then missing from merged groups and pooled samples of the cube:
+ import the profiles before filtering them, and leave out samples with few
+ cells in the analysis instead (``limma_voom`` has ``min_cells``).
+
+ An AnnData that CytePool exported:
+ Reading the record in ``uns["cytepool"]``, an import gives back the cube: the
+ same tables (column types included), layers of the same types and the same
+ values, so the same fingerprint, unless the data were changed in between. A
+ layer converted to another type is converted back when that keeps its values;
+ a layer whose values its type no longer holds was changed, and is refused
+ (give ``layers`` to read it as what it holds). Every argument overrides what
+ the record gives. The tables of samples and groups come from the record, so
+ ``obs`` columns added later are not read; features come from ``var``, so a
+ subset of features gives a cube of those features. The provenance is the
+ exported cube's, followed by a record of the import with the fingerprint that
+ the export stored, so that
+ ``cube.fingerprint() == adata.uns["cytepool"]["fingerprint"]`` says whether
+ the AnnData still holds the exported cube.
Args:
adata: The AnnData.
@@ -372,8 +353,9 @@ def from_anndata(
Returns:
A cube. Its provenance holds ``source`` and how the AnnData was read; for
an AnnData that CytePool made, it is the provenance of the exported cube,
- followed by a record of this import (``cytepool.cube.provenance``
- documents both).
+ followed by a record of this import
+ (https://docs.nygen.io/CytePool/reference/cube-files#provenance-keys
+ specifies both).
Raises:
TypeError: If ``adata`` is not an AnnData, an argument has the wrong type,
@@ -511,11 +493,14 @@ def to_mudata(cube: Cube) -> Any:
group (every sample, for a cube without cell numbers), named by sample id, with
``n_cells`` in its ``obs``, every layer of the cube under its own name, ``X``
empty, and the feature table in ``var``, whose names are ``:`` as LIANA writes them. The columns of the sample table are on
- the MuData's ``obs``, and ``uns["cytepool"]`` holds the cube's fingerprint,
- provenance and what ``from_mudata`` needs to give back the cube with the same
- fingerprint (see "Export" and "The record in uns" in the module
- documentation).
+ id>:`` as LIANA writes them, so that they are unique across
+ modalities. The columns of the sample table are on the MuData's ``obs``, and
+ table columns and their names are written as ``to_anndata`` writes them. A MuData
+ file cannot store a modality whose name holds ``/`` or NUL, or is ``.`` or
+ ``..``: rename such a group first with ``Cube.regroup``. ``uns["cytepool"]``
+ holds the cube's fingerprint, its provenance and what ``from_mudata`` needs to
+ give back the cube with the same fingerprint
+ (https://docs.nygen.io/CytePool/reference/cube-files#anndata-record).
Args:
cube: The cube. A lazy cube is read one group at a time.
@@ -588,8 +573,10 @@ def from_mudata(
``var_names``, without the prefix ``:`` that LIANA adds. The
feature table is the ``var`` of the first modality, and the samples' metadata
the columns of the MuData's ``obs``. With the defaults, the layout chooses the
- sources, and a MuData that ``to_mudata`` made gives back its cube with the same
- fingerprint (see the module documentation).
+ sources (the module documentation lists the layouts it recognises), and a MuData
+ that ``to_mudata`` made gives back its cube with the same fingerprint. Every
+ modality is read as ``from_anndata`` reads an AnnData ("What an import takes"
+ and "An AnnData that CytePool exported" there).
Args:
mdata: The MuData.
diff --git a/cytepool/io/cube_files.py b/cytepool/io/cube_files.py
index 561d56e..4386a6b 100644
--- a/cytepool/io/cube_files.py
+++ b/cytepool/io/cube_files.py
@@ -1,101 +1,20 @@
"""Cube files: a cube saved as a Zarr v3 group and read as it is used.
-A cube file is a directory, by convention named ``.cube.zarr``, holding a Zarr v3
-group, as the Zarr core specification (Miles et al. 2025) defines it, that any Zarr v3
-reader can open. ``write_cube`` saves a cube. ``read_cube`` opens one: it reads the
-metadata and the sample, group and feature tables, and leaves the layers on disk as
-``ZarrArray`` objects that the cube reads one group at a time, and only the chunks
-that hold the features it needs, so a cube far larger than memory can be opened and
-analysed group by group, or for a few features of every group.
-
-Format ``cytepool.cube``, version 1::
-
- .cube.zarr/
- zarr.json the root group; its attributes are listed below
- samples/ a group with one 1-D array per sample column
- groups/ a group with one 1-D array per group column
- features/ a group with one 1-D array per feature column
- n_cells integers (n_samples, n_groups); absent from bulk cubes
- layers/ a group with one array per layer
- counts (n_groups, n_samples, n_features)
- ... detected, log_sum, log_sum_sq and linear_sum, when present
-
-Root attributes (others are ignored, so a publisher may add, say, a title):
-
-- ``format``: ``"cytepool.cube"``.
-- ``format_version``: ``1``. A reader rejects other versions; any change to the layout
- increments the version.
-- ``axes``: ``["groups", "samples", "features"]``, the axes of every layer, in order.
-- ``layers``: the layer names, in the order ``LAYER_NAMES`` gives them.
-- ``has_n_cells``: whether the ``n_cells`` array is present (false for bulk cubes,
- where cell numbers are unknown).
-- ``normalization``: the cube's normalization mapping, or null.
-- ``provenance``: the cube's provenance mapping (``cytepool.cube.provenance``).
-- ``digests``: the digests of the cube's tables, cell numbers, normalization and of
- each group of each layer, and the fingerprint they make, as ``Cube.digests()``
- returns them (``cytepool.cube.fingerprints`` documents them). ``write_cube``
- computes them while it writes the layers. A file written before CytePool stored
- digests has none; it is read all the same, and its fingerprint is computed from
- its values when it is asked for.
-
-Tables. Each table group holds one 1-D array per column, with ``dimension_names`` set
-to the table's axis (``["samples"]``, ``["groups"]`` or ``["features"]``). Its
-attributes give the columns in table order: ``columns`` lists the column names and
-``arrays`` the name of each column's array. An array takes its column's name when that
-name is portable: ASCII letters, digits, ``_``, ``.`` and ``-``, at most 64
-characters, not starting with ``.``, ``-`` or ``__``, not ending with ``.``, not a
-Windows device name (``con``, ``nul``, ``com1``, ...), not ``zarr.json`` in any case,
-not of the form ``column_``, and not equal to another column's name when case is
-ignored.
-Otherwise the array is named ``column_``, counting from 0. Columns are stored
-as:
-
-- text: int32 codes with the attribute ``levels``, the distinct values in order of
- first appearance; code ``i`` stands for ``levels[i]`` and ``-1`` for a missing value;
-- datetimes and timedeltas: int64 counts of their unit, with the attribute
- ``numpy_dtype`` (for example ``"datetime64[s]"``); NaT is ``-2**63``, as in numpy;
-- booleans and numbers: their own data type.
-
-Layers have ``dimension_names`` ``["groups", "samples", "features"]`` and chunks of one
-group, every sample and up to 4,096 features, so reading one group reads only its own
-chunks, and reading some features of it only the chunks that hold them. (A chunk holds
-fewer samples only where it would otherwise exceed 256 MiB, ``LAYER_CHUNK_BYTES``:
-beyond 8,192 samples of float64 values.) ``n_cells`` is int32
-(int64 if a value does not fit), with ``dimension_names`` ``["samples", "groups"]``.
-
-Every array has the default chunk key encoding with ``/``, a fill value of zero
-(``false`` for bool) and the ``bytes`` codec, little endian, followed by ``gzip`` (the
-default, level 4), ``zstd`` (level 3, with checksums; Python 3.14 or newer) or no
-compression: the codecs of the Zarr specifications (Zarr developers 2023;
-Maitin-Shepard 2025), which store gzip streams (RFC 1952; Deutsch 1996) and Zstandard
-frames (RFC 8878; Collet and Kucherawy 2021). Chunks that hold only zeros are not
-stored, as Zarr allows, and read as zeros.
-
-Reading is strict: the format name and version, every attribute above, every node's
-metadata (see ``cytepool.io.zarr``), dimension names, data types and text codes are
-checked, and so are the values of every layer group when the cube reads it. The
-tables, cell numbers and normalization are checked against their stored digests when
-the file is opened, and each group of a layer whenever a read covers all of it, in
-any order, so a file changed after it was written is reported, not read as if it
-were the cube it held; ``Cube.verify_fingerprint()`` checks every group, also the
-groups that a selection of some samples or features reads in part. Digests describe values, not their
-encoding: a copy with other chunks, byte order or compression keeps them. A store
-replaced while ``read_cube`` reads it raises an error instead of mixing the tables of
-one cube with the layers of another, and so does a lazy layer whose file was replaced
-after it was opened.
-
-Writing is atomic: ``write_cube`` writes the store into a new hidden directory next to
-the target, ``..partial-``, writes the root ``zarr.json`` last, and
-renames the directory to the target only when everything is written. A write that
-fails or is interrupted removes the partial directory and leaves nothing at the
-target. With ``overwrite=True``, which replaces only a cube file or an empty
-directory, the old store is renamed aside to ``..replaced-``, the new one
-renamed into place and the old one deleted; if the new store cannot be moved into
-place, the old one is put back. A process killed outright can leave either hidden
-directory behind (between the two renames, the old store survives only under its
-hidden name); a partial directory has no root ``zarr.json``, so it does not open as a
-group. Files are not flushed to disk with fsync, so the guarantee covers failures of
-the writing process, not of the operating system.
+``write_cube`` saves a cube as a cube file: a directory, by convention named
+``.cube.zarr``, holding a Zarr v3 group, as the Zarr core specification (Miles et
+al. 2025) defines it, that any Zarr v3 reader can open. ``read_cube`` opens one: it
+reads the metadata and the sample, group and feature tables, and leaves the layers on
+disk as ``ZarrArray`` objects that the cube reads one group at a time, and only the
+chunks that hold the features it needs, so a cube far larger than memory can be opened
+and analysed group by group, or for a few features of every group. Reading checks what
+it reads against the digests stored with the file, and writing is atomic.
+
+The format, ``cytepool.cube`` version 1, is specified, for programs that read or write
+cube files, at https://docs.nygen.io/CytePool/reference/cube-files#cube-file-format.
+Its arrays use the ``bytes`` and ``gzip`` codecs of the Zarr specifications (Zarr
+developers 2023), or the ``zstd`` codec of its extensions (Maitin-Shepard 2025), which
+store gzip streams (RFC 1952; Deutsch 1996) and Zstandard frames (RFC 8878; Collet and
+Kucherawy 2021).
References:
Miles A, Striebel J, Rzepka N, Maitin-Shepard J, Moore J, eds (2025). Zarr core
@@ -179,7 +98,10 @@ def write_cube(
compression_level: int | None = None,
overwrite: bool = False,
) -> None:
- """Save a cube as a cube file, a Zarr v3 group (see the module documentation).
+ """Save a cube as a cube file, a Zarr v3 group.
+
+ https://docs.nygen.io/CytePool/reference/cube-files#cube-file-format specifies
+ the file.
The store appears at ``path`` only once it is complete, so a write that fails
leaves no store there; with ``overwrite=True`` it leaves the old store as it was.
@@ -213,7 +135,9 @@ def write_cube(
if not isinstance(cube, Cube):
raise TypeError(f"write_cube() saves a Cube, got {type(cube).__name__}")
if not isinstance(overwrite, bool):
- raise TypeError(f"overwrite must be True or False, got {overwrite!r}")
+ raise TypeError(
+ f"overwrite must be True or False, got {type(overwrite).__name__}"
+ )
level = check_compression(compression, compression_level)
target = local_path(path)
if not target.parent.is_dir():
diff --git a/cytepool/io/zarr.py b/cytepool/io/zarr.py
index 0318a81..808a79e 100644
--- a/cytepool/io/zarr.py
+++ b/cytepool/io/zarr.py
@@ -4,6 +4,11 @@
Zarr core specification, version 3.1 (Miles et al. 2025), that cube files need, with
the ``bytes`` and ``gzip`` codecs of the Zarr specifications (Zarr developers 2023)
and the ``zstd`` codec of its registered extensions (Maitin-Shepard 2025).
+:func:`open_array` opens an array for reading as a :class:`ZarrArray`,
+:func:`read_group_attributes` reads the attributes of a group, and
+:func:`write_group` and :func:`write_array` write groups and arrays;
+https://docs.nygen.io/CytePool/reference/cube-files#cube-file-format specifies the
+cube files made of them.
Reading supports:
@@ -44,11 +49,6 @@
decoding step stops just past the most bytes a valid chunk can produce, and a chunk
may hold at most 1,024 gzip members or zstd frames (writers use one).
-Writing produces arrays with a regular chunk grid, the default chunk key encoding with
-``/``, a fill value of zero (``false`` for bool) and the ``bytes`` codec, little
-endian, followed by ``gzip``, ``zstd`` (with a checksum) or no compression. A chunk
-whose bytes are all zero equals the fill value and is not stored.
-
References:
Miles A, Striebel J, Rzepka N, Maitin-Shepard J, Moore J, eds (2025). Zarr core
specification, version 3.1. https://zarr-specs.readthedocs.io/en/latest/v3/core/
@@ -548,9 +548,12 @@ def write_array(
) -> None:
"""Write an array as a new Zarr v3 array directory.
- Chunks are written first and ``zarr.json`` last, so a write that fails leaves a
- directory that does not open as an array. A chunk whose bytes are all zero is not
- stored, since zero is the fill value.
+ The array has a regular chunk grid, the default chunk key encoding with ``/``, a
+ fill value of zero (``false`` for bool) and the ``bytes`` codec, little endian,
+ followed by ``gzip``, ``zstd`` (with a checksum) or no compression. Chunks are
+ written first and ``zarr.json`` last, so a write that fails leaves a directory
+ that does not open as an array. A chunk whose bytes are all zero equals the fill
+ value and is not stored.
Args:
path: The directory to create; its parent must exist and it must not.
diff --git a/cytepool/meta_analysis_of_studies.py b/cytepool/meta_analysis_of_studies.py
index c459c90..1747b1e 100644
--- a/cytepool/meta_analysis_of_studies.py
+++ b/cytepool/meta_analysis_of_studies.py
@@ -1,16 +1,13 @@
"""Meta-analysis of the differential expression of several studies, feature by feature.
-The same cell types studied in several cohorts, tissues or platforms give, for every
-feature, a log2 fold change and its standard error in each study.
-:func:`~cytepool.meta_analysis` combines them into an estimate of a common log2 fold
-change for every group (cell type), contrast and feature, with the between-study
-variance and the heterogeneity statistics, as metafor's ``rma.uni`` does for one feature
-without moderators (Viechtbauer 2010);
-:func:`~cytepool.stats.meta_analysis.fit_meta_analysis` computes it, and its
-documentation gives the model, the estimators of the between-study variance and the
-tests. The studies are matched by group, contrast and feature id, and the result keeps
-the studies' results it combined, so that :func:`~cytepool.plotting.plot_forest` draws a
-feature's estimate in every study.
+:func:`~cytepool.meta_analysis` combines, for every group (cell type), contrast and
+feature, the log2 fold changes and standard errors of several studies' results into an
+estimate of a common log2 fold change with its between-study variance and
+heterogeneity statistics, as metafor's ``rma.uni`` computes them for a model without
+moderators (Viechtbauer 2010), and returns a :class:`~cytepool.MetaAnalysisResult`
+whose tables adjust the p-values over the features (Benjamini and Hochberg 1995);
+:func:`~cytepool.stats.meta_analysis.fit_meta_analysis` computes each meta-analysis.
+https://docs.nygen.io/CytePool/statistics/meta-analysis explains the method.
References:
Viechtbauer W (2010). Conducting meta-analyses in R with the metafor package.
@@ -280,8 +277,8 @@ def meta_analysis(
"""Meta-analyse the differential expression of several studies, feature by feature.
Each study is the result of :func:`~cytepool.limma_voom` or
- :func:`~cytepool.limma_trend` on its own cube, such as one cohort, tissue or
- platform: a :class:`~cytepool.StudyResults` from
+ :func:`~cytepool.limma_trend` on its own cube, such as the cube of one population,
+ tissue or platform: a :class:`~cytepool.StudyResults` from
:func:`~cytepool.analyse_each_study`, or a mapping of study name to result. For
every group (cell type) and contrast, the log2 fold changes of the studies that
tested it, with their standard errors (the tables' ``log2_fold_change`` and
@@ -634,7 +631,7 @@ def _check_table(table: Table, study: str, group: str, contrast: str) -> None:
def _min_studies(value: Any) -> int:
if isinstance(value, bool | np.bool_):
- raise TypeError("min_studies must be a whole number of studies, not bool")
+ raise TypeError("min_studies must be a whole number of studies, got bool")
number = index_integer(value, "min_studies", expected="a whole number of studies")
if number < 1:
raise ValueError(f"min_studies must be at least 1, got {number}")
diff --git a/cytepool/networks.py b/cytepool/networks.py
index 5389f2b..121d8a1 100644
--- a/cytepool/networks.py
+++ b/cytepool/networks.py
@@ -2,48 +2,45 @@
CollecTRI's transcription factors and PROGENy's pathways, downloaded from their
releases, and networks given as tables.
-A :class:`~cytepool.Network` maps each source, a transcription factor or a pathway, to
-its targets, the genes it regulates or that respond to it, each with a weight whose sign
-says whether the source raises the gene (positive) or lowers it (negative). The activity
-functions (:func:`~cytepool.ulm`, :func:`~cytepool.mlm` and
-:func:`~cytepool.weighted_aggregate`) take one, as decoupler takes its networks
-(Badia-i-Mompel et al. 2022): a table with the columns ``source``, ``target`` and
-``weight``. A network records its name, version, where it comes from, its licence and
-its citation, and, for a network read from a file, the name of the file and its SHA-256
-digest.
+A :class:`~cytepool.Network` holds the target genes of its sources, transcription
+factors or pathways, with signed weights, as the activity functions
+(:func:`~cytepool.ulm`, :func:`~cytepool.mlm` and :func:`~cytepool.weighted_aggregate`)
+take it, and as decoupler takes its networks (Badia-i-Mompel et al. 2022): a table with
+the columns ``source``, ``target`` and ``weight``. A network records its name, version,
+where it comes from, its licence and its citation, and, for a network read from a file,
+the name of the file and its SHA-256 digest. :func:`~cytepool.collectri` and
+:func:`~cytepool.progeny` return the two networks that CytePool downloads.
+https://docs.nygen.io/CytePool/statistics/activities#regulator-networks explains the
+networks, https://docs.nygen.io/CytePool/statistics/activities#activities-limits how
+they differ from decoupler's, and
+https://docs.nygen.io/CytePool/reference/datasets#networks gives their files, licences
+and the terms of CollecTRI's resources.
CollecTRI (:func:`~cytepool.collectri`):
- The signed regulons of 1,186 human transcription factors, 43,175 interactions,
- that Mueller-Dott et al. (2023) assembled from twelve resources of literature
- curation and text mining: version 2.0 of the file ``CollecTRI_regulons.csv`` of
- Zenodo record 8192729 (doi:10.5281/zenodo.8192729), which decoupler 2.2.0 reads
- too, under the Creative Commons Attribution 4.0 licence. Each interaction has a
- weight of 1 (activation) or -1 (repression), the resources that list it, the
- PubMed ids of its references and how its sign was decided. The complexes AP1
- and NFKB are sources of their own, as in the file.
-
- Licence levels. Each resource keeps its own terms, which OmniPath records
- (Tuerei et al. 2021). ``licence="commercial"``, the default, keeps the
- interactions that at least one resource listed for commercial use supports:
- every resource but GEREDB, which OmniPath lists for academic use (under the
- Academic Free License 3.0), so that the 3,279 interactions that GEREDB alone
- supports are left out, 39,896 remain. ``licence="academic"`` keeps every
- interaction. The terms of the other resources, as OmniPath lists them: CC BY 4.0
- (ExTRI, NTNU Curated, TFactS, SIGNOR, IntAct, CytReg, GOA and Pavlidis2021),
- CC BY-SA 4.0 (TRRUST), LGPL 3.0 (HTRIdb) and an informal licence (DoRothEA's
- level A). Use under any level is subject to these terms, such as attribution.
+ The signed regulons of human transcription factors that Mueller-Dott et al. (2023)
+ assembled from twelve resources of literature curation and text mining: version 2.0
+ of the file ``CollecTRI_regulons.csv`` of Zenodo record 8192729
+ (doi:10.5281/zenodo.8192729), which decoupler 2.2.0 reads too, under the Creative
+ Commons Attribution 4.0 licence. Each interaction has a weight of 1 (activation) or
+ -1 (repression), the resources that list it, the PubMed ids of its references and
+ how its sign was decided. The complexes AP1 and NFKB are sources of their own, as in
+ the file. Each resource keeps its own terms, which OmniPath records (Tuerei et al.
+ 2021): ``licence="commercial"``, the default, keeps the interactions that at least
+ one resource listed for commercial use supports, which leaves out those that
+ GEREDB, listed for academic use, alone supports; ``licence="academic"`` keeps every
+ interaction. Use under any level is subject to the terms of the resources it keeps.
PROGENy (:func:`~cytepool.progeny`):
The weights of the genes that respond to 14 signalling pathways, from the
perturbation experiments of Schubert et al. (2018), for human, and their
transfer to mouse (Holland, Szalai and Saez-Rodriguez 2020): the data frames
- ``model_human_full`` (22,479 genes) and ``model_mouse_full`` (17,426 genes) of
- the R package progeny 1.28.0, from its Bioconductor 3.20 release, under the
- Apache License 2.0, with a weight (a z-score) and a p-value for every gene and
- pathway. By default the genes with a p-value below 0.05 are kept, as decoupler's
- ``op.progeny`` keeps them; ``top`` keeps the genes of each pathway with the
- smallest p-values, those tied with the last one included, as progeny's
- ``getModel`` does (100 genes by default there).
+ ``model_human_full`` and ``model_mouse_full`` of the R package progeny 1.28.0, from
+ its Bioconductor 3.20 release, under the Apache License 2.0, with a weight (a
+ z-score) and a p-value for every gene and pathway. By default the genes with a
+ p-value below 0.05 are kept, as decoupler's ``op.progeny`` keeps them; ``top``
+ keeps the genes of each pathway with the smallest p-values, those tied with the
+ last one included, as progeny's ``getModel`` does (100 genes by default there).
+ Genes are named by the symbols that progeny 1.28.0 holds.
Downloads and the cache:
Each network is downloaded on first use from the address it was published at,
@@ -115,7 +112,7 @@
from ._columns import is_data_frame, text_column
from ._gene_ids import is_missing, listed_values
from ._masked_arrays import real_array, reject_masked
-from ._messages import KEEP_TOGETHER, close_matches, fitted_lines, quoted
+from ._messages import KEEP_TOGETHER, close_matches, fitted_lines, listing, quoted
from ._options import checked_choice, positive_integer, real_number
from .gene_sets import GeneSets
from .table import Table
@@ -461,14 +458,11 @@ def __repr__(self) -> str:
if self._file is not None:
digest = f", SHA-256 {self._sha256}" if self._sha256 else ""
lines.append(f" read from {self._file}{digest}")
- shown = [
+ described = [
f"{source} ({len(self._targets[source]):,})".replace(" ", KEEP_TOGETHER)
- for source in self._sources[:_SOURCES_SHOWN]
+ for source in self._sources
]
- more = len(self._sources) - len(shown)
- lines.append(
- " sources: " + ", ".join(shown) + (f" and {more:,} more" if more else "")
- )
+ lines.append(" sources: " + listing(described, limit=_SOURCES_SHOWN))
return fitted_lines("\n".join(lines))
def _record(self) -> dict[str, str | None]:
@@ -497,21 +491,26 @@ def collectri(
"""The CollecTRI network of human transcription factors and their target genes.
The signed regulons of Mueller-Dott et al. (2023), version 2.0 of its Zenodo
- record, at the licence level asked for: the module docstring says what each
- level holds and the terms of each resource. The file is downloaded on first use,
- checked against its pinned size and SHA-256 digest, and kept in the cache.
+ record: 43,175 interactions of 1,186 transcription factors, each with a weight of
+ 1 or -1, at the licence level asked for. Each of CollecTRI's twelve resources
+ keeps its own terms, which OmniPath records;
+ https://docs.nygen.io/CytePool/reference/datasets#networks lists them. The file
+ is downloaded on first use, checked against its pinned size and SHA-256 digest,
+ and kept in the cache.
Args:
- licence: ``"commercial"``, the interactions of resources that OmniPath lists
- for commercial use (39,896), or ``"academic"``, every interaction
- (43,175).
+ licence: ``"commercial"``, the interactions that at least one resource that
+ OmniPath lists for commercial use supports (39,896, which leaves out the
+ 3,279 that GEREDB, listed for academic use, alone supports), or
+ ``"academic"``, every interaction (43,175).
cache_dir: The directory that holds the downloaded file; by default
``cytepool/networks`` under ``XDG_CACHE_HOME`` or ``~/.cache``.
Returns:
A network with the columns ``source``, ``target``, ``weight`` (1 or -1),
``resources`` (the resources that list the interaction, separated by
- ``";"``), ``references`` (PubMed ids, separated by ``";"``) and
+ ``";"``), ``references`` (PubMed ids, separated by ``";"``; empty for the 185
+ interactions that the file lists without any, which are kept) and
``sign_decision``, in the order of the file.
Raises:
@@ -553,13 +552,14 @@ def progeny(
them.
The weights of the R package progeny 1.28.0 (Schubert et al. 2018; Holland,
- Szalai and Saez-Rodriguez 2020), under the Apache License 2.0: each pathway's
- genes in increasing order of their p-values, those with a p-value below
- ``max_p_value``, as decoupler's ``op.progeny`` keeps them, and, with ``top``,
- only the ``top`` genes with the smallest p-values, those tied with the last one
- included, as progeny's ``getModel`` keeps them. The package is downloaded on
- first use, checked against its pinned size and SHA-256 digest, and kept in the
- cache.
+ Szalai and Saez-Rodriguez 2020), from Bioconductor 3.20, under the Apache
+ License 2.0, with genes named by the symbols that the package holds: each
+ pathway's genes in increasing order of their p-values, those with a p-value
+ below ``max_p_value``, as decoupler's ``op.progeny`` keeps them, and, with
+ ``top``, only the ``top`` genes with the smallest p-values, those tied with the
+ last one included, as progeny's ``getModel`` keeps them. The package is
+ downloaded on first use, checked against its pinned size and SHA-256 digest, and
+ kept in the cache.
Args:
organism: ``"human"`` or ``"mouse"``.
diff --git a/cytepool/normalization.py b/cytepool/normalization.py
index 91e913b..5f29cac 100644
--- a/cytepool/normalization.py
+++ b/cytepool/normalization.py
@@ -1,4 +1,18 @@
-"""Library-size normalization and expression filtering for count data.
+"""Normalization factors, counts per million and filtering by expression, for counts.
+
+:func:`tmm_factors`, :func:`upper_quartile_factors` and :func:`rle_factors` give the
+normalization factors of edgeR's ``normLibSizes`` (``calcNormFactors``), which multiply
+the library sizes (:func:`library_sizes`) into effective library sizes;
+:func:`median_ratio_size_factors` gives DESeq2's size factors, which take the place of
+the library sizes; :func:`cpm` and :func:`log2_cpm` give counts per million and log2
+counts per million as edgeR's ``cpm`` computes them; and :func:`filter_by_expression`
+says which features edgeR's ``filterByExpr`` keeps (Robinson and Oshlack 2010; Bullard
+et al. 2010; Anders and Huber 2010; Chen, Lun and Smyth 2016).
+https://docs.nygen.io/CytePool/statistics/normalization-and-filtering#normalization-and-size-factors
+explains each and when to choose it, and
+https://docs.nygen.io/CytePool/statistics/normalization-and-filtering#normalization-limits
+how they differ from edgeR; each function's Raises names the inputs it refuses where
+edgeR returns a value.
Counts are samples x features (rows are samples, columns are genes): finite,
non-negative, integer or not. They are converted to float64, which holds every
@@ -7,31 +21,20 @@
around. R, whose integers have 32 bits, holds counts that large as doubles too.
A library size, a total of a feature's counts or a count per million beyond the
float64 range (about 1.8e308) raises OverflowError instead of becoming infinite
-or NaN. The functions reproduce edgeR and DESeq2:
-
-- normalization factors as edgeR's ``normLibSizes`` (``calcNormFactors``)
- computes them: :func:`tmm_factors`, the trimmed mean of M values (Robinson and
- Oshlack 2010), :func:`upper_quartile_factors` (Bullard et al. 2010) and
- :func:`rle_factors` (Anders and Huber 2010). They multiply the library sizes into
- effective library sizes, ``library_sizes * norm_factors``, and have geometric
- mean one;
-- DESeq2's size factors, which take the place of the library sizes:
- :func:`median_ratio_size_factors`, the median-of-ratios method of Anders and
- Huber (2010);
-- counts per million and log counts per million as edgeR's ``cpm`` computes
- them: :func:`cpm` and :func:`log2_cpm`;
-- edgeR's ``filterByExpr`` (Chen, Lun and Smyth 2016): :func:`filter_by_expression`.
-
-The routines repeat R's floating-point operations wherever an operation
-decides a discrete outcome: the rank-based trimming and the reference sample
-of TMM, and the comparisons of ``filterByExpr``. Sums and means that R
-accumulates in extended precision (``sum``, ``mean``, ``colSums``,
-``rowMeans``) are accumulated with error compensation and rounded once.
-Logarithms and exponentials come from the C math library, as in R. With these,
-the results agree with edgeR and DESeq2 to the last bit on the validation
-inputs (numba set up to use Intel's SVML library may change the last bit).
-Conventions that the documentation of edgeR, DESeq2 or R leaves open were
-found by running them as black boxes; comments mark them "found by probing".
+or NaN.
+
+Matching edgeR and DESeq2:
+ The functions follow edgeR 4.4.0's ``calcNormFactors``, ``cpm`` and
+ ``filterByExpr``, and DESeq2 1.46.0's size factors. Their documentation gives the
+ methods, and running them on chosen inputs settled the conventions it leaves open,
+ which comments in the code record. The routines repeat R's floating-point
+ operations wherever an operation decides a discrete outcome: the rank-based
+ trimming and the reference sample of TMM, and the comparisons of
+ ``filterByExpr``. Sums and means that R accumulates in extended precision
+ (``sum``, ``mean``, ``colSums``, ``rowMeans``) are accumulated with error
+ compensation and rounded once, and logarithms and exponentials come from the C
+ math library, as in R. numba set up to use Intel's SVML library may change the
+ last bit.
References:
Robinson MD, Oshlack A (2010). A scaling normalization method for differential
@@ -159,7 +162,7 @@ def tmm_factors(
is closest to the mean of that ratio over samples (the first one
when several are equally close). When the median of the ratios is
below 1e-20, as with very sparse counts, it is the sample with the
- largest sum of square-root counts instead (found by probing).
+ largest sum of square-root counts instead, as in edgeR.
log_ratio_trim: Fraction of features trimmed from each end of the M
values, in [0, 0.5).
sum_trim: Fraction of features trimmed from each end of the A values,
@@ -183,7 +186,9 @@ def tmm_factors(
trim_m = _fraction(log_ratio_trim, "log_ratio_trim")
trim_a = _fraction(sum_trim, "sum_trim")
if not isinstance(weighted, bool | np.bool_):
- raise TypeError(f"weighted must be a bool, got {type(weighted).__name__}")
+ raise TypeError(
+ f"weighted must be True or False, got {type(weighted).__name__}"
+ )
cutoff = real_number(a_cutoff, "a_cutoff", finite=False)
expressed = _expressed(y)
if expressed.shape[1] == 0:
@@ -253,8 +258,8 @@ def rle_factors(
Over the features with positive counts in every sample, a sample's counts
are divided by the features' geometric means across samples; the factor is
the median of these ratios divided by the library size. edgeR takes the
- median of the ratios, where DESeq2 takes the median of their logarithms
- (found by probing); the two differ when the number of features is even.
+ median of the ratios, where DESeq2 takes the median of their logarithms;
+ the two differ when the number of features is even.
The factors are scaled to geometric mean one.
Args:
@@ -290,16 +295,16 @@ def median_ratio_size_factors(
With ``method="ratio"`` (Anders and Huber 2010, equation 5), the size factor
of a sample is the median, over the features with positive counts in every
sample, of its counts divided by the features' geometric means across
- samples. As in DESeq2, the median is taken of the log ratios (found by
- probing), and the factors are not rescaled.
+ samples. As in DESeq2, the median is taken of the log ratios, and the
+ factors are not rescaled.
With ``method="poscounts"``, a feature's geometric mean is the n-th root of
the product of its positive counts, n being the number of samples, so
features with zeros take part. A sample's median runs over the features
where its count is positive. The factors are then scaled to geometric mean
- one, as DESeq2's ``estimateSizeFactors()``, and so ``DESeq()``, returns them
- (found by probing). The lower-level ``estimateSizeFactorsForMatrix()``
- returns them before this scaling.
+ one, as DESeq2's ``estimateSizeFactors()``, and so ``DESeq()``, returns them.
+ The lower-level ``estimateSizeFactorsForMatrix()`` returns them before this
+ scaling.
Args:
counts: Counts, samples x features.
@@ -328,6 +333,8 @@ def median_ratio_size_factors(
if missing.size:
raise ValueError(f"sample(s) {missing.tolist()} have no positive count")
factors = np.array([math.exp(m) for m in medians])
+ # estimateSizeFactors() scales the poscounts factors to geometric mean one, and
+ # estimateSizeFactorsForMatrix() does not (found by probing).
return _to_geometric_mean_one(factors) if positive_only else factors
@@ -432,8 +439,8 @@ def filter_by_expression(
``min_total_count``, where N are the effective library sizes (Chen, Lun and
Smyth 2016). n is the smallest group size or, with a design, the smallest
inverse leverage ``1 / max(h)``, where h is the diagonal of the hat matrix
- of the design with an intercept column added, as R's ``hat()`` adds it
- (found by probing); the two agree for a one-way design. When n exceeds
+ of the design with an intercept column added, as R's ``hat()`` adds it;
+ the two agree for a one-way design. When n exceeds
``large_n`` it is relaxed to ``large_n + (n - large_n) * min_prop``.
Without groups and design all samples form one group, as in edgeR, with a
warning.
@@ -488,6 +495,8 @@ def filter_by_expression(
min_samples = float(_smallest_group_size(sample_groups, n_samples))
elif design is not None:
matrix = _design_matrix(design, n_samples)
+ # filterByExpr takes the leverages of the design with an intercept column
+ # added, as R's hat() adds it (found by probing).
leverages = _leverages(np.column_stack([np.ones(n_samples), matrix]))
min_samples = 1.0 / float(np.max(leverages))
else:
diff --git a/cytepool/over_representation_analysis.py b/cytepool/over_representation_analysis.py
index 5a4f27c..4f89665 100644
--- a/cytepool/over_representation_analysis.py
+++ b/cytepool/over_representation_analysis.py
@@ -1,73 +1,53 @@
"""Over-representation analysis: gene sets among the genes called significant.
-:func:`~cytepool.over_representation` asks of each gene set whether the genes called
-significant in an analysis hold more of its genes than chance would give, by the
-one-sided hypergeometric test against a universe of genes, as limma's ``kegga`` and
-``goana`` compute it with their defaults (Young et al. 2010). From a
-differential-expression result it tests the genes that go up and the genes that go down
-separately, as ``kegga`` and ``goana`` test a fit of limma, and the genes that change in
-either direction together. Results match limma 3.62.1 except where noted under
-"Deliberate differences".
+:func:`~cytepool.over_representation` tests each gene set for over-representation among
+the genes called significant, by the one-sided hypergeometric test against a universe of
+genes, as limma's ``kegga`` and ``goana`` compute it with their defaults (Young et al.
+2010). It returns a table with a row for each set and list of selected genes: for a
+differential-expression result, the genes up, the genes down and both together.
+https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation
+explains the test and why the universe matters, and
+https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation-and-gsea-limits
+how it differs from ``kegga``.
The test:
- The universe holds the N genes that could have been called significant. Of a gene
- set, K genes are in the universe; n genes of the universe are selected, and k of
- them are in the set. Were the n genes drawn at random from the universe, without
- regard to the set, k would follow the hypergeometric distribution of n draws without
- replacement from N genes of which K are in the set, whose mean K n / N is the
- expected overlap. The p-value is P(X >= k), the one-sided p-value of Fisher's (1922)
- exact test of the 2 x 2 table of the genes of the universe, in the set or not and
- selected or not (Rivals et al. 2007); the hypergeometric test of functional
- categories goes back to Tavazoie et al. (1999). It is computed by
+ With N genes in the universe, n of them selected, K in a set and k in both, the
+ p-value is P(X >= k) for X hypergeometric, n draws without replacement from N
+ genes of which K are in the set: the one-sided p-value of Fisher's (1922) exact
+ test of the 2 x 2 table (Rivals et al. 2007), and the test of functional
+ categories of Tavazoie et al. (1999). It is computed by
:func:`~cytepool.stats.distributions.hypergeom_sf`, which keeps its relative
- accuracy far into the tail. The adjusted p-values are Benjamini and Hochberg's
- (1995), over the sets of each list of selected genes; limma's ``kegga`` and
- ``goana`` report p-values only.
+ accuracy far into the tail. The p-values of each list of selected genes are
+ adjusted over the sets by Benjamini and Hochberg's (1995) method.
The genes and the universe:
- - From a :class:`~cytepool.DifferentialExpressionResult`
- (one group and contrast) or any table of results, the genes whose adjusted
- p-value is below ``alpha`` are selected, as ``kegga`` selects the genes of a fit
- whose adjusted p-value is below its ``FDR`` (strictly, found by probing): up
- where the log2 fold change is positive, down where it is negative, and in either
- direction for both lists together. The universe is the genes tested: those
- with a p-value. A gene id on several rows, such as a symbol shared by two
- features, is one gene, in the universe when any of its rows was tested and
- selected up or down when any of its rows is, so that it can be in both lists,
- as ``kegga`` counts it; rows without an id are left out, as ``kegga`` leaves
- them out (both found by probing).
+ - From a :class:`~cytepool.DifferentialExpressionResult` (one group and contrast)
+ or any table of results, the genes whose adjusted p-value is below ``alpha`` are
+ selected: up where the log2 fold change is positive, down where it is negative,
+ and in either direction for both lists together. The universe is the genes
+ tested: those with a p-value. A gene id on several rows, such as a symbol shared
+ by two features, is one gene, in the universe when any of its rows was tested and
+ selected up or down when any of its rows is, so that it can be in both lists.
+ Rows without an id are left out.
- From a list of gene ids: the list is the selection, and the universe must be
given.
- A universe given replaces the default. Selected genes and genes of a set outside
- it are left out, and genes of the universe that are in no set stay in it, as in
- ``kegga`` by default (``restrict.universe = FALSE``).
-
- The universe matters: the genes of the genome, or of the sets, include genes that
- could not have been selected, such as genes not expressed or filtered out before
- the tests, and make p-values too small (Timmons et al. 2015; Wijesooriya et al.
- 2022). The default, the genes tested, is the universe that the test assumes.
-
-What the test assumes:
- The test takes genes as independent draws. The genes of a set are often correlated,
- as co-regulated genes are, which makes its p-values too small (Goeman and Buehlmann
- 2007); :func:`~cytepool.camera` allows for the correlation. The test also depends on
- the cutoff ``alpha`` and ignores the size of each change. In RNA-seq data, long and
- highly expressed genes are called significant more often, which biases the test
- towards sets of such genes (Young et al. 2010); the adjustment of ``kegga`` and
- ``goana`` for such a trend (``trend``, Wallenius' noncentral hypergeometric
- distribution) is not implemented.
-
-Deliberate differences from limma:
- - A set without genes in the universe has a row with no p-value (NaN), which the
- adjustment leaves out; ``kegga`` leaves the set out of its table.
- - ``kegga``'s method for a fit of limma keeps the genes without a p-value in the
- universe (found by probing); here a gene without a p-value was not tested and is
- left out, as the test assumes.
- - ``kegga``'s method for a fit adjusts the p-values of the rows with an id only,
- leaving the others out of the Benjamini-Hochberg adjustment, so that the genes
- it selects depend on which rows have an id, such as a symbol (found by
- probing). Here the adjusted p-values of the results are used as they are, so
- that the genes selected are those the analysis called significant.
+ it are left out, and genes of the universe that are in no set stay in it.
+
+ The test takes genes as independent draws, depends on the cutoff ``alpha`` and
+ ignores the size of each change; :func:`~cytepool.camera` allows for the
+ correlation between the genes of a set (Goeman and Buehlmann 2007).
+
+Matching limma:
+ The results match limma 3.62.1, whose ``kegga`` and ``goana`` report p-values only.
+ limma's documentation gives some of these conventions, and running limma on chosen
+ inputs gave the others: genes are selected where their adjusted p-value is below
+ ``kegga``'s ``FDR``, strictly; a gene id on several rows is counted once, in both
+ lists when one row goes up and another down; rows without an id are left out; and
+ genes of the universe that are in no set stay in it (``restrict.universe = FALSE``).
+ The adjustment of ``kegga`` and ``goana`` for genes called significant more often,
+ such as long ones (``trend``, Wallenius' noncentral hypergeometric distribution), is
+ not implemented.
References:
Young MD, Wakefield MJ, Smyth GK, Oshlack A (2010). Gene ontology analysis for
@@ -141,8 +121,12 @@ def over_representation(
The one-sided hypergeometric test of limma's ``kegga`` and ``goana`` (Young et al.
2010): whether the selected genes hold more of a set's genes than random draws
- from the universe would. The module docstring gives the method, how the genes and
- the universe are read, and the deliberate differences from limma.
+ from the universe would.
+ https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation
+ explains the test, the module documentation how the genes and the universe are
+ read, and
+ https://docs.nygen.io/CytePool/statistics/over-representation-and-gsea#over-representation-and-gsea-limits
+ the differences from limma.
Args:
genes: The genes to test: a
@@ -181,7 +165,7 @@ def over_representation(
selected genes in the universe (n); ``n_universe``, the genes of the
universe (N); and ``overlap_genes``, the selected genes of the set, in the
set's order, separated by ``", "``. A set without genes in the universe has
- NaN p-values.
+ NaN p-values, and the adjustment leaves it out.
Raises:
TypeError: If an argument has the wrong type, gene ids are not strings, a
@@ -375,6 +359,9 @@ def _selections(
f"the column {p_value_column!r} holds values outside [0, 1], such as "
f"{p[outside][0]}; it must hold p-values"
)
+ # kegga selects the genes whose adjusted p-value is below its FDR, strictly; it
+ # counts a gene id on several rows once, in both lists when one row goes up and
+ # another down, and leaves out rows without an id (found by probing).
tested = named & ~np.isnan(p)
with np.errstate(invalid="ignore"):
selected = tested & (p < alpha)
@@ -396,6 +383,9 @@ def _selections(
direction: list(dict.fromkeys(ids[rows].tolist()))
for direction, rows in lists.items()
}
+ # The universe is the genes tested. kegga's method for a fit of limma keeps genes
+ # without a p-value in it, and adjusts the p-values of the rows with an id only
+ # (found by probing); here the results' adjusted p-values are taken as they are.
return selections, list(dict.fromkeys(ids[tested].tolist()))
diff --git a/cytepool/pathway_scores.py b/cytepool/pathway_scores.py
index 6408243..73915d5 100644
--- a/cytepool/pathway_scores.py
+++ b/cytepool/pathway_scores.py
@@ -1,33 +1,31 @@
"""Per-sample scores of gene sets: GSVA, ssGSEA, combined z-scores and PLAGE, as the R
package GSVA computes them.
-Each function turns the expression values of every sample into one score per gene set,
-so that pathways can be compared across samples, drawn as a heatmap
-(:func:`~cytepool.plotting.plot_pathway_scores`) and tested with the designs and
-contrasts of the linear models, as GSVA's guide tests its scores with limma. They
-compute the four methods of GSVA 2.0.0 (Haenzelmann, Castelo and Guinney 2013) with its
-options and defaults: :func:`~cytepool.gsva`, the method of that paper;
+The functions compute the four methods of GSVA with its options and defaults:
+:func:`~cytepool.gsva`, the method of Haenzelmann, Castelo and Guinney (2013);
:func:`~cytepool.ssgsea`, single sample GSEA (Barbie et al. 2009);
:func:`~cytepool.combined_z_scores` (Lee et al. 2008); and :func:`~cytepool.plage`
-(Tomfohr, Lu and Kepler 2005). ``validation/pathway_scores.py`` compares them with GSVA
-run as a black box. GSVA is licensed under the GPL: the methods are implemented from the
-papers and GSVA's documentation, and what these leave open was found by running GSVA on
-chosen inputs, as marked "found by probing" below.
+(Tomfohr, Lu and Kepler 2005). Each returns a :class:`~cytepool.PathwayScores`, one
+score per sample and gene set, which :func:`~cytepool.plotting.plot_pathway_scores`
+draws as a heatmap and :func:`~cytepool.limma` tests with a design.
+https://docs.nygen.io/CytePool/statistics/pathway-scores#gsva explains the methods and
+how they differ, and
+https://docs.nygen.io/CytePool/statistics/pathway-scores#pathway-scores-limits how they
+differ from GSVA's; each function's docstring gives those that change what it returns.
The data:
Values are samples x genes: a 2-D array with one gene id per column, or a group
(cell type) of a :class:`~cytepool.Cube`, of which the samples with at least
``min_cells`` cells and some counts are scored, over the genes that edgeR's
``filterByExpr`` keeps (:func:`~cytepool.normalization.filter_by_expression`),
- as log2 counts per million after TMM normalization (``values="log2_cpm"``,
- edgeR's ``cpm(log = TRUE)``) or as counts (``values="counts"``), the values GSVA's
- guide passes. NaN marks a missing value; infinite values are refused. Each
- method's docstring says how it treats missing values.
-
- :func:`~cytepool.gsva`, :func:`~cytepool.combined_z_scores` and
- :func:`~cytepool.plage` leave out the genes whose values are all equal (constant),
- before the sets are matched, as GSVA does; :func:`~cytepool.ssgsea` keeps them, as
- GSVA does (found by probing).
+ as log2 counts per million after TMM normalization with a prior count of 2
+ (``values="log2_cpm"``, edgeR's ``cpm(log = TRUE)``) or as counts
+ (``values="counts"``), the values GSVA's guide passes. NaN marks a missing value;
+ infinite values are refused. Each method's docstring says how it treats missing
+ values. :func:`~cytepool.gsva`, :func:`~cytepool.combined_z_scores` and
+ :func:`~cytepool.plage` leave out the genes whose values are all equal
+ (constant), before the sets are matched, and :func:`~cytepool.ssgsea` keeps them,
+ as GSVA does.
Gene sets:
The sets are a :class:`~cytepool.GeneSets` or a mapping of set name to gene ids,
@@ -36,106 +34,22 @@
after matching); the others are left out, as GSVA leaves them out. The result keeps
the order of the sets.
-GSVA (:func:`~cytepool.gsva`):
- 1. Each gene's values are turned into the probability of a value at most as
- high under the gene's distribution across samples, estimated by a kernel
- (Haenzelmann, Castelo and Guinney 2013): for gene i in sample j,
-
- F_ij = (1/n) sum_k K(x_ij, x_ik),
-
- a sum over the n samples in their order, with K the Gaussian kernel
- Phi((x_ij - x_ik) / h_i), h_i a quarter of the gene's standard deviation, for
- continuous values; the Poisson kernel P(Y <= x_ij) for Y ~ Poisson(x_ik + 1/2)
- for counts; or, without a kernel, the indicator of x_ik <= x_ij, the
- empirical distribution function. GSVA evaluates Phi from a table of its
- values at multiples of 0.001: at |t| rounded down to such a multiple, and as
- one minus that value for negative t (found by probing; it moves F by less
- than 4e-4). ``kernel="auto"`` chooses the Poisson kernel for values stored as
- integers (an integer dtype, as GSVA chooses it for R's integer matrices), the
- Gaussian kernel otherwise, and no kernel from ``min_samples_without_kernel``
- samples on (found by probing).
- 2. The genes of each sample are ranked by the statistic z_ij = -log((1 - F_ij) /
- F_ij), from 1 for the lowest to p for the highest of the p genes; genes with
- equal statistics take their ranks in reverse order of the genes (found by
- probing). Each gene weighs |p/2 - r_ij|^tau, so that both ends of the
- ranking weigh most (``exponent``, GSVA's ``tau``).
- 3. A walk down the genes from rank p rises at each gene of a set by its share of
- the set's weight and falls at each other gene by 1/(p - k), for k genes in
- the set, as in gene set enrichment analysis. With E+ and E- the sizes of the
- walk's largest deviations above and below zero, the score is E+ - E-
- (``score_type="difference"``, the default, GSVA's ``maxDiff = TRUE``), which
- is large for a set whose genes are high and low for one whose genes are low;
- E+ + E- (``"kuiper"``, ``absRanking = TRUE``), Kuiper's statistic, large for
- a set whose genes are at either end; or the larger of the two deviations,
- with its sign (``"max_deviation"``, ``maxDiff = FALSE``; -E- when they are
- equal, found by probing).
-
- A set with every gene, or whose genes all weigh zero (one gene at rank p/2),
- has a NaN score, as in GSVA. With missing values, each gene's distribution is
- estimated from its observed samples and each sample ranks its observed genes,
- as GSVA's ``use = "na.rm"`` does; the walk then falls by 1/(p_j - k_j) at each
- of the sample's observed genes outside the set, with p_j and k_j counting the
- genes observed in sample j.
-
-ssGSEA (:func:`~cytepool.ssgsea`):
- Each sample ranks its genes by their values, and the walk down the ranking rises
- at each gene of a set by its rank to the power alpha (``exponent``), as a share
- of the set's total, and falls at each other gene by 1/(p - k) (Barbie et al.
- 2009). The score is the sum of the walk over all p genes, not its extreme:
-
- ES = sum_{g in G} w_g a_g / sum_{g in G} w_g - sum_{g not in G} a_g / (p - k),
-
- with a_g the gene's rank from the bottom of the walk. Genes with equal values
- weigh their average rank truncated to an integer, and the walk passes them in
- the order of the genes (found by probing). ``normalize=True`` divides every
- score by the range of all the scores, over sets and samples, as Barbie et al.
- did and GSVA does by default. Missing values are left out of each sample's
- ranking, as GSVA's ``use = "na.rm"`` does.
-
-Combined z-scores (:func:`~cytepool.combined_z_scores`):
- Each gene is standardized across samples, z = (x - mean) / sd, and a set's score
- in a sample is sum_{g in G} z_g / sqrt(k) (Lee et al. 2008). A missing value
- leaves its gene out of the mean and standard deviation, and makes the score of
- every set that holds the gene NaN in that sample, as in GSVA.
-
-PLAGE (:func:`~cytepool.plage`):
- The standardized values of a set's k genes form a k x n matrix, and the set's
- scores are the first right singular vector of that matrix: the samples'
- coordinates on the direction along which the set's genes vary most together
- (Tomfohr, Lu and Kepler 2005). Missing values are refused, as GSVA refuses them.
-
-Deliberate differences from GSVA:
- - PLAGE's sign. A singular vector has no sign of its own; GSVA returns the one
- its singular value decomposition gives (found by probing), which depends on
- the linear algebra library. Here the scores rise with the mean standardized
- expression of the set's genes: the sum of the set's loadings (the first left
- singular vector) is positive, or, when it is zero, the loading of largest
- size, the first of equal ones, is positive. A set of one gene then has the
- gene's standardized values, scaled to length one, where GSVA gives 1 in every
- sample.
- - GSVA with missing values: GSVA's ``use = "na.rm"`` lets the walk of a sample
- fall by 1/(p - k_j) at its observed genes outside the set, p counting the
- genes missing in the sample too, so that the walk ends above zero; here it
- falls by 1/(p_j - k_j) and ends at zero. GSVA's default, ``use =
- "everything"``, gives meaningless scores with the GSVA method (found by
- probing), and a sample without any value stops GSVA with an error; here such a
- sample has NaN scores.
- - ssGSEA with a set of every gene: its score is NaN and the normalization uses
- the other scores, where GSVA stops with an error.
- - ``kernel="auto"`` with an integer dtype and ``min_samples_without_kernel``
- samples or more uses no kernel, as GSVA documents; GSVA 2.0.0 stops with an
- error. The Poisson kernel refuses values that are not counts, where GSVA stops
- with an error on negative values and scores fractional ones (found by
- probing).
-
-Agreement with GSVA:
- The scores are GSVA's to rounding when the genes of each sample are ranked as
- GSVA ranks them. Ranks are decided by comparing the kernel estimates F of
- different genes, and two genes whose estimates are equal in exact arithmetic,
- such as genes with the same counts in different samples, can be ordered by the
- last bit of R's normal and Poisson probabilities, which CytePool computes
- accurately but not always to the same bit: such genes, mostly genes with low
- counts, can take each other's ranks.
+Matching GSVA:
+ The scores match GSVA 2.0.0, which is licensed under the GPL (>= 2): the methods are
+ implemented from the papers and GSVA's documentation, and running GSVA on chosen
+ inputs settled what these leave open, among them the conventions below and those
+ that each function's docstring gives.
+
+ - GSVA evaluates the normal distribution function of its Gaussian kernel from a
+ table of its values at multiples of 0.001: at |t| rounded down to such a
+ multiple, and as one minus that value for negative t, which moves a kernel
+ estimate by less than 4e-4. :func:`~cytepool.gsva` does the same, and sums each
+ estimate over the samples in their order, so that genes are ranked as in GSVA.
+ - In GSVA's method, genes with equal statistics take their ranks in reverse order
+ of the genes, and equal deviations above and below zero give
+ ``score_type="max_deviation"`` the one below.
+ - In ssGSEA, genes with equal values weigh their average rank truncated to an
+ integer, and the walk passes them in the order of the genes.
References:
Haenzelmann S, Castelo R, Guinney J (2013). GSVA: gene set variation analysis for
@@ -204,7 +118,7 @@
from ._options import (
checked_choice,
checked_flag,
- number_at_least,
+ non_negative_number,
positive_integer,
)
from ._ranked_genes import gene_set_collection
@@ -361,12 +275,40 @@ def gsva(
"""Score gene sets in every sample by gene set variation analysis (GSVA).
The method of Haenzelmann, Castelo and Guinney (2013) as GSVA 2.0.0's
- ``gsva(gsvaParam(...))`` computes it: each gene's values are turned into ranks
- of the probabilities of its distribution across samples, estimated by a kernel,
- and each sample scores a set by a walk down the ranking of its genes, as gene set
- enrichment analysis scores a ranking (Subramanian et al. 2005). Scores lie in
- [-1, 1], or in [0, 2] with ``score_type="kuiper"``. The module docstring gives
- the method, its missing values and the deliberate differences from GSVA.
+ ``gsva(gsvaParam(...))`` computes it:
+
+ 1. Each gene's values are turned into the probability of a value at most as high
+ under the gene's distribution across samples, estimated by a kernel: for gene
+ i in sample j, F_ij = (1/n) sum_k K(x_ij, x_ik) over the n samples, with K the
+ Gaussian kernel Phi((x_ij - x_ik) / h_i), h_i a quarter of the gene's standard
+ deviation, for continuous values; the Poisson kernel P(Y <= x_ij) for
+ Y ~ Poisson(x_ik + 1/2), for counts; or, without a kernel, the indicator of
+ x_ik <= x_ij, the empirical distribution function.
+ 2. The genes of each sample are ranked by the statistic z_ij = -log((1 - F_ij) /
+ F_ij), from 1 for the lowest to p for the highest of the p genes, and each gene
+ weighs |p/2 - r_ij|^tau, so that both ends of the ranking weigh most
+ (``exponent``, GSVA's ``tau``).
+ 3. A walk down the genes from rank p rises at each gene of a set by its share of
+ the set's weight and falls at each other gene by 1/(p - k), for k genes in the
+ set, as in gene set enrichment analysis (Subramanian et al. 2005). With E+ and
+ E- the sizes of the walk's largest deviations above and below zero, the score
+ is E+ - E- (``score_type="difference"``, GSVA's ``maxDiff = TRUE``), which is
+ large for a set whose genes are high and low for one whose genes are low;
+ E+ + E- (``"kuiper"``, ``absRanking = TRUE``), Kuiper's statistic, large for a
+ set whose genes are at either end; or the larger of the two deviations, with
+ its sign (``"max_deviation"``, ``maxDiff = FALSE``).
+
+ Scores lie in [-1, 1], or in [0, 2] with ``score_type="kuiper"``. A set with every
+ gene, or whose genes all weigh zero (one gene at rank p/2), has a NaN score, as in
+ GSVA. With missing values, each gene's distribution is estimated from its observed
+ samples and each sample ranks its observed genes, as GSVA's ``use = "na.rm"``
+ does; the walk then falls by 1/(p_j - k_j) at each of the sample's observed genes
+ outside the set, with p_j and k_j counting the genes observed in sample j, so that
+ it ends at zero; GSVA's falls by 1/(p - k_j), counting the missing genes too, and
+ ends above zero. A sample without any value has NaN scores, where GSVA stops with
+ an error.
+ https://docs.nygen.io/CytePool/statistics/pathway-scores#gsva explains the method,
+ and the module documentation the conventions it shares with GSVA.
Args:
data: ``(n_samples, n_genes)`` expression values, such as log-CPM or, for the
@@ -395,13 +337,16 @@ def gsva(
kernel: How each gene's distribution is estimated (GSVA's ``kcdf``):
``"gaussian"`` for continuous values, ``"poisson"`` for counts (whole
numbers of at least 0), ``"none"`` for the empirical distribution
- function, or ``"auto"``.
+ function, or ``"auto"``: the Poisson kernel for an integer dtype, as
+ GSVA chooses it for R's integer matrices, the Gaussian kernel otherwise,
+ and none from ``min_samples_without_kernel`` samples on, also for an
+ integer dtype, as GSVA documents (GSVA 2.0.0 stops with an error there).
min_samples_without_kernel: The number of samples from which ``"auto"``
uses no kernel, at least 1 (GSVA's ``kcdfNoneMinSampleSize``).
exponent: The power tau of the weights |p/2 - r|^tau of the ranks, at least
0 (GSVA's ``tau``).
- score_type: ``"difference"``, ``"kuiper"`` or ``"max_deviation"``, as the
- module docstring describes (GSVA's ``maxDiff`` and ``absRanking``).
+ score_type: ``"difference"``, ``"kuiper"`` or ``"max_deviation"``, as
+ above (GSVA's ``maxDiff`` and ``absRanking``).
min_size: The fewest of a set's genes among the genes scored for the set to
be scored, at least 1 (GSVA's ``minSize``).
max_size: The most of them, at least ``min_size``; None for no limit (GSVA's
@@ -439,7 +384,7 @@ def gsva(
without_kernel = positive_integer(
min_samples_without_kernel, "min_samples_without_kernel"
)
- power = number_at_least(exponent, "exponent", 0.0)
+ power = non_negative_number(exponent, "exponent")
score = _SCORE_TYPES.index(checked_choice(score_type, "score_type", _SCORE_TYPES))
collection = _collection(gene_sets)
expression = expression_values(
@@ -453,6 +398,9 @@ def gsva(
expression_filter,
)
n_samples = expression.values.shape[0]
+ # GSVA's kcdf = "auto": Poisson for R's integer matrices, Gaussian otherwise, and
+ # no kernel from kcdfNoneMinSampleSize samples on (found by probing GSVA 2.0.0,
+ # which stops with an error on integer data from that many samples).
if kind == "auto":
kind = (
"none"
@@ -506,11 +454,22 @@ def ssgsea(
"""Score gene sets in every sample by single-sample GSEA (ssGSEA).
The method of Barbie et al. (2009) as GSVA 2.0.0's ``gsva(ssgseaParam(...))``
- computes it: each sample ranks its genes by their values, and a set's score is
- the sum of the walk of gene set enrichment analysis down that ranking, with the
- genes of the set weighted by their ranks to the power ``exponent``. Only the order
- of the values within each sample matters. The module docstring gives the method,
- its ties and its missing values.
+ computes it: each sample ranks its genes by their values, and the walk down the
+ ranking rises at each gene of a set by its rank to the power alpha
+ (``exponent``), as a share of the set's total, and falls at each other gene by
+ 1/(p - k), for k of the p genes in the set. The score is the sum of the walk over
+ all p genes, not its extreme:
+
+ ES = sum_{g in G} w_g a_g / sum_{g in G} w_g - sum_{g not in G} a_g / (p - k),
+
+ with a_g the gene's rank from the bottom of the walk and w_g = a_g^alpha. Only the
+ order of the values within each sample matters. ``normalize=True`` divides every
+ score by the range of all the scores, over sets and samples, as Barbie et al. did
+ and GSVA does by default. Missing values are left out of each sample's ranking,
+ as GSVA's ``use = "na.rm"`` does. A set of every gene has a NaN score, which the
+ normalization leaves out, where GSVA stops with an error.
+ https://docs.nygen.io/CytePool/statistics/pathway-scores#ssgsea explains the
+ method, and the module documentation its ties.
Args:
data: ``(n_samples, n_genes)`` expression values; NaN marks a missing value,
@@ -550,7 +509,7 @@ def ssgsea(
KeyError: If the cube has no such group or features column.
"""
checked = _checked_size(min_size, max_size)
- power = number_at_least(exponent, "exponent", 0.0)
+ power = non_negative_number(exponent, "exponent")
scaled = checked_flag(normalize, "normalize")
collection = _collection(gene_sets)
expression = expression_values(
@@ -609,7 +568,11 @@ def combined_z_scores(
computes it: each gene is standardized across samples, and a set's score in a
sample is the sum of its genes' z-scores divided by the square root of their
number, which is a standard normal score when the genes are independent standard
- normal values.
+ normal values. A missing value leaves its gene out of the mean and standard
+ deviation, and makes the score of every set that holds the gene NaN in that
+ sample, as in GSVA.
+ https://docs.nygen.io/CytePool/statistics/pathway-scores#combined-z-scores
+ explains the method.
Args:
data: ``(n_samples, n_genes)`` expression values on a log scale; NaN marks a
@@ -695,8 +658,14 @@ def plage(
form a genes x samples matrix, and the set's scores are its first right singular
vector, the samples' coordinates on the axis along which the genes vary most
together. The scores of a set have mean zero and length one across samples, and
- rise with the mean standardized expression of its genes, a sign chosen here where
- GSVA's is arbitrary (see the module docstring).
+ rise with the mean standardized expression of its genes: the sum of the set's
+ loadings (the first left singular vector) is positive, or, when it is zero, the
+ loading of largest size, the first of equal ones. GSVA's sign is that of its
+ singular value decomposition, so the two agree up to sign. A set of one gene
+ scores the gene's standardized values divided by their length, where GSVA gives
+ 1 in every sample. Missing values are refused, as GSVA refuses them.
+ https://docs.nygen.io/CytePool/statistics/pathway-scores#plage explains the
+ method.
Args:
data: ``(n_samples, n_genes)`` expression values on a log scale, without
@@ -766,18 +735,22 @@ def plage(
def _check_counts(values: FloatArray) -> None:
+ # GSVA's Poisson kernel stops with an error on negative values and scores
+ # fractional ones (found by probing GSVA 2.0.0); here both are refused.
observed = values[~np.isnan(values)]
if observed.size and (
float(observed.min()) < 0.0 or not np.all(observed == np.floor(observed))
):
raise ValueError(
- "the Poisson kernel takes counts, whole numbers of at least 0; use "
+ "the Poisson kernel takes counts, non-negative whole numbers; use "
"kernel='gaussian' for continuous values such as log-CPM"
)
def _varying_genes(values: FloatArray) -> IntArray:
- """The genes whose observed values are not all equal, as GSVA keeps them."""
+ """The genes whose observed values are not all equal, as GSVA keeps them for its
+ method, combined z-scores and PLAGE; for ssGSEA it keeps every gene (found by
+ probing GSVA 2.0.0)."""
missing = np.isnan(values)
highest = np.where(missing, -np.inf, values).max(axis=0)
lowest = np.where(missing, np.inf, values).min(axis=0)
@@ -947,7 +920,8 @@ def _gene_statistics(
The kernel estimate of sample j is the mean over the observed samples k, summed
in their order, of the kernel: ``kernel`` 0 for the Gaussian kernel, which looks
up the normal distribution function in ``table`` at |t| rounded down to a
- multiple of 0.001, 1 for the Poisson kernel, and 2 for none.
+ multiple of 0.001, and takes one minus it for negative t, as GSVA does (found by
+ probing GSVA 2.0.0), 1 for the Poisson kernel, and 2 for none.
"""
n = x.size
observed = np.empty(n, dtype=np.int64)
@@ -993,7 +967,8 @@ def _gene_statistics(
def _gene_ranks(statistics: FloatArray) -> IntArray:
"""Ranks of the genes of each sample by their statistics, 1 for the lowest, and 0
- where missing; equal statistics take their ranks in reverse order of the genes."""
+ where missing; equal statistics take their ranks in reverse order of the genes, as
+ in GSVA (found by probing GSVA 2.0.0)."""
ranks = np.zeros(statistics.shape, dtype=np.int64)
by_chunks(
_gene_ranks_parallel,
@@ -1126,6 +1101,9 @@ def _gsva_sample(
if total == 0.0:
out[s] = np.nan
continue
+ # p and k count the sample's observed genes, so the walk ends at zero. GSVA's
+ # use = "na.rm" counts the missing genes in p too, and its default use =
+ # "everything" gives scores without meaning (found by probing GSVA 2.0.0).
others = float(p - k)
reached = 0.0
highest = -np.inf
@@ -1139,7 +1117,7 @@ def _gsva_sample(
out[s] = highest + lowest
elif score_type == 1:
out[s] = highest - lowest
- else:
+ else: # equal deviations give the one below, as in GSVA (found by probing)
out[s] = highest if highest > -lowest else lowest
@@ -1256,7 +1234,8 @@ def _standardized(values: FloatArray) -> FloatArray:
def _first_singular_vector(matrix: FloatArray) -> FloatArray:
"""The first right singular vector of a genes x samples matrix, with the sign that
makes the genes' loadings sum to a positive number, or else the largest loading,
- the first of equal ones, positive."""
+ the first of equal ones, positive. GSVA returns the sign its singular value
+ decomposition gives (found by probing GSVA 2.0.0)."""
u, _, vt = np.linalg.svd(matrix, full_matrices=False)
loadings = u[:, 0]
total = float(loadings.sum())
diff --git a/cytepool/plotting.py b/cytepool/plotting.py
index 12a3361..0a07f7e 100644
--- a/cytepool/plotting.py
+++ b/cytepool/plotting.py
@@ -1,5 +1,5 @@
-"""Figures of pseudobulk and bulk analyses, drawn as limma, edgeR, DESeq2, speckle,
-metafor, variancePartition, fgsea and decoupler draw them.
+"""Figures of pseudobulk and bulk analyses, each drawn as the tool whose method it shows
+draws it.
Each function draws one figure from the objects that CytePool's analyses return: a
cube, a voom result, a fit, the table of a differential-expression or gene-set test,
@@ -183,11 +183,6 @@
Proceedings of the National Academy of Sciences 100, 9440-9445.
doi:10.1073/pnas.1530509100
- Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
- Improving reproducibility of differentially expressed genes in single-cell
- transcriptomic studies of neurodegenerative diseases through meta-analysis.
- Nature Communications 16, 7436. doi:10.1038/s41467-025-62579-z
-
Pearson K (1901). On lines and planes of closest fit to systems of points in space.
The London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science 2,
559-572. doi:10.1080/14786440109462720
@@ -212,7 +207,7 @@
checked_choice,
checked_flag,
integral_number,
- number_at_least,
+ non_negative_number,
positive_integer,
real_number,
)
@@ -614,7 +609,7 @@ def _fraction(value: Any, name: str) -> float:
def _min_cells(value: Any) -> int:
number = integral_number(value, "min_cells")
if number < 0:
- raise ValueError(f"min_cells must be at least 0, got {number}")
+ raise ValueError(f"min_cells must be non-negative, got {number}")
return number
@@ -737,8 +732,9 @@ def plot_mds(
coordinates of a dimension are an eigenvector of the doubly centred matrix of
squared distances, -1/2 J D J, times the square root of its eigenvalue, and the
axis labels give each eigenvalue's share of their sum, limma's
- ``var.explained``. A dimension's sign is arbitrary; here its largest coordinate is
- positive.
+ ``var.explained``. A dimension's sign is arbitrary; here its coordinate of largest
+ absolute value is positive, the first of those within a relative 1e-9 of the
+ largest when several nearly tie.
A cube gives the pseudobulk samples of one group, prepared as the edgeR User's Guide
prepares counts for ``plotMDS``: samples with fewer than ``min_cells`` cells or no
@@ -767,7 +763,7 @@ def plot_mds(
12 levels.
labels: True to write each sample's id next to it (numbers from 1 for data
other than a cube), or one label per sample.
- top: The number of genes behind each distance, at least 1.
+ top: The number of genes that each distance uses, at least 1.
gene_selection: ``"pairwise"`` or ``"common"``.
dimensions: The two dimensions to plot, counted from 1 in decreasing order of
their eigenvalues.
@@ -914,7 +910,8 @@ def _mds(values: FloatArray, top: int, pairwise: bool) -> tuple[FloatArray, Floa
order = np.argsort(eigenvalues)[::-1]
eigenvalues, vectors = eigenvalues[order], vectors[:, order]
coordinates: FloatArray = vectors * np.sqrt(np.maximum(eigenvalues, 0.0))
- # The largest coordinate of each dimension is positive; of near ties, the first.
+ # The coordinate of largest absolute value of each dimension is positive; of near
+ # ties, the first.
magnitude = np.abs(coordinates)
largest = np.argmax(magnitude >= magnitude.max(axis=0) * (1.0 - 1e-9), axis=0)
signs = np.sign(coordinates[largest, np.arange(n)])
@@ -1438,7 +1435,7 @@ def plot_residual_variances(
whose two-sided p-value as a variance of the prior, from the F distribution on the
gene's residual and the prior's degrees of freedom (those of the genes that are not
outliers), is below 0.5 once adjusted by Benjamini and Hochberg (1995), as limma
- marks them (found by probing). Of a differential-expression result, the moderation
+ marks them. Of a differential-expression result, the moderation
of one group, which :meth:`~cytepool.DifferentialExpressionResult.fit` computes
again.
@@ -1942,7 +1939,7 @@ def plot_volcano(
level = _fraction(alpha, "alpha")
count = integral_number(top, "top")
if count < 0:
- raise ValueError(f"top must be at least 0, got {count}")
+ raise ValueError(f"top must be non-negative, got {count}")
if labels is not None:
reject_masked(labels, "labels")
x = _require(tests.log2_fold_change, "log2_fold_change", "plot_volcano")
@@ -2271,8 +2268,8 @@ def plot_barcode(
where the statistic is below -sqrt(2) or above sqrt(2); a bar at the rank of every
gene of the set; and above them the enrichment worm, the relative enrichment of
the set along the ranking: the moving average of the set's indicator over the
- ranked genes, with tricube weights over a window of ``span`` of the genes (limma's
- ``tricubeMovingAverage``, whose weights were found by probing), divided by the
+ ranked genes, with tricube weights over a window of ``span`` of the genes, as
+ limma's ``tricubeMovingAverage`` weighs them, divided by the
set's share of all genes. A worm above 1 (the dashed line) at the right shows a
set enriched among the genes with large statistics. The worm is drawn above the
bars, scaled to its largest value, which the axis gives, as limma draws it. The
@@ -2739,7 +2736,7 @@ def plot_enrichment(
contrast.
"""
_check_axes(ax)
- power = number_at_least(exponent, "exponent", 0.0)
+ power = non_negative_number(exponent, "exponent")
reject_masked(gene_set, "gene_set")
if isinstance(gene_set, str) or isinstance(gene_set, Mapping):
raise TypeError(
@@ -2848,7 +2845,7 @@ def plot_gsea_table(
if not isinstance(table, Table):
raise TypeError(f"table must be a Table, got {type(table).__name__}")
count = positive_integer(top, "top")
- power = number_at_least(exponent, "exponent", 0.0)
+ power = non_negative_number(exponent, "exponent")
reject_masked(gene_sets, "gene_sets")
collection = gene_set_collection(gene_sets)
needed = (
@@ -4243,7 +4240,7 @@ def _chosen_features(
for gene in list(genes):
if isinstance(gene, bool | np.bool_):
raise TypeError(
- "features must be feature ids, symbols or positions, not bool"
+ "features must be feature ids, symbols or positions, got bool"
)
if isinstance(gene, numbers.Integral):
position = int(gene)
@@ -4430,7 +4427,7 @@ def plot_sumrank(
reject_masked(top, "top")
named = integral_number(top, "top")
if named < 0:
- raise ValueError(f"top must be at least 0, got {named}")
+ raise ValueError(f"top must be non-negative, got {named}")
table = result.features
ids = [str(value) for value in table["feature_id"].tolist()]
names = ids
@@ -4719,11 +4716,11 @@ def plot_pca(
DESeq2's ``plotPCA`` (Love et al. 2014): the samples' coordinates on two components
of :func:`~cytepool.principal_components`, which takes the ``top`` genes of largest
variance over the samples, centres each on its mean, and decomposes them by singular
- values (Pearson 1901); the sign of each component makes its largest coordinate
- positive. The axes are labelled as ``plotPCA`` labels them, with each component's
- share of the variance rounded to a whole percent, as in "PC1: 45% variance", and
- drawn to the same scale, as ``plotPCA`` draws them, so that the distances between
- samples are drawn true. The coordinates are those of ``principal_components``; pass
+ values (Pearson 1901); the sign of each component makes its coordinate of largest
+ absolute value positive. The axes are labelled as ``plotPCA`` labels them, with each
+ component's share of the variance rounded to a whole percent, as in "PC1: 45%
+ variance", and drawn to the same scale, as ``plotPCA`` draws them, so that the
+ distances between samples are drawn true. The coordinates are those of ``principal_components``; pass
its result as ``data`` to draw the components it holds.
Draws the samples as points (gid ``"samples"``), coloured by ``color_by`` and
diff --git a/cytepool/principal_component_analysis.py b/cytepool/principal_component_analysis.py
index 58aea4d..4124f03 100644
--- a/cytepool/principal_component_analysis.py
+++ b/cytepool/principal_component_analysis.py
@@ -1,35 +1,23 @@
"""Principal components of samples, as DESeq2's ``plotPCA`` computes them.
:func:`~cytepool.principal_components` places the samples of a matrix of log-expression
-values, or of one group (cell type) of a cube, on their principal components (Pearson
-1901), as DESeq2's ``plotPCA`` does for its transformed values (Love et al. 2014): from
-the ``top`` genes of largest variance over the samples, each centred on its mean, by the
-singular value decomposition of that samples x genes matrix, X = U S V'. The coordinates
-of the samples are U S, the loadings of the genes are V, and the share of the variance
-that each component explains is its squared singular value over their sum, DESeq2's
-``percentVar``. :func:`~cytepool.plotting.plot_pca` draws them.
-
-What DESeq2's help page leaves open was found by running ``plotPCA`` 1.46.0 as a black
-box: it ranks the genes by their variance over the samples, keeps genes of equal
-variance in their order, takes every gene when ``ntop`` exceeds their number, and
-returns the coordinates of R's ``prcomp``, whose signs, its help page says, are
-arbitrary. Here the sign of each component makes the sample with its largest absolute
-coordinate positive (of near ties, within a relative 1e-9, the first), as
-:func:`~cytepool.plotting.plot_mds` orients its dimensions, so that the result does
-not depend on the order of the genes or the sign of the values.
-
-The values:
- A matrix holds one row per sample and one column per gene, finite, such as log-CPM
- values or the variance-stabilized values that DESeq2's guide takes. A
- :class:`~cytepool.stats.voom.VoomResult` gives its log-CPM values. A cube gives one
- group's samples prepared as :func:`~cytepool.plotting.plot_mds` prepares them, as
- the edgeR User's Guide prepares counts for ``plotMDS``: the samples with fewer than
- ``min_cells`` cells or no counts are left out, as :func:`~cytepool.limma_voom`
- leaves them out; the genes are kept by
- :func:`~cytepool.normalization.filter_by_expression` (Chen, Lun and Smyth 2016) with
- the samples as one group; and the values are their log2 counts per million with TMM
- normalization factors and an average count of 2 added
- (:func:`~cytepool.normalization.log2_cpm`).
+values, or of one group (cell type) of a cube, on the principal components of their most
+variable genes (Pearson 1901), as DESeq2's ``plotPCA`` does for its transformed values
+(Love et al. 2014), and returns a :class:`~cytepool.PrincipalComponents` with the
+samples' coordinates, each component's share of the variance and the genes' loadings;
+:func:`~cytepool.plotting.plot_pca` draws them.
+https://docs.nygen.io/CytePool/statistics/normalization-and-filtering#samples-at-a-glance
+explains the method, and
+https://docs.nygen.io/CytePool/statistics/normalization-and-filtering#normalization-limits
+how it differs from DESeq2's.
+
+Matching DESeq2:
+ The components match DESeq2 1.46.0's ``plotPCA``, up to the sign of each component;
+ running it on chosen inputs settled what its help page leaves open. It ranks the
+ genes by their variance over the samples, keeps genes of equal variance in their
+ order, takes every gene when ``ntop`` exceeds their number, gives each component's
+ share of the variance of all the components (``percentVar``), and returns the
+ coordinates of R's ``prcomp``, whose signs, its help page says, are arbitrary.
References:
Pearson K (1901). On lines and planes of closest fit to systems of points in space.
@@ -195,8 +183,27 @@ def principal_components(
As DESeq2's ``plotPCA``: the ``top`` genes of largest variance over the samples
(every gene when there are fewer, genes of equal variance in their order), each
- centred on its mean over the samples, decomposed by singular values. The module's
- docstring describes the values a cube gives and the sign of each component.
+ centred on its mean over the samples, form a samples x genes matrix X = U S V',
+ decomposed by singular values. The coordinates of the samples are U S, the loadings
+ of the genes are V, and the share of the variance that each component explains is
+ its squared singular value over their sum. The sign of each component makes its
+ coordinate of largest absolute value positive, the first of those within a
+ relative 1e-9 of the largest when several nearly tie, as
+ :func:`~cytepool.plotting.plot_mds` orients its dimensions; the coordinates then
+ stay the same when the genes are reordered or the values change sign.
+
+ A matrix holds one row per sample and one column per gene, such as log-CPM values
+ or the variance-stabilized values that DESeq2's guide takes; a
+ :class:`~cytepool.stats.voom.VoomResult` gives its log-CPM values. A cube gives one
+ group's samples prepared as the edgeR User's Guide prepares counts for
+ ``plotMDS``, as :func:`~cytepool.plotting.plot_mds` prepares them: the samples with
+ fewer than ``min_cells`` cells or no counts are left out, as
+ :func:`~cytepool.limma_voom` leaves them out; the genes are kept by
+ :func:`~cytepool.normalization.filter_by_expression` (Chen, Lun and Smyth 2016) with
+ the samples as one group; and the values are their log2 counts per million with TMM
+ normalization factors and an average count of 2 added.
+ https://docs.nygen.io/CytePool/statistics/normalization-and-filtering#samples-at-a-glance
+ explains the method.
Args:
data: ``(n_samples, n_genes)`` log-expression values, finite; a
@@ -334,7 +341,7 @@ def _cube_values(
_MIN_CELLS if min_cells is None else integral_number(min_cells, "min_cells")
)
if fewest < 0:
- raise ValueError(f"min_cells must be at least 0, got {fewest}")
+ raise ValueError(f"min_cells must be non-negative, got {fewest}")
ids = cube.groups["group_id"].tolist()
if group is None:
if cube.n_groups != 1:
diff --git a/cytepool/replication_across_studies.py b/cytepool/replication_across_studies.py
index 04051d2..e8f7cca 100644
--- a/cytepool/replication_across_studies.py
+++ b/cytepool/replication_across_studies.py
@@ -1,7 +1,5 @@
"""Replication of differential expression across studies.
-Most genes that one study finds are not found in the next (Nakatsuka et al. 2025), so a
-result of several studies says how well they agree next to what they find together.
:func:`~cytepool.replication` reads the differential expression of one group (cell type)
and contrast in every study of a collection, a :class:`~cytepool.StudyResults`, lined up
gene by gene by :meth:`StudyResults.table `, and gives for
@@ -10,9 +8,11 @@
and Heller 2008), how many studies find it and in which direction, and the heterogeneity
of its log2 fold changes; for every pair of studies, pi1 of one study's p-values among
the genes the other finds (Storey and Tibshirani 2003) and their sign agreement; and the
-number of genes found in at least k studies.
+number of genes found in at least k studies, in a :class:`~cytepool.ReplicationResult`.
:func:`~cytepool.plotting.plot_study_agreement` and
:func:`~cytepool.plotting.plot_replicated_genes` draw them.
+https://docs.nygen.io/CytePool/statistics/replication#replication-tables explains what
+each table answers.
The genes:
A study tested a gene when its table of the group and contrast has it; one that
@@ -29,32 +29,23 @@
Hochberg's procedure over the genes that have them.
Agreement between studies:
- For an ordered pair of studies, the discovery and the replication, the genes the
- discovery finds that the replication tested are compared: pi1 = 1 - pi0 of their
- p-values in the replication study, Storey's estimate of the proportion of them with
- an effect there (:func:`~cytepool.stats.multiple_testing.proportion_of_true_nulls`),
- which counts effects that the replication study is too small to find on its own; the
- share of them whose log2 fold changes have the same sign; and how many it finds too.
- pi1 measures replication in the replication study's p-values, so it is not
- symmetric.
-
- pi0 is estimated at lambda = 0.5 by default: the share of p-values above 0.5,
- over 0.5 (Storey and Tibshirani 2003). qvalue's default, a smoother of the
- estimates at lambda = 0.05, ..., 0.95 (``null_proportion={}``), reads the
- largest lambdas, where the few dozen p-values of the genes one study finds
- leave too few values, so that it often gives pi1 of 0 or 1 there.
+ For an ordered pair of studies, the discovery and the replication, pi1 = 1 - pi0
+ is Storey's estimate of the proportion with an effect among the replication
+ study's p-values of the genes that the discovery study finds
+ (:func:`~cytepool.stats.multiple_testing.proportion_of_true_nulls`), with pi0
+ estimated at lambda = 0.5 by default: the share of the p-values at least 0.5,
+ over 0.5 (Storey and Tibshirani 2003). qvalue's default (``null_proportion={}``),
+ a smoother of the estimates at lambda = 0.05, ..., 0.95 read at the largest
+ lambda, often gives pi1 of 0 or 1 on a few dozen p-values
+ (https://docs.nygen.io/CytePool/statistics/multiple-testing#multiple-testing-defaults).
+ https://docs.nygen.io/CytePool/statistics/replication#pi1-between-studies explains
+ pi1 and the sign agreement.
Heterogeneity:
Cochran's Q, its p-value, I^2 and tau^2 of the log2 fold changes and their
standard errors, by :func:`~cytepool.stats.meta_analysis.fit_meta_analysis` with
``meta_method``, for the genes that two studies or more tested; NaN for the others.
-Not done:
- The r-values of Heller, Bogomolov and Benjamini (2014), which claim replication
- in a follow-up study of the genes a primary study selects. Held-out prediction
- of case status from the genes the studies find (Nakatsuka et al. 2025) is
- :func:`cytepool.held_out_prediction`.
-
References:
Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
Improving reproducibility of differentially expressed genes in single-cell
@@ -294,8 +285,13 @@ def replication(
gets its combined p-value, its partial conjunction p-value of an effect in at
least ``at_least`` studies in one direction, the studies that find it, and the
heterogeneity of its log2 fold changes; every pair of studies its pi1 and sign
- agreement; and every k the features found in at least k studies. The module
- description gives the methods.
+ agreement; and every k the features found in at least k studies. Each study's
+ two-sided p-values become one-sided ones by the sign of its log2 fold change, so
+ that effects in opposite directions count against each other; a study finds a
+ feature when its adjusted p-value is below ``alpha``; and pi1 is one minus
+ Storey's estimate of pi0, at lambda = 0.5 by default.
+ https://docs.nygen.io/CytePool/statistics/replication#replication-tables explains
+ the tables.
Args:
studies: The differential-expression result of every study of a
@@ -326,8 +322,9 @@ def replication(
null_proportion: Options of
:func:`~cytepool.stats.multiple_testing.proportion_of_true_nulls` for pi1:
``lambdas``, ``method``, ``smooth_df`` and ``smooth_log``. None, the
- default, is ``{"lambdas": 0.5}``; ``{}`` takes qvalue's defaults (see
- the module description).
+ default, is ``{"lambdas": 0.5}``; ``{}`` takes qvalue's defaults, a
+ smoother that often gives pi1 of 0 or 1 on the few genes one study
+ finds.
Returns:
The tables of features, pairs of studies, counts by number of studies and
diff --git a/cytepool/rotation_gene_set_tests.py b/cytepool/rotation_gene_set_tests.py
index 73d9567..f516d15 100644
--- a/cytepool/rotation_gene_set_tests.py
+++ b/cytepool/rotation_gene_set_tests.py
@@ -1,58 +1,45 @@
"""Rotation gene-set tests of one contrast of a linear model: limma's roast, mroast and
romer.
-A rotation test (Langsrud 2005) draws random rotations of the residual space of the
-linear model fitted to every gene, which leave the joint distribution of the data
-unchanged under the null hypothesis, and compares a set statistic with its values under
-the rotations. :func:`~cytepool.roast` and :func:`~cytepool.mroast` are self-contained
-tests of one gene set and of many (Wu et al. 2010): whether the genes of a set are
-differentially expressed for the contrast at all. :func:`~cytepool.romer` is a
-competitive test of many sets (Majewski et al. 2010): whether the genes of a set rank
-higher than the other genes. :func:`~cytepool.fry` gives the limit of ``mroast`` with
-the mean statistic over infinitely many rotations without drawing any. Results match
-limma 3.62.1 within Monte Carlo error, and exactly when CytePool is given limma's
-rotations, except where noted under "Deliberate differences".
-
-Effects and rotations:
- Each gene's contrast effect u_0 and residual effects u_1, ..., u_d are those of
- :func:`~cytepool.camera` and :func:`~cytepool.fry`: coordinates of its data,
- whitened by its precision weights, in the basis of the Householder QR decomposition
- of the design reparametrised so that the contrast is its last coefficient. Under the
- null hypothesis they are independent with variance sigma^2, so one rotation applied
- to the (d + 1)-vectors of every gene leaves the joint distribution of the data, and
- the correlation between genes, unchanged (Wu et al. 2010, section 4.2). A rotation
- is given by its first row r, a point drawn uniformly from the unit sphere in d + 1
- dimensions (a vector of standard normal draws divided by its length): a gene's
- rotated contrast effect is u* = r'u and its rotated residual sum of squares |u|^2 -
- u*^2. The rotated moderated t-statistic of a gene uses the prior variance s_0^2 and
- prior df d_0 estimated once from the observed residual variances of all genes (Wu et
- al. 2010, section 2.4): t* = u* / s*, with s*^2 = (d_0 s_0^2 + |u|^2 - u*^2) / (d_0
- + d). The rotations come from ``rng.standard_normal``, 4,096 at a time, which bounds
- the memory they take; numpy gives the same stream however its draws are split, so a
- seeded generator gives the same p-values every time. limma draws its own rotations
- from R's generator, so the two programs agree within Monte Carlo error.
-
-roast and mroast:
+:func:`~cytepool.roast` and :func:`~cytepool.mroast` are self-contained rotation tests
+(Langsrud 2005; Wu et al. 2010) of one gene set and of many: whether the genes of a set
+are differentially expressed for the contrast at all; roast returns a table of one row,
+and mroast a row per set, with p-values adjusted over the sets. :func:`~cytepool.romer`
+is a competitive rotation test of many sets (Majewski et al. 2010): whether the genes of
+a set rank higher than the other genes. :func:`~cytepool.fry` gives the limit of
+``mroast`` with the mean statistic over infinitely many rotations without drawing any.
+https://docs.nygen.io/CytePool/statistics/gene-set-tests#roast explains the tests, and
+https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits how they
+differ from limma's.
+
+Rotations:
+ The tests read their data, design and contrast as :func:`~cytepool.camera` and
+ :func:`~cytepool.fry` do, and rotate each gene's contrast effect u_0 and residual
+ effects u_1, ..., u_d (Wu et al. 2010, section 4.2). A rotation is given by its
+ first row r, a point drawn uniformly from the unit sphere in d + 1 dimensions (a
+ vector of standard normal draws divided by its length): a gene's rotated contrast
+ effect is u* = r'u and its rotated residual sum of squares |u|^2 - u*^2. The
+ rotated moderated t-statistic of a gene uses the prior variance s_0^2 and prior df
+ d_0 estimated once from the observed residual variances of all genes (Wu et al.
+ 2010, section 2.4): t* = u* / s*, with s*^2 = (d_0 s_0^2 + |u|^2 - u*^2) / (d_0 +
+ d). The rotations come from ``rng.standard_normal``, 4,096 at a time, which bounds
+ the memory they take; numpy gives the same stream however its draws are split, so
+ a seeded generator gives the same p-values every time.
+
+Set statistics of roast and mroast:
Each gene's moderated t becomes a z-score by Bailey's (1980) normalizing
transformation, limma's default, z = (8 nu + 1) / (8 nu + 9) (nu + 19/12)^(1/2)
- log(1 + t^2 / (nu + 1/12))^(1/2) with the sign of t (limma's ``zscoreT(approx =
- TRUE)`` to 1e-14, found by probing), on nu = min(d_0 + d, 10^4) degrees of freedom,
- limma's limit (found by probing). With an infinite prior df the exact z-score is t
- itself, which the limit lowers by about (t^2 + 1) / (4 10^4) of its value
- (1.2e-4 at t = 2, 2.5e-3 at t = 10); the same transformation applies to the
- observed and the rotated statistics, so the test remains exact. With contribution
- weights a_g (1 unless given; a gene of weight zero leaves the set), A = sum |a_g|
- and m genes in the set, the set statistics of Wu et al. (2010, section 3.3) are, for
- the up, down and mixed alternatives:
+ log(1 + t^2 / (nu + 1/12))^(1/2) with the sign of t, on nu = min(d_0 + d, 10^4)
+ degrees of freedom. With contribution weights a_g (1 unless given; a gene of weight
+ zero leaves the set), A = sum |a_g| and m genes in the set, the set statistics of Wu
+ et al. (2010, section 3.3) are, for the up, down and mixed alternatives:
- ``"mean"``: sum a_g z_g / A, its negative, and sum |a_g z_g| / A.
- ``"floormean"``: sum max(a_g z_g, 0) / A, sum max(-a_g z_g, 0) / A, and
sum |a_g| max(|z_g|, c) / A, with c = 0.674, the square root of the median of
the chi-squared distribution on 1 df.
- ``"mean50"``: the mean of the h largest a_g z_g, of the h largest -a_g z_g, and
- of the h largest |a_g z_g|, with h = ceiling(m / 2), the smallest integer at least
- half the number of genes, as the paper's text and limma have it (its formula,
- ceiling((m + 1) / 2), is larger for even m).
+ of the h largest |a_g z_g|, with h = ceiling(m / 2).
- ``"msq"``: sum |a_g| z_g^2 / A over the genes with a_g z_g > 0, over those with
a_g z_g < 0, and over all genes.
@@ -61,81 +48,66 @@
extreme, and the p-value is (b + 1) / (B + 1) for b such rotations out of B
(Phipson and Smyth 2010). For the up and down alternatives each rotation r also
gives -r, which negates every rotated z-score, so b counts 2B rotated statistics
- and the p-value is (b + 1) / (2B + 1), as in limma. A gene is active for the up
- alternative when sign(a_g) z_g > sqrt(2), for the down alternative when
- sign(a_g) z_g < -sqrt(2), and for the mixed alternative when |z_g| > sqrt(2) (Wu
- et al. 2010, section 3.5); the two-sided alternative reports the larger of the up
- and down proportions, as limma does. mroast's direction is "up" when the up
- p-value is smaller than the down p-value and "down" otherwise (found by
- probing), and its adjusted p-values are Benjamini and Hochberg's, applied to the
- mid-p-values p - 1 / (2 (B + 1)) (Routledge 1994) and then raised to at least the
- p-value, as limma does by default. All sets share the same rotations.
-
-romer:
+ and the p-value is (b + 1) / (2B + 1). A gene is active for the up alternative when
+ sign(a_g) z_g > sqrt(2), for the down alternative when sign(a_g) z_g < -sqrt(2),
+ and for the mixed alternative when |z_g| > sqrt(2) (Wu et al. 2010, section 3.5);
+ the two-sided alternative reports the larger of the up and down proportions.
+ mroast's direction is "up" when the up p-value is smaller than the down p-value
+ and "down" otherwise, and its adjusted p-values are Benjamini and Hochberg's,
+ applied to the mid-p-values p - 1 / (2 (B + 1)) (Routledge 1994) and then raised to
+ at least the p-value. All sets share the same rotations.
+
+Set statistics of romer:
Every gene's observed moderated t-statistic is ranked among all genes (ties get
their average rank), and a set's statistic is the mean rank of its genes: the
ranks of t for the up alternative, of -t for the down alternative and of |t| for
the mixed alternative (``"mean"``); the same after setting negative values to
- zero, and for the mixed alternative |t| below 1 to 1 (``"floormean"``, the floor
- of 1 found by probing); or the mean of the ceiling(m / 2) largest ranks of the set
- (``"mean50"``). Each rotation ranks the rotated t-statistics of all genes again.
- The p-value of each alternative is (b + 1) / (B + 1), where b counts the rotations
- whose statistic is at least the observed one.
+ zero, and for the mixed alternative |t| below 1 to 1 (``"floormean"``); or the
+ mean of the ceiling(m / 2) largest ranks of the set (``"mean50"``). Each rotation
+ ranks the rotated t-statistics of all genes again. The p-value of each alternative
+ is (b + 1) / (B + 1), where b counts the rotations whose statistic is at least the
+ observed one.
Before rotating, each gene's contrast effect is shrunk, so that the
differential expression of the gene does not inflate its rotated statistics
- (limma's ``shrink.resid``; formula found by probing): u_0 becomes
- u_0 / (1 + P_g v_0 / v)^(1/2). P_g is the posterior probability that the gene is
- differentially expressed and v_0 the prior variance of the contrast for such genes,
- from the log-odds (B-statistics) of
- :func:`~cytepool.stats.empirical_bayes.moderated_statistics` with the t-statistics
- on d_0 + d degrees of freedom (their limit for an infinite prior df) and 1 - pi_0
- differentially expressed genes, and v is the unscaled variance of the contrast.
- The proportion of true null hypotheses pi_0 is (2 / (n + 1)) sum min(p_(i), i / n)
- over the n ordered p-values p_(i) of the moderated t-statistics (limma's
- ``propTrueNull(method = "lfdr")``, found by probing). The rotated residual sum of
- squares remains |u|^2 - u*^2 with |u| the length of the gene's unshrunk effects,
- as in limma (found by probing). romer multiplies the residual effects by the sign
- of the last diagonal element of R, as limma's romer does and its roast does not
- (found by probing); a uniformly random rotation does not depend on the basis of the
- residual space, but limma's rotations then mean the same here. The bounds of v_0
- are those of ``moderated_statistics`` (``stdev_coef_lim``), in log-expression
- units, so, as in limma, romer's p-values can change when the data are rescaled.
-
-Deliberate differences from limma:
- - roast and mroast count a rotated statistic equal to the observed one as at least
- as extreme, as Wu et al. (2010) and Phipson and Smyth (2010) define the p-value
- (and as limma's romer does); limma's roast counts only larger ones. The two
- differ only where the observed statistic is the smallest the statistic can
- take, which happens with positive probability for ``"floormean"`` and
- ``"msq"``: a set with no gene on one side of zero has a one-sided p-value of 1
- here and of about 1/2 in limma, and with ``"floormean"`` a set whose |z| are
- all below 0.674 has a mixed p-value of 1 here.
- - ``"msq"`` with contribution weights follows Wu et al. (2010). With weights of
- both signs, limma's up, down and two-sided msq p-values differ from it, and with
- more than 1,000 rotations (limma draws them 1,000 at a time) its msq p-values
- change when every weight is multiplied by the same number (found by probing).
- Its mixed msq p-values, and those of sets whose weights have one sign, agree with
- CytePool's for up to 1,000 rotations.
- - ``"floormean"``'s mixed statistic weighs each gene by |a_g|, as the paper's
- mixed statistics do; limma weighs it by a_g, so a negative weight turns a gene
- with a large |z| into evidence against the set (a one-gene set's mixed p-value
- goes from 0.003 to 0.997 when its weight changes sign).
- - A gene of contribution weight zero leaves the set: it counts neither in
- ``n_genes`` nor among the values that ``"mean50"`` averages. limma keeps it in
- both (and leaves it out of the active proportions, as CytePool does).
- - romer's posterior probability P_g is 1 / (1 + exp(-B)), of the log-odds B.
- limma computes exp(B) / (1 + exp(B)), which is NaN beyond B = 709.8 (|t| above
- about 38 with an infinite prior df); the gene's shrunk contrast effect is then
- NaN and the p-values of every set change (found by probing).
- - A set without genes, or whose genes all have weight zero, has NaN p-values and
- no direction and is left out of the adjustment of the other sets; limma reports
- "down" and NA, or the smallest possible p-value when all weights are zero. A
- gene listed twice counts once; limma counts it twice.
- - mroast tests every set with the same rotations; limma draws new rotations for
- each set. Each set's p-value is a valid rotation p-value either way.
- - A z-score is computed without overflow for |t| above 1e154, where limma's is
- infinite.
+ (limma's ``shrink.resid``): u_0 becomes u_0 / (1 + P_g v_0 / v)^(1/2). P_g is the
+ posterior probability that the gene is differentially expressed, 1 / (1 + exp(-B))
+ for its log-odds B, which stays finite for any B, and v_0 the prior variance of the
+ contrast for such genes, from the log-odds (B-statistics) of
+ :func:`~cytepool.stats.empirical_bayes.moderated_statistics` with the
+ t-statistics on d_0 + d degrees of freedom (their limit for an infinite prior df)
+ and 1 - pi_0 differentially expressed genes, and v is the unscaled variance of the
+ contrast. The proportion of true null hypotheses pi_0 is (2 / (n + 1)) sum
+ min(p_(i), i / n) over the n ordered p-values p_(i) of the moderated
+ t-statistics. The bounds of v_0 are those of ``moderated_statistics``
+ (``stdev_coef_lim``), in log-expression units, so, as in limma, romer's p-values
+ can change when the data are rescaled.
+
+Matching limma:
+ The results match limma 3.62.1 within Monte Carlo error, and exactly when CytePool
+ is given the rotations that limma draws from R's generator. limma's documentation
+ gives some of the conventions above and below, and running limma on chosen inputs
+ gave the others.
+
+ - Bailey's transformation is limma's ``zscoreT(approx = TRUE)`` to 1e-14, and its
+ limit of 10^4 degrees of freedom is limma's. With an infinite prior df the exact
+ z-score is t itself, which the limit lowers by about (t^2 + 1) / (4 10^4) of its
+ value (1.2e-4 at t = 2, 2.5e-3 at t = 10); the same transformation applies to the
+ observed and the rotated statistics, so the test remains exact.
+ - ``"mean50"`` averages the ceiling(m / 2) largest values, the smallest integer at
+ least half the number of genes, as the paper's text and limma have it; its
+ formula, ceiling((m + 1) / 2), is larger for even m.
+ - The p-values of the up and down alternatives count 2B rotated statistics, and
+ the two-sided alternative reports the larger of the up and down proportions of
+ active genes, as limma does; mroast's direction and its adjustment of mid-p-values
+ are limma's defaults.
+ - romer's floor mean raises |t| below 1 to 1 for the mixed alternative; its
+ shrinkage, the formula above with pi_0 from limma's ``propTrueNull(method =
+ "lfdr")``, is limma's; the rotated residual sum of squares keeps |u|, the length
+ of the gene's unshrunk effects; and romer multiplies the residual effects by the
+ sign of the last diagonal element of R, as limma's romer does and its roast does
+ not. A uniformly random rotation does not depend on the basis of the residual
+ space, but limma's rotations then mean the same here.
References:
Wu D, Lim E, Vaillant F, Asselin-Labat ML, Visvader JE, Smyth GK (2010). ROAST:
@@ -257,10 +229,15 @@ def roast(
differentially expressed for ``contrast``, down, up, in either direction
(two-sided) and mixed (in either direction, gene by gene), by comparing a set
statistic of the genes' moderated z-scores with its values under random rotations
- of the residual space. The module docstring gives the method and the deliberate
- differences from limma. With many sets, :func:`mroast` tests them together. A
- result of :func:`~cytepool.limma_voom` or :func:`~cytepool.limma_trend` is tested
- as :func:`~cytepool.camera` tests it.
+ of the residual space; the p-value is (b + 1) / (B + 1) for b of B rotations at
+ least as extreme, and (b + 1) / (2B + 1) for the up and down alternatives.
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#roast explains the test,
+ the module documentation gives the set statistics and the conventions that match
+ limma, and
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits the
+ differences from it. With many sets, :func:`mroast` tests them together. A result
+ of :func:`~cytepool.limma_voom` or :func:`~cytepool.limma_trend` is tested as
+ :func:`~cytepool.camera` tests it.
Args:
data: A :class:`~cytepool.DifferentialExpressionResult` of limma-voom or
@@ -272,9 +249,11 @@ def roast(
among them are left out), 0-based positions of columns of an array when
``feature_ids`` is not given, or a boolean mask over the genes; or a
mapping of gene (id or position) to its contribution weight. A gene
- listed twice counts once, and a gene of weight zero leaves the set. One
- set of a :class:`~cytepool.GeneSets`, ``gene_sets[name]``, is a tuple of
- gene ids.
+ listed twice counts once, and a gene of weight zero leaves the set: it
+ counts neither in ``n_genes`` nor among the values that ``"mean50"``
+ averages, where limma keeps it in both (both leave it out of the active
+ proportions). One set of a :class:`~cytepool.GeneSets`,
+ ``gene_sets[name]``, is a tuple of gene ids.
group: For a result, the group (cell type) whose fit is tested; it may be
left out when the result has one.
contrast: The contrast to test, as for :func:`~cytepool.camera`: for a
@@ -296,10 +275,13 @@ def roast(
negative weight marks a gene expected to change in the opposite
direction to the set, and larger weights count more in the set
statistic. Not with a set that gives its own weights.
- set_statistic: ``"mean"`` (limma's default), ``"floormean"``, ``"mean50"``
- or ``"msq"``; see the module docstring. The mean detects sets in which
- most genes change in the same direction; the others detect sets in which
- fewer genes change, msq the fewest.
+ set_statistic: ``"mean"`` (limma's default), the mean of the genes'
+ z-scores; ``"floormean"``, their mean after values on the wrong side of
+ zero are set to zero, and for the mixed alternative |z| below 0.674 to
+ 0.674; ``"mean50"``, the mean of the larger half; or ``"msq"``, the mean
+ square. The module documentation gives them with contribution weights.
+ The mean detects sets in which most genes change in the same direction;
+ the others detect sets in which fewer genes change, msq the fewest.
n_rotations: Number of rotations B, at least 1. The smallest p-value is
1 / (B + 1), and 1 / (2B + 1) for the up and down alternatives.
trend: Whether the prior variance follows the mean log-expression of the
@@ -319,8 +301,10 @@ def roast(
genes active down, up and in either direction; ``direction``, ``"up"`` or
``"down"`` (None where the p-value is NaN); ``p_value``, the two-sided
p-value (limma's ``UpOrDown``); and ``down_p_value``, ``up_p_value`` and
- ``mixed_p_value``, those of the other alternatives. A set without genes has
- NaN proportions and p-values.
+ ``mixed_p_value``, those of the other alternatives. A set without genes, or
+ whose genes all have weight zero, has NaN proportions and p-values and no
+ direction; limma reports "down" and NA for a set without genes, and the
+ smallest p-value it can when every weight is zero.
Raises:
TypeError: If an argument has the wrong type, or is or holds a numpy masked
@@ -393,8 +377,12 @@ def mroast(
"""Self-contained rotation tests of many gene sets for one contrast.
limma's ``mroast``: :func:`roast` for each set, all sets with the same rotations,
- and with their p-values adjusted for testing many sets. The module docstring gives
- the method and the deliberate differences from limma.
+ and with their p-values adjusted for testing many sets by Benjamini and Hochberg's
+ method, applied to mid-p-values.
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#roast explains the test,
+ the module documentation the conventions, and
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits the
+ differences from limma.
Args:
data: A differential-expression result, a VoomResult or log-expression
@@ -432,8 +420,10 @@ def mroast(
directional p-value; ``adjusted_p_value``, Benjamini-Hochberg over the sets
(Benjamini and Hochberg 1995), limma's default (its other ``adjust.method``
choices are not offered); ``mixed_p_value``, the p-value of the mixed
- alternative; and ``mixed_adjusted_p_value``. A set without genes has NaN
- proportions and p-values.
+ alternative; and ``mixed_adjusted_p_value``. A set without genes, or whose
+ genes all have weight zero, has NaN proportions and p-values and no direction,
+ and the adjustment leaves it out; limma reports "down" and NA for a set
+ without genes, and the smallest p-value it can when every weight is zero.
Raises:
TypeError: If an argument has the wrong type, or is or holds a numpy masked
@@ -442,7 +432,7 @@ def mroast(
any set is among the data's ids.
KeyError: As :func:`roast`.
"""
- use_mid_p = checked_flag(mid_p, "mid_p", expected="a bool")
+ use_mid_p = checked_flag(mid_p, "mid_p")
tests = _roast_tests(
data,
gene_sets,
@@ -486,7 +476,8 @@ def mroast(
def _direction(p: float, p_up: float, p_down: float) -> str | None:
"""The direction of a set with the two-sided p-value p: up when its up p-value is
- the smaller, down otherwise; None where p is NaN."""
+ the smaller, down otherwise; None where p is NaN. limma's mroast decides it so
+ (found by probing)."""
return None if math.isnan(p) else ("up" if p_up < p_down else "down")
@@ -514,11 +505,17 @@ def romer(
the set's mean rank with its values when every gene's rotated statistics are
ranked again. Like gene set enrichment analysis it pits the sets against one
another, but it uses rotations instead of permuting samples, so it works for any
- contrast of a linear model with residual degrees of freedom. The module docstring
- gives the method. A result of :func:`~cytepool.limma_trend` is tested as
- :func:`~cytepool.camera` tests it; romer has no observation weights, so a result
- of :func:`~cytepool.limma_voom`, whose voom weights are, is refused, and limma's
- romer moderates the variances without a trend.
+ contrast of a linear model with residual degrees of freedom. Before rotating,
+ each gene's contrast effect is shrunk by its posterior probability of being
+ differentially expressed.
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#romer explains the test,
+ the module documentation its statistics and shrinkage, and
+ https://docs.nygen.io/CytePool/statistics/gene-set-tests#gene-set-tests-limits the
+ differences from limma. A result of :func:`~cytepool.limma_trend` is tested as
+ :func:`~cytepool.camera` tests it, but romer moderates the variances without a
+ trend and without robust estimation, whatever the result used, as limma's romer,
+ which has neither option. romer has no observation weights, so a result of
+ :func:`~cytepool.limma_voom`, whose voom weights are, is refused.
Args:
data: A :class:`~cytepool.DifferentialExpressionResult` of limma-trend, or
@@ -535,8 +532,10 @@ def romer(
feature_ids: For an array, one id per gene, unique, to name genes by id.
weights: For an array, ``(n_samples,)`` sample weights, positive and finite
(limma's ``array.weights``). romer has no observation weights.
- set_statistic: ``"mean"`` (limma's default), ``"floormean"`` or ``"mean50"``;
- see the module docstring.
+ set_statistic: ``"mean"`` (limma's default), the mean rank of the set's
+ genes; ``"floormean"``, the same after negative t-statistics are set to
+ zero, and for the mixed alternative |t| below 1 to 1; or ``"mean50"``,
+ the mean of the larger half of the ranks.
n_rotations: Number of rotations B, at least 1; limma's default is 9999. The
smallest p-value is 1 / (B + 1).
shrink_contrast_effects: Whether each gene's contrast effect is shrunk before
@@ -564,9 +563,7 @@ def romer(
"""
statistic = _statistic(set_statistic, _ROMER_STATISTICS)
n_rotations = _rotation_count(n_rotations)
- shrink = checked_flag(
- shrink_contrast_effects, "shrink_contrast_effects", expected="a bool"
- )
+ shrink = checked_flag(shrink_contrast_effects, "shrink_contrast_effects")
generator = random_generator(rng)
with thread_limit(n_threads):
read = gene_set_data(
@@ -674,12 +671,8 @@ def _roast_tests(
"""The active proportions and p-values of each set, for roast and mroast."""
statistic = _statistic(set_statistic, _ROAST_STATISTICS)
n_rotations = _rotation_count(n_rotations)
- with_trend = (
- None if trend is None else checked_flag(trend, "trend", expected="a bool")
- )
- with_robust = (
- None if robust is None else checked_flag(robust, "robust", expected="a bool")
- )
+ with_trend = None if trend is None else checked_flag(trend, "trend")
+ with_robust = None if robust is None else checked_flag(robust, "robust")
generator = random_generator(rng)
with thread_limit(n_threads):
read = gene_set_data(
@@ -881,7 +874,11 @@ def _set_statistics(
"""The up, down and mixed statistics of the genes ``members[start:stop]``.
Larger values are more extreme for every alternative; the down statistic is the
- up statistic of -z.
+ up statistic of -z. With contribution weights, msq and floormean's mixed
+ statistic weigh each gene by |a| as Wu et al. (2010) do. limma's msq p-values
+ differ for weights of both signs and, beyond 1,000 rotations, change with the
+ scale of the weights, and its floormean weighs the mixed statistic by a (found by
+ probing).
"""
m = stop - start
if statistic == _MEAN50:
@@ -1113,7 +1110,8 @@ def _moderated_t(
@jit
def _bailey_z(t: float, df: float) -> float:
- """Bailey's (1980) normalizing transformation of t on df degrees of freedom."""
+ """Bailey's (1980) normalizing transformation of t on df degrees of freedom,
+ limma's ``zscoreT(approx = TRUE)`` to 1e-14 (found by probing)."""
if math.isinf(df) or t == 0.0 or math.isnan(t):
return t
a = abs(t)
@@ -1188,7 +1186,8 @@ def _romer_p_values(
with np.errstate(divide="ignore", invalid="ignore"):
t = effects[:, 0] / np.sqrt(squeezed.posterior_variance)
t = np.where(np.isnan(t), 0.0, t)
- norms = np.sum(effects**2, axis=1) # of the unshrunk effects, as in limma
+ # The squared lengths of the unshrunk effects, as in limma (found by probing).
+ norms = np.sum(effects**2, axis=1)
rotated = effects.copy()
if shrink:
rotated[:, 0] *= _shrinkage(
@@ -1236,7 +1235,8 @@ def _shrinkage(
P and v_0 are those of the log-odds of ``moderated_statistics`` (Smyth 2004), with
the t-statistics on d_0 + d degrees of freedom and the proportion of
- differentially expressed genes 1 - pi_0.
+ differentially expressed genes 1 - pi_0: limma's ``shrink.resid`` (formula found
+ by probing).
"""
df = np.asarray(df_prior, dtype=np.float64) + d
p_values = 2.0 * np.asarray(t_sf(np.abs(t), df), dtype=np.float64)
@@ -1263,6 +1263,8 @@ def _shrinkage(
lods = log_odds(
t[:, np.newaxis], unscaled, df, np.isinf(df), np.array([v0]), proportion
)[:, 0]
+ # 1 / (1 + exp(-B)) stays finite; limma's exp(B) / (1 + exp(B)) is NaN beyond
+ # B = 709.8 (found by probing).
with np.errstate(over="ignore"):
posterior = 1.0 / (1.0 + np.exp(-lods))
with np.errstate(invalid="ignore"):
@@ -1666,4 +1668,4 @@ def _statistic(value: Any, options: tuple[str, ...]) -> int:
def _rotation_count(value: Any) -> int:
reject_masked(value, "n_rotations")
- return positive_integer(value, "n_rotations", expected="a positive integer")
+ return positive_integer(value, "n_rotations")
diff --git a/cytepool/score_tests.py b/cytepool/score_tests.py
index 954ea39..3956b5c 100644
--- a/cytepool/score_tests.py
+++ b/cytepool/score_tests.py
@@ -1,25 +1,12 @@
"""Tests of per-sample scores with a design: limma on pathway scores and activities.
-Pathway scores (:func:`~cytepool.gsva` and the others) and activities of regulators
-(:func:`~cytepool.ulm` and the others) give every sample of a cube's group one value per
-gene set or source, which differ between conditions as expression values do. GSVA's
-guide tests its scores with limma, as decoupler's guides test activities: a linear model
-of every set's scores on the design (``lmFit``), the contrast (``contrasts.fit``),
-moderated t-tests (``eBayes``, Smyth 2004) and a table of the results (``topTable``).
-:func:`~cytepool.limma` takes these steps in one call, with the
-:class:`~cytepool.Design` built for the cube: its rows are matched to the scores by
-``sample_id``, so that the samples a score leaves out, such as those with too few cells,
-leave the fit too.
-
-The fit:
- The samples fitted are those of the scores that the design has, in the order of
- the scores. The design matrix is the design's rows for them, so every column keeps
- its meaning; when those rows do not determine every column (the samples left out
- took a donor's every sample, say), the fit keeps the columns that span them, as
- R's ``qr()`` keeps them and :func:`~cytepool.limma_voom` fits a group, and the
- contrast is tested when it is estimable from these samples (see "The design of a
- group" in the description of :func:`~cytepool.limma_voom`). Missing scores (NaN)
- leave a set out of the fit of the samples they miss, as limma's ``lmFit`` does.
+:func:`~cytepool.limma` tests a contrast of a design on values of samples: pathway
+scores (:func:`~cytepool.gsva` and the others), activities (:func:`~cytepool.ulm` and the
+others) or an array, by limma's ``lmFit``, ``contrasts.fit``, ``eBayes`` (Smyth 2004)
+and ``topTable``, the steps that GSVA's guide takes to test its scores, and returns a
+table with one row per set, source or feature.
+https://docs.nygen.io/CytePool/statistics/pathway-scores#testing-scores explains the
+tests.
References:
Smyth GK (2004). Linear models and empirical Bayes methods for assessing
@@ -76,9 +63,28 @@ def limma(
limma's ``lmFit``, ``contrasts.fit``, ``eBayes`` and ``topTable(sort.by =
"none")`` for one contrast: a linear model of the scores of every gene set,
source or feature on the design, moderated t-tests of the contrast, and
- Benjamini-Hochberg adjusted p-values over them. The module description says
- which samples are fitted and how a design that its samples do not determine is
- fitted. Sort the table to read it, as ``limma(scores, design).sort("p_value")``.
+ Benjamini-Hochberg adjusted p-values over them.
+ https://docs.nygen.io/CytePool/statistics/pathway-scores#testing-scores explains the
+ tests. Sort the table to read it, as ``limma(scores, design).sort("p_value")``.
+
+ The design's rows are matched to the scores by ``sample_id``, so that the samples a
+ score leaves out, such as those with too few cells, leave the fit too: the samples
+ fitted are those of the scores that the design has, in the order of the scores. The
+ design matrix is the design's rows for them, so every column keeps its meaning;
+ when those rows do not determine every column (the samples left out took a donor's
+ every sample, say), the fit keeps the columns that span them, as R's ``qr()`` keeps
+ them and :func:`~cytepool.limma_voom` fits a group, and the contrast is tested when
+ it is estimable from these samples
+ (https://docs.nygen.io/CytePool/statistics/linear-models#estimable-contrasts).
+ Missing scores (NaN) leave a set out of the fit of the samples they miss, as limma's
+ ``lmFit`` does.
+
+ The standard errors of the contrast are exact, as
+ :func:`~cytepool.stats.linear_models.contrasts_fit` computes them with
+ ``method="exact"``. They are limma's when one design fits every score: no missing
+ scores, and no weights or one weight per sample. Otherwise limma's
+ ``contrasts.fit`` approximates them where the contrast combines correlated
+ coefficients.
Args:
data: :class:`~cytepool.PathwayScores`, such as
@@ -154,7 +160,7 @@ def limma(
raise ValueError(
f"the contrast {name!r} is not estimable from the samples of the data, "
"which cannot separate the effects it depends on: "
- + "; ".join(violations[0])
+ + listing(violations[0], separator="; ")
)
columns = tuple(design.columns[j] for j in kept)
fit = fit_linear_model(
diff --git a/cytepool/stats/_fixed_effect_inference.py b/cytepool/stats/_fixed_effect_inference.py
index f9dece7..5c8ae65 100644
--- a/cytepool/stats/_fixed_effect_inference.py
+++ b/cytepool/stats/_fixed_effect_inference.py
@@ -10,7 +10,7 @@
contrast follow. ``chosen_parts`` takes each feature's parts from one of two sets.
- ``contrast_tests``: the t-tests of contrasts.
- ``f_test``: the F-test that several contrasts are all zero, by Satterthwaite's
- method as lmerTest's ``contestMD`` computes it or by Kenward and Roger's as
+ method as lmerTest's ``contestMD`` computes it or by Kenward-Roger's as
pbkrtest's ``KRmodcomp`` does.
- ``combined_df``: the denominator degrees of freedom of an F-statistic that is the
mean of the squares of independent t-statistics, from theirs.
@@ -103,7 +103,7 @@ class FTests(NamedTuple):
df1: int # numerator degrees of freedom: the number of independent contrasts
df2: FloatArray # (G,) denominator degrees of freedom
p_value: FloatArray # (G,)
- scaling: FloatArray # (G,) Kenward and Roger's scaling of the statistic (or 1)
+ scaling: FloatArray # (G,) Kenward-Roger's scaling of the statistic (or 1)
def satterthwaite_parts(fit: Any) -> InferenceParts:
@@ -120,7 +120,7 @@ def satterthwaite_parts(fit: Any) -> InferenceParts:
def kenward_roger_parts(fit: Any, average_weight: bool) -> InferenceParts:
- """The parts of Kenward and Roger's tests of a REML fit; see the module docstring.
+ """The parts of Kenward-Roger's tests of a REML fit; see the module docstring.
Args:
fit: A ``MixedModelFit`` by REML, with its weights and the levels of its
@@ -268,7 +268,7 @@ def f_test(
squared t-statistics, and its denominator degrees of freedom combine their
Satterthwaite degrees of freedom by ``combined_df`` (Fai and Cornelius 1996;
Kuznetsova et al. 2017).
- - Kenward and Roger's, as pbkrtest's ``KRmodcomp``: the Wald statistic with the
+ - Kenward-Roger's, as pbkrtest's ``KRmodcomp``: the Wald statistic with the
adjusted covariance matrix, divided by the rank and scaled by lambda, on
(rank, m) degrees of freedom (Kenward and Roger 1997, section 4). It does not
depend on how the hypothesis is spanned.
@@ -386,7 +386,7 @@ def _kenward_roger_f(
parts: InferenceParts,
rank: int,
) -> tuple[FloatArray, FloatArray, FloatArray]:
- """Kenward and Roger's F-statistic, denominator df and scaling, per feature.
+ """Kenward-Roger's F-statistic, denominator df and scaling, per feature.
With Theta = L' (L Phi L')^-1 L for the unadjusted Phi, A1 = sum_ij W_ij
tr(Theta E_i) tr(Theta E_j) and A2 = sum_ij W_ij tr(Theta E_i Theta E_j), where the
@@ -457,7 +457,7 @@ def _kenward_roger_features(
derivatives: FloatArray,
parameter_covariance: FloatArray,
) -> None:
- """Kenward and Roger's parts of features ``start:stop``, in the basis.
+ """Kenward-Roger's parts of features ``start:stop``, in the basis.
Everything is computed with Sigma / s^2 = R + sum_k gamma_k Z_k Z_k' (gamma_k =
sigma_k^2 / s^2), whose inverse is M, and U = M q: then X' Sigma^-1 G_i Sigma^-1 X
diff --git a/cytepool/stats/_scalar_special_functions.py b/cytepool/stats/_scalar_special_functions.py
index 8f4ee2a..b68b21c 100644
--- a/cytepool/stats/_scalar_special_functions.py
+++ b/cytepool/stats/_scalar_special_functions.py
@@ -1,4 +1,4 @@
-"""The special functions of scalars behind the vectorised ones, compiled with numba.
+"""The special functions of scalars that the vectorised ones call, compiled with numba.
These routines compute, for one value at a time, the logarithms and differences of
gamma and beta functions and the regularized incomplete gamma and beta functions
diff --git a/cytepool/stats/combined_p_values.py b/cytepool/stats/combined_p_values.py
index 8117738..792dc37 100644
--- a/cytepool/stats/combined_p_values.py
+++ b/cytepool/stats/combined_p_values.py
@@ -4,77 +4,27 @@
studies that tested it into one p-value of the global null hypothesis that no study
has an effect, by the methods of Fisher (1932), Stouffer et al. (1949), Tippett
(1931), Wilkinson (1951), Pearson (1933), Mudholkar and George (George and Mudholkar
-1983) and Simes (1986), as scipy's ``combine_pvalues`` and the R package metap compute
-them. :func:`partial_conjunction` gives the p-value that at least u of the n studies
-have an effect (Benjamini and Heller 2008): a gene that one study finds is not
-replicated by the others, and the global null hypothesis, which one study's effect
-rejects, cannot say so.
-
-The methods:
- For the p-values p_1, ..., p_k of the k studies that tested a feature, in order
- p_(1) <= ... <= p_(k):
-
- - ``"fisher"``: X = -2 sum log p_i against the chi-squared distribution with 2k
- degrees of freedom (metap's ``sumlog``).
- - ``"stouffer"``: Z = sum w_i z_i / sqrt(sum w_i^2), with z_i = Phi^-1(1 - p_i),
- against the standard normal distribution (Stouffer et al. 1949; Lipták 1958;
- metap's ``sumz``). The weights w_i are equal by default; the square roots of
- the studies' sample sizes are the usual choice (Zaykin 2011).
- - ``"tippett"``: the smallest p-value, with P(p_(1) <= s) = 1 - (1 - s)^k
- (metap's ``minimump``).
- - ``"wilkinson"``: the r-th smallest p-value, against the beta distribution with
- parameters r and k - r + 1 (metap's ``wilkinsonp``; r = 1 is Tippett's method
- and r = k metap's ``maximump``, p_(k)^k). It tests the global null hypothesis:
- that p_(k)^k is small says that every study has some evidence, not that every
- study has an effect, which :func:`partial_conjunction` tests.
- - ``"pearson"``: X = -2 sum log(1 - p_i), whose chi-squared distribution function
- with 2k degrees of freedom is the p-value: small when every p-value is small,
- and near 1 when any p-value is near 1 (scipy's ``"pearson"``, whose statistic
- is -X).
- - ``"mudholkar_george"``: the logit statistic T = sum log((1 - p_i) / p_i), times
- sqrt(3 / k) / pi sqrt((5k + 4) / (5k + 2)), against Student's t distribution
- with 5k + 4 degrees of freedom (metap's ``logitp``, whose statistic this is;
- scipy reports T).
- - ``"simes"``: min over j of k p_(j) / j, a p-value of the global null hypothesis
- that is valid for independent and positively dependent p-values (Simes 1986).
-
- The statistics are monotone in each p-value, and each p-value is exact for
- independent studies whose p-values are uniform under the null hypothesis. Fisher's
- and Tippett's methods respond to the strongest study; Stouffer's, the logit and
- Pearson's to consistent evidence across studies; a p-value near 1 weighs most on
- Pearson's.
+1983) and Simes (1986). :func:`partial_conjunction` gives the p-value that at least u of
+the n studies have an effect (Benjamini and Heller 2008). Both return a
+:class:`CombinedPValues`.
+https://docs.nygen.io/CytePool/statistics/replication#combined-p-values explains the
+methods and when to choose each, and
+https://docs.nygen.io/CytePool/statistics/replication#partial-conjunction the partial
+conjunctions.
One-sided and two-sided p-values:
- The methods are defined for one-sided p-values, all for the same direction of
- effect, which effects in the other direction push towards 1. Two-sided p-values
- combined as they are test whether some study has an effect in either direction,
- and effects in opposite directions do not cancel. ``directions``, the signs of
- the studies' effects (such as their log2 fold changes), turns two-sided p-values
- into one-sided ones, as metap's ``two2one``: p / 2 for an effect above zero and
- 1 - p / 2 for one below, and 1/2 for an effect of exactly zero, for the effects
- above zero; their complements, for the effects below. Each direction is
- combined, giving ``up_p_value`` and ``down_p_value``, and the p-value is
- min(1, 2 min(p_up, p_down)), the Bonferroni bound over the two directions (Owen
- 2009): for Stouffer's method it is the two-sided p-value of Z, 2 (1 - Phi(|Z|)),
- and effects in opposite directions cancel. The one-sided p-value near 0 is kept
- exactly: Stouffer's z_i is the normal quantile of p / 2 with the sign of the
- effect, and each method reads log(1 - p_i) from the complement p / 2 where p_i is
- 1 - p / 2.
-
-Partial conjunction:
- The hypothesis that fewer than u of the n studies that tested a feature have an
- effect is tested by combining the n - u + 1 largest p-values, p_(u), ..., p_(n),
- by a method valid for n - u + 1 independent p-values (Benjamini and Heller 2008):
- Fisher's, Stouffer's (without weights, as the paper defines it) or Simes's,
- min over i = u, ..., n of (n - u + 1) p_(i) / (i - u + 1). For u = 1 it is the
- global null hypothesis; for u = n, every method gives p_(n). With
- ``directions``, the effects must also share their direction: the one-sided
- p-values of each direction are combined as above, and the p-value is
- min(1, 2 min(p_up, p_down)). A feature that fewer than u studies tested has no
- p-value (NaN). Adjusting the p-values of the features by Benjamini and Hochberg's
- procedure controls the false discovery rate of the claims that a feature has an
- effect in at least u studies, for independent features (Benjamini and Heller
- 2008), as ``adjust_p_values(result.p_value, "bh")`` gives it.
+ The p-values are one-sided, for one direction of effect, unless ``directions``
+ gives the signs of the studies' effects (such as their log2 fold changes) for
+ two-sided ones. These become one-sided ones, as metap's ``two2one`` makes them:
+ for the effects above zero, p / 2 where a study's effect is above zero, 1 - p / 2
+ where it is below and 1/2 where it is exactly zero; for the effects below zero,
+ their complements. Each direction is combined, into ``up_p_value`` and
+ ``down_p_value``, and the p-value is min(1, 2 min(p_up, p_down)), the Bonferroni
+ bound over the two directions (Owen 2009); for Stouffer's method it equals
+ 2 (1 - Phi(|Z|)), in which effects in opposite directions cancel. The one-sided
+ p-value near 0 is kept exactly: Stouffer's z_i is the normal quantile of p / 2 with
+ the sign of the effect, and each method reads log(1 - p_i) from the complement
+ p / 2 where p_i is 1 - p / 2.
Studies that did not test a feature:
NaN marks them. Each feature is combined over the k studies that tested it,
@@ -89,14 +39,16 @@
makes Pearson's 1, and Stouffer's and the logit 1 unless another study's p-value
is 0, where the limit does not exist and the combined p-value is NaN.
-Deliberate differences from scipy and metap:
- ``validation/combined_p_values_and_replication.py`` reports them.
+Matching scipy and metap:
+ The combined p-values match scipy 1.18.1's ``combine_pvalues`` and metap 1.1, and
+ the partial conjunctions their definition computed with metap; running metap on
+ chosen inputs settled what its documentation leaves open.
- - metap leaves out p-values of 0 (and of 1 for ``sumz`` and ``logitp``), with a
- warning that "some studies omitted", and stops with fewer than two studies.
- Here they are combined as limits, and one study gives its own p-value.
- - scipy's ``combine_pvalues`` gives NaN for a feature with a missing p-value
- unless asked to omit it (``nan_policy="omit"``), which this module does.
+Differences from scipy and metap:
+ https://docs.nygen.io/CytePool/statistics/replication#replication-limits gives
+ those from metap. scipy's ``combine_pvalues`` gives NaN for a feature with a
+ missing p-value unless asked to omit it (``nan_policy="omit"``), as this module
+ always does.
References:
Fisher RA (1932). Statistical Methods for Research Workers, 4th edition. Oliver
@@ -236,9 +188,46 @@ def combine_p_values(
Every feature's p-values, over the studies that tested it, are combined into one
p-value of the hypothesis that no study has an effect, by the methods that
- scipy's ``combine_pvalues`` and metap offer; the module description gives each
- method, the handling of one-sided and two-sided p-values and of p-values of 0
- and 1.
+ scipy's ``combine_pvalues`` and metap offer. With ``directions``, two-sided
+ p-values become one-sided ones for each direction, each direction is combined,
+ and the p-value is min(1, 2 min(p_up, p_down)). NaN marks a study that did not
+ test a feature, and p-values of 0 and 1 are combined as the limits they are.
+ https://docs.nygen.io/CytePool/statistics/replication#combined-p-values explains
+ when to choose each method.
+
+ The methods:
+ For the p-values p_1, ..., p_k of the k studies that tested a feature, in order
+ p_(1) <= ... <= p_(k):
+
+ - ``"fisher"``: X = -2 sum log p_i against the chi-squared distribution with 2k
+ degrees of freedom (metap's ``sumlog``).
+ - ``"stouffer"``: Z = sum w_i z_i / sqrt(sum w_i^2), with z_i = Phi^-1(1 - p_i),
+ against the standard normal distribution (Stouffer et al. 1949; Lipták 1958;
+ metap's ``sumz``). The weights w_i are equal by default; the square roots of
+ the studies' sample sizes are the usual choice (Zaykin 2011).
+ - ``"tippett"``: the smallest p-value, with P(p_(1) <= s) = 1 - (1 - s)^k
+ (metap's ``minimump``).
+ - ``"wilkinson"``: the r-th smallest p-value, against the beta distribution with
+ parameters r and k - r + 1 (metap's ``wilkinsonp``; r = 1 is Tippett's method
+ and r = k metap's ``maximump``, p_(k)^k). It tests the global null hypothesis,
+ also with r = k: that every study has an effect is the alternative of
+ :func:`partial_conjunction` with ``at_least`` = k.
+ - ``"pearson"``: X = -2 sum log(1 - p_i), whose chi-squared distribution function
+ with 2k degrees of freedom is the p-value: small when every p-value is small,
+ and near 1 when any p-value is near 1 (scipy's ``"pearson"``, whose statistic
+ is -X).
+ - ``"mudholkar_george"``: the logit statistic T = sum log((1 - p_i) / p_i), times
+ sqrt(3 / k) / pi sqrt((5k + 4) / (5k + 2)), against Student's t distribution
+ with 5k + 4 degrees of freedom (metap's ``logitp``, whose statistic this is;
+ scipy reports T).
+ - ``"simes"``: min over j of k p_(j) / j, a p-value of the global null hypothesis
+ that is valid for independent and positively dependent p-values (Simes 1986).
+
+ The statistics are monotone in each p-value, and each p-value is exact for
+ independent studies whose p-values are uniform under the null hypothesis.
+
+ With ``directions``, a two-sided p-value below the smallest normal double
+ (2.2e-308) loses digits when halved, and one of 5e-324 halves to 0.
Args:
p_values: ``(n_studies, n_features)`` p-values in [0, 1], or ``(n_studies,)``
@@ -310,11 +299,18 @@ def partial_conjunction(
than u = ``at_least`` of the n studies that tested a feature have an effect is
tested by combining its n - u + 1 largest p-values by Simes's, Fisher's or
Stouffer's method. With ``at_least=2`` a small p-value says that the effect is
- replicated: one study's effect alone does not make it small. With
- ``directions``, the effects must share their direction. The module description
- gives the method; adjust the p-values over the features with
+ replicated: one study's effect alone does not make it small. Simes's p-value is
+ the minimum over i = u, ..., n of (n - u + 1) p_(i) / (i - u + 1), for the
+ p-values in order p_(1) <= ... <= p_(n); Fisher's and Stouffer's (without
+ weights, as the paper defines it) combine the n - u + 1 largest p-values. For
+ u = 1 it is the global null hypothesis, and for u = n every method gives p_(n).
+ With ``directions``, the effects must share their direction: the one-sided
+ p-values of each direction are combined, and the p-value is min(1, 2 min(p_up,
+ p_down)). Adjust the p-values over the features with
``adjust_p_values(result.p_value, "bh")`` to control the false discovery rate of
- the replication claims.
+ the replication claims, for independent features (Benjamini and Heller 2008).
+ https://docs.nygen.io/CytePool/statistics/replication#partial-conjunction explains
+ the test.
Args:
p_values: ``(n_studies, n_features)`` p-values in [0, 1], or ``(n_studies,)``
diff --git a/cytepool/stats/deseq2.py b/cytepool/stats/deseq2.py
index bec0399..bb477c4 100644
--- a/cytepool/stats/deseq2.py
+++ b/cytepool/stats/deseq2.py
@@ -1,95 +1,17 @@
"""DESeq2's dispersion estimates and its Wald and likelihood-ratio tests.
-DESeq2 (Love, Huber and Anders 2014) models the count y_gi of gene g in sample i as
-
- y_gi ~ NB(mu_gi, alpha_g), var(y_gi) = mu_gi + alpha_g mu_gi^2,
- log2(mu_gi) = log2(s_i) + x_i' beta_g,
-
-with s_i the size factor of sample i (by default the median-of-ratios factors of
-Anders and Huber (2010), :func:`~cytepool.normalization.median_ratio_size_factors`),
-x_i its row of the design
-and beta_g the coefficients of gene g on the log2 scale. :func:`fit_deseq2` estimates
-the dispersions and fits every gene, as ``DESeq()`` does; :func:`deseq2_wald_test`
-tests a coefficient or contrast and :func:`deseq2_likelihood_ratio_test` a reduced
-design against the full one, with the columns of ``results()``. They follow DESeq2's
-documentation and paper, with DESeq2's defaults; conventions the documentation leaves
-open were found by running DESeq2 1.46.0 as a black box and are marked "found by
-probing". Genes without counts are left out of every step and get NaN.
-
-Dispersions (:func:`deseq2_dispersions`, as ``estimateDispersions()``):
-
-1. The means of the likelihood. For a one-way layout, whose design rows take as many
- distinct values as it has columns, the linear model of the normalized counts
- y / s: s_i times the mean normalized count of the samples with sample i's row.
- For other designs the fit described below, at a rough dispersion: the smaller of
- a moment estimate from that linear model's residuals, its fitted values raised to
- at least 1, and one from the mean and variance of the normalized counts (found by
- probing, as PyDESeq2 computes them). Means below ``min_mean`` (0.5) are raised to
- it.
-2. Gene-wise estimates: the maximum of the Cox-Reid adjusted profile log-likelihood
- (Cox and Reid 1987; McCarthy, Chen and Smyth 2012), conditional on those means,
-
- l(alpha) - 1/2 log det(X' W X), W = diag(mu / (1 + alpha mu)),
-
- over [1e-8, max(10, n_samples)], the bounds of DESeq2 (found by probing). A gene
- whose rough dispersion is at the lower bound keeps the bound as its estimate, as in
- DESeq2, even where the maximum lies above it (found by probing).
-3. The trend of the dispersions on the mean normalized count m, alpha_tr(m) = a0 +
- a1 / m: a gamma-family generalized linear model with identity link, fitted to the
- genes whose gene-wise estimate is above 1e-6 (found by probing) and lies within
- (1e-4, 15) times the current trend, from a0 = 0.1 and a1 = 1, and fitted again until
- the squared logarithmic changes of a0 and a1 add up to less than 1e-6 after a fit
- that converged, in at most 11 fits. Each fit is that of R's ``glm()`` with its
- default control, as DESeq2 makes it: iteratively reweighted least squares from the
- coefficients of the fit before, stopped when the deviance changes by less than 1e-8
- of it or after 25 steps, with a step that makes a mean non-positive halved towards
- the coefficients before it (found by probing; with these, the coefficients agree
- with DESeq2's to rounding). The trend is then evaluated at every gene. With
- ``trend="mean"``, the trend is the mean of the gene-wise estimates above 1e-7,
- trimmed by 0.1% at each end, instead.
-4. The prior log(alpha_g) ~ N(log(alpha_tr(m_g)), sigma_d^2). With s_lr the median
- absolute deviation, scaled as R's ``mad``, of the log residuals log(alpha_gw /
- alpha_tr) over the genes whose gene-wise estimate is at least 1e-6 (found by
- probing), and d the residual degrees of freedom, sigma_d^2 = max(s_lr^2 -
- trigamma(d / 2), 0.25). For d of 3 or fewer the paper matches the density of the
- log residuals to that of simulated ones, log(chi^2_d / d) + N(0, sigma_d^2); here
- sigma_d^2 is the maximum likelihood estimate in that model instead, over [0, 8] (the
- range of DESeq2's grid), and at least 0.25.
-5. Maximum a posteriori estimates: the maximum of the adjusted log-likelihood plus the
- logarithm of the prior density, over the same interval and with the same means.
-6. A gene whose log gene-wise estimate exceeds the log trend by more than 2 s_lr is a
- dispersion outlier: it keeps its gene-wise estimate, and the others take their
- maximum a posteriori estimate.
-
-Each maximum is found from the sign changes of the derivative on a grid of 30 log
-dispersions, refined to 1e-12 by the Illinois variant of false position (Dowell and
-Jarratt 1971), the largest of several being kept. The derivative is
-computed without cancellation where the dispersion is small: as series in the
-dispersion and from logarithms of ratios.
-
-Fits (:func:`fit_deseq2`, as ``nbinomWaldTest()``): DESeq2's iteratively reweighted
-least squares raises fitted values below ``min_mean`` to it in the weights and working
-values, and adds a ridge penalty of 1e-6 beta^2 / 2 on the log2 scale (found by
-probing). Its fixed point maximizes a concave function: the log-likelihood in which the
-term of each observation is continued, below a fitted value of ``min_mean``, by its
-quadratic expansion there in the linear predictor, with the expected information as
-curvature, less the penalty. Each gene is fitted by Newton's method on that function,
-to steps of 1e-10 in the log fitted values; a design that is an intercept alone is
-fitted by the log of the mean normalized count, as DESeq2 does (found by probing). The
-covariance of the coefficients is (I + L)^-1 I (I + L)^-1, with I = X' W X, W =
-mu / (1 + alpha mu) at the raised fitted values and L the penalty; the hat values are
-the diagonal of W^(1/2) X (I + L)^-1 X' W^(1/2). The fitted values reported are not
-raised.
-
-Cook's distances are r^2 / p h / (1 - h)^2, with h the hat value, p the number of
-coefficients and r the Pearson residual (y - mu) / sqrt(mu + a mu^2) at a robust moment
-estimate a of the dispersion (found by probing, with the constants of PyDESeq2): a = (v
-- m) / m^2, at least 0.04, with m the mean normalized count and v a robust variance of
-the normalized counts, the largest over the groups of three or more samples that share
-a design row of a trimmed mean of squared deviations from the group's trimmed mean (at
-each end 1/3 of the samples of a group of 3, 1/4 below 24 and 1/8 from 24, scaled by
-2.04, 1.86 or 1.51), or, without such groups, the same over all samples with 1/8 and
-1.51.
+:func:`deseq2_dispersions` estimates every gene's dispersion as DESeq2's
+``estimateDispersions()`` does, into a :class:`DESeq2Dispersions`; :func:`fit_deseq2`
+fits every gene at those dispersions as ``DESeq()`` does, into a :class:`DESeq2Fit`;
+and :func:`deseq2_wald_test` tests a coefficient or contrast and
+:func:`deseq2_likelihood_ratio_test` a reduced design against the full one, each into a
+:class:`DESeq2Results` with the columns of ``results()`` (Love, Huber and Anders 2014).
+The size factors are those of
+:func:`~cytepool.normalization.median_ratio_size_factors` by default (Anders and Huber
+2010), and genes without counts are left out of every step and get NaN.
+https://docs.nygen.io/CytePool/statistics/count-models#deseq2 gives the model and the
+steps, and https://docs.nygen.io/CytePool/statistics/count-models#count-models-limits
+how the results differ from DESeq2's.
Tests and ``results()``:
@@ -98,60 +20,38 @@
p-values. When a contrast has weights of both signs and every count is zero in the
samples with a nonzero entry in a column of positive weight, and in those with one in
a column of negative weight, the log2 fold change and the statistic are 0 and the
- p-value 1 (found by probing; ``results()`` documents this for a comparison of two
- groups). The standard error is kept.
+ p-value 1, as ``results()`` documents for a comparison of two groups. The standard
+ error is kept.
- Likelihood ratio test: the difference of the deviances of the reduced and the full
fit, against the chi-squared distribution with as many degrees of freedom as the full
design has more columns. Its log2 fold change and standard error are those of a
coefficient of the full fit.
-- A gene's p-value is removed (NaN) when its largest Cook's distance over the samples
- whose design row occurs three or more times exceeds the 0.99 quantile of F(p, n - p);
- in a comparison of two groups (one factor of two levels, with or without an
- intercept) not when three or more samples have larger counts than the sample with
- the largest distance over all samples (found by probing). A numeric covariate of two
- values is not such a comparison; a design matrix that is not a ``Design`` is, when
- its rows take two distinct values of zeros and ones.
+- A gene's p-value is removed (NaN) when its largest Cook's distance (Cook 1977) over
+ the samples whose design row occurs three or more times exceeds the 0.99 quantile of
+ F(p, n - p); in a comparison of two groups (one factor of two levels, with or without
+ an intercept) not when three or more samples have larger counts than the sample with
+ the largest distance over all samples. A numeric covariate of two values is not such
+ a comparison; a design matrix that is not a ``Design`` is, when its rows take two
+ distinct values of zeros and ones.
- Adjusted p-values are Benjamini-Hochberg's (Benjamini and Hochberg 1995), after
independent filtering on the mean normalized count (Bourgon, Gentleman and Huber
2010): at 50 quantiles of the mean counts, from the fraction of genes without counts
to 0.95, the adjusted p-values of the genes at or above the quantile, and the number
of them below ``alpha``. The quantile kept is the lowest whose number exceeds the
maximum of a LOWESS fit (span 1/5) less the root mean square of its residuals, or
- failing that 90% and then 80% of that maximum (found by probing), or the lowest
- quantile; and the lowest quantile when no number exceeds 10. Genes below it get NaN.
-
-Deliberate differences from DESeq2, found by probing:
-
-- DESeq2 maximizes the dispersions by a line search that stops when an iteration gains
- less than 1e-6, and that can stall where it starts, returning its starting value: its
- gene-wise estimates are often about 1e-4 from the maximum on the log scale and a few
- up to 1 or more, and its maximum a posteriori estimates up to about 1e-2 from theirs,
- relative. Here each is the maximum, but for the genes of step 2 that keep the lower
- bound. DESeq2's own maximum a posteriori routine, given a nearly flat prior, finds
- the same gene-wise maxima.
-- DESeq2's fits stop when the deviance changes by less than 1e-8 of it: its
- coefficients up to about 3e-3 from the fixed point, relative to the larger of the
- coefficient and 1, and the means of its first fit, at the rough dispersion, up to
- about 1e-3. Here every fit converges.
-- For three or fewer residual degrees of freedom DESeq2 chooses sigma_d^2 from a grid of
- 1,000 values on [0, 8] by comparing densities of simulated residuals; its choice moves
- by several percent with the sample of residuals, and is high for small variances (0.55
- to 0.58 for 0.5). The maximum likelihood estimate used here is the limit of its
- criterion as those densities become exact.
-- Where the parametric trend fails, a coefficient not positive or no convergence in 11
- fits, DESeq2 substitutes a local regression, which CytePool does not have; the mean
- trend is used instead, with a warning, as PyDESeq2 does.
-
-Not implemented: the replacement of outlier counts that ``DESeq()`` makes for design
-rows that occur seven times or more (the results here are those of ``DESeq(...,
-minReplicatesForReplace = Inf)``), shrunken fold changes (``lfcShrink``), the local
-trend and glmGamPoi, observation weights, normalization factors that differ between
-genes, thresholds on the fold change (``lfcThreshold``) and t-distributed Wald tests
-(``useT``).
-
-The constants and the structure taken from PyDESeq2 (Muzellec et al. 2023), in the
-robust dispersions of Cook's distances, the rough dispersions and the fallback to the
-mean trend, are recorded, with PyDESeq2's license, in ``THIRD_PARTY_NOTICES.md``.
+ failing that 90% and then 80% of that maximum, or the lowest quantile; and the lowest
+ quantile when no number exceeds 10. Genes below it get NaN.
+
+Matching DESeq2:
+ The results match DESeq2 1.46.0, run with ``DESeq(..., minReplicatesForReplace =
+ Inf)``: its documentation and paper give the method, and running it on chosen
+ inputs settled the conventions they leave open, which comments in the code record:
+ the bounds of the dispersions, the genes that the trend and the prior are estimated
+ from, the trend's iterations, the ridge penalty of the fits, the rules of the Wald
+ test, Cook's distances and independent filtering above. The constants and the
+ structure taken from PyDESeq2 (Muzellec et al. 2023), in the robust dispersions of
+ Cook's distances, the rough dispersions and the fallback to the mean trend, are
+ recorded, with PyDESeq2's license, in ``THIRD_PARTY_NOTICES.md``.
References:
Love MI, Huber W, Anders S (2014). Moderated estimation of fold change and
@@ -590,12 +490,55 @@ def deseq2_dispersions(
) -> DESeq2Dispersions:
"""Estimate every gene's dispersion as DESeq2's ``estimateDispersions()``.
- The module description gives the steps: gene-wise estimates, a trend, a prior on
- the log dispersions around it, and maximum a posteriori estimates. Gene-wise
- estimates, values of the trend and the prior's variance may be given in
+ For counts y_gi ~ NB(mu_gi, alpha_g) with var(y_gi) = mu_gi + alpha_g mu_gi^2:
+
+ 1. The means of the likelihood. For a one-way layout, whose design rows take as
+ many distinct values as it has columns, the linear model of the normalized
+ counts y / s: s_i times the mean normalized count of the samples with sample
+ i's row. For other designs the fit of :func:`fit_deseq2` at a rough dispersion,
+ the smaller of a moment estimate from that linear model's residuals, its fitted
+ values raised to at least 1, and one from the mean and variance of the
+ normalized counts. Means below ``min_mean`` are raised to it.
+ 2. Gene-wise estimates: the maximum of the Cox-Reid adjusted profile
+ log-likelihood (Cox and Reid 1987; McCarthy, Chen and Smyth 2012), conditional
+ on those means, l(alpha) - 1/2 log det(X' W X) with W = diag(mu / (1 + alpha
+ mu)), over [1e-8, max(10, n_samples)]: the largest of the maxima found from the
+ sign changes of its derivative on a grid of 30 log dispersions, each refined to
+ 1e-12 by the Illinois variant of false position (Dowell and Jarratt 1971). A
+ gene whose rough dispersion is at the lower bound keeps it, as in DESeq2, even
+ where the maximum lies above.
+ 3. The trend on the mean normalized count m, alpha_tr(m) = a0 + a1 / m: a
+ gamma-family generalized linear model with identity link, fitted to the genes
+ whose gene-wise estimate is above 1e-6 and lies within (1e-4, 15) times the
+ current trend, from a0 = 0.1 and a1 = 1, and fitted again until the squared
+ logarithmic changes of a0 and a1 add up to less than 1e-6 after a fit that
+ converged, in at most 11 fits. Each fit is that of R's ``glm()`` with its
+ default control: iteratively reweighted least squares from the coefficients of
+ the fit before, stopped when the deviance changes by less than 1e-8 of it or
+ after 25 steps, with a step that makes a mean non-positive halved towards the
+ coefficients before it. Where a coefficient is not positive, or no fit
+ converges in the 11, the mean trend is used, with a warning. With
+ ``trend="mean"``, the trend is the mean of the gene-wise estimates above 1e-7,
+ trimmed by 0.1% at each end, instead.
+ 4. The prior log(alpha_g) ~ N(log(alpha_tr(m_g)), sigma_d^2). With s_lr the median
+ absolute deviation, scaled as R's ``mad``, of the log residuals log(alpha_gw /
+ alpha_tr) over the genes whose gene-wise estimate is at least 1e-6, and d the
+ residual degrees of freedom, sigma_d^2 = max(s_lr^2 - trigamma(d / 2), 0.25).
+ For d of 3 or fewer it is the maximum likelihood estimate in the model
+ log(chi^2_d / d) + N(0, sigma_d^2) of the log residuals, over [0, 8], and at
+ least 0.25.
+ 5. Maximum a posteriori estimates: the maximum of the adjusted log-likelihood plus
+ the logarithm of the prior density, over the same interval and with the same
+ means, found as in step 2.
+ 6. A gene whose log gene-wise estimate exceeds the log trend by more than 2 s_lr is
+ a dispersion outlier: it keeps its gene-wise estimate, and the others take their
+ maximum a posteriori estimate.
+
+ Gene-wise estimates, values of the trend and the prior's variance may be given in
place of their estimates, as DESeq2's lower-level functions allow
(``estimateDispersionsGeneEst``, a custom ``dispersionFunction`` and the
``dispPriorVar`` of ``estimateDispersionsMAP``).
+ https://docs.nygen.io/CytePool/statistics/count-models#deseq2 explains the steps.
Args:
counts: ``(n_samples, n_features)`` counts: whole numbers, finite and
@@ -673,13 +616,41 @@ def fit_deseq2(
) -> DESeq2Fit:
"""Fit DESeq2's negative binomial model to every gene, as ``DESeq()`` does.
- Estimates the size factors and dispersions unless they are given, fits every gene
- as ``nbinomWaldTest()`` does, and computes the standard errors, hat values and
- Cook's distances; the module description gives the methods. Test the fit with
+ Estimates the size factors and dispersions unless they are given
+ (:func:`deseq2_dispersions`), fits every gene as ``nbinomWaldTest()`` does, and
+ computes the standard errors, hat values and Cook's distances. Test the fit with
:func:`deseq2_wald_test` or :func:`deseq2_likelihood_ratio_test`. Outlier counts
are not replaced: the fit is that of ``DESeq(..., minReplicatesForReplace =
Inf)``, which is DESeq2's default where no design row occurs seven or more times.
+ Fits:
+ log2(mu_gi) = log2(s_i) + x_i' beta_g. DESeq2's iteratively reweighted least
+ squares raises fitted values below ``min_mean`` to it in the weights and
+ working values, and adds a ridge penalty of 1e-6 beta^2 / 2 on the log2 scale.
+ Its fixed point maximizes a concave function: the log-likelihood in which the
+ term of each observation is continued, below a fitted value of ``min_mean``,
+ by its quadratic expansion there in the linear predictor, with the expected
+ information as curvature, less the penalty. Each gene is fitted by Newton's
+ method on that function, to steps of 1e-10 in the log fitted values; a design
+ that is an intercept alone is fitted by the log of the mean normalized count,
+ as DESeq2 does. The covariance of the coefficients is (I + L)^-1 I (I + L)^-1,
+ with I = X' W X, W = mu / (1 + alpha mu) at the raised fitted values and L the
+ penalty; the hat values are the diagonal of W^(1/2) X (I + L)^-1 X' W^(1/2).
+ The fitted values reported are not raised.
+
+ Cook's distances:
+ r^2 / p h / (1 - h)^2, with h the hat value, p the number of coefficients and
+ r the Pearson residual (y - mu) / sqrt(mu + a mu^2) at a robust moment estimate
+ a of the dispersion: a = (v - m) / m^2, at least 0.04, with m the mean
+ normalized count and v a robust variance of the normalized counts, the largest
+ over the groups of three or more samples that share a design row of a trimmed
+ mean of squared deviations from the group's trimmed mean (at each end 1/3 of
+ the samples of a group of 3, 1/4 below 24 and 1/8 from 24, scaled by 2.04,
+ 1.86 or 1.51), or, without such groups, the same over all samples with 1/8 and
+ 1.51.
+
+ https://docs.nygen.io/CytePool/statistics/count-models#deseq2 explains the model.
+
Args:
counts: ``(n_samples, n_features)`` counts: whole numbers, finite and
non-negative.
diff --git a/cytepool/stats/distributions.py b/cytepool/stats/distributions.py
index 607587e..066da1c 100644
--- a/cytepool/stats/distributions.py
+++ b/cytepool/stats/distributions.py
@@ -14,9 +14,9 @@
``TypeError``, and so do numpy masked arrays, whose mask numpy would drop: NaN marks a
missing value instead.
-The compiled scalar routines behind the public functions are named
-``_scalar`` (for example ``t_sf_scalar``) and can be called from other compiled
-code, such as per-gene loops.
+Each public function calls a compiled scalar routine named ``_scalar`` (for
+example ``t_sf_scalar``), which other compiled code, such as a per-gene loop, can call
+too.
Methods:
- Normal: Q(x) = erfc(x / sqrt 2) / 2 from the C library's ``erfc``, with the
diff --git a/cytepool/stats/dream.py b/cytepool/stats/dream.py
index dbb704f..1def867 100644
--- a/cytepool/stats/dream.py
+++ b/cytepool/stats/dream.py
@@ -1,117 +1,73 @@
"""dream: voom weights from mixed models, and moderated tests of their fixed effects.
-dream (Hoffman and Roussos 2021) is limma-voom with a linear mixed model in place of
-the linear model, for designs with repeated measures: several samples of a donor,
-tissues of one patient, time courses. The donor (or another grouping factor) gets a
-random intercept, whose variance is estimated for every gene, and contrasts of the
-fixed effects are tested with Kenward and Roger's (1997) degrees of freedom in small
-studies and Satterthwaite's (1946) in larger ones. dreamlet (Hoffman et al. 2026) runs
-it on the pseudobulk profiles of every cell type. The steps, as variancePartition runs
-them (``voomWithDreamWeights``, ``dream``, ``eBayes`` and ``topTable``):
-
-1. Precision weights, :func:`voom_with_dream_weights`. voom's (Law et al. 2014): the
- log2 counts per million are fitted, the square roots of the residual standard
- deviations are smoothed by LOWESS against the average log2 counts, and each
- observation gets the inverse fourth power of the trend at its fitted value
- (:mod:`cytepool.stats.voom`). The fit is the mixed model of every feature, by
- maximum likelihood, and the fitted values include the predicted random effects.
-2. Fits and tests: :func:`~cytepool.stats.mixed_models.fit_mixed_model` by REML with
- those weights, and :func:`~cytepool.stats.mixed_models.fixed_effect_tests`, with
- the degrees of freedom of dream's ``ddf``: by default (``"adaptive"``) Kenward and
- Roger's for fewer than 20 samples, with their adjusted standard errors, and
- Satterthwaite's otherwise.
-3. Moderation, :func:`moderated_fixed_effect_tests`. The residual variance of every
- feature, that of an observation of its average weight, is shrunk towards a prior
- by limma's empirical Bayes (Smyth 2004) on the approximate residual degrees of
- freedom of the mixed model, tr((I - H)' (I - H)) for its hat matrix H
- (``MixedModelFit.df_residual``). The moderated t-statistic divides an estimate by
- its standard error with the posterior variance in place of the residual
- variance, and has the degrees of freedom of the test plus the prior's. Several
- contrasts are tested at once by the moderated F-statistic t' R^-1 t / r, limma's,
- for their moderated t-statistics and the correlation matrix R of their estimates,
- with r the number of linearly independent contrasts.
-
-Conventions that the documentation leaves open were found by running
-variancePartition 1.36.2, with limma 3.62.1 and lme4 1.1-38, as a black box:
-
-- ``voomWithDreamWeights`` fits the mixed models by maximum likelihood (its weights
- agree with those of maximum likelihood fits to lme4's tolerance, and differ from
- those of REML fits by up to 12% in an example), and reads the trend at lme4's
- ``fitted`` values, which include the predicted random effects. Without random
- effects it is limma's ``voom``.
-- It divides each feature's prior weights by their mean before the fits, for sample
- weights and observation weights alike. By default it returns the trend's weights
- alone (``rescaleWeightsAfter = FALSE``), as limma's ``voom`` does; with
- ``rescaleWeightsAfter = TRUE``, the trend's weights times the scaled prior
- weights, which are the weights a fit needs and the ones returned here. They do
- not change when the prior weights are multiplied by a constant.
-- ``dream`` also fits each feature with its weights divided by their mean, so its
- ``sigma`` is the residual standard deviation of an observation of the feature's
- average weight: ``residual_variance / mean_weights`` of a fit to the weights as
- given. With voom's weights this is about the variance that the trend predicts at
- the feature's expression, so the variances that ``eBayes`` moderates keep the
- mean-variance trend that the weights removed; ``trend=True`` lets the prior follow
- it. Its ``sigma`` therefore differs from limma's with the same weights; with
- every variance component at zero, the fit is limma's weighted fit, and the
- moderated tests are limma's for the weights of each feature divided by their mean.
-- ``eBayes`` moderates those variances on the approximate residual degrees of
- freedom ``rdf``, with limma's ``squeezeVar`` and its default estimator: the
- likelihood estimator (Chen et al. 2025) when the degrees of freedom differ between
- features, as they usually do, and the method of moments otherwise
- (``legacy = NULL``). The moderated t-statistics and the B-statistics are those of
- limma's ``eBayes`` for these variances on these degrees of freedom; the p-values
- use Satterthwaite's degrees of freedom plus the prior's, which, unlike limma's
- total degrees of freedom, are not capped at the sum of the residual degrees of
- freedom.
-- ``topTable`` adds ``z.std``, sign(t) times the standard normal quantile of half
- the two-sided p-value: the z-statistic with the p-value of the t-statistic, so
- that tests on different degrees of freedom can be compared. It computes it from
- the p-value floored at 1e-300.
-- ``dream(ddf = "adaptive")``, the default, uses Kenward and Roger's method for 19
- samples and Satterthwaite's for 20; the help page says "<= 20 samples". With
- ``ddf = "Kenward-Roger"`` it tests with lmerTest's ``contest``, which calls
- pbkrtest: Kenward and Roger's adjustment is computed with every observation at the
- average weight (see :mod:`cytepool.stats.mixed_models`), the standard errors are
- the adjusted ones, and ``eBayes`` moderates the tests as it does Satterthwaite's. A gene whose adjusted
- covariance matrix of the coefficients is not positive definite or has a condition
- number above 1e6, by R's estimate ``kappa``, is tested by Satterthwaite's method
- instead (variancePartition's NEWS for 1.31.16 states the rule; the bound was found
- by probing). The condition number depends on the units of continuous covariates.
-- ``topTable`` with several coefficients gives the coefficients, ``AveExpr``, ``F``,
- ``P.Value``, ``adj.P.Val`` and ``F.std``: F is limma's moderated F-statistic, from
- the moderated t-statistics and each gene's covariance matrix of the coefficients,
- Kenward and Roger's adjusted one under their method, and ``F.std`` the F-statistic
- on infinite denominator degrees of freedom with the same p-value.
-
-Deliberate differences from variancePartition, reported by ``validation/dream.py``
-and ``validation/kenward_roger_and_f_tests.py``:
-
-- The LOWESS span is chosen from the number of features by default, as
- :func:`~cytepool.stats.voom.voom` and limma's ``voom`` (since limma 3.66) choose it.
- ``voomWithDreamWeights`` uses 0.5, which ``span=0.5`` reproduces.
-- Beyond a p-value of 1e-300, the z-statistic is computed from the logarithm of the
- tail probability, so it stays exact where ``topTable`` stops at 37.07.
-- A feature whose mixed model has no minimum, which the fixed and random effects
- reproduce, is not fitted (see :class:`~cytepool.stats.mixed_models.MixedModelFit`):
- it is left out of the trend, its weights are read at its values, which the effects
- reproduce, and its tests are NaN. lme4 fits it with a residual standard deviation
- near zero, which enters the trend, and ``dream`` tests it.
-- ``topTable`` computes the p-values of an F-test of every gene on the total degrees
- of freedom of the first gene for the first coefficient listed (found by probing),
- so that they change with the order of the genes and of the coefficients. Here each
- gene has its own: F is the mean of the squares of r uncorrelated moderated
- t-statistics, those of the eigenvectors of R, whose degrees of freedom are those of
- their contrasts plus the prior's, and the denominator degrees of freedom combine
- them as lmerTest combines those of its F-tests (2 E / (E - r), with E = sum nu_m /
- (nu_m - 2); see :mod:`cytepool.stats.mixed_models`). With one contrast, the F-test
- is the moderated t-test. ``F.std`` uses the number of coefficients where it
- differs from their rank; here the rank, the F-test's numerator degrees of freedom.
-- ``dream_voom`` decides which genes fall back to Satterthwaite's method from the
- exact condition number, where dream uses R's estimate: they agree except within
- about 2% of 1e6.
-
-Random slopes are not implemented yet; variancePartition's variance partitioning is
-:func:`cytepool.variance_partitioning`.
+:func:`voom_with_dream_weights` gives voom's precision weights from the mean-variance
+trend of mixed model fits, as variancePartition's ``voomWithDreamWeights`` does, in a
+:class:`~cytepool.stats.voom.VoomResult`, and :func:`moderated_fixed_effect_tests`
+moderates the tests of :func:`~cytepool.stats.mixed_models.fixed_effect_tests` by
+empirical Bayes, as its ``eBayes`` and ``topTable`` do for ``dream`` fits, in a
+:class:`ModeratedFixedEffectTests`. dream (Hoffman and Roussos 2021) is limma-voom (Law
+et al. 2014) on linear mixed models; :func:`~cytepool.dream_voom` runs it on the cell
+types of a cube, as dreamlet does (Hoffman et al. 2026).
+https://docs.nygen.io/CytePool/statistics/mixed-models#dream gives the steps.
+
+Matching variancePartition:
+ The results match variancePartition 1.36.2, with limma 3.62.1 and lme4 1.1-38: its
+ documentation gives the method, and running it on chosen inputs settled the
+ conventions it leaves open.
+
+ - ``voomWithDreamWeights`` fits the mixed models by maximum likelihood, and reads
+ the trend at lme4's ``fitted`` values, which include the predicted random effects.
+ Without random effects it is limma's ``voom``.
+ - It divides each feature's prior weights by their mean before the fits, for sample
+ weights and observation weights alike. By default it returns the trend's weights
+ alone (``rescaleWeightsAfter = FALSE``), as limma's ``voom`` does; with
+ ``rescaleWeightsAfter = TRUE``, the trend's weights times the scaled prior
+ weights, which are the weights a fit needs and the ones returned here. They do not
+ change when the prior weights are multiplied by a constant.
+ - ``dream`` also fits each feature with its weights divided by their mean, so its
+ ``sigma`` is the residual standard deviation of an observation of the feature's
+ average weight: ``residual_variance / mean_weights`` of a fit to the weights as
+ given (https://docs.nygen.io/CytePool/statistics/mixed-models#dream says what
+ that means for the trend). With every variance component at zero, the fit is
+ limma's weighted fit, and the moderated tests are limma's for the weights of each
+ feature divided by their mean.
+ - ``eBayes`` moderates those variances on the approximate residual degrees of
+ freedom ``rdf``, with limma's ``squeezeVar`` and its default estimator: the
+ likelihood estimator (Chen et al. 2025) when the degrees of freedom differ between
+ features, as they usually do, and the method of moments otherwise
+ (``legacy = NULL``). The moderated t-statistics and the B-statistics are those of
+ limma's ``eBayes`` (Smyth 2004) for these variances on these degrees of freedom;
+ the p-values use the degrees of freedom of the tests, Satterthwaite's or
+ Kenward-Roger's, plus the prior's, which, unlike limma's total degrees of freedom,
+ are not capped at the sum of the residual degrees of freedom.
+ - ``topTable`` adds ``z.std``, sign(t) times the standard normal quantile of half
+ the two-sided p-value: the z-statistic with the p-value of the t-statistic, so
+ that tests on different degrees of freedom can be compared. It computes it from
+ the p-value floored at 1e-300.
+ - ``dream(ddf = "adaptive")``, the default, uses Kenward-Roger's method (Kenward
+ and Roger 1997) for 19 samples and Satterthwaite's (1946) for 20; its help page
+ says that it "uses KR for <= 20 samples". With ``ddf = "Kenward-Roger"`` it
+ tests with lmerTest's ``contest``, which calls pbkrtest: Kenward-Roger's
+ adjustment is computed with every observation at the average weight (see
+ :mod:`cytepool.stats.mixed_models`), the standard errors are the adjusted ones,
+ and ``eBayes`` moderates the tests as it does Satterthwaite's. A gene whose
+ adjusted covariance matrix of the coefficients is not positive definite or has a
+ condition number above 1e6, by R's estimate ``kappa``, is tested by
+ Satterthwaite's method instead, the rule that variancePartition's NEWS for
+ 1.31.16 states. The condition number depends on the units of continuous
+ covariates.
+ - ``topTable`` with several coefficients gives the coefficients, ``AveExpr``,
+ ``F``, ``P.Value``, ``adj.P.Val`` and ``F.std``: F is limma's moderated
+ F-statistic, from the moderated t-statistics and each gene's covariance matrix of
+ the coefficients, Kenward-Roger's adjusted one under their method, and
+ ``F.std`` the F-statistic on infinite denominator degrees of freedom with the same
+ p-value.
+
+Differences from variancePartition:
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-models-limits lists
+ them; the docstrings of :func:`voom_with_dream_weights`,
+ :class:`ModeratedFixedEffectTests` and :func:`~cytepool.dream_voom` give those that
+ change what they return.
References:
Hoffman GE, Roussos P (2021). Dream: powerful differential expression analysis for
@@ -209,8 +165,8 @@ class ModeratedFixedEffectTests:
``tests``. The arrays are read-only.
Attributes:
- tests: The tests moderated, with their fit, contrasts and Satterthwaite's
- degrees of freedom.
+ tests: The tests moderated, with their fit, contrasts and degrees of
+ freedom, Satterthwaite's or Kenward-Roger's.
residual_variance_at_mean_weight: ``(n_features,)`` the variances
moderated: the residual variance of an observation of each feature's
average weight, ``fit.residual_variance / fit.mean_weights`` (dream's
@@ -226,18 +182,20 @@ class ModeratedFixedEffectTests:
t: ``(n_features, n_contrasts)`` moderated t-statistics: the estimates over
their standard errors times (residual_variance_at_mean_weight /
posterior_variance)^(1/2).
- df_total: ``(n_features, n_contrasts)`` their degrees of freedom:
- Satterthwaite's plus the prior's (``df.total``).
+ df_total: ``(n_features, n_contrasts)`` their degrees of freedom: those
+ of the tests plus the prior's (``df.total``).
p_value: Two-sided p-values of ``t`` on ``df_total`` degrees of freedom.
log_odds: Log-odds that each contrast is non-zero (limma's B-statistic,
``lods``), as limma's ``eBayes`` gives them for these variances on
``df_residual``.
z: Signed z-statistics with the same p-values, sign(t) times the standard
- normal quantile of half the p-value (``topTable``'s ``z.std``).
+ normal quantile of half the p-value (``topTable``'s ``z.std``). Below
+ p-values of 1e-300 they come from the log of the tail probability, so
+ they keep growing where ``z.std`` stops at 37.07.
coefficient_prior_variance: ``(n_contrasts,)`` unscaled prior variance of
a non-zero contrast (``var.prior``).
- proportion: The assumed proportion of non-zero contrasts behind
- ``log_odds``.
+ proportion: The assumed proportion of non-zero contrasts that
+ ``log_odds`` takes.
prior_estimator: The estimator of the prior: ``"moments"`` or
``"likelihood"``.
f_statistic: ``(n_features,)`` moderated F-statistics testing every
@@ -246,13 +204,18 @@ class ModeratedFixedEffectTests:
(``topTable``'s ``F`` for these contrasts).
f_df1: The numerator degrees of freedom of the F-tests, the number of
linearly independent contrasts.
- f_df2: ``(n_features,)`` their denominator degrees of freedom; see the
- module docstring.
+ f_df2: ``(n_features,)`` their denominator degrees of freedom, those of
+ the r uncorrelated moderated t-statistics combined as 2 E / (E - r),
+ with E = sum nu_m / (nu_m - 2) over their degrees of freedom nu_m, as
+ lmerTest combines those of its F-tests: each feature's own, where
+ ``topTable`` uses the first gene's for every gene. With one contrast,
+ the F-test is the moderated t-test.
f_p_value: ``(n_features,)`` p-values of ``f_statistic`` on ``f_df1`` and
``f_df2`` degrees of freedom.
standardized_f_statistic: ``(n_features,)`` the F-statistic on ``f_df1``
and infinite degrees of freedom with the same p-value (``topTable``'s
- ``F.std``), so that tests on different degrees of freedom compare.
+ ``F.std``, which takes the number of coefficients where it differs from
+ their rank), so that tests on different degrees of freedom compare.
"""
tests: FixedEffectTests
@@ -299,10 +262,13 @@ def voom_with_dream_weights(
variancePartition's ``voomWithDreamWeights``: voom (see
:func:`~cytepool.stats.voom.voom`), with the linear model replaced by the mixed
model of ``design``, fitted to every feature by maximum likelihood, and the trend
- read at the fitted values with the predicted random effects; the module
- docstring gives the conventions. Continue with ``fit_mixed_model(result.log2_cpm,
- design, weights=result.weights)``, ``fixed_effect_tests`` and
- :func:`moderated_fixed_effect_tests`.
+ read at the fitted values with the predicted random effects. A feature whose
+ mixed model has no minimum, as one that the fixed and random effects reproduce
+ (see :class:`~cytepool.stats.mixed_models.MixedModelFit`), is left out of the
+ trend, and its weights are read from the trend at its own values. Continue with
+ ``fit_mixed_model(result.log2_cpm, design, weights=result.weights)``,
+ ``fixed_effect_tests`` and :func:`moderated_fixed_effect_tests`.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#dream explains the steps.
Args:
counts: ``(n_samples, n_features)`` counts, as for ``voom``: finite and
@@ -319,8 +285,8 @@ def voom_with_dream_weights(
weights: Prior weights: ``(n_samples,)`` sample weights, such as each
pseudobulk sample's number of cells, or ``(n_samples, n_features)``
observation weights; finite and positive. Each feature's weights are
- divided by their mean, then weight the fits behind the trend, and
- multiply the weights returned.
+ divided by their mean, then weight the fits from which the trend is
+ estimated, and multiply the weights returned.
method: ``"ml"``, maximum likelihood, as ``voomWithDreamWeights`` fits, or
``"reml"``.
n_threads: The most threads for the fits, as for ``fit_mixed_model``.
@@ -361,6 +327,9 @@ def voom_with_dream_weights(
# A copy, so that the result does not change with the caller's array.
sizes = np.array(effective_library_sizes(y, library_sizes, norm_factors))
fraction = trend_span(span, y.shape[1])
+ # voomWithDreamWeights divides each feature's prior weights by their mean, and with
+ # rescaleWeightsAfter = TRUE returns the trend's weights times them (found by
+ # probing).
prior = scaled_prior_weights(weights, y.shape)
with thread_limit(n_threads):
return _dream_weights(y, sizes, fraction, parts, prior, ml)
@@ -384,9 +353,10 @@ def moderated_fixed_effect_tests(
empirical Bayes (:func:`~cytepool.stats.empirical_bayes.moderated_statistics`,
with its estimators and options), each contrast's t-statistic uses the posterior
variance, and its degrees of freedom are those of the tests (Satterthwaite's or
- Kenward and Roger's) plus the prior's. The moderated F-statistic tests every
- contrast of ``tests`` at once, as ``topTable`` does for several coefficients.
- See the module docstring.
+ Kenward-Roger's) plus the prior's. The moderated F-statistic tests every
+ contrast of ``tests`` at once, as ``topTable`` does for several coefficients,
+ with degrees of freedom of its own for every feature.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#dream explains the steps.
Args:
tests: Tests from :func:`~cytepool.stats.mixed_models.fixed_effect_tests`,
@@ -456,7 +426,8 @@ def _moderated_tests(
"fixed_effect_tests tested none; add fixed effects to the design, such as "
"an intercept"
)
- # dream's sigma^2: the residual variance of an observation of average weight.
+ # dream's sigma^2: the residual variance of an observation of average weight, as
+ # dream fits each feature with its weights divided by their mean (found by probing).
variance = fit.residual_variance / fit.mean_weights
fitted = np.isfinite(variance) & np.isfinite(fit.df_residual)
if not fitted.any():
@@ -469,7 +440,7 @@ def _moderated_tests(
with np.errstate(divide="ignore", invalid="ignore"):
stdev_unscaled = tests.standard_error / sigma[:, np.newaxis]
# The covariance matrix that the tests use: dream's cov.coefficients.list,
- # Kenward and Roger's adjusted one under their method (found by probing).
+ # Kenward-Roger's adjusted one under their method (found by probing).
per_feature = tests.covariance / variance[:, np.newaxis, np.newaxis]
cross = fit.design.T @ fit.design
linear = LinearModelFit(
@@ -485,6 +456,9 @@ def _moderated_tests(
cov_coefficients_per_feature=per_feature,
contrasts=matrix,
)
+ # eBayes moderates on the approximate residual degrees of freedom with limma's
+ # default estimator, and its p-values take the tests' degrees of freedom plus the
+ # prior's, without limma's cap (found by probing).
moderated = moderated_statistics(
linear,
proportion=proportion,
@@ -577,6 +551,8 @@ def _dream_weights(
ml: Whether to fit by maximum likelihood.
"""
log2_cpm = voom_log_cpm(y, sizes)
+ # voomWithDreamWeights fits by maximum likelihood, and reads the trend at the
+ # fitted values with the predicted random effects (found by probing).
fit = _fit(log2_cpm, prior, ml, parts)
mean_log_count = voom_mean_log_count(fit.mean_expression, sizes, y)
sigma = np.sqrt(fit.residual_variance)
diff --git a/cytepool/stats/empirical_bayes.py b/cytepool/stats/empirical_bayes.py
index ed92e53..cc9c2c5 100644
--- a/cytepool/stats/empirical_bayes.py
+++ b/cytepool/stats/empirical_bayes.py
@@ -1,57 +1,24 @@
"""Empirical Bayes moderation of feature-wise variances, as in limma's ``eBayes``.
-A linear model fit gives each feature g a residual variance s_g^2 on d_g degrees of
-freedom. The hierarchical model of Smyth (2004) assumes
-
- s_g^2 | sigma_g^2 ~ sigma_g^2 chi^2(d_g) / d_g,
- 1 / sigma_g^2 ~ chi^2(d0) / (d0 s0^2),
-
-so that s_g^2 ~ s0^2 F(d_g, d0) marginally. The prior (s0^2, d0) is estimated from all
-features, and each variance is replaced by its posterior value
-s~_g^2 = (d0 s0^2 + d_g s_g^2) / (d0 + d_g), the inverse of the posterior mean of
-1 / sigma_g^2 (Smyth 2004). A moderated t-statistic, which uses s~_g in
-place of s_g, follows a t distribution on d0 + d_g degrees of freedom under the null
-hypothesis; an infinite d0 gives every feature the prior variance.
-
-Estimation of the prior, by one of two estimators (``prior_estimator``):
- - The method of moments (``"moments"``, limma's ``legacy=TRUE``):
- :func:`fit_f_distribution` matches the mean and variance of e_g = log s_g^2 -
- psi(d_g / 2) + log(d_g / 2), whose variance is psi'(d0 / 2) plus the mean of
- psi'(d_g / 2) (Smyth 2004, section 6.2). When the variance of the e_g is no larger
- than chi-squared sampling explains, d0 is infinite and s0^2 is the mean variance.
- With a covariate (limma-trend uses the mean log-expression) the moments are taken
- about a natural cubic spline fit of e_g, and s0^2 becomes a trend (Phipson et al.
- 2016, section 10.1). :func:`fit_f_distribution_robust` matches the moments of the
- log variances Winsorized at their 5% and 90% quantiles to those of a Winsorized
- log F variable, computed by Gauss-Legendre quadrature, so that hypervariable
- features do not lower d0. Each feature then gets its own prior df, smaller for
- outliers (Phipson et al. 2016). With a covariate, the log variances are first
- detrended by LOWESS.
- - Weighted moments and profile likelihood (``"likelihood"``, limma's
- ``fitFDistUnequalDF1``): :func:`fit_f_distribution_likelihood` estimates log s0^2
- by a weighted mean of the e_g, which gives less weight to variances on fewer
- degrees of freedom, and d0 by maximising the likelihood of the variances under
- s0^2 F(d_g, d0) with that scale (Chen et al. 2025). It suits degrees of freedom
- that differ between features, or are small or fractional. With a covariate the
- weighted mean becomes a weighted LOWESS trend; its robust variant downweights
- outliers by prior weights instead of Winsorizing.
-
- By default (``prior_estimator=None``) the likelihood estimator is used when the
- positive degrees of freedom differ between features, as with missing values or
- zero observation weights, and the method of moments otherwise: limma's default,
- ``legacy=NULL``, since limma 3.61.8.
-
-Statistics: :func:`moderated_statistics` computes moderated t and F statistics, their
-p-values and the log-odds of differential expression (the B-statistic of Loennstedt
-and Speed 2002, in the form of Smyth 2004, section 5); :func:`treat` tests whether
-coefficients exceed a threshold in absolute value (McCarthy and Smyth 2009).
-
-Results match limma 3.62.1 (``fitFDist``, ``fitFDistRobustly``, ``fitFDistUnequalDF1``,
-``squeezeVar``, ``eBayes`` and ``treat``, with ``legacy`` NULL, TRUE or FALSE).
-Conventions that limma's documentation leaves open were settled by running it as a
-black box; comments mark them "found by probing". The robust likelihood estimator
-deliberately differs from limma's where some features have no usable variance or
-prior weights are given (see :func:`fit_f_distribution_likelihood`).
+:func:`moderated_statistics` moderates the residual variances of a linear model fit
+and returns moderated t and F statistics, their p-values and the B-statistic in a
+:class:`ModeratedFit`; :func:`treat` tests whether coefficients exceed a threshold in
+absolute value (:class:`TreatFit`); :func:`squeeze_variances` moderates any set of
+variances (:class:`SqueezedVariances`); and :func:`fit_f_distribution`,
+:func:`fit_f_distribution_robust` and :func:`fit_f_distribution_likelihood` estimate
+the prior that the variances are shrunk towards, by the estimator that
+``prior_estimator`` chooses (:func:`squeeze_variances` gives the choices).
+https://docs.nygen.io/CytePool/statistics/empirical-bayes explains the model, the
+estimators and the statistics, and
+https://docs.nygen.io/CytePool/statistics/empirical-bayes#empirical-bayes-limits how
+they differ from limma; each function's docstring gives the differences that change
+what it returns.
+
+Matching limma:
+ The results match limma 3.62.1's ``fitFDist``, ``fitFDistRobustly``,
+ ``fitFDistUnequalDF1``, ``squeezeVar``, ``eBayes`` and ``treat``, with ``legacy``
+ NULL, TRUE or FALSE. Running limma settled the conventions that its documentation
+ leaves open; the functions below state them.
References:
Smyth GK (2004). Linear models and empirical Bayes methods for assessing
@@ -313,12 +280,12 @@ class ModeratedFit:
f_statistic: ``(n_features,)`` moderated F-statistics testing all
coefficients equal to zero.
f_p_value: P-values of ``f_statistic`` on ``f_df1`` and
- ``df_residual + df_prior`` degrees of freedom; limma does not cap the
- denominator degrees of freedom here (found by probing).
+ ``df_residual + df_prior`` degrees of freedom, which are not capped as
+ ``df_total`` is, as in limma.
f_df1: Numerator degrees of freedom of the F-tests: the rank of the
correlation matrix of the coefficients.
- proportion: The assumed proportion of non-zero coefficients behind
- ``log_odds``.
+ proportion: The assumed proportion of non-zero coefficients that
+ ``log_odds`` uses.
prior_estimator: The estimator of the prior: ``"moments"`` or
``"likelihood"`` (see :func:`squeeze_variances`); with at most two
features neither is used.
@@ -439,14 +406,15 @@ def fit_f_distribution(
) -> FDistributionFit:
"""Fit a scaled F distribution to variances by the method of moments.
- Each variance is modelled as ``scale * F(df_g, df2)``. Matches limma's
- ``fitFDist``. The moments are those of the logs of the variances, each bounded
- below by 1e-5 times their median (limma does the same). With a covariate, the
- scale follows a natural cubic spline in it: with n usable features, the spline
- has 4 degrees of freedom including the intercept for n >= 30, 3 for n from 6 to
- 29, 2 for n from 3 to 5 and 1 (a constant) below, and never more than the
- covariate has distinct values; its interior knots are at quantiles of the
- covariate.
+ Each variance is modelled as ``scale * F(df_g, df2)``, as limma's ``fitFDist``
+ fits it. The moments are those of the logs of the variances, each bounded below
+ by 1e-5 times their median (limma does the same). With a covariate, the scale
+ follows a natural cubic spline in it: with n usable features, the spline has 4
+ degrees of freedom including the intercept for n >= 30, 3 for n from 6 to 29, 2
+ for n from 3 to 5 and 1 (a constant) below, and never more than the covariate
+ has distinct values; its interior knots are at quantiles of the covariate.
+ https://docs.nygen.io/CytePool/statistics/empirical-bayes#estimating-the-prior
+ explains the estimators.
A variance is usable when it is finite and its df is positive. Unusable
features do not enter the estimate; with a covariate, their scale is the spline
@@ -489,8 +457,9 @@ def fit_f_distribution_robust(
) -> RobustFDistributionFit:
"""Fit a scaled F distribution robustly, with prior df for each feature.
- The robust empirical Bayes estimator of Phipson et al. (2016), matching limma's
- ``fitFDistRobustly``:
+ The robust empirical Bayes estimator of Phipson et al. (2016), as limma's
+ ``fitFDistRobustly`` computes it
+ (https://docs.nygen.io/CytePool/statistics/empirical-bayes#estimating-the-prior):
1. Variances are bounded below by 1e-12 times their median. When their df
differ, each is mapped to the largest df d by matching tail probabilities
@@ -502,9 +471,9 @@ def fit_f_distribution_robust(
3. The log variances are Winsorized at their ``winsor_tail_p`` quantiles (R's
type 7), and ``df2`` solves var(Winsorized log variances) = var(log win(F)),
with the Winsorized moments of log F computed by 128-point Gauss-Legendre
- quadrature (sections 3 and 10.3). It is never below the moment estimate (a
- rule found by probing). log s0^2 is the Winsorized mean minus the mean of the
- Winsorized log F.
+ quadrature (sections 3 and 10.3). It is never below the moment estimate, as
+ in limma. log s0^2 is the Winsorized mean minus the mean of the Winsorized
+ log F.
4. Each feature gets a tail probability p_g = P(F > s_g^2 / s0^2), and
pi_g = min(1, p_g / ((r_g - 0.5) / n)), with r_g the rank of s_g^2 / s0^2
from the largest, is its probability of not being an outlier;
@@ -516,7 +485,7 @@ def fit_f_distribution_robust(
iterations, as in limma). When ``df2`` is infinite, the cumulative maximum
alone makes the pi_g non-decreasing, and ``df2_shrunk`` is infinite where
pi_g = 1 and pi_g n d elsewhere, the df of all n variances on the common
- df d (both found by probing).
+ df d, both as in limma.
Unusable features (NaN variance or zero df) are left out of the fit and get
``df2``; with a covariate, their scale is interpolated linearly in the
@@ -524,13 +493,12 @@ def fit_f_distribution_robust(
itself is returned, with ``df2_shrunk`` equal to its ``df2``, when the number
of usable features times the larger tail proportion is below one (fewer than
10 features with the default proportions), and when the moment estimate of
- ``df2`` is infinite but the Winsorized one is not (both rules found by
- probing). With more than two usable features, more than half of which have a
- zero variance, the estimator is undefined and raises an error, as limma does.
+ ``df2`` is infinite but the Winsorized one is not, as limma does. With more
+ than two usable features, more than half of which have a zero variance, the
+ estimator is undefined and raises an error, as limma does.
- limma solves the equation of step 3 only to a tolerance, so its ``df2`` agrees
- with this exact solution to about 1e-7 relative. Two limitations are shared
- with limma, whose results agree with these. The quadrature of step 3 is
+ Step 3 is solved exactly, where limma stops at a tolerance. Two limitations are
+ shared with limma, whose results agree with these. The quadrature of step 3 is
accurate to better than 1e-12 when both df are at least 1 and the tail
proportions are the defaults, but not when a df is well below 1 (at df = 0.3
the Winsorized variance of log F is off by 14% to 32%) or, near df = 1, with
@@ -589,9 +557,8 @@ def fit_f_distribution_likelihood(
mean becomes a weighted trend of the e_g on the covariate.
A variance is usable when it is finite, its df is at least 0.01 and its prior
- weight is positive; at least two usable variances must be above zero. The
- conventions of limma 3.62.1 that its documentation leaves open were found by
- probing it:
+ weight is positive; at least two usable variances must be above zero. These
+ conventions follow limma 3.62.1 where its documentation leaves them open:
- Variances at or below zero are raised to 1e-12 times the median of the
positive usable variances.
@@ -639,18 +606,12 @@ def fit_f_distribution_likelihood(
3.62.1 in two respects. Only the usable variances enter steps 1 and 3, where
limma also counts the others, a missing variance as a zero one: it becomes the
most extreme outlier and lowers the adjusted probabilities of the others, so
- missing values change which features are outliers. In a simulation where 40 of
- 2,000 genes had no residual df, limma's robust prior df was 11.5% larger with a
- constant prior, where it also gave 45 genes a smaller prior df as outliers and
- this function gives every gene the same, and 6% to 10% larger with a trend.
- Without a covariate the result is limma's fit of the usable variances alone.
- And the second fit keeps the prior weights, which limma's drops, bringing back
- the features they leave out. The two agree when every variance is usable and
- there are no prior weights.
-
- With df of 1e7 and more, limma's likelihood loses precision: its d0 differed
- from this function's by up to 1e-5 relative at 1e7 and 5e-2 at 1e12, where this
- function's agrees with the likelihood computed to 60 digits.
+ missing values change which features are outliers. Without a covariate the
+ result is limma's fit of the usable variances alone. And the second fit keeps
+ the prior weights, which limma's drops, bringing back the features they leave
+ out. The two agree when every variance is usable and there are no prior weights.
+ https://docs.nygen.io/CytePool/statistics/empirical-bayes#estimating-the-prior
+ explains the estimators.
Args:
variances: ``(n,)`` non-negative variances; NaN marks a missing value.
@@ -668,7 +629,7 @@ def fit_f_distribution_likelihood(
Raises:
TypeError: If an input is not real-valued or is a numpy masked array, or
- ``robust`` is not a bool.
+ ``robust`` is not True or False.
ValueError: As :func:`fit_f_distribution`, if ``prior_weights`` is
invalid, or if fewer than two usable variances are above zero (limma
returns NA), in the fit or, with ``robust``, in its second fit.
@@ -678,7 +639,7 @@ def fit_f_distribution_likelihood(
weights = _prior_weight_vector(prior_weights, x.size)
usable = _likelihood_usable(x, d, weights)
z = _covariate_vector(covariate, usable, _LIKELIHOOD_UNUSABLE)
- checked_flag(robust, "robust", expected="a bool")
+ checked_flag(robust, "robust")
_check_zero_variances(x, usable, robust=False)
return _fit_likelihood(x, d, z, weights, bool(robust))
@@ -694,7 +655,9 @@ def squeeze_variances(
) -> SqueezedVariances:
"""Shrink variances towards a prior by empirical Bayes.
- Matches limma's ``squeezeVar``. The prior is estimated by the method of
+ This is limma's ``squeezeVar``, and
+ https://docs.nygen.io/CytePool/statistics/empirical-bayes#empirical-bayes-model
+ gives the model. The prior is estimated by the method of
moments, :func:`fit_f_distribution` or, when ``robust``,
:func:`fit_f_distribution_robust`; or by weighted moments and profile
likelihood, :func:`fit_f_distribution_likelihood`, robustly when ``robust``.
@@ -725,7 +688,7 @@ def squeeze_variances(
Raises:
TypeError: If an input is not real-valued or is a numpy masked array, or
- ``robust`` is not a bool.
+ ``robust`` is not True or False.
ValueError: As :func:`fit_f_distribution` or
:func:`fit_f_distribution_likelihood`, or if an option is invalid.
"""
@@ -735,7 +698,7 @@ def squeeze_variances(
usable = _usable(x, d)
z = _covariate_vector(covariate, usable)
lower, upper = _tail_proportions(winsor_tail_p)
- checked_flag(robust, "robust", expected="a bool")
+ checked_flag(robust, "robust")
estimator = _prior_estimator(prior_estimator, given)
return _squeeze(x, d, z, usable, bool(robust), lower, upper, estimator)
@@ -763,6 +726,8 @@ def moderated_statistics(
when the residual degrees of freedom differ between the features that have
some, as with missing values or zero observation weights, and by the method
of moments otherwise.
+ https://docs.nygen.io/CytePool/statistics/empirical-bayes#empirical-bayes-model
+ gives the model and the statistics.
The log-odds (B-statistic) of coefficient j of feature g is (Smyth 2004)
@@ -770,13 +735,13 @@ def moderated_statistics(
with p = ``proportion``, d = ``df_total``, r = v / (v + v0), v the squared
unscaled standard error and v0 the coefficient's prior variance; with an
- infinite prior df the last term is its limit t^2 (1 - r) / 2 (found by
- probing). v0 is estimated from the largest |t| (Smyth 2004, section 6.3): with
- G the number of finite t-statistics, each of the ceiling(p G / 2) largest gives
- an estimate, clipped to [lim1^2, lim2^2] / median(prior variance) from
- ``stdev_coef_lim``, and v0 is their mean. The proportion used there is at least
- the share of the largest values, and t-statistics on unequal df are first
- mapped to the largest df by matching tail probabilities (found by probing).
+ infinite prior df the last term is its limit t^2 (1 - r) / 2, as in limma. v0 is
+ estimated from the largest |t| (Smyth 2004, section 6.3): with G the number of
+ finite t-statistics, each of the ceiling(p G / 2) largest gives an estimate,
+ clipped to [lim1^2, lim2^2] / median(prior variance) from ``stdev_coef_lim``,
+ and v0 is their mean. As in limma, the proportion used there is at least the
+ share of the largest values, and t-statistics on unequal df are first mapped to
+ the largest df by matching tail probabilities.
The moderated F-statistic tests all coefficients of a feature equal to zero:
t' R^+ t / r, with R the correlation matrix of the coefficients and r its rank,
@@ -833,6 +798,9 @@ def moderated_statistics(
infinite = np.isinf(df_prior)
prior_variance = np.broadcast_to(squeezed.prior_variance, df_total.shape)
median_prior = float(np.nanmedian(prior_variance))
+ # As in limma, v0 takes a proportion at least the share of the largest values,
+ # and the log-odds of an infinite prior df the limit of the last term (both
+ # found by probing; coefficient_prior_variance and log_odds implement them).
v0 = np.array(
[
coefficient_prior_variance(
@@ -851,6 +819,8 @@ def moderated_statistics(
v0[failed] = np.reciprocal(prior_variance[0])
lods = log_odds(t, fit.stdev_unscaled, df_total, infinite, v0, p)
f_value, f_df1 = _f_statistics(t, fit, exact)
+ # limma caps the df of the t-tests at the total residual df, but not those of
+ # the F-tests (found by probing).
f_df2 = df_prior + fit.df_residual
f_p_value = _as_array(f_sf(f_value, float(f_df1), f_df2))
return ModeratedFit(
@@ -896,8 +866,8 @@ def treat(
Give the threshold as a log2 fold change ``lfc`` or as a fold change ``fc``,
with ``lfc = log2(fc)``; the default is ``fc = 1.2``, as in limma. The
threshold is |lfc|, so ``fc = 0.8`` and ``fc = 1.25`` are the same test.
- Typical values are fc of 1.1, 1.2 or 1.5: estimated fold changes must be
- clearly above the threshold to be significant.
+ https://docs.nygen.io/CytePool/statistics/empirical-bayes#treat-test explains
+ the test and the choice of a threshold.
Args:
fit: A fit from :func:`~cytepool.stats.linear_models.fit_linear_model` or
@@ -1137,9 +1107,9 @@ def _fit_robust(
low, high = np.quantile(detrended, [p_lower, 1.0 - p_upper])
winsorized = np.clip(detrended, low, high)
target = float(np.var(winsorized, ddof=1))
- # The Winsorized estimate is never below the moment estimate. When that is
+ # The Winsorized estimate is never below the moment estimate, and when that is
# infinite but the Winsorized one is not, limma returns the moment estimate
- # (found by probing).
+ # (both found by probing).
if _winsorized_moments(df1, math.inf, p_lower, p_upper)[1] >= target:
df2 = math.inf
elif math.isinf(moments.df2):
@@ -1399,12 +1369,14 @@ def _fit_likelihood(
usable = _likelihood_usable(x, d, prior_weights)
positive = int(np.count_nonzero(x[usable] > 0.0))
if positive < 2:
+ # limma's fitFDistUnequalDF1 returns NA here and squeezeVar stops (found by
+ # probing).
raise ValueError(
"the likelihood estimator needs at least two usable variances above zero "
f"(finite, on at least {LIKELIHOOD_MIN_DF} degrees of freedom and with a "
f"positive prior weight), got {positive}; limma's fitFDistUnequalDF1 "
- "returns NA and squeezeVar stops here (found by probing). The method of "
- "moments (prior_estimator='moments') does not need two"
+ "returns NA and squeezeVar stops here. The method of moments "
+ "(prior_estimator='moments') does not need two"
)
weights = np.where(usable, prior_weights, 0.0)
location, df2, trended = _likelihood_location_and_df2(
@@ -1691,7 +1663,7 @@ def _moderate(
prior_estimator: Any,
) -> tuple[SqueezedVariances, FloatArray, str]:
"""Squeezed variances of a fit, the total df of its tests and the estimator."""
- checked_flag(robust, "robust", expected="a bool")
+ checked_flag(robust, "robust")
lower, upper = _tail_proportions(winsor_tail_p)
variances = fit.sigma**2
df = _zero_tiny_df(fit.df_residual)
@@ -1893,9 +1865,7 @@ def _tail_proportions(winsor_tail_p: Any) -> tuple[float, float]:
f"got {values.size}"
)
if not ((values > 0.0) & (values < 0.5)).all():
- raise ValueError(
- f"winsor_tail_p must lie strictly between 0 and 0.5, got {values.tolist()}"
- )
+ raise ValueError(f"winsor_tail_p must be in (0, 0.5), got {values.tolist()}")
return float(values[0]), float(values[1])
@@ -1910,7 +1880,7 @@ def _trend_covariate(trend: Any, fit: LinearModelFit) -> FloatArray | None:
)
if z.shape != fit.sigma.shape:
raise ValueError(
- "trend must be a bool or one covariate value per feature "
+ "trend must be True, False or one covariate value per feature "
f"({fit.sigma.size}), got shape {z.shape}"
)
return z
@@ -1970,7 +1940,7 @@ def _coefficient_limits(limits: Any) -> tuple[float, float]:
def _open_unit_interval(value: Any, name: str) -> float:
number = real_number(value, name)
if not 0.0 < number < 1.0:
- raise ValueError(f"{name} must lie strictly between 0 and 1, got {number}")
+ raise ValueError(f"{name} must be in (0, 1), got {number}")
return number
diff --git a/cytepool/stats/linear_models.py b/cytepool/stats/linear_models.py
index fadcbbf..12f75d8 100644
--- a/cytepool/stats/linear_models.py
+++ b/cytepool/stats/linear_models.py
@@ -1,17 +1,21 @@
-"""Linear models fitted to many features at once.
+"""Linear models fitted to many features at once, as limma's ``lmFit`` fits them.
-Every feature (gene) of a samples x features matrix is fitted with the same
-design by ordinary, weighted or generalised least squares, and the fitted
-coefficients can be re-expressed as contrasts. The results match limma's
-``lmFit`` and ``contrasts.fit``. For feature g with observed responses y_g,
+:func:`fit_linear_model` fits one design to every feature (gene) of a samples x
+features matrix, by ordinary, weighted or generalised least squares, and returns a
+:class:`LinearModelFit`; :func:`contrasts_fit` re-expresses a fit as contrasts of its
+coefficients. https://docs.nygen.io/CytePool/statistics/linear-models#one-model-per-gene
+gives the model, and https://docs.nygen.io/CytePool/statistics/linear-models#contrasts
+the contrasts and their standard errors.
- E(y_g) = X a_g, var(y_g) = sigma_g^2 W_g^(-1/2) R W_g^(-1/2),
+Matching limma:
+ The results match limma 3.62.1's ``lmFit`` and ``contrasts.fit``. Running limma
+ settled the conventions that its documentation leaves open, such as which
+ covariance matrix of the coefficients a fit reports.
-where W_g is the diagonal matrix of the feature's precision weights and R is
-the identity, or the correlation matrix of a known correlation between samples
-of the same block (Smyth 2004; Smyth, Michaud and Scott 2005). A feature with missing
-values or weights of its own is fitted to its observed samples by a Householder QR
-decomposition of its own (Golub and Van Loan 2013).
+Differences from limma:
+ https://docs.nygen.io/CytePool/statistics/linear-models#linear-models-limits lists
+ them; the docstrings of :func:`fit_linear_model` and :func:`contrasts_fit` give
+ those that change their results.
References:
Smyth GK (2004). Linear models and empirical Bayes methods for assessing
@@ -111,8 +115,8 @@ class LinearModelFit:
Raises:
TypeError: If ``coefficient_names`` is a string,
- ``exact_standard_errors`` is not a bool, or a field is a numpy masked
- array.
+ ``exact_standard_errors`` is not True or False, or a field is a numpy
+ masked array.
ValueError: If shapes disagree, names are not unique, or a fit marked
exact has neither per-feature covariance matrices nor standard
errors that ``cov_coefficients`` implies.
@@ -182,7 +186,10 @@ def __post_init__(self) -> None:
self.cov_coefficients, "cov_coefficients", shape=square
)
if not isinstance(self.exact_standard_errors, (bool, np.bool_)):
- raise TypeError("exact_standard_errors must be a bool")
+ raise TypeError(
+ "exact_standard_errors must be True or False, got "
+ f"{type(self.exact_standard_errors).__name__}"
+ )
exact = bool(self.exact_standard_errors)
per_feature = self.cov_coefficients_per_feature
if per_feature is not None:
@@ -247,8 +254,9 @@ def fit_linear_model(
Each column of ``y`` is regressed on ``design``: by ordinary least
squares, by weighted least squares with precision ``weights``, and by
generalised least squares when ``block`` and ``correlation`` set a known
- correlation between samples of the same block. Results match limma's
- ``lmFit``.
+ correlation between samples of the same block, as limma's ``lmFit`` does.
+ https://docs.nygen.io/CytePool/statistics/linear-models#one-model-per-gene
+ gives the model.
NaN in ``y`` marks a missing value of one feature, and a zero weight
leaves an observed value out of that feature's fit (it still counts in
@@ -299,14 +307,17 @@ def fit_linear_model(
sample weights, one factorisation serves every feature. Otherwise each
feature is fitted on its own, in parallel on up to ``n_threads`` threads,
and the fit stores the covariance matrix of every feature:
- ``n_features * n_coefficients**2`` floats.
+ ``n_features * n_coefficients**2`` floats of 8 bytes, so 16 MB for 20,000
+ features and 10 coefficients, and 1.6 GB for 100 coefficients.
Raises:
TypeError: If an input is not real-valued, a label is not comparable, or an
input is or holds a numpy masked array (mark missing values of ``y``
with NaN).
ValueError: If shapes disagree, ``y`` has infinite values, ``design``
- has random effects that the fit would drop, is not finite or is not
+ has random effects that the fit would drop (the message says what can
+ be fitted: ``fit_mixed_model`` estimates random intercepts, and no
+ function estimates random slopes yet), is not finite or is not
of full column rank (the message names the aliased columns), a
weight is negative or not finite where ``y`` is observed, ``block``
and ``correlation`` are invalid, or ``coefficient_names`` move a
@@ -375,9 +386,12 @@ def contrasts_fit(
approximate, because P then comes from the whole design without weights
rather than from each feature's own samples and weights. On a design whose
coefficients are correlated, such as group comparisons adjusted for a
- covariate, they can be off by a factor of two or more; and with sample
+ covariate, they can be far from the exact ones, and they change with the
+ parametrisation of the design, where the exact ones do not; with sample
weights, a single missing value anywhere, or a single zero weight, makes
them approximate for every feature.
+ https://docs.nygen.io/CytePool/statistics/linear-models#contrasts explains
+ why the exact standard errors are the default.
A contrast with a non-zero multiplier on a coefficient that is NaN for a
feature is NaN for that feature, as in limma. This includes contrasts that
@@ -395,7 +409,7 @@ def contrasts_fit(
..., n - 1) are taken in the order of ``fit.coefficient_names``; a pandas
index of other labels that are not strings is an error. limma's
``contrasts.fit`` always takes the rows in order, and only warns when their
- names differ from the coefficient names (found by probing).
+ names differ from the coefficient names.
Args:
fit: A fit from :func:`fit_linear_model` or from this function.
@@ -439,6 +453,9 @@ def contrasts_fit(
"contrasts_fit with method='limma'), so its contrasts cannot be exact; "
"contrast the original fit, or use method='limma'"
)
+ # Weights that name their coefficients are matched to them by name. limma's
+ # contrasts.fit takes the rows in order and only warns when the names differ
+ # (found by probing).
matrix, contrast_names = contrast_matrix(contrasts, names, fit.coefficient_names)
n_features = fit.coefficients.shape[0]
n_contrasts = matrix.shape[1]
diff --git a/cytepool/stats/meta_analysis.py b/cytepool/stats/meta_analysis.py
index e38d443..864da30 100644
--- a/cytepool/stats/meta_analysis.py
+++ b/cytepool/stats/meta_analysis.py
@@ -1,116 +1,51 @@
"""Meta-analysis of the effects of many features across studies, as metafor computes it.
-:func:`fit_meta_analysis` combines, for every feature (gene), the estimates of one
-effect in several studies, each with its standard error, into an estimate of a common
-effect, with the between-study variance and the heterogeneity statistics, as metafor's
-``rma.uni`` does for one feature without moderators (Viechtbauer 2010).
+:func:`fit_meta_analysis` meta-analyses, for every feature (gene), the estimates of
+one effect in several studies, each with its standard error, by the fixed-effect or a
+random-effects model, with the estimate of the between-study variance tau^2 and the
+heterogeneity statistics that metafor's ``rma.uni`` reports for a model without
+moderators (Viechtbauer 2010), into a :class:`MetaAnalysisFit`;
:func:`cytepool.meta_analysis` does it for the differential-expression results of
-several studies, such as the same cell types in several cohorts, tissues or
-platforms, matched by group, contrast and feature id.
-
-The model:
- Study i estimates the effect of a feature by y_i, with standard error s_i, and its
- sampling variance v_i = s_i^2 is taken as known. In the random-effects model the
- true effects of the studies vary around a mean mu with the between-study variance
- tau^2: y_i = mu + u_i + e_i, with u_i ~ N(0, tau^2) and e_i ~ N(0, v_i)
- (DerSimonian and Laird 1986). With the weights w_i = 1 / (v_i + tau^2), mu is
- estimated by mu_hat = sum w_i y_i / sum w_i, with standard error
- 1 / sqrt(sum w_i). The fixed-effect model, which assumes one true effect (metafor's
- equal-effects model, ``method = "EE"``), is the case tau^2 = 0: the
- inverse-variance weighted mean.
-
-The between-study variance (``method``), over the k studies that measured a feature:
- - ``"reml"``, metafor's default: the restricted maximum likelihood estimate
- (Viechtbauer 2005), the tau^2 >= 0 that maximizes
- -1/2 sum log(v_i + tau^2) - 1/2 log(sum w_i) - 1/2 sum w_i (y_i - mu_hat)^2.
- This function can have more than one maximum, as when an imprecise study lies
- far from precise ones that agree. Its maxima are zero, where the score is not
- positive there, and the roots where the score, sum w_i^2 (y_i - mu_hat)^2 -
- tr(P), with P = W - w w' / sum w_i for the diagonal matrix W of the weights,
- falls from positive to negative. The score is negative beyond
- sum (y_i - y_bar)^2 / (k - 1) + 2 max(v_i). The roots are bracketed by the
- signs of the score at 8 points per decade of tau^2, from 1e-3 min(v_i) to twice
- that bound, so that rounding cannot hide one near the bound; each is found by
- Newton's method with the observed information, kept in its bracket by
- bisection, and the estimate is the maximum with the largest restricted
- likelihood.
- - ``"dl"``: the DerSimonian and Laird (1986) estimate, the method of moments
- with the weights 1 / v_i: max(0, (Q - (k - 1)) / (sum w_i - sum w_i^2 /
- sum w_i)), with Q below.
- - ``"pm"``: the Paule and Mandel (1982) estimate, at which the generalized Q
- statistic sum w_i (y_i - mu_hat)^2 equals its expectation k - 1, or 0 where Q
- is at most k - 1. The statistic falls with tau^2 and is convex, and the root
- lies between sum (y_i - y_bar)^2 / (k - 1) minus the largest and minus the
- smallest v_i, so Newton's method from the lower end converges to it from below.
- - ``"fixed"``: tau^2 = 0.
-
-Tests and confidence intervals (``test``):
- - ``"z"``: z = mu_hat / SE against the standard normal distribution, and the
- interval mu_hat +/- z_(1 - alpha / 2) SE, as metafor's default ``test = "z"``.
- - ``"knapp_hartung"``: the standard error is multiplied by sqrt(q), with
- q = sum w_i (y_i - mu_hat)^2 / (k - 1), and the statistic is referred to a t
- distribution with k - 1 degrees of freedom (Hartung 1999; Sidik and Jonkman
- 2002; Knapp and Hartung 2003; metafor's ``test = "knha"``), which metafor's
- documentation recommends for few studies. Where q < 1, as when the studies
- agree more closely than their standard errors imply, the adjusted standard
- error is smaller than the unadjusted one; with the fixed-effect model it is
- the standard error scaled by sqrt(Q / (k - 1)), which metafor computes with a
- warning that the method is not meant for that model.
-
-Heterogeneity:
- Cochran's (1954) Q = sum (y_i - mu_fixed)^2 / v_i, with the fixed-effect estimate
- mu_fixed, and its p-value from the chi-squared distribution with k - 1 degrees of
- freedom. I^2 and H^2 are those metafor reports: I^2 = tau^2 / (tau^2 + s^2) and
- H^2 = (tau^2 + s^2) / s^2, where s^2 = (k - 1) sum w_i / ((sum w_i)^2 -
- sum w_i^2), with w_i = 1 / v_i, is the typical within-study variance (Higgins and
- Thompson 2002, equation 9), so that they follow the estimate of tau^2. For the
- fixed-effect model they are I^2 = max(0, (Q - (k - 1)) / Q) and H^2 = Q / (k - 1),
- the conventional definitions, which equal the others for DerSimonian-Laird where
- Q > k - 1; where Q <= k - 1, I^2 is 0 for both, and H^2 is Q / (k - 1) for the
- fixed effect and 1 for DerSimonian-Laird. I^2 is a proportion here; metafor
- reports it in percent.
+several studies. https://docs.nygen.io/CytePool/statistics/meta-analysis explains the
+model, the estimators and the tests, and
+https://docs.nygen.io/CytePool/statistics/meta-analysis#meta-analysis-limits how they
+differ from metafor's.
Studies that did not measure a feature:
- NaN marks them, in the effect or the standard error. Each feature is analysed
- over the k studies that measured it, as metafor leaves out studies with missing
- values. A feature measured in one study has that study's estimate and standard
- error and its z-test, also with ``"knapp_hartung"``, which needs two studies;
- tau^2 = 0, Q = 0 with a p-value of 1, I^2 = 0 and H^2 = 1: metafor's results for
- one study (found by probing). A feature measured in no study has NaN.
-
-Accuracy:
- Every estimate is computed for the deviations of the effects from their mean and
- the standard errors divided, exactly, by a power of two near the largest of them,
- so that the results do not depend on the scale of the data, and no square
- overflows or underflows however much larger the spread of the effects is than
- their standard errors. The sums sum w_i - sum w_i^2 / sum w_i and tr(P^2), which
- cancel when one study is far more precise than the others, are formed from the
- sum of the weights of the other studies for each study, which does not cancel.
-
-Deliberate differences from metafor (``validation/meta_analysis.py`` reports them):
- - The REML estimate is the largest maximum of the restricted likelihood. metafor
- finds a maximum by Fisher scoring from the Hedges estimate,
- max(0, sum (y_i - y_bar)^2 / (k - 1) - mean(v_i)), and replaces it by zero, with
- a warning that Fisher scoring "may have gotten stuck at a local maximum", where
- the restricted likelihood is larger at zero (found by probing). Where the
- likelihood has another, larger maximum, metafor's estimate is the smaller one or
- zero. Fisher scoring also oscillates without converging where the observed
- information exceeds twice the expected one: metafor then stops with an error,
- or, at its default threshold, within the oscillation, and its help page's remedy
- ``control = list(stepadj = 0.5)`` converges to the estimate here.
- - The iterative estimates converge to rounding. metafor stops REML when tau^2
- changes by less than 1e-5 from one step to the next, and Paule-Mandel at the
- tolerance of R's ``uniroot``, about 1.2e-4 on tau^2; ``control = list(threshold
- = 1e-13)`` and ``control = list(tol = 1e-14)`` make it converge to the values
- here.
- - Where the restricted likelihood is largest at zero although the Hedges estimate
- is positive, the REML estimate is zero; metafor's step halving approaches zero
- and stops near its threshold, at about 1e-6 by default (found by probing).
- - metafor's Paule-Mandel search stops with an error when the root lies above
- max(100, 10 mad(y)^2); here it has no upper limit.
- - Standard errors must be positive and finite where they are given. For a zero
- standard error metafor stops (fixed effect, DerSimonian-Laird, Paule-Mandel) or
- fits without Q, I^2 and H^2 (REML); for an infinite one it stops.
+ NaN marks them, in the effect or the standard error. Each feature is analysed over
+ the k studies that measured it, as metafor leaves out studies with missing values.
+ A feature measured in one study has that study's estimate and standard error and
+ its z-test, also with ``"knapp_hartung"``, which needs two studies; tau^2 = 0,
+ Q = 0 with a p-value of 1, I^2 = 0 and H^2 = 1: metafor's results for one study. A
+ feature measured in no study has NaN.
+
+The REML estimate:
+ The restricted log-likelihood, -1/2 sum log(v_i + tau^2) - 1/2 log(sum w_i) -
+ 1/2 sum w_i (y_i - mu_hat)^2 with w_i = 1 / (v_i + tau^2), can have more than one
+ maximum. Its maxima are zero, where the score is not positive there, and the roots
+ where the score, sum w_i^2 (y_i - mu_hat)^2 - tr(P), with P = W - w w' / sum w_i
+ for the diagonal matrix W of the weights, falls from positive to negative. The
+ score is negative beyond sum (y_i - y_bar)^2 / (k - 1) + 2 max(v_i). The roots are
+ bracketed by the signs of the score at 8 points per decade of tau^2, from
+ 1e-3 min(v_i) to twice that bound, so that rounding cannot hide one near the bound;
+ each is found by Newton's method with the observed information, kept in its
+ bracket by bisection, and the estimate is the maximum with the largest restricted
+ likelihood. The Paule-Mandel statistic falls with tau^2 and is convex, and its root
+ lies between sum (y_i - y_bar)^2 / (k - 1) minus the largest and minus the smallest
+ v_i, so Newton's method from the lower end converges to it from below.
+
+Matching metafor:
+ The results match metafor 5.0.1 run to convergence, with
+ ``control = list(threshold = 1e-13)`` for REML and
+ ``control = list(tol = 1e-14)`` for Paule-Mandel: by default metafor stops REML
+ when tau^2 changes by less than 1e-5 from one step to the next, and Paule-Mandel
+ at the tolerance of R's ``uniroot``, about 1.2e-4 on tau^2. Its documentation
+ gives the methods, and running it on chosen inputs settled the conventions it
+ leaves open, such as its results for one study above. I^2 and H^2 follow the
+ estimate of tau^2, as metafor reports them (Higgins and Thompson 2002, equation 9).
+ The Knapp-Hartung adjustment of the fixed-effect model scales the standard error
+ by sqrt(Q / (k - 1)), which metafor computes with a warning that the method is
+ not meant for that model.
References:
Benjamini Y, Hochberg Y (1995). Controlling the false discovery rate: a practical
@@ -273,8 +208,45 @@ def fit_meta_analysis(
For every feature, the effects of the studies that measured it are combined into
an estimate of a common effect by the fixed-effect or a random-effects model, with
the between-study variance tau^2, Cochran's Q and its p-value, I^2 and H^2, as
- ``rma.uni(yi, sei = ..., method = ..., test = ...)`` gives them; the module
- description gives the model, the estimators of tau^2 and the tests.
+ ``rma.uni(yi, sei = ..., method = ..., test = ...)`` gives them. Study i estimates
+ the effect by y_i with the sampling variance v_i = s_i^2, taken as known; in the
+ random-effects model y_i = mu + u_i + e_i, with u_i ~ N(0, tau^2) and
+ e_i ~ N(0, v_i). With the weights w_i = 1 / (v_i + tau^2), the estimate is
+ mu_hat = sum w_i y_i / sum w_i, with standard error 1 / sqrt(sum w_i).
+
+ The estimators of tau^2 (``method``), over the k studies that measured a feature:
+ ``"reml"``, the restricted maximum likelihood estimate (Viechtbauer 2005), the
+ largest maximum of the restricted likelihood (see the module docstring);
+ ``"dl"``, DerSimonian and Laird's, max(0, (Q - (k - 1)) / (sum w_i - sum w_i^2 /
+ sum w_i)) with w_i = 1 / v_i; ``"pm"``, Paule and Mandel's (1982), at which the
+ generalized Q statistic sum w_i (y_i - mu_hat)^2 equals its expectation k - 1, or
+ 0 where it is at most k - 1; and ``"fixed"``, tau^2 = 0.
+
+ The tests (``test``): ``"z"``, z = mu_hat / SE against the standard normal
+ distribution, with the interval mu_hat +/- z_(1 - alpha / 2) SE; or
+ ``"knapp_hartung"``, the standard error times sqrt(q), with q = sum w_i (y_i -
+ mu_hat)^2 / (k - 1), against a t distribution with k - 1 degrees of freedom
+ (Hartung 1999; Sidik and Jonkman 2002; Knapp and Hartung 2003). Where q < 1 the
+ adjusted standard error is the smaller.
+
+ Heterogeneity: Cochran's (1954) Q = sum (y_i - mu_fixed)^2 / v_i, with the
+ fixed-effect estimate mu_fixed, and its p-value from the chi-squared distribution
+ with k - 1 degrees of freedom. I^2 = tau^2 / (tau^2 + s^2) and H^2 = (tau^2 + s^2)
+ / s^2, where s^2 = (k - 1) sum w_i / ((sum w_i)^2 - sum w_i^2), with w_i = 1 / v_i,
+ is the typical within-study variance (Higgins and Thompson 2002). For the
+ fixed-effect model they are I^2 = max(0, (Q - (k - 1)) / Q) and H^2 = Q / (k - 1),
+ which equal the others for DerSimonian-Laird where Q > k - 1; where Q <= k - 1, I^2
+ is 0 for both, and H^2 is Q / (k - 1) for the fixed effect and 1 for
+ DerSimonian-Laird. https://docs.nygen.io/CytePool/statistics/meta-analysis
+ explains the model.
+
+ Every estimate is computed for the deviations of the effects from their mean and
+ the standard errors divided, exactly, by a power of two near the largest of them,
+ so that the results do not depend on the scale of the data, and no square
+ overflows or underflows however much larger the spread of the effects is than
+ their standard errors. Sums that cancel when one study is far more precise than
+ the others, such as sum w_i - sum w_i^2 / sum w_i, are formed from the sum of the
+ weights of the other studies for each study, which does not cancel.
Args:
effects: ``(n_studies, n_features)`` estimates of the effect of every feature
@@ -731,6 +703,8 @@ def _meta_analyse(
tau2 = 0.0
if k == 1:
+ # metafor's results for one study, the z-test of its estimate also with
+ # the Knapp-Hartung adjustment (found by probing).
q = 0.0
q_p_value = 1.0
i_squared = 0.0
diff --git a/cytepool/stats/mixed_models.py b/cytepool/stats/mixed_models.py
index e9f4920..b02ef24 100644
--- a/cytepool/stats/mixed_models.py
+++ b/cytepool/stats/mixed_models.py
@@ -1,172 +1,63 @@
"""Linear mixed models with random intercepts, fitted to many features by REML or ML.
-Every feature (gene) of a samples x features matrix is fitted with the same model,
-
- y = X beta + Z_1 b_1 + ... + Z_K b_K + e,
- b_k ~ N(0, sigma_k^2 I), e ~ N(0, sigma^2 W^-1),
-
-where X is the fixed-effects design, Z_k assigns each sample to a level of the k-th
-grouping factor (one random intercept per level, as ``(1 | donor)`` in lme4), and W
-holds the feature's precision weights (the identity without weights). The grouping
-factors may be nested, such as ``(1 | donor/sample)``, or crossed, such as
-``(1 | donor) + (1 | batch)``. The variance parameters are estimated by restricted
-maximum likelihood (REML; Patterson and Thompson 1971), or by maximum likelihood
-(``method="ml"``), and the fixed effects by generalised least squares given them, as
-lme4's ``lmer`` with ``REML = TRUE`` (or ``FALSE``) does (Bates et al. 2015). Contrasts
-of the fixed effects are tested one at a time by t-tests (:func:`fixed_effect_tests`),
-or several at once by F-tests (:func:`fixed_effect_f_test`), with Satterthwaite's
-degrees of freedom, as lmerTest computes them (Kuznetsova et al. 2017), or Kenward and
-Roger's, as pbkrtest computes them (Halekoh and Hojsgaard 2014). variancePartition's
-``dream`` uses both for repeated measures in gene expression (Hoffman and Roussos
-2021); :mod:`cytepool.stats.dream` builds dream's precision weights and moderated
-tests on these fits.
-
-The REML criterion:
- With theta_k = sigma_k / sigma, the relative standard deviations that lme4
- estimates, and V = W^-1 + sum_k theta_k^2 Z_k Z_k', the fixed effects and sigma^2
- are profiled out: beta(theta) is the generalised least squares estimate,
- r^2(theta) = (y - X beta)' V^-1 (y - X beta), and sigma^2(theta) = r^2 / (n - p).
- The criterion minimised is lme4's profiled REML deviance (Bates et al. 2015,
- section 3.4),
-
- log det V + log det(X' V^-1 X) + (n - p) (1 + log(2 pi r^2 / (n - p))),
-
- whose value at the estimate is lme4's ``REMLcrit``. With weights, log det V
- includes -sum(log w), so the criterion, the fixed effects and the variance
- components do not change when every weight is multiplied by the same constant;
- only sigma^2 scales with it. Maximum likelihood minimises the profiled deviance
- log det V + n (1 + log(2 pi r^2 / n)), lme4's ``deviance`` of a fit with ``REML =
- FALSE``, and estimates sigma^2 by r^2 / n.
-
-Computation:
- The criterion, its gradient and its Hessian are computed from weighted cross-
- products of the samples, without forming any n x n matrix. The random effects of
- the grouping factor with the most levels are absorbed in closed form: the
- covariance matrix of the samples of one of its levels is a diagonal matrix plus a
- matrix of rank one. The random effects of the other factors, and the fixed
- effects, enter a dense Cholesky factorisation whose size is their number of
- levels plus the number of coefficients, as in the penalised least squares
- formulation of lme4 (Bates et al. 2015, section 3). With one grouping factor each
- evaluation costs O(levels x coefficients^2). The fixed effects are handled in an
- orthonormal basis of the columns of X, which keeps the cross-products as well
- conditioned as X itself, and the estimates are transformed back.
-
- The derivatives of the criterion with respect to gamma_k = theta_k^2 are the
- classical ones of REML (Harville 1977): with P = V^-1 - V^-1 X (X' V^-1 X)^-1 X'
- V^-1, they need only Z_k' P Z_l and Z_k' P y; those of maximum likelihood take the
- traces and norms of Z_k' V^-1 Z_l in place of those of Z_k' P Z_l, which the same
- factorisation gives before its fixed-effect pivots. Newton's method minimises the
- criterion over u_k = log(1 + c_k gamma_k), where c_k is the mean total weight of a
- level of factor k: in a balanced one-way design the criterion is convex in u, and
- the boundary gamma_k = 0 is u_k = 0. A component whose gradient points outward at
- the boundary is held there (projected Newton method, Bertsekas 1982), so variance
- components at zero are exactly zero. An indefinite Hessian is made positive definite
- by taking the absolute values of its eigenvalues, a step changes no u_k by more than
- 5, and a backtracking line search ensures descent.
- The fit converges when a step moves no u_k by more than 1e-10, as a full
- Newton step or where the full step promises no decrease beyond rounding; as
- Newton's method converges quadratically, the estimates are then as accurate as
- the cross-products allow. Those of the absorbed factor are taken about the means
- of its levels, which keeps them accurate however much the levels differ; those
- of the other factors enter the Cholesky factorisation scaled by their theta, as
- in lme4, and lose about as many digits as theta^2 times their levels' weights
- has. Every fit starts at theta_k = 1 relative to the residual standard deviation
- of an observation of average weight: lme4's default start (theta = 1) when there
- are no weights, and one that keeps the fit independent of the scale of the
- weights. Like lme4's optimiser, Newton's method finds a local minimum: when the
- criterion has several, the fit can stop at one that is not the lowest. When the
- fixed and random effects together reproduce a feature's values, the criterion
- has no minimum (it decreases without bound as sigma^2 goes to zero), so such a
- feature is left unfitted; see :class:`MixedModelFit`.
-
-Fitted values and residual degrees of freedom:
- The random effects are predicted at the estimates by their best linear unbiased
- predictors, b_k = gamma_k Z_k' P y (Henderson 1975), and the fitted values are
- X beta + sum_k Z_k b_k, lme4's ``fitted``. Their hat matrix H, on the scale of the
- weighted values, gives the approximate residual degrees of freedom tr((I - H)'
- (I - H)), variancePartition's ``rdf.merMod``, which ``dream`` moderates on: with
- I - H = W^-1/2 P W^-1/2 and P V P = P,
-
- tr((I - H)' (I - H)) = n - p - 2 sum_k gamma_k tr(Z_k' P Z_k)
- + sum_kl gamma_k gamma_l |Z_k' P Z_l|^2,
-
- which is n - p with every component at zero, and in a balanced one-way design
- q (m - 1) + (q - 1) / (1 + m gamma)^2 for q groups of m samples.
-
-Satterthwaite's degrees of freedom:
- For a contrast c of the fixed effects with estimated variance v = c' C c, where
- C is the covariance matrix of the estimates, the degrees of freedom are
- 2 v^2 / (g' A g) (Satterthwaite 1946; Giesbrecht and Burns 1985), where g holds
- the derivatives of v with respect to the variance parameters and A is their
- asymptotic covariance matrix: twice the inverse of the Hessian of the REML
- deviance (the observed information, as lmerTest computes it), or of the deviance
- of maximum likelihood for such a fit. The degrees of
- freedom do not depend on how the variance parameters are parametrised; here
- they are the variances (sigma_1^2, ..., sigma_K^2, sigma^2). lmerTest
- parametrises them by (theta, sigma), in which the deviance is an even function
- of each theta_k, so a component estimated at zero contributes nothing to g' A g;
- such components are left out here in the same way, and with every component at
- zero the degrees of freedom are n - p, those of the fixed-effects model (found
- by probing lmerTest). lmerTest computes the derivatives numerically; here they
- are exact.
-
-Kenward and Roger's method:
- With few samples the variance parameters are estimated imprecisely, and the
- covariance matrix C of the estimates, computed as if they were known,
- understates the variance of the fixed effects. Kenward and Roger (1997) add to C
- a correction for its own bias and for the variability of the variance parameters,
- of order 1 / n, to second order in their estimation error, whose covariance they
- take as the inverse of the expected information of REML; the t-statistic of a
- contrast uses the adjusted matrix, and its degrees of freedom are Satterthwaite's
- formula with that expected information in place of the observed one. For an
- F-test of l contrasts they scale the Wald statistic and match its first two
- moments to an F distribution on (l, m) degrees of freedom. The covariance matrix
- of the observations is linear in the variance parameters, so the second
- derivatives that their formula also allows for vanish. A variance component
- estimated at zero takes part, as in pbkrtest, so that the degrees of freedom are
- smaller than Satterthwaite's, which leave it out. In balanced designs, such as a
- randomised block design, the adjustment vanishes and the method gives the tests
- of the analysis of variance, which are exact there.
-
- With weights, pbkrtest builds the covariance matrix of the observations without
- the prior weights of an lme4 fit, with the residual variance of an observation
- of weight one, while it takes C from the fit, which the weights enter (found by
- probing pbkrtest 0.5.3); lmerTest's Kenward-Roger tests are pbkrtest's. dream
- divides each feature's weights by their mean before it fits, so that its tests
- are those of every observation at the average weight, which do not depend on the
- scale of the weights. ``kenward_roger_weights="average"`` computes these.
- ``"precision"`` uses each observation's own weight, the covariance
- matrix of the model fitted: Kenward and Roger's method for it, which pbkrtest
- gives for the same model fitted without weights to the values, fixed effects and
- random effects multiplied by the square roots of the weights. With voom's
- weights the two can differ by tens of percent in the degrees of freedom and by
- several percent in the standard errors, and the first can make standard errors
- smaller than the unadjusted ones, which the second never does (the Validation
- page gives sizes). Each feature costs O(n^3) operations for n samples, as in
- pbkrtest.
-
-F-tests:
- :func:`fixed_effect_f_test` tests that the contrasts L beta are all zero. With
- Satterthwaite's method, as lmerTest's ``contestMD``, the contrasts are replaced by
- the eigenvectors of their covariance matrix L C L' with positive eigenvalues
- (in the order of R's ``eigen``, the largest first), whose estimates are
- uncorrelated; F is the mean of their squared t-statistics, and its denominator
- degrees of freedom are 2 E / (E - l) with E = sum nu_m / (nu_m - 2) over their
- Satterthwaite degrees of freedom nu_m, which matches the mean of F (Fai and
- Cornelius 1996): their mean when they differ by less than 1e-8, and 2 when one is
- at most 2, as lmerTest's ``get_Fstat_ddf`` documents. These depend on how the
- contrasts span the hypothesis, as lmerTest's do. Kenward and Roger's F-test, as
- pbkrtest's ``KRmodcomp``, does not. The F-test of one contrast is its t-test, on
- the same degrees of freedom even below 2, as ``contestMD`` gives it (found by
- running it).
-
-Limits of this version:
- Random slopes are not implemented yet.
- :mod:`cytepool.stats.variance_fractions` partitions the variance of these fits.
- Every feature needs a value for every sample, with a positive weight. Designs
- whose fixed and random effects together span every sample, and so reproduce
- every feature exactly, are refused, although their criterion can have a minimum
- (lme4 fits them).
+:func:`fit_mixed_model` fits every feature (gene) of a samples x features matrix with
+the same linear mixed model, the fixed effects and random intercepts of a
+:class:`~cytepool.Design`, by restricted maximum likelihood (REML; Patterson and
+Thompson 1971) or maximum likelihood, as lme4's ``lmer`` does (Bates et al. 2015), into
+a :class:`MixedModelFit`. :func:`fixed_effect_tests` tests contrasts of the fixed
+effects one at a time by t-tests, into a :class:`FixedEffectTests`, and
+:func:`fixed_effect_f_test` several at once by F-tests, into a
+:class:`FixedEffectFTest`, with Satterthwaite's degrees of freedom, as lmerTest computes
+them (Kuznetsova et al. 2017), or Kenward-Roger's, as pbkrtest computes them
+(Halekoh and Hojsgaard 2014). :mod:`cytepool.stats.dream` builds dream's precision
+weights and moderated tests on these fits (Hoffman and Roussos 2021).
+https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-model-reml gives the model
+and the methods.
+
+Weights:
+ With precision weights w, the residual variance of an observation is sigma^2 / w.
+ The criterion, the fixed effects and the variance components do not change when
+ every weight of a feature is multiplied by the same constant; only sigma^2 scales
+ with it.
+
+Matching lme4, lmerTest and pbkrtest:
+ The results match lme4 2.0.6 and lmerTest 3.2.1, and pbkrtest 0.5.3 with lmerTest
+ 3.1-3 and lme4 1.1-38: their documentation gives the methods, and running them on
+ chosen inputs settled the conventions it leaves open.
+
+ - The criterion minimized is lme4's profiled REML deviance (Bates et al. 2015,
+ section 3.4), whose value at the estimates is lme4's ``REMLcrit``, or for maximum
+ likelihood its profiled deviance, lme4's ``deviance`` of a fit with
+ ``REML = FALSE``. A variance component estimated at zero is held exactly at zero,
+ where lme4 reports a singular fit.
+ - Satterthwaite's degrees of freedom (Satterthwaite 1946; Giesbrecht and Burns 1985)
+ take the asymptotic covariance of the variance parameters as twice the inverse of
+ the Hessian of the deviance, as lmerTest does. lmerTest parametrizes them by
+ (theta, sigma), in which the deviance is an even function of each theta_k, so a
+ component estimated at zero contributes nothing; with every component at zero
+ the degrees of freedom are n - p, those of the fixed-effects model.
+ - Kenward-Roger's method (Kenward and Roger 1997), as pbkrtest computes it,
+ takes the inverse of the expected information of REML, and a variance component
+ estimated at zero takes part. With weights, pbkrtest builds the covariance matrix
+ of the observations without the prior weights of an lme4 fit, with the residual
+ variance of an observation of weight one, while it takes the covariance matrix
+ of the coefficients from the fit; lmerTest's Kenward-Roger tests are pbkrtest's.
+ - With Satterthwaite's method, the F-test of :func:`fixed_effect_f_test` is
+ lmerTest's ``contestMD``: the contrasts are replaced by the eigenvectors of their
+ covariance matrix L C L' with positive eigenvalues (in the order of R's
+ ``eigen``, the largest first), whose estimates are uncorrelated; F is the mean of
+ their squared t-statistics, and its denominator degrees of freedom are
+ 2 E / (E - l) with E = sum nu_m / (nu_m - 2) over their degrees of freedom
+ (Fai and Cornelius 1996): their mean when they differ by less than 1e-8, and 2
+ when one is at most 2, as lmerTest's ``get_Fstat_ddf`` documents. These depend
+ on how the contrasts span the hypothesis, as lmerTest's do; Kenward-Roger's
+ F-test, as pbkrtest's ``KRmodcomp``, does not. The F-test of one contrast is its
+ t-test, on the same degrees of freedom even below 2, as ``contestMD`` gives it.
+
+Differences from lme4 and lmerTest:
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-models-limits lists
+ them; :func:`fit_mixed_model` and :class:`MixedModelFit` give the data they refuse
+ and the features they leave unfitted.
References:
Bates D, Maechler M, Bolker B, Walker S (2015). Fitting linear mixed-effects models
@@ -395,14 +286,14 @@ class FixedEffectTests:
contrast_names: Names of the contrasts.
df_method: How the degrees of freedom were computed: ``"satterthwaite"`` or
``"kenward_roger"``.
- kenward_roger_weights: For Kenward and Roger's method, how its adjustment
+ kenward_roger_weights: For Kenward-Roger's method, how its adjustment
took the weights: ``"average"`` or ``"precision"`` (see
:func:`fixed_effect_tests`); None for Satterthwaite's.
estimate: ``(n_features, n_contrasts)`` estimates c' beta.
standard_error: ``(n_features, n_contrasts)`` standard errors
(c' C c)^(1/2), with C the covariance matrix of the coefficients:
- the fit's (``cov_coefficients``) for Satterthwaite's method, Kenward
- and Roger's adjusted one for theirs.
+ the fit's (``cov_coefficients``) for Satterthwaite's method, and the
+ adjusted one for Kenward-Roger's.
t: ``(n_features, n_contrasts)`` t-statistics, the estimates over their
standard errors.
df: ``(n_features, n_contrasts)`` degrees of freedom; NaN where they are
@@ -450,7 +341,7 @@ class FixedEffectFTest:
kenward_roger_weights: ``"average"`` or ``"precision"`` for Kenward and
Roger's method, as in :class:`FixedEffectTests`; None for
Satterthwaite's.
- f_statistic: ``(n_features,)`` F-statistics; Kenward and Roger's are
+ f_statistic: ``(n_features,)`` F-statistics; Kenward-Roger's are
scaled by ``scaling``.
df1: The numerator degrees of freedom, the number of linearly independent
contrasts.
@@ -493,9 +384,45 @@ def fit_mixed_model(
Each column of ``y`` is fitted with the fixed effects and the random intercepts
of ``design``, as lme4's ``lmer(y ~ ... + (1 | donor), REML = TRUE, weights =
- w)`` fits one feature: see the module docstring for the model, the criterion
- and the algorithm. Test contrasts of the fixed effects with
+ w)`` fits one feature: y = X beta + Z_1 b_1 + ... + Z_K b_K + e, with
+ b_k ~ N(0, sigma_k^2 I) and e ~ N(0, sigma^2 W^-1). With theta_k = sigma_k / sigma
+ and V = W^-1 + sum_k theta_k^2 Z_k Z_k', the fixed effects and sigma^2 are
+ profiled out, and the criterion minimized is lme4's profiled REML deviance,
+ log det V + log det(X' V^-1 X) + (n - p) (1 + log(2 pi r^2 / (n - p))) with
+ r^2 = (y - X beta)' V^-1 (y - X beta), or for maximum likelihood
+ log det V + n (1 + log(2 pi r^2 / n)). Test contrasts of the fixed effects with
:func:`fixed_effect_tests`.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-model-reml explains
+ the model.
+
+ Algorithm:
+ The criterion, its gradient and its Hessian are computed from weighted
+ cross-products of the samples, without forming any n x n matrix. The random
+ effects of the grouping factor with the most levels are absorbed in closed
+ form: the covariance matrix of the samples of one of its levels is a diagonal
+ matrix plus a matrix of rank one. The random effects of the other factors,
+ and the fixed effects, enter a dense Cholesky factorisation whose size is their
+ number of levels plus the number of coefficients, as in the penalised least
+ squares formulation of lme4 (Bates et al. 2015, section 3). The fixed effects
+ are handled in an orthonormal basis of the columns of X, and the estimates are
+ transformed back. The derivatives with respect to gamma_k = theta_k^2 are the
+ classical ones of REML (Harville 1977). Newton's method minimizes the
+ criterion over u_k = log(1 + c_k gamma_k), where c_k is the mean total weight
+ of a level of factor k: in a balanced one-way design the criterion is convex
+ in u, and the boundary gamma_k = 0 is u_k = 0. A component whose gradient
+ points outward at the boundary is held there (projected Newton method,
+ Bertsekas 1982). An indefinite Hessian is made positive definite by taking the
+ absolute values of its eigenvalues, a step changes no u_k by more than 5, and
+ a backtracking line search ensures descent. The fit converges when a step
+ moves no u_k by more than 1e-10. Every fit starts at theta_k = 1 relative to
+ the residual standard deviation of an observation of average weight, lme4's
+ default start without weights. Like lme4's optimiser, Newton's method finds a
+ local minimum: when the criterion has several, the fit can stop at one that is
+ not the lowest. The random effects are predicted by their best linear unbiased
+ predictors (Henderson 1975). The variance components of the factors other
+ than the absorbed one enter the factorisation scaled by their theta, as in
+ lme4, and lose about as many digits as theta^2 times their levels' weights
+ has.
Args:
y: ``(n_samples, n_features)`` expression values on a log scale, such as
@@ -522,10 +449,11 @@ def fit_mixed_model(
thread they are fitted on the calling thread.
Returns:
- The fits. They store, for every feature, the derivatives of the covariance
- matrix of the coefficients with respect to each variance parameter:
- ``n_features * (n_components + 1) * n_coefficients**2`` floats, and the
- fitted values, as many floats as ``y``.
+ The fits. They store, for every feature, the covariance matrix of the
+ coefficients and its derivatives with respect to each variance parameter:
+ ``n_features * (n_components + 2) * n_coefficients**2`` floats, 23 MB for
+ 20,000 genes, two random intercepts and 6 coefficients; and the fitted values,
+ as many floats as ``y``.
Raises:
TypeError: If ``design`` is not a ``Design``, an input is not real-valued,
@@ -679,13 +607,22 @@ def fixed_effect_tests(
For a contrast c, the t-statistic is c' beta / (c' C c)^(1/2). With
``df_method="satterthwaite"``, C is the fit's covariance matrix and the degrees
of freedom are Satterthwaite's, as lmerTest's ``contest(fit, L, joint = FALSE)``
- and ``summary`` compute them. With ``"kenward_roger"``, C is Kenward and Roger's
+ and ``summary`` compute them. With ``"kenward_roger"``, C is Kenward-Roger's
adjusted covariance matrix, which allows for the estimation of the variance
parameters, and the degrees of freedom are theirs, as lmerTest's ``contest1D(fit,
L, ddf = "Kenward-Roger")`` computes them with pbkrtest's ``vcovAdj`` and
- ``get_Lb_ddf``. The module docstring gives both methods. Without contrasts, each
- coefficient is tested, as in lmerTest's coefficient table.
- :func:`fixed_effect_f_test` tests several contrasts at once.
+ ``get_Lb_ddf``. Without contrasts, each coefficient is tested, as in lmerTest's
+ coefficient table. :func:`fixed_effect_f_test` tests several contrasts at once.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#kenward-roger explains both
+ methods.
+
+ Satterthwaite's degrees of freedom are 2 v^2 / (g' A g) for v = c' C c, with g the
+ derivatives of v with respect to the variance parameters and A twice the inverse
+ of the Hessian of the deviance. Kenward-Roger's method inverts each feature's
+ n x n covariance matrix of the observations, as pbkrtest does: O(n^3) operations
+ per feature for n samples, so that its tests of many samples take far longer than
+ Satterthwaite's. The Validation page times both on 20,000 genes, at 18 and at 120
+ samples (https://docs.nygen.io/CytePool/validation#validation-timings).
Contrasts that name the coefficients they weigh are matched to
``fit.coefficient_names`` by name, as by
@@ -695,7 +632,7 @@ def fixed_effect_tests(
its index. Weights without names are taken in the order of the coefficients.
Args:
- fit: A fit from :func:`fit_mixed_model`; by REML for Kenward and Roger's
+ fit: A fit from :func:`fit_mixed_model`; by REML for Kenward-Roger's
method.
contrasts: The contrasts, one per column of a ``(n_coefficients,
n_contrasts)`` matrix: a ``Contrasts``, a pandas DataFrame, a mapping of
@@ -705,14 +642,16 @@ def fixed_effect_tests(
``Contrasts``; see ``contrasts_fit``.
df_method: ``"satterthwaite"`` (lmerTest's default) or
``"kenward_roger"``.
- kenward_roger_weights: How Kenward and Roger's adjustment and degrees of
+ kenward_roger_weights: How Kenward-Roger's adjustment and degrees of
freedom take a fit's weights: ``"average"`` computes them with every
observation at its feature's average weight, as variancePartition's
``dream`` does with pbkrtest, which leaves out the weights of an lme4 fit
(lmerTest's ``ddf = "Kenward-Roger"`` gives the same for weights of mean
one); ``"precision"`` with each observation's own weight, the covariance
of the model fitted. The two agree without weights or with equal
- weights; see the module docstring. Ignored for Satterthwaite's method.
+ weights. With voom's weights the first can make standard errors smaller
+ than the unadjusted ones, and the second never does. Ignored for
+ Satterthwaite's method.
Returns:
The estimates, standard errors, t-statistics, degrees of freedom and
@@ -723,7 +662,7 @@ def fixed_effect_tests(
``names`` is not valid or is or holds a numpy masked array.
ValueError: If the contrasts do not match the coefficients or are not
finite, ``df_method`` or ``kenward_roger_weights`` is unknown, or
- Kenward and Roger's method is asked of a maximum likelihood fit.
+ Kenward-Roger's method is asked of a maximum likelihood fit.
"""
fit = _checked_fit(fit)
matrix, contrast_names = _contrasts_of(fit, contrasts, names)
@@ -766,15 +705,16 @@ def fixed_effect_f_test(
contrasts that span the hypothesis (Fai and Cornelius 1996; Kuznetsova et al.
2017). With ``"kenward_roger"`` it is pbkrtest's ``KRmodcomp(fit, L)`` (and
lmerTest's ``contestMD(fit, L, ddf = "Kenward-Roger")``): the Wald statistic with
- the adjusted covariance matrix, scaled, on Kenward and Roger's degrees of freedom.
- The module docstring gives both. Contrasts that are linear combinations of the
+ the adjusted covariance matrix, scaled, on Kenward-Roger's degrees of freedom.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-model-f-tests explains
+ both. Contrasts that are linear combinations of the
others add nothing to the hypothesis: the numerator degrees of freedom are the
number of independent contrasts, by the rank rule of R's ``qr()``.
Contrasts are matched to the coefficients as by :func:`fixed_effect_tests`.
Args:
- fit: A fit from :func:`fit_mixed_model`; by REML for Kenward and Roger's
+ fit: A fit from :func:`fit_mixed_model`; by REML for Kenward-Roger's
method.
contrasts: The contrasts, one per column of a ``(n_coefficients,
n_contrasts)`` matrix, in any form :func:`fixed_effect_tests` takes.
@@ -837,7 +777,7 @@ def _contrasts_of(
def _inference_parts(
fit: MixedModelFit, df_method: Any, kenward_roger_weights: Any
) -> tuple[InferenceParts, str | None]:
- """The parts of the tests by the method asked for, and how Kenward and Roger's
+ """The parts of the tests by the method asked for, and how Kenward-Roger's
adjustment took the weights (None for Satterthwaite's method)."""
method = checked_choice(df_method, "df_method", ("satterthwaite", "kenward_roger"))
variant = checked_choice(
@@ -847,7 +787,7 @@ def _inference_parts(
return satterthwaite_parts(fit), None
if fit.method != "reml":
raise ValueError(
- "Kenward and Roger's method is defined for REML fits, and this fit "
+ "Kenward-Roger's method is defined for REML fits, and this fit "
"maximised the likelihood; fit with method='reml', as lmerTest and "
"pbkrtest require (pbkrtest refits such a model by REML)"
)
@@ -1971,7 +1911,9 @@ def _estimates(
# The Hessian of the REML (or ML) deviance in (gamma_k of the free components,
# sigma^2), twice its inverse, then mapped to (sigma_1^2, ..., sigma_K^2, sigma^2)
- # by the Jacobian of sigma_k^2 = gamma_k sigma^2.
+ # by the Jacobian of sigma_k^2 = gamma_k sigma^2. A component at zero is left out,
+ # as in lmerTest, whose deviance is even in each theta_k: with every component at
+ # zero, Satterthwaite's degrees of freedom are n - p (found by probing lmerTest).
norms_of_pairs = work.frobenius_v if ml else work.frobenius
free = work.free
n_free = np.int64(0)
diff --git a/cytepool/stats/multiple_testing.py b/cytepool/stats/multiple_testing.py
index 68fd12b..ae4c668 100644
--- a/cytepool/stats/multiple_testing.py
+++ b/cytepool/stats/multiple_testing.py
@@ -1,47 +1,35 @@
"""Adjustment of p-values for multiple testing, and the proportion of true nulls.
-:func:`adjust_p_values` follows R's ``p.adjust`` exactly (R Core Team 2025),
-including its conventions for missing values and for families declared larger than
-the p-values observed in them, so adjusted p-values can be compared with R output
-directly.
-
-:func:`proportion_of_true_nulls` estimates pi0, the proportion of the tests whose null
-hypothesis is true, from their p-values, as qvalue's ``pi0est`` (Storey and
-Tibshirani 2003); 1 - pi0, pi1, is the proportion with an effect, which the p-values
-of one study among the genes that another study finds measure as a replication rate.
-
-The proportion of true nulls:
- Null p-values are uniform, so the m p-values at least lambda hold about
- pi0 m (1 - lambda) null ones, and pi0(lambda) = #{p >= lambda} / (m (1 - lambda))
- estimates pi0 with a bias that falls, and a variance that grows, as lambda rises
- (Storey 2002). qvalue counts the p-values at least lambda, where the paper counts
- those above it (found by probing). With one lambda this is the estimate; with
- several, in increasing order:
-
- - ``"smoother"`` (qvalue's default): the value at the largest lambda of a cubic
- smoothing spline with ``smooth_df`` equivalent degrees of freedom through
- pi0(lambda), or with ``smooth_log`` through log pi0(lambda), back-transformed
- (Storey and Tibshirani 2003), as R's ``smooth.spline`` fits it (found by
- probing; see ``cytepool.stats.smoothing``).
- - ``"bootstrap"``: pi0(lambda) at the lambda that minimizes a bootstrap estimate
- of its mean squared error (Storey, Taylor and Siegmund 2004), in closed form:
- the variance of a binomial count, W (1 - W / m) / (m (1 - lambda))^2 for the
- W = #{p >= lambda}, plus the square of pi0(lambda) minus a reference value,
- which qvalue takes as the 10% quantile (R's type 7) of the pi0(lambda) (found by
- probing); the smallest lambda where several minimize it.
-
- The estimate is capped at 1.
-
- Deliberate differences from qvalue 2.38.0:
- - qvalue stops with an error where no p-value reaches the largest lambda, with
- the smoother and the bootstrap alike (found by probing). Here a count of zero
- enters the smoother as it is, and has no variance in the bootstrap; the
- smoother of logarithms, which a count of zero leaves without a value,
- refuses it.
- - qvalue stops where the estimate is at most 0; here it is 0.
- - With more than 49 values of lambda, R's smoother keeps a knot at some of
- them only, a regression spline; here every lambda is a knot, as for fewer.
- Duplicated values of lambda are refused.
+:func:`adjust_p_values` adjusts p-values as R's ``p.adjust`` does (R Core Team 2025),
+with its conventions for missing values and for families declared larger than the
+p-values observed in them, so that adjusted p-values can be compared with R's
+directly. :func:`proportion_of_true_nulls` estimates pi0, the proportion of the tests
+whose null hypothesis is true, from their p-values, as qvalue's ``pi0est`` does
+(Storey and Tibshirani 2003), and returns a :class:`TrueNullProportion`; 1 - pi0,
+pi1, is the proportion of the tests with an effect.
+https://docs.nygen.io/CytePool/statistics/multiple-testing explains both, and
+https://docs.nygen.io/CytePool/statistics/multiple-testing#multiple-testing-limits where
+the estimate of pi0 differs from qvalue's on purpose.
+
+Matching qvalue:
+ :func:`proportion_of_true_nulls` matches qvalue 2.38.0's ``pi0est``, with its
+ defaults and options. Running qvalue settled the conventions that its
+ documentation leaves open:
+
+ - pi0(lambda) = #{p >= lambda} / (m (1 - lambda)) counts the p-values at least
+ lambda, where Storey (2002) counts those above it.
+ - With several values of lambda, ``"smoother"`` (qvalue's default) takes the
+ value at the largest lambda of a cubic smoothing spline with ``smooth_df``
+ equivalent degrees of freedom through pi0(lambda), or with ``smooth_log``
+ through log pi0(lambda), back-transformed (Storey and Tibshirani 2003), fitted
+ as R's ``smooth.spline`` fits it (``cytepool.stats.smoothing``).
+ - ``"bootstrap"`` takes pi0(lambda) at the lambda that minimizes a bootstrap
+ estimate of its mean squared error (Storey, Taylor and Siegmund 2004), in
+ closed form: the variance of a binomial count, W (1 - W / m) / (m (1 -
+ lambda))^2 for W = #{p >= lambda}, plus the square of pi0(lambda) minus the 10%
+ quantile (R's type 7) of the pi0(lambda); the smallest lambda where several
+ minimize it.
+ - The estimate is capped at 1.
References:
Holm S (1979). A simple sequentially rejective multiple test procedure. Scandinavian
@@ -98,7 +86,9 @@
_DEFAULT_LAMBDAS = np.append(0.05 + 0.05 * np.arange(18.0), 0.95)
_PI0_METHODS = ("smoother", "bootstrap")
_FEWEST_LAMBDAS = 4 # several lambdas must be at least this many, as for qvalue
-_BOOTSTRAP_QUANTILE = 0.1 # of the pi0(lambda), the reference of the bootstrap
+# The quantile of the pi0(lambda) that qvalue's bootstrap measures the bias from
+# (found by probing).
+_BOOTSTRAP_QUANTILE = 0.1
# Accepted method names (lower case) and the procedure each one selects.
_PROCEDURES = {
@@ -141,6 +131,8 @@ def adjust_p_values(
- ``"none"``: ``p_(i)``.
Every method takes O(m log m) time for m p-values, Hommel's included.
+ https://docs.nygen.io/CytePool/statistics/multiple-testing#error-rates explains
+ the error rates and when to use each method.
Args:
p_values: p-values in [0, 1]. NaN marks a missing value; it stays NaN and is
@@ -437,10 +429,19 @@ def proportion_of_true_nulls(
Storey's (2002) estimate pi0(lambda) = #{p >= lambda} / (m (1 - lambda)), at one
lambda or smoothed over several (Storey and Tibshirani 2003) or chosen by a
bootstrap estimate of its mean squared error (Storey, Taylor and Siegmund 2004),
- with the defaults and options of qvalue's ``pi0est``; the module description
- gives the method and the deliberate differences from qvalue. 1 - pi0 (``pi1``) of
- one study's p-values among the genes another study finds is the proportion of
- those genes with an effect in it (Storey and Tibshirani 2003).
+ with the defaults and options of qvalue's ``pi0est``. The module documentation
+ states how the smoother and the bootstrap follow qvalue, and
+ https://docs.nygen.io/CytePool/statistics/multiple-testing#proportion-of-true-nulls
+ explains the estimate. 1 - pi0 (``pi1``) of one study's p-values among the genes
+ another study finds is the proportion of those genes with an effect in it
+ (Storey and Tibshirani 2003).
+
+ Some inputs on which qvalue stops with an error give an estimate here. A lambda
+ that no p-value reaches has pi0(lambda) = 0, which the smoother fits like any
+ other value and the bootstrap takes to have no variance; only ``smooth_log``
+ refuses it, as the logarithm of 0 has no value. An estimate at most 0 is
+ returned as 0. Every lambda is a knot of the smoother, also with more than 49 of
+ them, where R's ``smooth.spline`` keeps knots at only some, a regression spline.
Args:
p_values: p-values in [0, 1]; NaN marks a missing one, which is left out, as
@@ -488,10 +489,14 @@ def proportion_of_true_nulls(
f"smooth_df must be above 1 and at most the number of lambdas "
f"({grid.size}), got {df}"
)
- # W(lambda), the p-values at least lambda.
+ # W(lambda), the p-values at least lambda, as qvalue counts them, where Storey
+ # (2002) counts those above lambda (found by probing).
counts = m - np.searchsorted(values, grid, side="left").astype(np.float64)
pi0_lambda = counts / (m * (1.0 - grid))
smooth = None
+ # qvalue stops with an error where no p-value reaches the largest lambda, with
+ # the smoother and the bootstrap alike (found by probing); a count of zero is
+ # used here as it is. qvalue's smoother is R's smooth.spline (found by probing).
if grid.size == 1:
estimate = float(pi0_lambda[0])
elif chosen == "smoother":
diff --git a/cytepool/stats/negative_binomial_models.py b/cytepool/stats/negative_binomial_models.py
index 16922a8..c6e454d 100644
--- a/cytepool/stats/negative_binomial_models.py
+++ b/cytepool/stats/negative_binomial_models.py
@@ -1,83 +1,23 @@
"""Negative binomial generalized linear models fitted to every gene, as edgeR's glmFit.
-For the count y_gi of gene g in sample i, the model is
-
- y_gi ~ NB(mu_gi, phi_gi), var(y_gi) = mu_gi + phi_gi mu_gi^2,
- log(mu_gi) = x_i' beta_g + o_gi,
-
-with x_i the design row of sample i, phi_gi a known dispersion and o_gi an offset,
-usually the log effective library size log(N_i f_i) (McCarthy, Chen and Smyth 2012).
-The coefficients beta_g, on the natural log scale, maximize the log-likelihood weighted
-by prior weights w_gi; equivalently they minimize the deviance (Dunn and Smyth 2018)
-
- D_g = sum_i w_gi d(y_gi, mu_gi, phi_gi),
- d(y, mu, phi) = 2 [y log(y / mu) - (y + 1 / phi) log((1 + phi y) / (1 + phi mu))],
-
-whose limit at phi = 0 is the Poisson unit deviance 2 [y log(y / mu) - (y - mu)]. The
-log-likelihood is concave in beta_g, so its maximum is unique when it exists.
-
-Each gene is fitted as edgeR's documentation describes ``glmFit``:
-
-- When the rows of the design take as many distinct values as it has columns, the
- design is a one-way layout in some parametrization: group means, groups against a
- reference, an intercept alone, or one coefficient per sample. Each group's mean is
- then fitted on its own (edgeR's ``mglmOneGroup`` and ``mglmOneWay``), starting from
- the closed form of the Poisson fit, log(sum w y / sum w exp(o)), which is the answer
- when the group's offsets and dispersions are equal, and solving the group's score
- equation by Newton's method kept inside a bracket, to the last bits. The coefficients
- solve the distinct rows of the design for the group means.
-- Other designs are fitted by Newton's method with Levenberg (1944) damping, from the
- fit of one group of all samples, edgeR's default start (the null model). The iteration
- works with an orthonormal basis Q of the design's column space, design = Q R, which
- McCarthy, Chen and Smyth (2012) describe for ``glmFit``, so that neither the scale of
- the columns nor how nearly collinear they are affects it; only the coefficients,
- R^(-1) times those of the basis, carry the conditioning of the design. Each step
- solves (X' W X) step = X' s, with s the scores and W the information of the
- observations, from the triangular factor of a Householder QR factorization (Golub and
- Van Loan 2013) of W^(1/2) X and with X' s formed directly; a least squares solution
- would carry the rounding of residuals W^(-1/2) s that can be far larger than their
- rows, as for zero counts with a large dispersion times mean. A column whose part
- orthogonal to the columns before it has a norm of at most 1e-11 times its own is
- aliased and not moved: a step along it would be rounding. Zero counts whose fitted
- values are below 1e-30 of the largest are left out of the fit, since the deviance no
- longer depends on them.
-- A step that would increase the deviance is damped, by adding a multiple of the
- diagonal of the information matrix (Marquardt's scaling, Marquardt 1963), until it
- does not. A step is shortened so that it raises no log fitted value, and lowers none
- of a positive count, by more than 2, where the quadratic model of the step can fail;
- lowers no fitted value of a zero count below a thousandth of that 1e-30 level, beyond
- which lowering it further would only enlarge the coefficients, and with them the
- rounding of every fitted value; and changes no log fitted value by more than 1e3. A
- whole step that lowers the deviance by more than 1.1 times what it predicts is doubled
- while the deviance keeps falling: so it does where fitted values of zero counts tend
- to zero, and the deviance falls exponentially. The weights of each gene are divided by
- a power of two that brings the largest into [0.5, 1), which changes no fitted value,
- so that the tolerances refer to weights of order one.
-- The steps are Newton's: they use the observed information where edgeR's
- documentation describes Fisher scoring, which uses the expected information. For
- this model the observed information, w mu (1 + phi y) / (1 + phi mu)^2 for each
- observation, is positive too, and it is the exact curvature of the log-likelihood.
- The expected information, w mu / (1 + phi mu), misjudges it by the factor
- (1 + phi y) / (1 + phi mu), so that with dispersions and weights that differ between
- observations Fisher scoring can stop far from the maximum, as edgeR's fits do on
- such genes (below). Both methods have the same maximum.
-- A fit stops, as converged, when a step would change no log fitted value by more than
- 1e-10 (leaving out zero counts below the 1e-30 level), when the decrease of the
- deviance that it predicts is below 1e-16 of the deviance, or when the deviance has
- fallen below 1e-16 of its starting value, as it does where every count can be fitted
- exactly in the limit. That last step is taken too unless it would raise the
- deviance by more than rounding: 1e-12 of it plus twice its rounding error, which the
- linear predictors' rounding sets, the same slack with which any step is accepted. A
- fit does not converge if its deviance is infinite, or if the deviance still falls
- along an aliased direction. A fit converges in about 4 to 8 steps when its maximum
- is finite.
+:func:`fit_negative_binomial_model` fits one negative binomial log-linear model to every
+gene at given dispersions, as edgeR's ``glmFit`` does (McCarthy, Chen and Smyth 2012),
+into a :class:`NegativeBinomialFit` with the maximum likelihood coefficients, the
+coefficients shrunk by a prior count (Phipson 2013), the fitted values and the
+deviances. :func:`negative_binomial_deviance` and
+:func:`negative_binomial_unit_deviance` compute the deviances exactly (Dunn and Smyth
+2018), which edgeR's ``nbinomDeviance`` and ``nbinomUnitDeviance`` approximate.
+https://docs.nygen.io/CytePool/statistics/count-models#negative-binomial-model gives
+the model, and
+https://docs.nygen.io/CytePool/statistics/count-models#count-models-limits how the fits
+differ from edgeR's.
Counts of zero:
- A group of a one-way layout whose counts are all zero has fitted values of zero. Its
- log mean, minus infinity, is represented by -1e8, as edgeR represents it (found by
- probing), so that the coefficients stay finite and reproduce the fitted values;
- coefficients that involve such a group are therefore near -1e8 or 1e8.
+ log mean, minus infinity, is represented by -1e8, as edgeR represents it, so that the
+ coefficients stay finite and reproduce the fitted values; coefficients that involve
+ such a group are therefore near -1e8 or 1e8.
- In other designs the maximum lies at infinity when the fitted values of some samples
with zero counts can tend to zero while the others stay, as when a group's counts
are all zero, or a count is the only one at one end of a covariate. Each step then
@@ -85,51 +25,22 @@
predicts falls below 1e-16 of the deviance: typically 10 to 20 steps, after which the
fitted values that tend to zero are below about 1e-15 of the largest and the deviance
is at its limit to rounding. The coefficients that reach them are large and depend on
- where the fit stops; the other fitted values and the deviance do not. edgeR stops
- earlier, ``glmFit`` with such fitted values near 1e-7 (found by probing).
+ where the fit stops; the other fitted values and the deviance do not.
- A gene without counts has fitted values and deviance of zero. Its coefficients are
those of the constant log mean -1e8 when a vector of ones lies in the column space of
the design, by the rank rule of R's qr() (an intercept, or group indicators that add
- up to one), as edgeR's one-way fit gives them; edgeR's Levenberg fit returns NA there
- (found by probing). Otherwise the iteration finds its maximum from coefficients of
- zero.
-
-Deliberate differences from edgeR, found by probing:
-
-- The deviance is exact. edgeR adds 1e-8 to each count and mean, so that the unit
- deviance of a zero count is up to about 5e-7 lower than it should be, and a small
- fraction of it for means below about 1e-8. Below a dispersion of 1e-4 it uses an
- approximation whose relative error grows with the dispersion times the mean: about
- 1e-6 at 1e-3, 1e-4 at 1e-2 and 1e-2 at 0.1; from about 1 it is wrong by factors, and
- from about 10 it gives zero counts no deviance. From a dispersion times the mean of
- 1e6 it uses the limit of the gamma distribution, which gives zero counts 1.6 to 2.6
- times their deviance.
-- The fits of other designs than one-way layouts converge to the maximum. On the
- validation's data, edgeR's ``glmFit`` stops with coefficients up to about 3e-4 from
- it (1e-3 with a dispersion of 2e-5), 1e-2 with dispersions and weights per
- observation, and 1.6 with a covariate such as a date next to an intercept, where
- fitted values that should tend to zero stay near 1e2. ``mglmLevenberg`` with a
- lower tolerance (its ``tol``) stops about 2e-7 from it, but 2e-3 with dispersions
- and weights per observation, 2e-6 with sparse counts, 1e-5 with a date or for counts
- near 1e8, and 2e-3 with a dispersion of 2e-5, where the errors of its deviance are
- largest. With dispersions and weights per observation, ``glmFit`` also reports for
- a few genes a deviance that is not that of its own fitted values.
-- Inputs that edgeR runs on without end, infinite dispersions or offsets, raise
- errors here.
-
-Known limits:
-
-- Where weights differ by many orders of magnitude between samples, the coefficients
- that only the samples of small weight determine are found to a relative accuracy of
- about 1e-16 times the factor between the weights (1e-6 for a factor of 1e10), as
- rounding in the scores of the other samples, which carry most of the information,
- swamps theirs. Beyond a factor of about 1e14 those coefficients are not determined,
- although the fit can report convergence. edgeR is less accurate still (found by
- probing).
-- Where a zero count's dispersion times mean is so large, above about 1e22, that its
- information is below rounding next to that of the other samples while its deviance
- still falls with its fitted value, its direction is aliased from the start and the
- fit is reported as not converged.
+ up to one), as edgeR's one-way fit gives them. Otherwise the iteration finds its
+ maximum from coefficients of zero.
+
+Matching edgeR:
+ The fits match edgeR 4.4.0's ``glmFit``: its documentation describes the method,
+ and running it on chosen inputs settled the conventions it leaves open. A design
+ whose rows take as many distinct values as it has columns, a one-way layout, is
+ fitted group by group, as ``mglmOneGroup`` and ``mglmOneWay`` fit it, to the last
+ bits; other designs are fitted by Newton's method with Levenberg (1944) damping,
+ from the fit of all samples as one group, edgeR's default start, in an orthonormal
+ basis of the design's columns, as McCarthy, Chen and Smyth (2012) describe for
+ ``glmFit``.
References:
McCarthy DJ, Chen Y, Smyth GK (2012). Differential expression analysis of
@@ -257,8 +168,9 @@ class NegativeBinomialFit:
coefficients: ``(n_features, n_coefficients)`` maximum likelihood estimates,
on the natural log scale: ``fitted_values`` is ``exp(design @
coefficients.T + offsets)``. edgeR reports them as
- ``unshrunk.coefficients``. A coefficient whose estimate is infinite is
- large and finite instead; see the module description.
+ ``unshrunk.coefficients``. A coefficient whose estimate is infinite, as
+ for a group whose counts are all zero, is large and finite instead (near
+ -1e8 or 1e8 in a one-way layout).
shrunk_coefficients: ``(n_features, n_coefficients)`` coefficients estimated
after adding ``prior_count`` to the counts as edgeR's ``predFC`` does
(Phipson 2013), which shrinks log fold changes towards zero and keeps them
@@ -327,12 +239,60 @@ def fit_negative_binomial_model(
"""Fit a negative binomial log-linear model to every gene, as edgeR's glmFit.
Every gene is fitted with the same design, with its own dispersions, offsets and
- weights; the module description gives the model and the methods. The dispersions
- are taken as known: give estimates, such as
- :func:`~cytepool.stats.quasi_likelihood.common_dispersion`, or a
- value from similar data (edgeR's User's Guide gives biological coefficients of
- variation of 0.4 for human samples, 0.1 for genetically identical model organisms
- and 0.01 for technical replicates; the dispersion is their square).
+ weights, by maximum likelihood: y_gi ~ NB(mu_gi, phi_gi), with
+ var(y_gi) = mu_gi + phi_gi mu_gi^2 and log(mu_gi) = x_i' beta_g + o_gi for gene g
+ in sample i, and the coefficients minimize the deviance
+ sum_i w_gi d(y_gi, mu_gi, phi_gi) of :func:`negative_binomial_deviance`. The
+ dispersions are taken as known: give estimates, such as
+ :func:`~cytepool.stats.quasi_likelihood.common_dispersion`, or a value from similar
+ data, the square of a biological coefficient of variation.
+ https://docs.nygen.io/CytePool/statistics/count-models#negative-binomial-model
+ explains the model and gives typical values.
+
+ Fits:
+ - A one-way layout fits each group's mean on its own, from the closed form of
+ the Poisson fit, log(sum w y / sum w exp(o)), which is the answer when the
+ group's offsets and dispersions are equal, by Newton's method kept inside a
+ bracket, to the last bits; the coefficients solve the distinct rows of the
+ design for the group means.
+ - Other designs are fitted by Newton's method from the fit of all samples as
+ one group. The iteration works with an orthonormal basis Q of the design's
+ column space, design = Q R, so that neither the scale of the columns nor how
+ nearly collinear they are affects it; only the coefficients, R^(-1) times
+ those of the basis, carry the conditioning of the design. Each step solves
+ (X' W X) step = X' s, with s the scores and W the observed information of the
+ observations, w mu (1 + phi y) / (1 + phi mu)^2, from the triangular factor of
+ a Householder QR factorization (Golub and Van Loan 2013) of W^(1/2) X. A
+ column whose part orthogonal to the columns before it has a norm of at most
+ 1e-11 times its own is aliased and not moved. Zero counts whose fitted values
+ are below 1e-30 of the largest are left out of the fit, since the deviance no
+ longer depends on them.
+ - A step that would increase the deviance is damped, by adding a multiple of
+ the diagonal of the information matrix (Marquardt 1963), until it does not.
+ A step is shortened so that it raises no log fitted value, and lowers none
+ of a positive count, by more than 2; lowers no fitted value of a zero count
+ below a thousandth of that 1e-30 level; and changes no log fitted value by
+ more than 1e3. A whole step that lowers the deviance by more than 1.1 times
+ what it predicts is doubled while the deviance keeps falling. The weights of
+ each gene are divided by a power of two that brings the largest into
+ [0.5, 1), which changes no fitted value.
+ - A fit stops, as converged, when a step would change no log fitted value by
+ more than 1e-10 (leaving out zero counts below the 1e-30 level), when the
+ decrease of the deviance that it predicts is below 1e-16 of the deviance,
+ or when the deviance has fallen below 1e-16 of its starting value. A fit
+ does not converge if its deviance is infinite, or if the deviance still
+ falls along an aliased direction. A fit converges in about 4 to 8 steps
+ when its maximum is finite.
+
+ Where weights differ by many orders of magnitude between samples, the
+ coefficients that only the samples of small weight determine are found to a
+ relative accuracy of about 1e-16 times the factor between the weights (1e-6 for a
+ factor of 1e10), as rounding in the scores of the other samples swamps theirs, and
+ beyond a factor of about 1e14 they are not determined, although the fit can report
+ convergence. Where a zero count's dispersion times mean is above about 1e22, its
+ information is below rounding next to that of the other samples while its
+ deviance still falls with its fitted value, and the fit is reported as not
+ converged.
Args:
counts: ``(n_samples, n_features)`` counts: finite and non-negative, integer or
@@ -379,7 +339,8 @@ def fit_negative_binomial_model(
ValueError: If shapes disagree, counts are negative or not finite, the design
is not finite, has random effects or is not of full column rank (the
message names the aliased columns), a dispersion is negative or not
- finite, an offset is not finite, a weight is not positive and finite, both
+ finite, an offset is not finite (edgeR's fit runs without end on an
+ infinite dispersion or offset), a weight is not positive and finite, both
offsets and library sizes or normalization factors are given, a library
size is zero, or ``prior_count`` or ``max_iterations`` is out of range.
"""
@@ -399,9 +360,7 @@ def fit_negative_binomial_model(
w = weight_matrix(weights, n_samples, n_features)
prior = non_negative_number(prior_count, "prior_count")
reject_masked(max_iterations, "max_iterations")
- limit = positive_integer(
- max_iterations, "max_iterations", expected="a positive integer"
- )
+ limit = positive_integer(max_iterations, "max_iterations")
layout = _layout(x)
counts_by_gene = np.ascontiguousarray(y.T)
@@ -453,8 +412,9 @@ def negative_binomial_unit_deviance(
If mu and phi are the true mean and dispersion, the unit deviance is
approximately chi-squared on one degree of freedom. edgeR's
- ``nbinomUnitDeviance`` computes the same quantity with an offset and
- approximations; see the module description. The arguments broadcast with numpy's
+ ``nbinomUnitDeviance`` computes the same quantity with 1e-8 added to the counts
+ and means, an approximation below a dispersion of 1e-4, and the gamma limit from
+ a dispersion times mean of 1e6. The arguments broadcast with numpy's
rules: with counts of shape ``(n_samples, n_features)``, one dispersion per gene
has shape ``(n_features,)``.
diff --git a/cytepool/stats/quasi_likelihood.py b/cytepool/stats/quasi_likelihood.py
index 90a62ad..586991c 100644
--- a/cytepool/stats/quasi_likelihood.py
+++ b/cytepool/stats/quasi_likelihood.py
@@ -1,129 +1,60 @@
-"""edgeR's quasi-likelihood F-tests: glmQLFit and glmQLFTest as edgeR 4 runs them.
-
-The quasi-likelihood model of Lund, Nettleton, McCarthy and Smyth (2012) gives each gene
-g a negative binomial model for its counts with a second, gene-wise dispersion:
-
- var(y_gi) = sigma_g^2 (mu_gi + phi mu_gi^2),
-
-with phi the negative binomial (NB) dispersion, shared by the genes, and sigma_g^2
-the quasi-dispersion. The quasi-dispersions are estimated from the deviances of the
-NB fits, moderated by empirical Bayes as limma moderates residual variances, and
-genes are tested with F-tests whose denominator is the posterior quasi-dispersion.
-:func:`fit_quasi_likelihood` and :func:`quasi_likelihood_f_test` follow edgeR 4.4's
-``glmQLFit`` and ``glmQLFTest`` with their defaults (``legacy = FALSE``), the method of
-Chen, Chen, Lun, Baldoni and Smyth (2025), as documented and, where the documentation
-is silent, as found by running edgeR 4.4.0 as a black box ("found by probing").
-
-The steps:
-
-1. Abundance. :func:`average_log2_cpm` is edgeR's ``aveLogCPM``: the log2 of the mean
- count per million of a gene, fitted as one group of samples by a NB model after a
- prior count of 2, scaled by library size, is added to every count (edgeR's
- ``addPriorCount``). ``glmQLFit`` computes it once, at the given NB dispersion, or
- at 0.05 when it estimates the dispersion (found by probing); it ranks the genes
- and is the covariate of both trends below. The tests report it at the NB
- dispersion.
-2. NB dispersion. Unless given, it is the Cox-Reid common dispersion
- (:func:`common_dispersion`, edgeR's ``estimateGLMCommonDisp``) of the most highly
- expressed genes: the ``ceiling(p G)`` of the G genes with the largest average log
- CPM, with ``p = chooseLowessSpan(G sqrt(n - k), small.n = 20, min.span = 0.02)``
- for n samples and k coefficients, as edgeR's help page says (the count of genes
- is a ceiling, found by probing).
-3. Fits. Every gene gets the NB fit of
- :func:`~cytepool.stats.negative_binomial_models.fit_negative_binomial_model` at
- the working dispersion phi / A, where A is the average quasi-dispersion below.
-4. Bias-adjusted deviances. The unit deviance d_i of an observation is approximated
- by a scaled chi-squared variable s_i chi^2(k_i) whose mean and variance are those
- of the unit deviance (Chen et al. 2025): s_i = V_i / (2 E_i) and k_i = 2 E_i^2 /
- V_i, with E_i and V_i from :func:`negative_binomial_unit_deviance_moments`. The
- adjusted unit deviance is d_i / s_i and its degrees of freedom are k_i (1 - h_i),
- with h_i the leverage of the observation. An observation of leverage 1, which the
- fit reproduces, has a residual deviance and degrees of freedom of zero, as in
- exact arithmetic, not the rounding of its fitted value. A gene's adjusted
- residual deviance and degrees of freedom are their sums over its samples, and its
- quasi-dispersion is their ratio, on degrees of freedom that are fractional and
- differ between genes.
- The moments are those of the divided counts (Baldoni et al. 2024): with
- quasi-dispersion A, var(y) = A (mu + (phi / A) mu^2), and y / A follows a NB
- distribution with mean mu / A and dispersion phi. E_i and V_i are taken at the
- mean mu_i / A and the dispersion phi, and d_i at the working dispersion (found by
- probing).
-5. Average quasi-dispersion (found by probing). A starts at 1 and is updated twice:
- each time the genes are fitted at phi / A and their quasi-dispersions formed as in
- step 4; the fourth roots of the quasi-dispersions of the genes with residual
- degrees of freedom are smoothed against the abundance by R's ``lowess`` with a
- span of 0.5 (three robustness iterations), and A becomes the fourth power of the
- 0.9 quantile of the smoothed values, or 1 if that is smaller. The fits of step 3
- use the final A, which is edgeR's ``average.ql.dispersion``: its help page says
- that it "scales the NB dispersion".
-6. Moderation. The quasi-dispersions are squeezed by limma's ``squeezeVar`` with its
- estimator for unequal degrees of freedom (``fitFDistUnequalDF1``, ``legacy =
- FALSE`` whatever the degrees of freedom; found by probing), through
- :func:`~cytepool.stats.empirical_bayes.squeeze_variances` with
- ``prior_estimator="likelihood"``: towards a trend in the abundance by default, and
- robustly when asked. ``winsor.tail.p`` plays no part, as in edgeR (found by
- probing).
-7. Tests. :func:`quasi_likelihood_f_test` fits the null model at the working
- dispersion, without the tested coefficients or with the tested contrasts set to
- zero, and divides the likelihood ratio statistic by its degrees of freedom r (the
- rank of the contrasts) and by the posterior quasi-dispersion. The F-statistic is
- referred to the F distribution on r and min(d0 + d_g, sum of n - k over the genes)
- degrees of freedom, with d0 the prior and d_g the adjusted residual degrees of
- freedom (found by probing). Log fold changes are the coefficients shrunk with a
- prior count of 0.125 (Phipson 2013), divided by log 2, and adjusted p-values are
- Benjamini and Hochberg's (1995), as edgeR's ``topTags`` gives them.
-
-Prior weights (found by probing). edgeR uses them in the fits as prior weights, and
-in the Cox-Reid likelihood as the number of independent counts each count averages
-(edgeR's ``adjustedProfileLik``). In the adjusted deviances it treats an observation
-of weight w as w technical replicates: its moments are taken at the mean w mu / A, its
-unit deviance at the dispersion w phi / A, and its leverage is that of the weight mu /
-(1 + w phi mu / A) rather than of the weight of the fit; its adjusted unit deviance is
-per unit of weight, so that the gene's adjusted deviance is the sum of the unit values
-times the weights, and its degrees of freedom the plain sum. This module follows edgeR
-in each. The weights are therefore not relative: multiplying all of them by a constant
-changes the quasi-dispersions and the F-statistics, in both.
-
-Deliberate differences from edgeR 4.4.0:
-
-- The mean and variance of the unit deviance are exact, to about 1e-14. edgeR
- approximates them (found by probing): the scale implied by its adjusted unit
- deviances is off by up to about 1.2e-3 at dispersions up to 0.2 and 1.3e-2 at a
- dispersion of 1, and the degrees of freedom by up to about 2e-4 and 4e-3. The
- adjusted deviances, quasi-dispersions and F-statistics inherit differences of up
- to about 3e-3.
-- The Cox-Reid dispersion is the maximum of the adjusted profile likelihood, located
- to about 1e-7 relative; edgeR's search stops within its tolerance of 1e-5. Where
- edgeR approximates the deviance, below a dispersion of 1e-4, its likelihood is
- noisy (found by probing), so that for counts close to Poisson the two estimates
- differ by more than that, both being near zero.
-- The fits converge to the maximum, where edgeR's Levenberg fits stop short of it by
- up to about 3e-4 in the coefficients (see
- :mod:`cytepool.stats.negative_binomial_models`), so that the log fold changes of
- designs that are not one-way layouts differ by up to about that. Fitted values
- that tend to zero, as in a group without counts, are zero in both for one-way
- layouts; otherwise they stop near 1e-8 in edgeR's fits and far lower here, and
- both give such observations the leverages of the limit. Either way they carry
- almost no degrees of freedom. A gene with no others, such as one counted in only
- one condition of a paired design, has the ratio of their almost-zero deviances
- and degrees of freedom as its quasi-dispersion, which therefore differs (0.053 in
- edgeR and 0.021 here, for such a gene in eight paired samples). It enters the
- average quasi-dispersion in both.
-- The robust prior tests only the genes with residual degrees of freedom for
- outliers, as :func:`~cytepool.stats.empirical_bayes.fit_f_distribution_likelihood`
- does; limma counts a gene without them, such as one without counts, as a zero
- variance, which then lowers the outlier probabilities of the others.
-- With offsets per observation, edgeR's ``aveLogCPM`` gives the first gene a value
- that does not depend on its offsets (found by probing: a copy of the gene in
- another row gets the value computed here), which then enters the ranking, the
- trends and the log CPM of that gene.
-
-Not implemented yet: edgeR's ``legacy = TRUE`` methods (the deviances of
-``glmQLFit`` in edgeR 3, with the degrees of freedom adjusted for groups without
-counts, and the Poisson bound of ``glmQLFTest``). This module follows edgeR's method
-for a matrix of counts. edgeR's method for a DGEList agrees with it, except when the
-DGEList holds a trended dispersion from ``estimateDisp``: it then takes one
-dispersion from that trend in place of the Cox-Reid estimate (found by probing).
+"""edgeR's quasi-likelihood F-tests: glmQLFit and glmQLFTest as edgeR 4.4 runs them.
+
+:func:`fit_quasi_likelihood` fits negative binomial models of every gene with empirical
+Bayes quasi-dispersions (Lund, Nettleton, McCarthy and Smyth 2012), as edgeR's
+``glmQLFit`` does with its defaults (``legacy = FALSE``; Chen, Chen, Lun, Baldoni and
+Smyth 2025), into a :class:`QuasiLikelihoodFit`, and :func:`quasi_likelihood_f_test`
+tests coefficients or contrasts by F-tests, as ``glmQLFTest`` does, into a
+:class:`QuasiLikelihoodTest` with the columns of ``topTags``. :func:`common_dispersion`
+gives the Cox-Reid common dispersion (``estimateGLMCommonDisp``),
+:func:`average_log2_cpm` the average log2 counts per million (``aveLogCPM``), and
+:func:`negative_binomial_unit_deviance_moments` the exact mean and variance of the unit
+deviance in a :class:`UnitDevianceMoments`.
+https://docs.nygen.io/CytePool/statistics/count-models#quasi-likelihood gives the model
+and its steps, and
+https://docs.nygen.io/CytePool/statistics/count-models#count-models-limits how the
+results differ from edgeR's.
+
+Prior weights:
+ edgeR uses them in the fits as prior weights, and in the Cox-Reid likelihood as the
+ number of independent counts each count averages (edgeR's ``adjustedProfileLik``).
+ In the adjusted deviances it treats an observation of weight w as w technical
+ replicates: its moments are taken at the mean w mu / A, its unit deviance at the
+ dispersion w phi / A, and its leverage is that of the weight mu /
+ (1 + w phi mu / A) rather than of the weight of the fit; its adjusted unit deviance
+ is per unit of weight, so that the gene's adjusted deviance is the sum of the unit
+ values times the weights, and its degrees of freedom the plain sum. This module
+ follows edgeR in each. The weights are therefore not relative: multiplying all of
+ them by a constant changes the quasi-dispersions and the F-statistics, in both.
+
+Matching edgeR:
+ The results match edgeR 4.4.0, with limma 3.62.1, run on a matrix of counts: their
+ documentation gives the method, and running them on chosen inputs settled the
+ conventions it leaves open, which comments in the code record.
+
+ - ``glmQLFit`` computes the average log CPM once, at the given dispersion or at 0.05
+ when it estimates the dispersion; it ranks the genes and is the covariate of both
+ trends. The tests report it at the negative binomial dispersion phi.
+ - The dispersion is estimated from the ``ceiling(p G)`` of the G genes with the
+ largest average log CPM, with ``p = chooseLowessSpan(G sqrt(n - k), small.n = 20,
+ min.span = 0.02)`` for n samples and k coefficients, as edgeR's help page says.
+ - The moments of a unit deviance are those of the counts divided by the average
+ quasi-dispersion A, at the mean mu / A and the dispersion phi (Baldoni, Chen and
+ Smyth 2024), and the unit deviance is taken at the working dispersion phi / A.
+ - A starts at 1 and is updated twice, each time from the quasi-dispersions of fits
+ at phi / A: the fourth roots of those of the genes with residual degrees of
+ freedom are smoothed against the abundance by R's ``lowess`` with a span of 0.5
+ (three robustness iterations), and A becomes the fourth power of the 0.9 quantile
+ of the smoothed values, or 1 if that is smaller.
+ - The quasi-dispersions are squeezed by limma's ``squeezeVar`` with its estimator
+ for unequal degrees of freedom (``fitFDistUnequalDF1``, ``legacy = FALSE``),
+ whatever the degrees of freedom, and ``winsor.tail.p`` plays no part.
+ - The F-statistic's denominator degrees of freedom are min(d0 + d_g, the sum of
+ n - k over the genes), with d0 the prior's and d_g the gene's adjusted residual
+ degrees of freedom.
+ - edgeR's method for a DGEList agrees with its method for a matrix of counts, but
+ for a DGEList that holds a trended dispersion from ``estimateDisp``: it then takes
+ one dispersion from that trend in place of the Cox-Reid estimate.
References:
Lund SP, Nettleton D, McCarthy DJ, Smyth GK (2012). Detecting differential
@@ -354,8 +285,7 @@ def negative_binomial_unit_deviance_moments(
at 64, by Gauss-Legendre panels on the log of the count. The moments agree to
about 1e-14 relative with sums at 30 digits for means up to 300, with the gamma
limit of large means and with the large-mean expansion of the Poisson moments.
- edgeR's approximations of them differ by up to about 1e-2 (see the module
- description).
+ edgeR approximates them.
Args:
means: Means, finite and non-negative; at most 1e100.
@@ -893,6 +823,9 @@ def _adjusted_deviances(
mu = np.asarray(fit.fitted_values)
phi = np.broadcast_to(dispersion, mu.shape)
working = phi / average
+ # The moments are those of the counts divided by the average quasi-dispersion, at
+ # the mean mu / A and the dispersion phi, and the unit deviance is taken at the
+ # working dispersion phi / A (found by probing).
means = weights * mu / average
scale, df = _chi_squared_match(means, dispersion)
deviance = _unit_deviances_at(counts, mu, weights * working)
@@ -1077,22 +1010,24 @@ def common_dispersion(
W_g the expected information of the fitted values, w mu / (1 + phi mu) with prior
weights w. With prior weights each count is the average of w independent negative
binomial counts, as for edgeR's ``adjustedProfileLik``: its log-likelihood is
- that of w y under NB(w mu, phi / w). The adjustment, which a REML criterion
- parallels, removes most of the downward bias of the maximum likelihood estimate
- (Robinson and Smyth 2008). Observations whose fitted values tend to zero, below
- 1e-12 of the gene's largest, carry no information and are left out of the
- determinant, whose remaining factor does not depend on phi.
+ that of w y under NB(w mu, phi / w). Observations whose fitted values tend to
+ zero, below 1e-12 of the gene's largest, carry no information and are left out of
+ the determinant, whose remaining factor does not depend on phi.
As edgeR does (its help pages): genes with fewer than ``min_row_sum`` counts in all
are left out; when more than ``subset`` genes remain, a systematic subset of them,
evenly spaced in average log CPM, is used (``systematicSubset``: every s-th gene in
- increasing average log CPM, s = floor(G / subset), from gene floor(s / 2) + 1; found
- by probing), which keeps all G genes while G < 2 ``subset`` and from ``subset`` to
+ increasing average log CPM, s = floor(G / subset), from gene floor(s / 2) + 1),
+ which keeps all G genes while G < 2 ``subset`` and from ``subset`` to
about 1.5 ``subset`` of them otherwise; and the maximum is searched over
``interval``. The search, Brent's (1973) method as R's ``optimize``, locates the
maximum to about 1e-7 relative, as closely as the rounding of the likelihood allows;
- edgeR's stops within its tolerance of 1e-5. A maximum at an end of the interval is
- returned with a warning.
+ edgeR's stops within its tolerance of 1e-5. Below a dispersion of 1e-4, where edgeR
+ approximates the deviance, its likelihood is noisy, so that for counts close to
+ Poisson the two estimates differ by more than that, both being near zero. A
+ maximum at an end of the interval is returned with a warning.
+ https://docs.nygen.io/CytePool/statistics/count-models#cox-reid explains the
+ adjustment.
Args:
counts: ``(n_samples, n_features)`` counts, finite and non-negative.
@@ -1130,11 +1065,9 @@ def common_dispersion(
low, high = _search_interval(interval)
least = non_negative_number(min_row_sum, "min_row_sum")
reject_masked(subset, "subset")
- most = positive_integer(subset, "subset", expected="a positive integer")
+ most = positive_integer(subset, "subset")
reject_masked(max_iterations, "max_iterations")
- limit = positive_integer(
- max_iterations, "max_iterations", expected="a positive integer"
- )
+ limit = positive_integer(max_iterations, "max_iterations")
log_sizes = offset_matrix(offsets, library_sizes, norm_factors, y)
w = weight_matrix(weights, n_samples, n_features)
return _common_dispersion(
@@ -1278,7 +1211,8 @@ def _systematic_subset(n: int, order_by: FloatArray) -> npt.NDArray[np.int64]:
def _top_genes(abundance: FloatArray, proportion: float) -> npt.NDArray[np.int64]:
- """The ceiling(p G) genes with the largest average log CPM, in index order."""
+ """The ceiling(p G) genes with the largest average log CPM, in index order (a
+ ceiling, found by probing)."""
n_top = min(abundance.size, math.ceil(proportion * abundance.size))
order = np.argsort(-abundance, kind="stable")
return np.sort(order[:n_top]).astype(np.int64)
@@ -1425,16 +1359,42 @@ def fit_quasi_likelihood(
"""Fit negative binomial models with empirical Bayes quasi-dispersions, as edgeR's
glmQLFit.
- edgeR 4's default method (``legacy = FALSE``), described in the module
- description: the negative binomial dispersion phi is given or estimated as the
- Cox-Reid common dispersion of the most highly expressed genes; every gene is
- fitted at the working dispersion phi / A, with A the average quasi-dispersion;
- its quasi-dispersion is its bias-adjusted deviance over its adjusted residual
- degrees of freedom; and the quasi-dispersions are squeezed towards a prior that
- follows the average log CPM, by limma's estimator for unequal degrees of
- freedom. edgeR's help page says that a dispersion that is given should come from
- a global model, common or trended, not from each gene on its own: the
- quasi-dispersions carry the variation between genes.
+ edgeR 4.4's default method (``legacy = FALSE``; Chen et al. 2025), in which
+ var(y_gi) = sigma_g^2 (mu_gi + phi mu_gi^2) for a negative binomial dispersion phi
+ shared by the genes and a quasi-dispersion sigma_g^2 of each gene:
+
+ 1. The average log CPM of every gene (:func:`average_log2_cpm`), at the given
+ dispersion or at 0.05 when it is estimated, ranks the genes and is the covariate
+ of the trends.
+ 2. phi is given, or estimated as the Cox-Reid common dispersion
+ (:func:`common_dispersion`) of the ``top_proportion`` of the genes with the
+ largest average log CPM.
+ 3. Every gene is fitted at the working dispersion phi / A
+ (:func:`~cytepool.stats.negative_binomial_models.fit_negative_binomial_model`),
+ where A, the average quasi-dispersion, starts at 1 and is updated twice from the
+ quasi-dispersions of such fits, as the 0.9 quantile of their smoothed fourth
+ roots, raised to the fourth power and at least 1.
+ 4. Each unit deviance is approximated by a scaled chi-squared variable s chi^2(k)
+ with its exact mean and variance (:func:`negative_binomial_unit_deviance_moments`);
+ divided by s, it has k (1 - h) degrees of freedom for its leverage h. A gene's
+ quasi-dispersion is the sum of these adjusted unit deviances over the sum of
+ their degrees of freedom.
+ 5. The quasi-dispersions are squeezed towards a prior that follows the average log
+ CPM (:func:`~cytepool.stats.empirical_bayes.squeeze_variances`), by limma's
+ estimator for unequal degrees of freedom.
+
+ edgeR's help page says that a dispersion that is given should come from a global
+ model, common or trended, not from each gene on its own: the quasi-dispersions
+ carry the variation between genes.
+
+ A gene whose counts are fitted exactly but for zero counts whose fitted values tend
+ to zero, such as one counted in only one condition of a paired design, has as its
+ quasi-dispersion the ratio of their almost-zero adjusted deviances and degrees of
+ freedom. The ratio depends on how far the fit lowered those fitted values, so it
+ differs from edgeR's, whose fits stop higher, and it enters the average
+ quasi-dispersion in both.
+ https://docs.nygen.io/CytePool/statistics/count-models#quasi-likelihood explains
+ the model.
Args:
counts: ``(n_samples, n_features)`` counts, finite and non-negative, integer or
@@ -1456,13 +1416,19 @@ def fit_quasi_likelihood(
normalization factors: one value, one per sample, or one per
observation.
weights: Prior weights, positive and finite: one value, one per sample, or one
- per observation; see the module description for how edgeR uses them.
+ per observation. As in edgeR, an observation of weight w counts as w
+ technical replicates in the adjusted deviances, so the weights are not
+ relative: multiplying all of them by a constant changes the results.
trend: Whether the prior quasi-dispersion follows the average log CPM
(edgeR's ``abundance.trend``), or ``(n_features,)`` covariate values for
it to follow instead (``covariate.trend``).
robust: Whether to estimate the prior robustly, so that genes whose
quasi-dispersions are outliers count less (Phipson et al. 2016), as
- edgeR's User's Guide recommends.
+ edgeR's User's Guide recommends. Only the genes with residual degrees
+ of freedom are tested as outliers, as by
+ :func:`~cytepool.stats.empirical_bayes.fit_f_distribution_likelihood`;
+ limma would test a gene without them, such as a gene without counts,
+ as a zero variance.
prior_count: Average count added to each observation for the shrunk
coefficients, whose differences are the log fold changes of the tests;
at least 0.
@@ -1508,12 +1474,10 @@ def fit_quasi_likelihood(
"leaves no residual degrees of freedom to estimate quasi-dispersions"
)
reject_masked(robust, "robust")
- checked_flag(robust, "robust", expected="a bool")
+ checked_flag(robust, "robust")
prior = non_negative_number(prior_count, "prior_count")
reject_masked(max_iterations, "max_iterations")
- limit = positive_integer(
- max_iterations, "max_iterations", expected="a positive integer"
- )
+ limit = positive_integer(max_iterations, "max_iterations")
log_sizes = offset_matrix(offsets, library_sizes, norm_factors, y)
w = weight_matrix(weights, n_samples, n_features)
covariate = _trend_values(trend, n_features)
@@ -1602,6 +1566,9 @@ def fit_quasi_likelihood(
quasi = np.where(df > 0.0, adjusted.deviance / df, np.nan)
_check_moderation(quasi, df)
trend_values = abundance if covariate is True else covariate
+ # limma's estimator for unequal degrees of freedom whatever the degrees of
+ # freedom (fitFDistUnequalDF1, legacy = FALSE); winsor.tail.p plays no part
+ # (found by probing).
squeezed = squeeze_variances(
quasi,
df,
@@ -1745,8 +1712,10 @@ def quasi_likelihood_f_test(
full-rank parametrisation of the same null model gives the same deviance). The
likelihood ratio statistic, the null deviance less the full one, over the rank r
of the contrasts and the posterior quasi-dispersion, is referred to the F
- distribution on r and ``df_total`` degrees of freedom (see the module
- description). Several coefficients or contrasts are tested together, as edgeR's
+ distribution on r and ``df_total`` degrees of freedom, the prior's plus the gene's
+ adjusted residual degrees of freedom, at most the residual degrees of freedom of
+ all the genes together, as in edgeR. Several coefficients or contrasts are tested
+ together, as edgeR's
matrix of contrasts is; test each named contrast in its own call for a table per
contrast. A gene without counts gets an F-statistic of 0 and a p-value of 1 (NaN
when the fit has at most two genes, whose quasi-dispersions are not moderated).
@@ -1817,6 +1786,8 @@ def quasi_likelihood_f_test(
with np.errstate(divide="ignore", invalid="ignore"):
f_statistic = likelihood_ratio / rank / posterior
n_samples, n_features = model.fitted_values.shape
+ # The denominator degrees of freedom are capped at the residual degrees of freedom
+ # of all the genes together (found by probing).
cap = float(n_features * (n_samples - n_coefficients))
df_total = np.minimum(
np.asarray(fit.df_prior) + np.asarray(fit.df_residual_adjusted), cap
@@ -1919,11 +1890,12 @@ def _trend_values(trend: Any, n_features: int) -> Any:
values = unmasked_array(trend, "trend", missing="NaN")
if values.dtype.kind not in "iuf":
raise TypeError(
- f"trend must be a bool or real covariate values, got dtype {values.dtype}"
+ "trend must be True, False or real covariate values, got dtype "
+ f"{values.dtype}"
)
if values.shape != (n_features,):
raise ValueError(
- "trend must be a bool or one covariate value per feature "
+ "trend must be True, False or one covariate value per feature "
f"({n_features},), got shape {values.shape}"
)
return np.array(values, dtype=np.float64)
diff --git a/cytepool/stats/sample_weights.py b/cytepool/stats/sample_weights.py
index ea76527..817ff99 100644
--- a/cytepool/stats/sample_weights.py
+++ b/cytepool/stats/sample_weights.py
@@ -1,9 +1,12 @@
"""Sample quality weights from a heteroscedastic linear model, as in limma's arrayWeights.
-Samples differ in quality: pooled from fewer cells, sequenced less deeply, or taken
-from an outlying donor, some follow the linear model less closely than others. Ritchie
-et al. (2006) estimate a weight for each sample from the residuals of every gene, and
-Liu et al. (2015) extend the model to groups of samples and combine it with voom.
+:func:`sample_quality_weights` estimates a weight for each sample, or each group of
+samples, from how closely its values follow the linear model across genes, as limma's
+``arrayWeights`` does: Ritchie et al. (2006) estimate a weight for each sample from
+the residuals of every gene, and Liu et al. (2015) extend the model to groups of
+samples and combine it with voom.
+https://docs.nygen.io/CytePool/statistics/limma-voom#sample-quality-weights explains
+when to use them.
For gene g and sample j, with design rows x_j and prior weights w_gj (one when none
are given), the model is
@@ -12,11 +15,9 @@
with the log variance factors u = Z gamma shared by every gene. Z is an orthonormal
basis of the columns of the variance design projected off the constant vector, so u
-sums to zero and only the span of the variance design matters (found by probing: limma
-gives the same weights for any parametrisation of it, with or without an intercept).
-The default variance design gives each sample a variance of its own, and
-``variance_groups`` one per group. The sample quality weights are exp(-u_j), whose
-product is one.
+sums to zero and only the span of the variance design matters. The default variance
+design gives each sample a variance of its own, and ``variance_groups`` one per
+group. The sample quality weights are exp(-u_j), whose product is one.
gamma is estimated by residual maximum likelihood (REML) with the gene variances
sigma_g^2 profiled out. For a weighted least squares fit of gene g at weights
@@ -30,13 +31,12 @@
where V_g has elements (delta_jk - h_g,jk)^2, h_g,jk being those of the hat matrix
(Smyth 2002). The weights are squeezed toward equality by a prior equivalent to
``prior_n`` genes at equal weights: it adds prior_n Z'(exp(-u) - 1) to the score and
-prior_n Z'Z, the identity, to the information (found by probing). Two algorithms:
+prior_n Z'Z, the identity, to the information. Two algorithms:
- ``"reml"``: Fisher scoring from equal weights, gamma <- gamma + d with the step
d = I^-1 U, U and I summed over the genes and the prior, until the criterion
d'U / ((G + prior_n) q) is below ``tolerance``, for q columns of Z and G genes;
- where the criterion rises instead, the iterations stop before that step (found by
- probing).
+ where the criterion rises instead, the iterations stop before that step.
- ``"gene_by_gene"``: one pass through the genes in order (Ritchie et al. 2006). Each
gene adds its information, with V_g replaced by the diagonal matrix of its row sums,
diag(1 - h_g), to an information matrix that starts at prior_n Z'Z, and moves gamma
@@ -45,44 +45,48 @@
- ``"auto"``: REML without prior weights or missing values, and gene by gene
otherwise, as limma chooses.
-Results match limma 3.62.1. Conventions that its documentation leaves open, found by
-probing (running limma as a black box):
-
-- Gene by gene, a gene is skipped where it has fewer than two residual degrees of
- freedom, or a residual variance below 1e-15 at the current weights. Its samples are
- those with an observed value and a positive weight.
-- REML uses only the genes with every value observed and every prior weight positive.
- Without prior weights, it leaves out the genes whose residual variance at equal
- weights is below 1e-15. With prior weights, every gene adds its information, a gene
- whose residual variance at the current weights is below 1e-15 adds no score, and the
- iterations stop when d'U / ((G + prior_n)^2 q) is below ``tolerance``, going on
- where the criterion rises.
-- Every weight is one when there are fewer than two genes, when the design leaves
- fewer than two residual degrees of freedom, or, for REML without prior weights, when
- fewer than two genes have a positive residual variance; CytePool warns.
-
-REML stops where limma stops, so that the weights are limma's. With the default
-``tolerance`` they are then within about half a percent of the REML estimate
-without prior weights, and up to about 15% from it with prior weights, where the
-criterion falls below the tolerance after one or two iterations (as with the voom
-weights of :func:`~cytepool.stats.voom.voom_with_quality_weights`), or where the
-criterion rises first. A smaller ``tolerance`` converges further where the iterations
-converge. A step that would give weights that are not finite is not taken: REML stops
-before it, with a warning.
-
-Deliberate deviations, where limma returns values without meaning or fails:
-
-- ``variance_design`` and ``variance_groups`` together are an error; limma uses
- ``var.group`` and ignores ``var.design``.
-- ``prior_n`` must be at least zero; limma accepts a negative prior.
-- A design that is not of full column rank is an error, as in
- :func:`~cytepool.stats.linear_models.fit_linear_model`; limma drops aliased columns.
-- REML with fewer than two complete genes gives weights of one, with a warning; limma
- fails when one is left.
-- An information matrix that is singular is an error: some sample carries no
- information about its variance, such as the only sample of a group of the design,
- and ``prior_n`` is zero or too small to make up for it. limma's REML returns weights
- that rounding error decides.
+Matching limma:
+ The weights match limma 3.62.1, whose documentation leaves these conventions
+ open; running limma on chosen inputs settled them, together with the prior's
+ terms and the stopping rule above.
+
+ - Gene by gene, a gene is skipped where it has fewer than two residual degrees of
+ freedom, or a residual variance below 1e-15 at the current weights. Its samples
+ are those with an observed value and a positive weight.
+ - REML uses only the genes with every value observed and every prior weight
+ positive. Without prior weights, it leaves out the genes whose residual
+ variance at equal weights is below 1e-15. With prior weights, every gene adds
+ its information, a gene whose residual variance at the current weights is
+ below 1e-15 adds no score, and the iterations stop when
+ d'U / ((G + prior_n)^2 q) is below ``tolerance``, going on where the criterion
+ rises.
+ - Every weight is one when there are fewer than two genes, when the design leaves
+ fewer than two residual degrees of freedom, or, for REML without prior weights,
+ when fewer than two genes have a positive residual variance; CytePool warns.
+
+Where REML stops:
+ REML stops where limma stops, so that the weights are limma's. With the default
+ ``tolerance`` they are then within about half a percent of the REML estimate
+ without prior weights, and up to about 15% from it with prior weights, where the
+ criterion falls below the tolerance after one or two iterations (as with the voom
+ weights of :func:`~cytepool.stats.voom.voom_with_quality_weights`), or where the
+ criterion rises first. A smaller ``tolerance`` converges further where the
+ iterations converge. A step that would give weights that are not finite is not
+ taken: REML stops before it, with a warning.
+
+Differences from limma:
+ - ``variance_design`` and ``variance_groups`` together are an error; limma uses
+ ``var.group`` and ignores ``var.design``.
+ - ``prior_n`` must be at least zero; limma accepts a negative prior.
+ - A design that is not of full column rank is an error, as in
+ :func:`~cytepool.stats.linear_models.fit_linear_model`; limma drops aliased
+ columns.
+ - REML with fewer than two complete genes gives weights of one, with a warning;
+ limma fails when one is left.
+ - An information matrix that is singular is an error: some sample carries no
+ information about its variance, such as the only sample of a group of the
+ design, and ``prior_n`` is zero or too small to make up for it. limma's REML
+ returns weights that rounding error decides.
References:
Ritchie ME, Diyagama D, Neilson J, van Laar R, Dobrovic A, Holloway A, Smyth GK
@@ -126,7 +130,7 @@
unmasked_array,
)
from cytepool._options import (
- number_at_least,
+ non_negative_number,
positive_finite_number,
positive_integer,
)
@@ -187,7 +191,10 @@ def sample_quality_weights(
docstring gives the model, the algorithms and the conventions. Samples whose
values have larger residuals across genes get lower weights. Multiply them into
the precision weights of a fit, as
- :func:`~cytepool.stats.voom.voom_with_quality_weights` does for voom.
+ :func:`~cytepool.stats.voom.voom_with_quality_weights` does for voom. The
+ gene-by-gene algorithm takes longer the more samples there are: on one thread of
+ an Intel Core Ultra 7 265U, 20,000 genes took about 0.25 s with 40 samples and
+ 2.5 s with 100.
Args:
y: ``(n_samples, n_features)`` log-expression values, such as the log2
@@ -405,6 +412,8 @@ def _reml_scoring(
responses, weight_rows, x, log_variances, RANK_TOLERANCE
)
information = information + np.tril(information, -1).T
+ # The prior adds prior_n Z'(exp(-u) - 1) to the score and prior_n Z'Z to
+ # the information (found by probing).
score = basis.T @ (score + model.prior_n * (sample_weights - 1.0))
information = basis.T @ information @ basis
information[np.diag_indices(n_parameters)] += model.prior_n
@@ -555,6 +564,8 @@ def _variance_model(
# The constant column first: R's rank rule then drops any column that only adds
# a constant, and the rest of the basis is orthogonal to it.
space = column_space(np.column_stack([np.ones(n_samples), matrix]))
+ # Only the span of the variance design matters: limma gives the same weights
+ # for any parametrisation of it, with or without an intercept (found by probing).
basis = np.ascontiguousarray(space.basis[:, 1:])
if basis.shape[1] == 0:
raise ValueError(
@@ -566,7 +577,7 @@ def _variance_model(
raise ValueError(f"method must be one of {listed}, got {method!r}")
return _VarianceModel(
basis=basis,
- prior_n=number_at_least(prior_n, "prior_n", 0.0),
+ prior_n=non_negative_number(prior_n, "prior_n"),
method=method,
max_iterations=positive_integer(max_iterations, "max_iterations"),
tolerance=positive_finite_number(tolerance, "tolerance"),
diff --git a/cytepool/stats/smoothing.py b/cytepool/stats/smoothing.py
index 4f87ff9..62912cb 100644
--- a/cytepool/stats/smoothing.py
+++ b/cytepool/stats/smoothing.py
@@ -20,12 +20,9 @@
its value.
:func:`choose_lowess_span` is limma's rule for the span of a LOWESS trend
-through n points (limma's ``chooseLowessSpan``): 1 for n up to ``small_n``
-points, and min_span + (1 - min_span) (small_n / n)^power for larger n. With
-limma's settings (``small_n`` = 50, ``min_span`` = 0.3, ``power`` = 1/3), the
-part of the span above 0.3 falls in proportion to n^(-1/3), as the width of a
-histogram's bins does in the rules of Scott (1979) and Freedman and Diaconis
-(1981): the span is 1 at 50 points, 0.65 at 400 and 0.395 at 20,000.
+through n points (limma's ``chooseLowessSpan``), with which
+:func:`~cytepool.stats.voom.voom` chooses its span
+(https://docs.nygen.io/CytePool/statistics/limma-voom#voom-span).
:func:`natural_spline_basis` returns a basis of the natural cubic splines
with given interior and boundary knots: cubic between the knots, twice
@@ -46,15 +43,8 @@
de Boor C (1978). A Practical Guide to Splines. Springer, New York.
doi:10.1007/978-1-4612-6333-3
- Freedman D, Diaconis P (1981). On the histogram as a density estimator: L2 theory.
- Zeitschrift fuer Wahrscheinlichkeitstheorie und verwandte Gebiete 57, 453-476.
- doi:10.1007/BF01025868
-
Hastie TJ (1992). Generalized additive models. Chapter 7 of Statistical Models in S,
eds Chambers JM and Hastie TJ. Wadsworth & Brooks/Cole, Pacific Grove.
-
- Scott DW (1979). On optimal and data-based histograms. Biometrika 66, 605-610.
- doi:10.1093/biomet/66.3.605
"""
import math
@@ -324,7 +314,9 @@ def natural_spline_basis(
"x must not contain infinite values; NaN marks a missing value"
)
if not isinstance(intercept, (bool, np.bool_)):
- raise TypeError(f"intercept must be a bool, got {type(intercept).__name__}")
+ raise TypeError(
+ f"intercept must be True or False, got {type(intercept).__name__}"
+ )
with_intercept = bool(intercept)
if df is not None:
if isinstance(df, (bool, np.bool_)) or not isinstance(df, numbers.Integral):
diff --git a/cytepool/stats/special.py b/cytepool/stats/special.py
index 02a12bc..bedd341 100644
--- a/cytepool/stats/special.py
+++ b/cytepool/stats/special.py
@@ -11,8 +11,8 @@
(strings, complex numbers, objects) raise ``TypeError``, and so do numpy masked arrays,
whose mask numpy would drop: NaN marks a missing value instead.
-The compiled scalar routines behind the public functions are named ``_scalar``
-and can be called from other compiled code.
+Each public function calls a compiled scalar routine named ``_scalar``,
+which other compiled code can call too.
Methods:
- ``lgamma`` is the C library's ``lgamma``.
diff --git a/cytepool/stats/sumrank.py b/cytepool/stats/sumrank.py
index 3a6f6be..31d0960 100644
--- a/cytepool/stats/sumrank.py
+++ b/cytepool/stats/sumrank.py
@@ -1,122 +1,32 @@
"""SumRank: genes whose differential expression ranks high in study after study.
-Most genes that one single-cell study finds differentially expressed are not found by
-the next (Nakatsuka et al. 2025). SumRank asks instead whether a gene ranks high in
-every study: each study ranks its genes by their signed evidence of change, and a
-gene's ranks are summed over the studies, so that a gene ranked high everywhere has a
-small sum and a gene found by one study alone does not, however small its p-value
-there. :func:`fit_sumrank` computes the statistic and its p-values from a signed
-statistic of every gene in every study; :func:`cytepool.sumrank` does it for the
-studies' differential-expression results of one group (cell type) and contrast, with
-p-values from permutations that re-run each study's analysis on its samples with
-their labels permuted.
-
-The statistic, as the paper defines it and its authors' code (Apache-2.0) computes it:
- 1. Each study ranks the genes by their signed statistic, from 1 for the largest
- (the gene most up); tied genes share the average of their ranks, as R's
- ``rank``, which the authors' code calls, gives them. Results of
- differential expression give the signed -log10 p-value: -log10(p_value),
- negative where the log2 fold change is negative, as the paper ranks DESeq2's
- results.
- 2. A gene of rank r among G genes has the normalized rank u = (r - 1) / (G - 1):
- 0 for the gene most up and 1 for the gene most down.
- 3. For each gene, the m smallest of its normalized ranks in the k studies are
- summed: those of the studies that rank it best, which leaves out studies that
- rank it low for reasons of their own. m is given (``n_studies_summed``), or
- m = floor(p k), at least 1, for a proportion p of the studies
- (``proportion_of_studies``), the product rounded to 9 decimals first, so that
- 0.57 of 100 studies is 57; by default every study is summed (m = k). The paper
- sums every study when there are few (its 3 to 6 studies of Parkinson's,
- Huntington's disease and schizophrenia) and chose p by prediction in held-out
- studies when there are many (0.65 of 17 studies of Alzheimer's disease, 0.55
- of 16 of COVID-19); :func:`~cytepool.held_out_prediction` gives that
- prediction for a chosen m or p.
- 4. A sum above m / 2, the mean of a sum of m uniform ranks, is set to m / 2, so
- that genes without consistent evidence share one value.
-
- The result is the statistic of genes up (``up_statistic``): small for a gene that
- ranks high in its best m studies. Genes down have the same statistic of the
- negated statistics (``down_statistic``), whose normalized ranks are 1 - u, as the
- paper tests genes down by testing controls against cases.
-
-Genes that a study did not measure or did not test:
- The genes ranked are those that every study tested (a finite or infinite statistic,
- not NaN), as the paper ranks the genes that every dataset measured, the intersection
- of their genes. A rank says where a gene stands among the genes ranked with it, so
- the studies must rank the same genes; and a gene ranked in fewer studies would have
- a statistic, and a distribution under the null, of its own. The other genes have
- NaN. A study that did not analyse the group or the contrast at all is left out by
- :func:`cytepool.sumrank`, as the authors' code leaves out a dataset without the cell
- type: k counts the studies that did.
+:func:`fit_sumrank` computes SumRank (Nakatsuka et al. 2025), the sum of each gene's
+best normalized ranks over the studies, with its Irwin-Hall p-values and, given the
+statistics of permuted data, its permutation p-values, from each study's signed
+statistics of the genes, and returns a :class:`SumRankFit`. :func:`cytepool.sumrank`
+computes it for the studies' differential-expression results of one group (cell type)
+and contrast, and runs the permutations.
+https://docs.nygen.io/CytePool/statistics/replication#sum-of-ranks explains the
+statistic and its p-values, and
+https://docs.nygen.io/CytePool/statistics/replication#replication-limits how they
+differ from the paper's and its authors' code.
Irwin-Hall p-values:
- If every study ranked the genes in an order of its own, at random and
- independently of the others, each normalized rank would be uniform on [0, 1],
- and the sum of m of them would follow the Irwin-Hall distribution of a sum of m
- independent uniform variables (Irwin 1927; Hall 1927), whose distribution
- function is
-
- F_m(x) = (1/m!) sum_{j=0}^{floor(x)} (-1)^j C(m, j) (x - j)^m.
-
- ``up_irwin_hall_p_value`` is F_m(s) for the statistic s of a gene below the cap,
- and 1 at the cap: the probability under that null of a statistic at most s.
- It treats the ranks as continuous: a gene ranked first in every study (s = 0)
- has p-value 0, where its probability is G^-m.
-
- With every study summed (m = k), the p-value is exact under that null, apart from
- the steps of the ranks. A sum of the m best of k ranks (m < k) is smaller than a sum
- of m ranks, so its Irwin-Hall p-values are too small: the paper takes its p-values
- from permutations instead, as below, and so should an analysis with m < k, as
- :func:`cytepool.sumrank` warns. Genes whose ranks are not uniform under the null,
- such as genes with a p-value of 1 in many permutations, depart from it too.
-
- The alternating sum above loses every digit to cancellation near the middle of
- the distribution for tens of studies. F_m is computed instead as a sum of
- positive terms, F_m(x) = sum_{i=0}^{floor(x)} N_{m+1}(x - floor(x) + i), where
- N_{m+1} is the cardinal B-spline of order m + 1, the density of the Irwin-Hall
- distribution of m + 1 variables, which de Boor's recurrence evaluates with
- positive terms only (de Boor 1978): N_{m+1}' (x) = N_m(x) - N_m(x - 1), so the
- sum telescopes to the integral of N_m. It agrees with a high-precision
- computation to about 1e-14 relative for up to 200 studies
- (``validation/sumrank.py``).
-
-Permutation p-values:
- The paper permutes the case and control labels of the samples of every study,
- runs each study's analysis and SumRank again, and pools the statistics of every
- gene in every permutation into one null distribution: with B permutations of G
- genes, N = B G null statistics. The p-value of a gene of statistic s is
-
- p = (1 + the number of null statistics at most s) / (1 + N),
-
- as Phipson and Smyth (2010) form p-values from random permutations, so that it
- is never zero, and a gene at the cap has p = 1. Pooling over the genes gives
- small p-values from few permutations (100 permutations of 10,000 genes give a
- p-value as small as 1e-6), for genes whose null distributions are alike. The
- statistics of a permutation are computed as those of the data, over the genes
- that every study's analysis of that permutation tested.
-
- :func:`fit_sumrank` takes the statistics of the permutations as an array;
- :func:`cytepool.sumrank` draws them by permuting the labels of each study's
- samples and running its analysis again.
-
-Deliberate differences from the paper and its authors' code:
- - The Irwin-Hall p-value is the lower tail of the distribution, as above. The
- paper calls its Irwin-Hall p-values two-sided, which would double these, and
- the authors' code computes the density of the Irwin-Hall distribution at the
- statistic instead (the unifed package's ``dirwin.hall``), which is not a
- probability. Its permutation p-values compare -log10 of that density, which
- falls as the statistic rises up to the cap for m >= 2: they order genes as the
- statistic does, so the permutation p-values here do not depend on this choice.
- - The authors' code takes as the permutation p-value the share of null
- statistics strictly below the gene's (``1 - ecdf`` of -log10 of the density),
- without the one above: a gene beyond every null statistic has the p-value 0,
- and a gene at the cap the share of null statistics below the cap rather than
- 1. Here ties count as at least as extreme, which keeps the p-value valid.
- - The authors' code sums the normalized ranks in floating point, in each gene's
- own order, so that genes with equal statistics can compare as unequal in its
- calibration; here the sums are exact. It stops when it sums one study (m = 1),
- and takes floor(p k) of the product in floating point, which for 0.57 of 100
- studies is 56.
+ The Irwin-Hall p-value of a statistic s below the cap is F_m(s), and 1 at the cap,
+ with F_m the distribution function of a sum of m independent uniform variables
+ (Irwin 1927; Hall 1927),
+ F_m(x) = (1/m!) sum_{j=0}^{floor(x)} (-1)^j C(m, j) (x - j)^m. The ranks are taken
+ as continuous: a gene ranked first in every study (s = 0) has the p-value 0, where
+ its probability is G^-m. The alternating sum loses every digit to cancellation
+ near the middle of the distribution for tens of studies, so F_m is computed as a
+ sum of positive terms, F_m(x) = sum_{i=0}^{floor(x)} N_{m+1}(x - floor(x) + i),
+ with N_{m+1} the cardinal B-spline of order m + 1, which de Boor's recurrence
+ evaluates with positive terms only (de Boor 1978):
+ N_{m+1}'(x) = N_m(x) - N_m(x - 1).
+
+Matching the authors' code:
+ The statistic is the paper's, as its authors' code (Apache-2.0) computes it, run on
+ the validation's data.
References:
Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
@@ -227,11 +137,17 @@ def fit_sumrank(
) -> SumRankFit:
"""Rank every gene in every study and sum its best ranks, as SumRank does.
- Each study ranks the genes that every study tested by their statistic, from the
- largest; a gene's normalized ranks in the studies that rank it best are summed and
- capped at their mean under the null, for genes up and, with the statistics
- negated, for genes down. The module description gives the statistic, its
- Irwin-Hall p-values and the permutation p-values.
+ Each study ranks the genes that every study tested by their statistic, from 1 for
+ the largest, tied genes sharing their average rank, as R's ``rank`` gives it. A
+ gene of rank r among G genes has the normalized rank u = (r - 1) / (G - 1), and its
+ m smallest normalized ranks over the k studies, those of the studies that rank it
+ best, are summed and capped at m / 2, their mean under the null, for genes up and,
+ with the statistics negated, for genes down. The Irwin-Hall p-value is the
+ distribution function of a sum of m uniform ranks at the statistic, 1 at the cap;
+ with m < k these are too small, and permutation p-values should be used. A permutation p-value is (1 + the null
+ statistics at most the gene's) / (1 + their number), over the statistics of every
+ gene in every permutation, 1 at the cap and never zero (Phipson and Smyth 2010).
+ https://docs.nygen.io/CytePool/statistics/replication#sum-of-ranks explains them.
Args:
statistics: ``(n_studies, n_features)`` signed statistics, larger for a gene
@@ -243,11 +159,13 @@ def fit_sumrank(
default every study.
proportion_of_studies: Instead of ``n_studies_summed``, the proportion p of
the k studies whose ranks are summed, in (0, 1]: m = floor(p k), at
- least one; 1 sums every study.
+ least one, with p k rounded to 9 decimals first, so that 0.57 of 100
+ studies sums 57; 1 sums every study.
null_statistics: ``(n_permutations, n_studies, n_features)``: the statistics
of the same studies with their labels permuted, one study's analysis
re-run on each permutation, NaN where an analysis did not test a gene;
- or None for no permutation p-values.
+ or None for no permutation p-values. Each permutation is ranked as the
+ data are, over the genes that every study's analysis of it tested.
Returns:
The statistics of every gene, up and down, their Irwin-Hall p-values and,
@@ -525,7 +443,7 @@ def _studies_summed(
if n_studies_summed is not None:
if isinstance(n_studies_summed, bool | np.bool_):
raise TypeError(
- "n_studies_summed must be a whole number of studies, not bool"
+ "n_studies_summed must be a whole number of studies, got bool"
)
summed = index_integer(
n_studies_summed, "n_studies_summed", expected="a whole number of studies"
diff --git a/cytepool/stats/variance_fractions.py b/cytepool/stats/variance_fractions.py
index 2215bec..756148b 100644
--- a/cytepool/stats/variance_fractions.py
+++ b/cytepool/stats/variance_fractions.py
@@ -1,94 +1,49 @@
"""Variance partitioning: the share of each gene's variance that each variable explains.
-Which variables drive the differences between samples, and which must be adjusted
-for? For every gene, variance partitioning (Hoffman and Schadt 2016) fits one linear
-mixed model with every variable of the design, so that each is assessed together with
-all the others, and reports the fraction of the gene's variance that each variable
-explains, and the residual fraction. It is what variancePartition's
-``fitExtractVarPartModel`` computes.
-
-:func:`variance_fractions` partitions the variance of every feature of a samples x
-features matrix of log-expression values, and :func:`cytepool.variance_partitioning`
-of every group (cell type) of a cube, after the steps of
-:func:`~cytepool.dream_voom` up to voom's precision weights.
-
-The model:
- Every feature is fitted with the model of
- :mod:`cytepool.stats.mixed_models`,
-
- y = X beta + Z_1 b_1 + ... + Z_K b_K + e,
- b_k ~ N(0, sigma_k^2 I), e ~ N(0, sigma^2 W^-1),
-
- where categorical variables, such as the donor, the condition or the batch, are
- random intercepts, ``(1 | donor)``, and continuous variables, such as age, are
- fixed effects (``~ age``). Without random effects it is a linear model, fitted by
- weighted least squares. The variances are estimated by maximum likelihood, as
- ``fitExtractVarPartModel`` does by default (``REML = FALSE``): its documentation
- explains that REML, which estimates the random effects after removing the fixed
- effects, biases the fractions of variance, while maximum likelihood estimates
- the fixed and random effects together. ``method="reml"`` fits by REML instead.
-
-The fractions:
- Each variable contributes a variance, and its fraction is that variance over
- their sum:
-
- - a random effect k, its variance component sigma_k^2;
- - the residual, sigma^2 / mean(w): the residual variance of an observation of
- the feature's average weight (sigma^2 itself without weights);
- - a fixed-effect term j, the variance of its fitted contribution X_j beta_j
- over the samples, the post hoc variance of Hoffman and Schadt (2016). The
- variances of fitted values are weighted, sum_i w_i (v_i - v_w)^2 / sum_i w_i
- about the weighted mean v_w, with the denominator of a maximum likelihood
- variance (n without weights). With random effects, the terms share the
- variance of all fitted fixed effects, X beta, in proportion to the variances
- of their own contributions; without random effects, each term has the
- variance of its own contribution. The two agree when the terms are
- uncorrelated over the samples.
-
- The fractions of a feature sum to one, and a random effect whose variance is
- estimated at zero (a singular fit in lme4) has a fraction of exactly zero. They
- do not change when the weights of a feature are multiplied by a constant, when
- the levels of a factor are relabelled, or when the values of a feature are
- multiplied by a constant or shifted.
-
-Conventions found by probing variancePartition 1.36.2, with lme4 1.1-38, as a black
-box, where its documentation leaves them open:
-
-- The variances of the fixed effects are those above: weighted, with the
- denominator of a maximum likelihood variance, and shared among the terms in a
- mixed model but not in a linear model (``calcVarPart`` of ``lmer`` and ``lm``
- fits).
-- ``fitExtractVarPartModel`` divides each gene's precision weights by their mean,
- as ``dream`` does, so the residual variance is that of an observation of average
- weight and the fractions do not depend on the scale of the weights. Without
- random effects the residual variance is the weighted mean of the squared
- residuals, the maximum likelihood estimate.
-- A design with random effects must give every categorical variable as a random
- effect: a factor among its fixed effects is refused, as variancePartition
- refuses it ("Must model either _all_ or _no_ categorical variables as random
- effects"), and so is a formula without an intercept. Without random effects,
- factors are fixed effects of the linear model.
-- variancePartition lists the random effects in alphabetical order, then the fixed
- effects; here they are in the order of the design.
-
-Deliberate differences from variancePartition, reported by
-``validation/variance_partitioning.py``:
-
-- A feature that the fixed and random effects reproduce, such as one that is
- constant within every donor, has no maximum likelihood estimate (see
- :class:`~cytepool.stats.mixed_models.MixedModelFit`) and its fractions are NaN.
- lme4 fits it with a residual variance near zero, which gives it a fraction near
- one for the donor.
-- ``method="reml"`` without random effects takes the REML estimate of the residual
- variance, the residual sum of squares over n - p. variancePartition fits such a
- design with ``lm`` and takes the residual sum of squares over n whatever ``REML``
- says.
-
-Limits of this version:
- Random slopes, which variancePartition uses to partition the variance within
- subsets of the samples, such as ``(tissue + 0 | donor)``, generalized linear
- mixed models, and bootstrap confidence intervals of the fractions
- (``varPartConfInf``) are not implemented yet.
+:func:`variance_fractions` fits every feature of a samples x features matrix of
+log-expression values with one linear mixed model that holds every variable of a design
+(Hoffman and Schadt 2016), with the model of :mod:`cytepool.stats.mixed_models` (Bates
+et al. 2015), and returns, in a :class:`VarianceFractions`, the fraction of the
+feature's variance that each variable explains and the residual fraction, as
+variancePartition's ``fitExtractVarPartModel`` computes them;
+:func:`cytepool.variance_partitioning` does it for every group (cell type) of a cube.
+https://docs.nygen.io/CytePool/statistics/mixed-models#variance-fractions explains the
+model and the fractions.
+
+The fractions of a feature sum to one, and a random effect whose variance is estimated
+at zero (a singular fit in lme4) has a fraction of exactly zero. They do not change when
+the weights of a feature are multiplied by a constant, when the levels of a factor are
+relabelled, or when the values of a feature are multiplied by a constant or shifted.
+
+Matching variancePartition:
+ The fractions match variancePartition 1.36.2 with lme4 1.1-38: its documentation
+ gives the method, and running it on chosen inputs settled the conventions it leaves
+ open.
+
+ - The variances are estimated by maximum likelihood, as ``fitExtractVarPartModel``
+ does by default (``REML = FALSE``): its help page says that maximum likelihood
+ gave unbiased fractions, where REML (Patterson and Thompson 1971) can show
+ substantial bias.
+ - The variance of a fixed-effect term is the weighted variance of its fitted
+ contribution, with the denominator of a maximum likelihood variance; in a mixed
+ model the terms share the variance of all the fitted fixed effects in proportion
+ to their own, and in a linear model each term has its own (``calcVarPart`` of
+ ``lmer`` and ``lm`` fits).
+ - ``fitExtractVarPartModel`` divides each gene's precision weights by their mean,
+ as ``dream`` does (Hoffman and Roussos 2021), so the residual variance is that of
+ an observation of average weight and the fractions do not depend on the scale of
+ the weights. Without random effects the residual variance is the weighted mean of
+ the squared residuals, the maximum likelihood estimate.
+ - A design with random effects must give every categorical variable as a random
+ effect: a factor among its fixed effects is refused, as variancePartition refuses
+ it ("Must model either _all_ or _no_ categorical variables as random effects"),
+ and so is a formula without an intercept. Without random effects, factors are
+ fixed effects of the linear model.
+
+Differences from variancePartition:
+ https://docs.nygen.io/CytePool/statistics/mixed-models#mixed-models-limits lists
+ them; :func:`variance_fractions` and :class:`VarianceFractions` give those that
+ change the results.
References:
Hoffman GE, Schadt EE (2016). variancePartition: interpreting drivers of variation
@@ -206,8 +161,16 @@ def variance_fractions(
is fitted with the design's fixed effects and random intercepts, by maximum
likelihood as lme4's ``lmer(REML = FALSE)`` fits it (see
:func:`~cytepool.stats.mixed_models.fit_mixed_model`), and its variance is
- partitioned among the random effects, the fixed-effect terms and the residual,
- as the module documentation describes. Categorical variables are given as random
+ partitioned among the random effects, the fixed-effect terms and the residual.
+ Each variable contributes a variance, and its fraction is that variance over their
+ sum: a random effect k its variance component sigma_k^2; a fixed-effect term j the
+ variance of its fitted contribution X_j beta_j over the samples, weighted,
+ sum_i w_i (v_i - v_w)^2 / sum_i w_i about the weighted mean v_w, and with random
+ effects scaled so that the terms share the variance of all the fitted fixed
+ effects, X beta, in proportion to their own; and the residual sigma^2 / mean(w),
+ the residual variance of an observation of the feature's average weight.
+ https://docs.nygen.io/CytePool/statistics/mixed-models#variance-fractions explains
+ the fractions. Categorical variables are given as random
effects and continuous ones as fixed effects, as in ``Design(samples, "~ age",
random="(1 | donor) + (1 | condition)")``. A design without random effects is
fitted by weighted least squares, and its factors are fixed effects.
@@ -226,7 +189,9 @@ def variance_fractions(
variance of an observation is sigma^2 / weight. They must be finite
and positive; only their ratios within a feature matter.
method: ``"ml"`` for maximum likelihood, variancePartition's default, or
- ``"reml"`` for restricted maximum likelihood.
+ ``"reml"`` for restricted maximum likelihood. Without random effects,
+ ``"reml"`` divides the residual sum of squares by n - p, where
+ variancePartition divides it by n whatever its ``REML`` says.
n_threads: The most threads for fitting features, as for
``fit_mixed_model``; the result does not depend on it.
@@ -432,6 +397,10 @@ def _mixed_fractions(
if weights is None
else _positive_weight_rows(weights, (parts.x.shape[0], n_features))
)
+ # In a mixed model the fixed-effect terms share the variance of all the fitted
+ # fixed effects; the residual variance is that of an observation of average
+ # weight, as fitExtractVarPartModel divides the weights by their mean (found by
+ # probing).
fixed = _fixed_variances(parts.x, terms, fit.coefficients, rows, apportioned=True)
residual = fit.residual_variance / fit.mean_weights
variances = np.column_stack([fit.variance_components, fixed, residual])
@@ -449,6 +418,8 @@ def _linear_fractions(
"""The fractions of weighted least squares fits of a design without random
effects."""
n, p = x.shape
+ # In a linear model each term has the variance of its own contribution, as
+ # calcVarPart gives it for an lm fit (found by probing).
fixed = _fixed_variances(x, terms, fit.coefficients, rows, apportioned=False)
# sigma^2 is the residual variance of an observation of weight one, sum w r^2 /
# (n - p); maximum likelihood takes the sum over n.
diff --git a/cytepool/stats/voom.py b/cytepool/stats/voom.py
index 7510c9c..d9a6c55 100644
--- a/cytepool/stats/voom.py
+++ b/cytepool/stats/voom.py
@@ -1,88 +1,61 @@
"""Precision weights for log counts from their mean-variance trend, as in limma's voom.
-For counts r_gi of feature g in sample i and effective library sizes R_i (library
-sizes times normalization factors), voom (Law, Chen, Shi and Smyth 2014) works with
-the log2 counts per million
-
- y_gi = log2((r_gi + 0.5) / (R_i + 1) * 1e6).
-
-The design is fitted to every feature by ordinary least squares, which gives fitted
-values mu_gi and residual standard deviations s_g. The square roots s_g^(1/2) are
-smoothed by LOWESS against the average log2 count of each feature,
-
- x_g = mean_i(y_gi) + mean_i(log2(R_i + 1)) - log2(1e6),
-
-and the smoothed curve lo() is interpolated linearly between its points and held
-constant beyond them. Each observation is then weighted by the inverse of its
-predicted variance, w_gi = lo(lambda_gi)^(-4), where
-
- lambda_gi = log2(2^mu_gi (R_i + 1) / 1e6)
-
-is its fitted log2 count. A weighted least squares fit of the log2 counts per million
-with these weights, followed by empirical Bayes moderation, is limma-voom.
-
-With prior weights p_gi, for samples, observations or features, the design is fitted
-by weighted least squares with them, which gives the residual standard deviations
-and fitted values of the trend, and the weights are w_gi p_gi. limma's ``voom``
-returns w_gi alone, which its help page says modify the prior weights in a
-multiplicative fashion: limma's weights times the prior weights are CytePool's, the
-weights a fit needs.
-
-:func:`voom_with_quality_weights` combines the weights with sample quality weights
-(Liu et al. 2015), as limma's ``voomWithQualityWeights`` does (found by probing):
-voom; sample quality weights a from its log2 counts per million and weights, by
-:func:`~cytepool.stats.sample_weights.sample_quality_weights`; voom again, with a as
-prior weights; sample quality weights b from the new trend's weights w'; and the
-weights w'_gi b_i.
-
-Results match limma 3.62.1, called with ``adaptive.span = TRUE`` where the span is
-chosen. Conventions that the paper leaves open were settled from limma's
-documentation (its help page and NEWS) or by running limma as a black box; comments
-mark the latter "found by probing":
-
-- Features with no counts in any sample are left out of the trend (found by probing);
- their weights are read from it at their fitted log2 counts, as for any other
- feature.
-- The LOWESS is R's ``lowess`` with its defaults other than the span: three
- robustness iterations, and interpolation within 1% of the range of x (found by
- probing).
-- By default the span is chosen from the number of features n by limma's rule,
- :func:`~cytepool.stats.smoothing.choose_lowess_span`: 1 up to 50 features, and
- 0.3 + 0.7 (50 / n)^(1/3) beyond. limma's voom chooses it so with
- ``adaptive.span = TRUE``, an option since limma 3.60 and the default since 3.66
- (limma's NEWS); its default span was 0.5 before. n counts every feature, also those
- without counts that the trend leaves out, as limma's ``nrow(counts)`` does (its help
- page; confirmed by probing). A span given as a number is used as it is, as with
- ``adaptive.span = FALSE``.
-- The interpolation averages the curve over tied values of x (limma's NEWS); LOWESS
- gives tied points the same value, so each distinct x is one point of the curve.
- The x are computed as R computes them, to the last bit, so that their ties are
- limma's: LOWESS treats tied points differently from points a rounding error apart.
-- x averages every log2 count per million of a feature, those of zero prior weight
- included, as limma's ``Amean`` does (found by probing).
-- A design that leaves no residual degrees of freedom gives every observation weight
- one, times its prior weight, with a warning (limma's NEWS).
-- There must be at least two features, as limma requires, and at least two with counts
- (and residual degrees of freedom under the prior weights) to fit a trend.
-
-Deliberate deviations, where limma returns values without meaning or fails:
-
-- A sample whose library size is zero is an error. limma gives it the log2 counts per
- million of a count of 0.5 in a library of one read.
-- A trend that is not positive where weights are read from it is an error. limma takes
- its inverse fourth power anyway, which is positive for a negative trend.
-- When every feature in the trend has the same x, the curve is constant; limma fails.
-- A design that is not of full column rank is rejected by
- :func:`~cytepool.stats.linear_models.fit_linear_model`; limma drops the aliased
- columns.
-- ``span`` must lie in (0, 1], as limma documents; limma accepts larger values.
-- A feature whose prior weights leave no residual degrees of freedom is left out of
- the trend. limma passes its missing standard deviation to R's ``lowess``, which
- then returns a curve without meaning for every feature.
-- Where zero prior weights leave coefficients of a feature that its other samples
- cannot estimate, its fitted values are those of the least squares fit over the
- samples of positive weight, and its observations of zero prior weight get weight
- zero. limma returns missing weights for every observation of such a feature.
+:func:`voom` gives the log2 counts per million of a samples x features matrix of
+counts with a precision weight for each observation, from the trend of their variance
+against their mean (Law, Chen, Shi and Smyth 2014), in a :class:`VoomResult`.
+:func:`voom_with_quality_weights` combines those weights with sample quality weights
+(Liu et al. 2015), as limma's ``voomWithQualityWeights`` does. A weighted least squares
+fit of the log2 counts per million with these weights, followed by empirical Bayes
+moderation, is limma-voom; https://docs.nygen.io/CytePool/statistics/limma-voom#voom
+gives the method.
+
+Matching limma:
+ The results match limma 3.62.1, called with ``adaptive.span = TRUE`` where the
+ span is chosen. limma's documentation (its help page and NEWS) gives some of the
+ conventions below, and running limma on chosen inputs gave the others.
+
+ - Features with no counts in any sample are left out of the trend; their weights
+ are read from it at their fitted log2 counts, as for any other feature.
+ - The LOWESS is R's ``lowess`` with its defaults other than the span: three
+ robustness iterations, and interpolation within 1% of the range of x.
+ - The span that :func:`voom` chooses by default follows the number of features,
+ which counts every feature, also those without counts that the trend leaves
+ out, as limma's ``nrow(counts)`` does. limma's ``adaptive.span = TRUE``, which
+ chooses it, is an option since limma 3.60 and the default since 3.66 (limma's
+ NEWS); a span given as a number is used as it is, as with
+ ``adaptive.span = FALSE``.
+ - The trend is interpolated linearly between its points and held constant beyond
+ them. The interpolation averages the curve over tied values of x (limma's
+ NEWS); LOWESS gives tied points the same value, so each distinct x is one point
+ of the curve. The x are computed as R computes them, to the last bit, so that
+ their ties are limma's: LOWESS treats tied points differently from points a
+ rounding error apart.
+ - x averages every log2 count per million of a feature, those of zero prior
+ weight included, as limma's ``Amean`` does.
+ - A design that leaves no residual degrees of freedom gives every observation
+ weight one, times its prior weight, with a warning (limma's NEWS).
+ - There must be at least two features, as limma requires, and at least two with
+ counts (and residual degrees of freedom under the prior weights) to fit a
+ trend.
+
+Differences from limma:
+ - A sample whose library size is zero is an error. limma gives it the log2 counts
+ per million of a count of 0.5 in a library of one read.
+ - A trend that is not positive where weights are read from it is an error. limma
+ takes its inverse fourth power anyway, which is positive for a negative trend.
+ - When every feature in the trend has the same x, the curve is constant; limma
+ fails.
+ - A design that is not of full column rank is rejected by
+ :func:`~cytepool.stats.linear_models.fit_linear_model`; limma drops the aliased
+ columns.
+ - ``span`` must lie in (0, 1], as limma documents; limma accepts larger values.
+ - A feature whose prior weights leave no residual degrees of freedom is left out of
+ the trend. limma passes its missing standard deviation to R's ``lowess``, which
+ then returns a curve without meaning for every feature.
+ - Where zero prior weights leave coefficients of a feature that its other samples
+ cannot estimate, its fitted values are those of the least squares fit over the
+ samples of positive weight, and its observations of zero prior weight get weight
+ zero. limma returns missing weights for every observation of such a feature.
References:
Law CW, Chen Y, Shi W, Smyth GK (2014). voom: precision weights unlock linear model
@@ -262,10 +235,15 @@ def voom(
) -> VoomResult:
"""Log2 counts per million with precision weights from the mean-variance trend.
- limma's ``voom`` (Law et al. 2014); the module docstring gives the method. The
- design is fitted by :func:`~cytepool.stats.linear_models.fit_linear_model`,
- which checks it. Filter out features with very low counts first, for example
- with :func:`~cytepool.normalization.filter_by_expression`, as limma advises.
+ limma's ``voom`` (Law et al. 2014). The design is fitted to every feature by
+ :func:`~cytepool.stats.linear_models.fit_linear_model`, which checks it; the
+ square roots of the residual standard deviations are smoothed by LOWESS against
+ each feature's average log2 count; and each observation is weighted by the
+ inverse of the variance that this trend predicts at its fitted log2 count, times
+ its prior weight. https://docs.nygen.io/CytePool/statistics/limma-voom#voom gives
+ the equations, and the module documentation the conventions that match limma.
+ Filter out features with very low counts first, for example with
+ :func:`~cytepool.normalization.filter_by_expression`, as limma advises.
Continue with ``fit_linear_model(result.log2_cpm, design,
weights=result.weights)`` and
@@ -298,11 +276,13 @@ def voom(
weights: Prior weights, as limma's ``voom(weights = ...)`` takes them:
``(n_samples,)`` sample weights, such as sample quality weights,
``(n_samples, n_features)`` observation weights, or ``(1, n_features)``
- feature weights. Finite and non-negative; a zero weight leaves an
- observation out of the fit of the trend and gives it weight zero. The
- returned weights include them. A vector is always sample weights, also
- when there are as many features as samples, where limma takes it for
- feature weights.
+ feature weights. Finite and non-negative. They weight the least
+ squares fits from which the trend is estimated, so that a zero weight
+ leaves an observation out of them and gives it weight zero, and the
+ returned weights include them, the weights a fit needs (limma's
+ ``voom`` returns the trend's alone). A vector is always sample weights,
+ also when there are as many features as samples, where limma takes it
+ for feature weights.
Returns:
The log2 counts per million, their weights (finite and non-negative;
@@ -360,12 +340,16 @@ def voom_with_quality_weights(
) -> VoomResult:
"""voom's precision weights combined with sample quality weights.
- limma's ``voomWithQualityWeights`` (Liu et al. 2015): the module docstring gives
- the steps, and :func:`~cytepool.stats.sample_weights.sample_quality_weights`
- the sample weights. Samples that follow the model less closely, such as
- pseudobulk samples pooled from few cells or from an outlying donor, get lower
- weights instead of being removed. Continue as after :func:`voom`, with
- ``fit_linear_model(result.log2_cpm, design, weights=result.weights)``.
+ limma's ``voomWithQualityWeights`` (Liu et al. 2015), in its steps: :func:`voom`;
+ sample quality weights a from its log2 counts per million and weights, by
+ :func:`~cytepool.stats.sample_weights.sample_quality_weights`; :func:`voom`
+ again, with a as prior weights; sample quality weights b from the new trend's
+ weights w'; and the weights w'_gi b_i. Samples that follow the model less
+ closely, such as pseudobulk samples pooled from few cells or from an outlying
+ donor, get lower weights instead of being removed
+ (https://docs.nygen.io/CytePool/statistics/limma-voom#sample-quality-weights).
+ Continue as after :func:`voom`, with ``fit_linear_model(result.log2_cpm, design,
+ weights=result.weights)``.
Args:
counts: ``(n_samples, n_features)`` counts, as for :func:`voom`.
diff --git a/cytepool/study_results.py b/cytepool/study_results.py
index 655839c..002ea3f 100644
--- a/cytepool/study_results.py
+++ b/cytepool/study_results.py
@@ -1,18 +1,19 @@
"""The differential expression of every study of a collection, analysed alike.
-Studies of one question, such as cohorts of a disease, are compared or combined only
-once each is analysed on the collection's axes: its aligned cube
-(:meth:`~cytepool.CubeCollection.aligned_cube`), with a design built from its own
-samples. :func:`~cytepool.analyse_each_study` runs one analysis per study that way, and
-:class:`~cytepool.StudyResults` keeps the results with their collection: it lines them
-up feature by feature (:meth:`~cytepool.StudyResults.table`), with the reason a study
-has no result for a feature (it did not measure it, did not test it, or did not analyse
-the group or contrast), and gives one column of every study as a studies x features
-matrix (:meth:`~cytepool.StudyResults.matrix`), the form that combined p-values
-(:func:`~cytepool.stats.combined_p_values.combine_p_values`) take. Every function that
-combines studies takes it: :func:`~cytepool.meta_analysis`,
-:func:`~cytepool.replication`, :func:`~cytepool.sumrank` and
-:func:`~cytepool.held_out_prediction`.
+Each study of a :class:`~cytepool.CubeCollection` is analysed on the collection's axes:
+its aligned cube (:meth:`~cytepool.CubeCollection.aligned_cube`), with a design built
+from its own samples. :func:`~cytepool.analyse_each_study` runs one analysis per study
+that way, and :class:`~cytepool.StudyResults` keeps the results with their collection:
+it lines them up feature by feature (:meth:`~cytepool.StudyResults.table`), with the
+reason a study has no result for a feature (it did not measure it, did not test it, or
+did not analyse the group or contrast), and gives one column of every study as a
+studies x features matrix (:meth:`~cytepool.StudyResults.matrix`), the form that
+:func:`~cytepool.stats.combined_p_values.combine_p_values` takes.
+:func:`~cytepool.meta_analysis`, :func:`~cytepool.replication`,
+:func:`~cytepool.sumrank` and :func:`~cytepool.held_out_prediction` take a
+``StudyResults``.
+https://docs.nygen.io/CytePool/statistics/aligning-studies#one-analysis-per-study
+explains it.
Memory:
Each result keeps the cube it analysed (see
diff --git a/cytepool/sumrank_across_studies.py b/cytepool/sumrank_across_studies.py
index 60d2b0a..d70737d 100644
--- a/cytepool/sumrank_across_studies.py
+++ b/cytepool/sumrank_across_studies.py
@@ -1,11 +1,14 @@
"""SumRank of the differential expression of the studies of a collection.
-:func:`~cytepool.sumrank` ranks the genes of every study, sums each gene's best ranks
-and gives their p-values, as SumRank does (Nakatsuka et al. 2025), for one group (cell
-type) and contrast of the studies' results: a :class:`~cytepool.StudyResults`, such as
-:func:`~cytepool.analyse_each_study` gives, whose studies each analysed their aligned
-cube. :mod:`cytepool.stats.sumrank` describes the statistic, its Irwin-Hall p-values and
-its permutation p-values, which :func:`~cytepool.stats.sumrank.fit_sumrank` computes.
+:func:`~cytepool.sumrank` computes SumRank (Nakatsuka et al. 2025) for a
+:class:`~cytepool.StudyResults`, such as :func:`~cytepool.analyse_each_study` gives,
+whose studies each analysed their aligned cube, in one group (cell type) and contrast,
+and returns a :class:`~cytepool.SumRankResult`.
+:func:`~cytepool.stats.sumrank.fit_sumrank` computes the statistic, its Irwin-Hall
+p-values and its permutation p-values.
+https://docs.nygen.io/CytePool/statistics/replication#sum-of-ranks explains them, and
+https://docs.nygen.io/CytePool/statistics/replication#replication-limits how the
+permutations differ from the paper's.
The studies' statistics:
Each study ranks the genes by its signed -log10 p-value of the contrast:
@@ -20,32 +23,20 @@
group, with a ``numpy.random.Generator``, and the study's analysis,
``limma_voom`` or ``limma_trend`` with the design and settings its result
records, is run again on its aligned cube, for that group and contrast only.
- The variable (``permute``) is read from the contrast when it compares two
- levels of one factor in every study's design, such as ``conditioncase``, the
- level ``case`` of ``condition`` against its reference; otherwise it is given.
+ Only designs without random effects are permuted, whose samples are exchangeable
+ under the null; a design with several samples of a donor would need the donors'
+ labels permuted instead. The variable (``permute``) is read from the contrast
+ when it compares two levels of one factor in every study's design, such as
+ ``conditioncase``, the level ``case`` of ``condition`` against its reference;
+ otherwise it is given.
The permutations are drawn in a fixed order, permutation after permutation and
study after study, so a seed gives the same permutations, and the analyses do
not depend on the number of threads.
Cost:
- A permutation runs the analysis of every study once, for one group: with
- ``limma_voom``, 0.21 s for a study of 40 samples and 10,000 genes on a laptop on
- 2 threads (``limma_trend`` 0.14 s; ``validation/sumrank.py --benchmark``), so 100
- permutations of 20 such studies take about 7 minutes; the SumRank statistics of a
- permutation take milliseconds. The memory is that of one study's analysis and a
- few arrays of the genes, whatever the number of permutations.
-
-Deliberate differences from the paper:
- - The labels are permuted among the samples of each study's fit of the group,
- which keeps the number of cases and controls of the fit. The paper permutes
- them among all the individuals of a dataset, for all its cell types at once,
- and draws again until every cell type has at least two cases and two controls.
-
-Not done:
- Studies are not weighted, a limit the paper notes too. Permutations re-run
- designs without random effects only (``limma_voom`` and ``limma_trend``), whose
- samples are exchangeable under the null; a design with several samples of a
- donor needs the donors' labels permuted instead.
+ A permutation runs the analysis of every study once, for one group; the SumRank
+ statistics of a permutation take milliseconds. The memory is that of one study's
+ analysis and a few arrays of the genes, whatever the number of permutations.
References:
Nakatsuka N, Adler D, Jiang L, Hartman A, Cheng E, Klann E, Satija R (2025).
@@ -255,16 +246,18 @@ def sumrank(
:func:`~cytepool.analyse_each_study` gives. For the group and contrast, the
studies whose results test them each rank the genes that every one of them
tested by their signed -log10 p-value, and each gene's best ranks are summed, as
- :func:`~cytepool.stats.sumrank.fit_sumrank` sums them; the module description
- gives the statistic and its p-values.
+ :func:`~cytepool.stats.sumrank.fit_sumrank` sums them, with Irwin-Hall p-values.
+ https://docs.nygen.io/CytePool/statistics/replication#sum-of-ranks explains the
+ statistic and its p-values.
With ``n_permutations``, the values of the design variable ``permute``, such as
the condition, are permuted among the samples of each study's fit of the group,
and each study's analysis is run again on its aligned cube with the design and
settings its result records (``limma_voom`` or ``limma_trend``), for this group
and contrast only; the SumRank statistics of every permutation make the null
- distribution of the p-values. Each permutation costs one analysis of the group in
- every study (see "Cost" in the module description).
+ distribution of the p-values. Each permutation runs the analysis of the group
+ once in every study; the permutations are drawn permutation after permutation
+ and study after study, so a seed gives the same permutations.
Args:
studies: The differential-expression result of every study of a
@@ -446,10 +439,10 @@ def sumrank(
def _n_permutations(value: Any) -> int:
if isinstance(value, bool | np.bool_):
- raise TypeError("n_permutations must be a whole number, not bool")
+ raise TypeError("n_permutations must be a whole number, got bool")
number = index_integer(value, "n_permutations", expected="a whole number")
if number < 0:
- raise ValueError(f"n_permutations must be at least 0, got {number}")
+ raise ValueError(f"n_permutations must be non-negative, got {number}")
return number
diff --git a/cytepool/table.py b/cytepool/table.py
index 0125f94..387f918 100644
--- a/cytepool/table.py
+++ b/cytepool/table.py
@@ -4,6 +4,8 @@
``Table`` because pandas is not a dependency. A table holds 1-D numpy arrays of equal
length, one per column name. Columns are copied in and read-only, and every method
that changes something returns a new table.
+https://docs.nygen.io/CytePool/statistics/cubes#what-a-cube-records describes the
+tables of a cube.
Column types:
@@ -134,7 +136,7 @@ def __getitem__(self, name: str) -> np.ndarray:
if not isinstance(name, str):
raise TypeError(
"columns are looked up by name (str); use take() or filter() to choose "
- f"rows and select() to choose several columns, not {type(name).__name__}"
+ f"rows and select() to choose several columns, got {type(name).__name__}"
)
try:
return self._columns[name].view()
diff --git a/cytepool/variance_components.py b/cytepool/variance_components.py
index c33d3cc..23f0c41 100644
--- a/cytepool/variance_components.py
+++ b/cytepool/variance_components.py
@@ -1,14 +1,14 @@
"""Variance partitioning of every group (cell type) of a cube.
-:func:`~cytepool.variance_partitioning` takes, in every group of a cube, the steps of
-:func:`~cytepool.dream_voom` up to voom's precision weights (Law et al. 2014), the
-mean-variance trend of the design's mixed model (Hoffman and Roussos 2021), and
-partitions the variance of every feature among the variables of the design, as
-variancePartition's ``fitExtractVarPartModel`` does (Hoffman and Schadt 2016):
-:mod:`cytepool.stats.variance_fractions` gives the model, the fractions and the
-conventions found by probing variancePartition, and
-:func:`~cytepool.stats.variance_fractions.variance_fractions` partitions the variance
-of a samples x features matrix.
+:func:`~cytepool.variance_partitioning` partitions the variance of every feature among
+the variables of a design in every group of a cube, as variancePartition's
+``fitExtractVarPartModel`` does (Hoffman and Schadt 2016), after the steps of
+:func:`~cytepool.dream_voom` up to voom's precision weights (Law et al. 2014; Hoffman
+and Roussos 2021), into a :class:`~cytepool.VariancePartitioningResult`;
+:func:`~cytepool.stats.variance_fractions.variance_fractions` does it for a samples x
+features matrix.
+https://docs.nygen.io/CytePool/statistics/mixed-models#variance-fractions explains the
+method.
References:
Hoffman GE, Schadt EE (2016). variancePartition: interpreting drivers of variation
@@ -85,7 +85,7 @@
# Columns of the tables of a cube's groups other than the fractions.
_IDENTITY = ("feature_id", "symbol")
_METHOD = "variance_partitioning"
-# The fits behind the precision weights, recorded in the provenance: maximum
+# The fits of the precision weights, recorded in the provenance: maximum
# likelihood, as variancePartition's voomWithDreamWeights fits.
_FIXED_SETTINGS = {"weights_fit": "ml"}
@@ -358,8 +358,8 @@ def variance_partitioning(
dreamlet weights samples; or None. Needs a cube that records cell
numbers.
method: ``"ml"`` (maximum likelihood, variancePartition's default) or
- ``"reml"``, for the fits behind the fractions; the fits behind the
- precision weights are by maximum likelihood, as
+ ``"reml"``, for the fits from which the fractions come; the fits from
+ which the precision weights come are by maximum likelihood, as
``voomWithDreamWeights`` fits them.
n_threads: The most threads for the fits of the features, at least 1; None
keeps numba's setting. The results do not depend on it.
diff --git a/docs/build_docs.py b/docs/build_docs.py
index 6d896f2..913e777 100644
--- a/docs/build_docs.py
+++ b/docs/build_docs.py
@@ -376,25 +376,39 @@ def handle_endtag(self, tag: str) -> None:
def include_api_navigation(site: Path) -> None:
+ """Give the API reference entry of the staged table of contents the pages of the
+ API index as its children, in the groups of the index's captioned tables of
+ contents; pages listed before any caption come first, ungrouped."""
import yaml
base_url = os.environ.get("BASE_URL", "").rstrip("/")
source = DOCS / "build" / "api" / "index.html"
navigation = html_fragment(source.read_text(), "docs-api-tree")
- links = []
- for href, title in re.findall(
- r']*\bhref="([^"]+)"[^>]*>(.*?)', navigation, re.DOTALL
+ links: list[dict[str, Any]] = []
+ group: list[dict[str, Any]] = links
+ for caption, href, title in re.findall(
+ r']*\bclass="caption-text"[^>]*>(.*?)'
+ r'|]*\bhref="([^"]+)"[^>]*>(.*?)',
+ navigation,
+ re.DOTALL,
):
+ if caption:
+ group = []
+ links.append({"title": _plain_text(caption), "children": group})
+ continue
href = unescape(href)
if urlsplit(href).scheme or href.startswith("//") or not href.endswith(".html"):
raise ValueError(f"Expected an API page link in {source}: {href}")
- links.append(
+ group.append(
{
- "title": " ".join(unescape(re.sub(r"<[^>]+>", "", title)).split()),
+ "title": _plain_text(title),
"url": urljoin(f"{base_url}/api/", href),
"open_in_same_tab": True,
}
)
+ empty = [entry["title"] for entry in links if entry.get("children") == []]
+ if empty:
+ raise ValueError(f"API navigation groups without pages in {source}: {empty}")
if not links:
raise ValueError(f"No API navigation links in {source}")
config = yaml.safe_load((site / "myst.yml").read_text())
@@ -793,6 +807,11 @@ def _figures(cell: dict[str, Any]) -> list[dict[str, Any]]:
]
+def _plain_text(html: str) -> str:
+ """The text of an HTML fragment, without tags, entities or runs of spaces."""
+ return " ".join(unescape(re.sub(r"<[^>]+>", "", html)).split())
+
+
def _text(source: str | list[str]) -> str:
return source if isinstance(source, str) else "".join(source)
diff --git a/docs/coverage_exemptions.toml b/docs/coverage_exemptions.toml
index 4f04490..b8fba99 100644
--- a/docs/coverage_exemptions.toml
+++ b/docs/coverage_exemptions.toml
@@ -9,52 +9,51 @@
"cytepool.design.Contrasts.items" = "a method of the read-only mapping that Contrasts is; pages use contrast names and Contrasts.select"
"cytepool.design.Contrasts.keys" = "a method of the read-only mapping that Contrasts is; pages use contrast names and Contrasts.select"
"cytepool.design.Contrasts.values" = "a method of the read-only mapping that Contrasts is; pages use contrast names and Contrasts.select"
-"cytepool.io.zarr.ZarrArray" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.io.zarr.check_compression" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.io.zarr.open_array" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.io.zarr.read_group_attributes" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.io.zarr.write_array" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.io.zarr.write_group" = "part of the minimal Zarr v3 reader and writer behind cube files; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
-"cytepool.stats.distributions.beta_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.beta_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.chi2_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.chi2_isf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.chi2_logsf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.chi2_ppf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.chi2_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.f_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.f_isf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.f_logsf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.f_ppf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.f_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.gamma_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.gamma_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.hypergeom_logsf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.hypergeom_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_isf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_logcdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_logsf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_ppf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.norm_sf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.t_cdf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.t_isf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.t_logsf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.distributions.t_ppf" = "a tail probability or quantile behind the tests' p-values, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.betainc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.betaincc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.digamma" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.gammainc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.gammaincc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.lgamma" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.log_beta" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.log_betainc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.log_betaincc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.log_gammainc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.log_gammaincc" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.trigamma" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.stats.special.trigamma_inverse" = "a special function behind the distributions and the moderation, documented in the API reference and checked against mpmath (validation/distributions.py)"
-"cytepool.Table.equals" = "a method of CytePool's minimal table of columns, documented in the API reference; the guides show the common ones"
+"cytepool.io.zarr.ZarrArray" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.io.zarr.check_compression" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.io.zarr.open_array" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.io.zarr.read_group_attributes" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.io.zarr.write_array" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.io.zarr.write_group" = "part of the minimal Zarr v3 reader and writer that cube files use; users read and write cubes with read_cube and write_cube, and other Zarr stores with zarr-python"
+"cytepool.stats.distributions.beta_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.beta_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.chi2_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.chi2_isf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.chi2_logsf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.chi2_ppf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.chi2_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.f_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.f_isf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.f_logsf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.f_ppf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.f_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.gamma_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.gamma_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.hypergeom_logsf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.hypergeom_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_isf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_logcdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_logsf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_ppf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.norm_sf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.t_cdf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.t_isf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.t_logsf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.distributions.t_ppf" = "a tail probability or quantile that the tests' p-values use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.betainc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.betaincc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.digamma" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.gammainc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.gammaincc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.lgamma" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.log_beta" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.log_betainc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.log_betaincc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.log_gammainc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.log_gammaincc" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.trigamma" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
+"cytepool.stats.special.trigamma_inverse" = "a special function that the distributions and the moderation use, documented in the API reference and checked against mpmath (validation/distributions.py)"
"cytepool.Table.rename" = "a method of CytePool's minimal table of columns, documented in the API reference; the guides show the common ones"
"cytepool.Table.take" = "a method of CytePool's minimal table of columns, documented in the API reference; the guides show the common ones"
"cytepool.Table.to_dict" = "a method of CytePool's minimal table of columns, documented in the API reference; the guides show the common ones"
diff --git a/docs/coverage_report.py b/docs/coverage_report.py
index 16f4986..2c57163 100644
--- a/docs/coverage_report.py
+++ b/docs/coverage_report.py
@@ -7,9 +7,10 @@
none. The public names are those the API reference must document (the names of the
top level, ``cytepool.__all__``, and of a module's ``__all__``, or else the public
functions and classes it defines, as ``tests/test_documentation.py`` defines them) and
-the public methods of their classes. A name is covered when a guide or tutorial uses it (its code imports it, or
-its prose links to it) or an explanation links to it (``page_index.py``); index
-pages, such as the capability matrix and the glossary, do not count.
+the public methods of their classes. A name is covered when a guide or tutorial uses it
+(its code imports it, or its prose links to it) or an explanation links to it
+(``page_index.py``); index pages, such as the Validation page and the glossary, do not
+count.
``coverage_exemptions.toml`` lists the names that need no page, each with its reason
(``[exempt]``), and the names a page that is planned will cover, each with that page
diff --git a/docs/page_index.py b/docs/page_index.py
index 3017665..9495506 100644
--- a/docs/page_index.py
+++ b/docs/page_index.py
@@ -13,9 +13,9 @@
- The API reference lists under each entry the pages that use it and the pages that
explain it (``docs/source/reference/api/conf.py``). A guide or tutorial uses a
name when its code imports it from CytePool, or when its prose links to it with
- the role or a link to the API reference; an explanation (the pages of "How the
- statistics work" and the reference of cube files) explains the names its prose
- links to. Index pages, which list or point to many names (the overview,
+ the role or a link to the API reference; an explanation (the chapters of
+ "Understand the methods" and the reference of cube files) explains the names its
+ prose links to. Index pages, which list or point to many names (the overview,
installation, and the reference pages other than cube files), give no such links.
- ``coverage_report.py`` lists the public names that no guide, tutorial or
explanation uses or explains.
@@ -49,18 +49,20 @@
SOURCE = DOCS / "source"
API_SOURCE = SOURCE / "reference" / "api"
-# The top-level groups of the navigation, in order (myst.yml).
+# The top-level groups of pages in the navigation, in order (myst.yml). The API
+# reference follows them, as a link whose pages the site build adds.
SECTIONS = (
"Start here",
"Build and share cubes",
"Compare conditions",
- "Other designs and data",
- "How the statistics work",
+ "Combine studies",
+ "Understand the methods",
"Reference",
)
+API_REFERENCE = "API reference"
# Pages whose prose explains the names it links to: their links become "Explained
# in" on the API entries.
-EXPLANATION_SECTIONS = frozenset({"How the statistics work"})
+EXPLANATION_SECTIONS = frozenset({"Understand the methods"})
EXPLANATION_PAGES = frozenset({"reference/cube_files.md"})
# Pages that list or point to many names without using or explaining them: their
# links give no backlinks and do not count as coverage.
@@ -69,10 +71,8 @@
"index.md",
"installation.md",
"validation.md",
- "reference/capabilities.md",
"reference/datasets.md",
"reference/related_software.md",
- "reference/literature.md",
"reference/bibliography.md",
"reference/glossary.md",
"reference/citing.md",
diff --git a/docs/source/guides/anndata_and_mudata.ipynb b/docs/source/guides/anndata_and_mudata.ipynb
index fa6c7dd..79f0987 100644
--- a/docs/source/guides/anndata_and_mudata.ipynb
+++ b/docs/source/guides/anndata_and_mudata.ipynb
@@ -8,14 +8,16 @@
"(anndata-and-mudata)=\n",
"# Work with AnnData and MuData\n",
"\n",
- "Single-cell analyses in Python keep their cells in an AnnData [@virshup2024], and\n",
- "scverse tools keep pseudobulk profiles in one of two layouts: a long AnnData with a row per\n",
- "sample and cell type, as scanpy's `get.aggregate` and decoupler's `pp.pseudobulk` make\n",
- "it [@wolf2018; @badiaimompel2022], or a MuData with a modality per cell type, as LIANA\n",
- "makes it for its analyses of several views [@bredikhin2022; @dimitrov2024]. CytePool\n",
- "pools cells from an AnnData, reads and writes both layouts, and keeps a cube's\n",
- "fingerprint through the conversion, so that a cube can go to these tools and come back\n",
- "as the same cube. The conversions need CytePool's `anndata` or `mudata` extra\n",
+ "Move cubes between CytePool and the scverse tools: {api}`to_anndata` and\n",
+ "{api}`from_anndata` convert a cube to and from a long AnnData, and {api}`to_mudata` and\n",
+ "{api}`from_mudata` to and from a MuData, keeping its fingerprint, so that a cube comes\n",
+ "back as the same cube. Single-cell analyses in Python keep their cells in an AnnData\n",
+ "[@virshup2024], and scverse tools keep pseudobulk profiles in one of two layouts: a long\n",
+ "AnnData with a row per sample and cell type, as scanpy's `get.aggregate` and\n",
+ "decoupler's `pp.pseudobulk` make it [@wolf2018; @badiaimompel2022], or a MuData with a\n",
+ "modality per cell type, as LIANA makes it for its analyses of several views\n",
+ "[@bredikhin2022; @dimitrov2024]. CytePool pools cells from an AnnData too. The\n",
+ "conversions need CytePool's `anndata` or `mudata` extra\n",
"([Installation](../installation.md)); the cells are those of the simulated PBMC study\n",
"([Datasets](../reference/datasets.md)).\n",
"\n",
@@ -36,7 +38,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb, anndata 0.13.4\n",
+ "CytePool 0.1.dev70+g194507939, anndata 0.13.4\n",
"AnnData object with n_obs × n_vars = 19151 × 2000\n",
" obs: 'sample', 'donor', 'condition', 'cell_type'\n",
" layers: None (.X) \n",
@@ -296,12 +298,12 @@
"source": [
"## Profiles that scanpy, decoupler or LIANA pooled\n",
"\n",
- "{api}`from_anndata` recognises the layouts of `scanpy.get.aggregate` and\n",
+ "{api}`from_anndata` recognizes the layouts of `scanpy.get.aggregate` and\n",
"`decoupler.pp.pseudobulk`, and {api}`from_mudata` that of LIANA's views, and reads from\n",
"each the counts, the number of cells that detect each gene and the number of cells of\n",
"every profile. Each gives the counts, detecting cells and cell numbers that\n",
"{api}`from_cells` computes from the same cells, down to the fingerprint, as the\n",
- "[Validation](../validation.md#anndata-and-mudata) page shows for scanpy 1.12.4,\n",
+ "[Validation](#validation-anndata-conversion) page shows for scanpy 1.12.4,\n",
"decoupler 2.2.0 and LIANA 1.10.0:\n",
"\n",
"```python\n",
@@ -325,9 +327,11 @@
"```\n",
"\n",
"LIANA's views carry no cell numbers, so they come from the cells, as the crosstab of\n",
- "their samples and cell types. Means, variances and medians are refused: a cube keeps\n",
- "sums, and every tool that gives means gives sums too, with `func=\"sum\"` in scanpy and\n",
- "`mode=\"sum\"`, its default, in decoupler.\n",
+ "their samples and cell types. Means, variances and medians are refused, since a cube\n",
+ "keeps sums ([Profiles pooled by other\n",
+ "tools](../statistics/cubes.md#profiles-from-other-tools)), and every tool that gives\n",
+ "means gives sums too, with `func=\"sum\"` in scanpy and `mode=\"sum\"`, its default, in\n",
+ "decoupler.\n",
"\n",
"## Choices\n",
"\n",
@@ -352,7 +356,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:22:22+00:00",
+ "executed_at": "2026-10-28T00:18:28+00:00",
"executed_code_sha256": "f32dc3e2bab44d675e06640817d7ca96a07d0cd1479cb82544982ded068997cc",
"executed_datasets": {
"simulated_pbmc": {
@@ -360,8 +364,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 2.5,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 3.0,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/bulk_rna_seq.ipynb b/docs/source/guides/bulk_rna_seq.ipynb
index 758241a..e867727 100644
--- a/docs/source/guides/bulk_rna_seq.ipynb
+++ b/docs/source/guides/bulk_rna_seq.ipynb
@@ -8,12 +8,15 @@
"(bulk-rna-seq)=\n",
"# Bulk RNA-seq\n",
"\n",
- "A bulk RNA-seq experiment is a matrix of counts, samples x genes. CytePool tests it\n",
- "with the methods of limma, edgeR and DESeq2, each checked against the R package\n",
- "([Validation](../validation.md)). This page runs the three on one simulated\n",
- "experiment whose true changes are known, and compares their calls with the truth.\n",
- "[Choosing a method](../statistics/choosing_a_method.md) explains how they differ,\n",
- "and the table at the end of the page gives the R function of each step.\n",
+ "Test which genes change in a bulk RNA-seq experiment with {api}`limma_voom` on a cube of\n",
+ "one group, which {api}`from_bulk` makes from a samples x genes matrix of counts: the\n",
+ "analysis of pseudobulk profiles, applied to bulk samples. edgeR's quasi-likelihood\n",
+ "F-tests and DESeq2, which model the counts themselves, are the alternatives, each\n",
+ "checked against its R package ([Validation](../validation.md)). This page runs the\n",
+ "three on one simulated experiment whose true changes are known, and compares their\n",
+ "calls with the truth. [One matrix of\n",
+ "counts](../statistics/count_models.md#one-matrix-of-counts) says how the methods\n",
+ "differ, and the table at the end of the page gives the R function of each step.\n",
"\n",
"The experiment is the simulated bulk dataset ([Datasets](../reference/datasets.md)):\n",
"three treatments (control, drug A and drug B) with four replicates each, sequenced in\n",
@@ -32,7 +35,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb, numpy 2.5.4\n",
+ "CytePool 0.1.dev70+g194507939, numpy 2.5.4\n",
"Cube with 12 samples x 1 groups x 10,000 features\n",
" layers: counts (uint32, lazy)\n",
" cells per entry: not recorded\n",
@@ -118,10 +121,11 @@
"\n",
"{api}`limma_voom` runs on the cube's one group: genes are filtered by expression, TMM\n",
"factors normalize the libraries, voom gives every observation a precision weight,\n",
- "and the contrasts are moderated by empirical Bayes. `quality_weights=True` also\n",
- "estimates a weight for every sample from how closely it follows the model\n",
- "([Precision weights](../statistics/precision_weights.md)). The noisy sample gets a\n",
- "weight well below the others, so it counts less instead of being removed."
+ "and the contrasts are moderated by empirical Bayes ([What runs in each cell\n",
+ "type](../statistics/limma_voom.md#limma-voom-steps)). `quality_weights=True` also\n",
+ "estimates a weight for every sample from how closely it follows the model ([Sample\n",
+ "quality weights](../statistics/limma_voom.md#sample-quality-weights)). The noisy sample\n",
+ "gets a weight well below the others, so it counts less instead of being removed."
]
},
{
@@ -165,7 +169,26 @@
"id": "cafc47eb",
"metadata": {},
"source": [
- "## edgeR's quasi-likelihood F-tests and DESeq2\n",
+ "## Choices\n",
+ "\n",
+ "- **limma-voom, edgeR or DESeq2.** {api}`limma_voom` by default, whose tables carry the\n",
+ " moderated standard errors that a [meta-analysis](../statistics/meta_analysis.md)\n",
+ " combines across studies; edgeR's quasi-likelihood F-tests for the stricter error\n",
+ " control that edgeR's User's Guide describes; DESeq2 with its Wald tests, or its\n",
+ " likelihood ratio test of a reduced design, such as the batch alone here ([One matrix\n",
+ " of counts](../statistics/count_models.md#one-matrix-of-counts)). The three are\n",
+ " compared below.\n",
+ "- **Sample quality weights.** `quality_weights=True`, as above, when a sample may be of\n",
+ " poor quality; [Normalization, filtering and sample\n",
+ " quality](normalization_and_quality.ipynb) compares the calls with and without them.\n",
+ "- **The genes and the factors of edgeR and DESeq2.** {api}`fit_quasi_likelihood` takes\n",
+ " the counts it is given, so filter them and pass TMM factors, as below;\n",
+ " {api}`fit_deseq2` takes every gene, with DESeq2's size factors ([Defaults and\n",
+ " choices](../statistics/count_models.md#count-models-defaults)).\n",
+ "- **The steps one by one.** The functions that {api}`limma_voom` calls also run alone\n",
+ " on a matrix of counts, as the table at the end of the page lists them.\n",
+ "\n",
+ "### edgeR's quasi-likelihood F-tests and DESeq2\n",
"\n",
"{api}`fit_quasi_likelihood` and {api}`quasi_likelihood_f_test` take the counts of the\n",
"genes that pass edgeR's filter ({api}`filter_by_expression`) with TMM factors\n",
@@ -236,9 +259,9 @@
"edgeR squeezes each gene's quasi-dispersion towards a prior trend that changes little\n",
"with the expression level. DESeq2 shrinks each gene's dispersion towards a fitted trend\n",
"that falls with the mean count to about 0.07, except for 64 dispersion outliers, whose\n",
- "own estimates lie far above the trend and are kept.\n",
+ "own estimates lie far above the trend and are kept ([DESeq2](../statistics/count_models.md#deseq2)).\n",
"\n",
- "## Calls compared with the truth\n",
+ "### Calls compared with the truth\n",
"\n",
"At a false discovery rate of 5%, each method's calls are compared with the genes the\n",
"simulation changed: how many it called, how many of those truly changed, and the\n",
@@ -262,13 +285,7 @@
"drug_a_vs_control limma-voom 355 337 5.1%\n",
"drug_a_vs_control edgeR QL 320 312 2.5%\n",
"drug_a_vs_control DESeq2 Wald 360 338 6.1%\n",
- " called by all 320\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ " called by all 320\n",
"drug_b_vs_control limma-voom 204 192 5.9%\n",
"drug_b_vs_control edgeR QL 193 184 4.7%\n",
"drug_b_vs_control DESeq2 Wald 238 203 14.7%\n",
@@ -317,11 +334,7 @@
"fold change of about 1.4, are hard to detect with four replicates. In this\n",
"simulation DESeq2's Wald test made more false calls for drug B than the 5% that the\n",
"adjustment allows on average, also when the noisy sample is left out; one simulated\n",
- "data set is no benchmark, and the [Literature](../reference/literature.md) lists\n",
- "comparisons on many. Which method to prefer depends mostly on the design and on what\n",
- "comes next: limma's tables carry moderated standard errors that a\n",
- "[meta-analysis](../statistics/meta_analysis.md) combines across studies, and DESeq2's\n",
- "likelihood ratio test asks about several coefficients at once."
+ "data set is no benchmark."
]
},
{
@@ -364,7 +377,7 @@
"datasets": [
"simulated_bulk"
],
- "executed_at": "2026-10-27T15:22:26+00:00",
+ "executed_at": "2026-10-28T00:18:57+00:00",
"executed_code_sha256": "779df2750fcccb5965d8884cb46cab035b3f0f36241ad2a00db4bc70fe7e1d00",
"executed_datasets": {
"simulated_bulk": {
@@ -372,8 +385,8 @@
"simulated_bulk_truth.npz": "sha256:a8a0275532e3cafb02401846176a7ddfa037579aceb804c5f9c007700f022977"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 4.6,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 4.3,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/combine_studies.ipynb b/docs/source/guides/combine_studies.ipynb
index a12b07c..6ede0b3 100644
--- a/docs/source/guides/combine_studies.ipynb
+++ b/docs/source/guides/combine_studies.ipynb
@@ -6,17 +6,17 @@
"metadata": {},
"source": [
"(combine-studies)=\n",
- "# Combine studies\n",
+ "# Align and combine studies\n",
"\n",
- "Cohorts of one disease ask the same question of different donors, and each answers it\n",
- "with its own samples, genes and names of cell types. To answer it with all of them,\n",
- "align the cohorts' cubes in a {api}`CubeCollection`, analyse each cohort on its own,\n",
- "and combine the results gene by gene by meta-analysis, with the forest plots and the\n",
- "heterogeneity that metafor reports [@viechtbauer2010], or stack the cohorts into one\n",
- "cube for a joint model.\n",
- "[Meta-analysis](../statistics/meta_analysis.md) gives the statistics, and\n",
- "[Replication across studies](replication_across_studies.ipynb) asks which changes\n",
- "replicate.\n",
+ "Combine the results of several studies of one question with {api}`meta_analysis`,\n",
+ "which pools each gene's estimates from every study by a random-effects model and\n",
+ "measures how far the studies disagree, as metafor does [@viechtbauer2010]. A\n",
+ "{api}`CubeCollection` first aligns the studies' cubes, and {api}`analyse_each_study`\n",
+ "tests each study on its own. This page combines six cohorts of one disease, which ask\n",
+ "the same question of different donors, each with its own samples, genes and names of\n",
+ "cell types. [Aligning studies](../statistics/aligning_studies.md) and\n",
+ "[Meta-analysis](../statistics/meta_analysis.md) give the statistics, and [Replication\n",
+ "across studies](replication_across_studies.ipynb) asks which changes replicate.\n",
"\n",
"The cohorts are simulated, so that the truth is known: the `simulated_cohorts`\n",
"dataset of the [Datasets](../reference/datasets.md) page, 164 donors in six\n",
@@ -38,7 +38,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb, numpy 2.5.4\n",
+ "CytePool 0.1.dev70+g194507939, numpy 2.5.4\n",
"cohort_1: 60 donors (30 cases), 2,000 genes\n",
" B cells, CD4 T cells, NK cells, CD14 monocytes\n",
"cohort_2: 32 donors (16 cases), 2,000 genes\n",
@@ -95,16 +95,16 @@
"measure, which its feature column `measured` marks: a zero there is not a count. And\n",
"every cohort names its cell types in its own words.\n",
"\n",
- "{api}`CubeCollection` aligns them. It names every sample `study::sample_id`, aligns\n",
- "the genes by `feature_id` and records which cohort measured which\n",
- "({api}`CubeCollection.measured`), and aligns the cell types by their Cell Ontology\n",
- "ids [@diehl2016], which every cube's group table holds in `cell_ontology_id`:\n",
- "CL:0000236 (B cell), CL:0000624 (CD4-positive, alpha-beta T cell), CL:0000623\n",
- "(natural killer cell) and CL:0001054 (CD14-positive monocyte). cohort_4 annotated\n",
- "its CD4 T cells more finely, as naive (CL:0000895) and memory (CL:0000897) cells,\n",
- "which the ontology places under CL:0000624; `group_mapping` rolls them up to it, and\n",
- "the collection merges the two, exactly, since a cube's layers are sums\n",
- "([The cube and its additive layers](../statistics/cubes.md#several-studies)).\n",
+ "{api}`CubeCollection` aligns them ([Samples and\n",
+ "donors](../statistics/aligning_studies.md#aligning-samples) and the sections after it).\n",
+ "It names every sample `study::sample_id`, aligns the genes by `feature_id` and records\n",
+ "which cohort measured which ({api}`CubeCollection.measured`), and aligns the cell types\n",
+ "by their Cell Ontology ids [@diehl2016], which every cube's group table holds in\n",
+ "`cell_ontology_id`: CL:0000236 (B cell), CL:0000624 (CD4-positive, alpha-beta T cell),\n",
+ "CL:0000623 (natural killer cell) and CL:0001054 (CD14-positive monocyte). cohort_4\n",
+ "annotated its CD4 T cells more finely, as naive (CL:0000895) and memory (CL:0000897)\n",
+ "cells, which the ontology places under CL:0000624; `group_mapping` rolls them up to\n",
+ "it, and the collection merges the two, exactly, since a cube's layers are sums.\n",
"`group_names` names the aligned cell types for the tables and figures that follow,\n",
"such as CD14 monocytes for CL:0001054, and the collection's group table keeps their\n",
"ids in `cell_ontology_id`."
@@ -280,12 +280,12 @@
"\n",
"## Meta-analysis\n",
"\n",
- "{api}`meta_analysis` combines, for every cell type and gene,\n",
- "the log2 fold changes of the cohorts that tested it, each with its standard error,\n",
- "into one estimate. By default it fits the random-effects model, which lets the\n",
- "cohorts' true effects vary around their mean by the between-study variance τ²\n",
- "[@dersimonian1986], estimated by restricted maximum likelihood (REML) as metafor\n",
- "estimates it by default, and tests the mean with a z-test. A gene that two cohorts or\n",
+ "{api}`meta_analysis` combines, for every cell type and gene, the log2 fold changes of\n",
+ "the cohorts that tested it, each with its standard error, into one estimate. By\n",
+ "default it fits the random-effects model, whose between-study variance τ² it\n",
+ "estimates by restricted maximum likelihood (REML), as metafor does by default, and\n",
+ "tests the mean with a z-test ([The\n",
+ "model](../statistics/meta_analysis.md#meta-analysis-model)). A gene that two cohorts or\n",
"more tested is analysed over those cohorts, so the genes that cohort_3 and cohort_5\n",
"did not measure are analysed over the other cohorts. {api}`plot_volcano` draws the\n",
"monocytes' table."
@@ -404,14 +404,41 @@
"In the upper plot the cohorts agree: every estimate lies below zero, I² is 0%, and\n",
"the pooled log2 fold change, -0.96, has a narrow interval; cohort_3 did not measure\n",
"the gene and keeps its row, marked not measured. In the lower one, cohort_1 changes the\n",
- "gene by 1.47 and the other cohorts' estimates lie around zero. Cochran's Q\n",
- "[@cochran1954] finds the cohorts far more different than their standard errors allow,\n",
- "and I² [@higgins2002] puts 91% of the variation down to heterogeneity: the\n",
- "random-effects model widens the pooled interval to [-0.43, 0.74] and does not find the\n",
- "gene. The fixed-effect model gives cohort_1, the most precise cohort, the largest\n",
- "weight and finds it, with an adjusted p-value of 9e-08.\n",
+ "gene by 1.47 and the other cohorts' estimates lie around zero. Cochran's Q finds the\n",
+ "cohorts far more different than their standard errors allow, and I² puts 91% of the\n",
+ "variation down to heterogeneity ([Tests and\n",
+ "heterogeneity](../statistics/meta_analysis.md#heterogeneity)): the random-effects model\n",
+ "widens the pooled interval to [-0.43, 0.74] and does not find the gene. The\n",
+ "fixed-effect model gives cohort_1, the most precise cohort, the largest weight and\n",
+ "finds it, with an adjusted p-value of 9e-08.\n",
"\n",
- "## Stack the cohorts into one cube\n",
+ "## Choices\n",
+ "\n",
+ "- **Meta-analysis or a joint model.** A meta-analysis of each cohort's results, the\n",
+ " default, needs only the results, keeps the genes that some cohorts did not measure,\n",
+ " lets each cohort keep its own design, normalization and variances, and measures how\n",
+ " far the cohorts disagree. A joint model of the stacked cohorts suits cohorts made\n",
+ " alike, with one protocol and platform, that are too small to estimate their\n",
+ " variances alone ([A joint model of stacked\n",
+ " studies](../statistics/aligning_studies.md#stacked-studies)). Both are compared below.\n",
+ "- **Fixed or random effects.** The random-effects model by default; the fixed-effect\n",
+ " model (`method=\"fixed\"`) answers for the cohorts at hand and assumes that they\n",
+ " estimate one effect, as above. τ² can also be estimated by DerSimonian and Laird's\n",
+ " method (`\"dl\"`) or Paule and Mandel's (`\"pm\"`), and `test=\"knapp_hartung\"` gives the\n",
+ " t-tests that metafor's documentation recommends for few studies ([Defaults and\n",
+ " choices](../statistics/meta_analysis.md#meta-analysis-defaults)).\n",
+ "- **Genes that some cohorts did not test.** `min_studies`, two by default, sets how\n",
+ " many cohorts must test a gene; {api}`StudyResults.table` says whether a cohort did\n",
+ " not measure a gene or left it out.\n",
+ "- **Cell types and ids.** Cell types are aligned by `cell_ontology_id` when every\n",
+ " cohort has it, by `group_id` otherwise, or by another column (`align_groups_by`);\n",
+ " `group_mapping` names the aligned id of any group, or None to leave it out, and\n",
+ " `group_names` gives aligned cell types the names that tables and figures show.\n",
+ " Columns of ids other than `sample_id`, such as donors with several samples, are kept\n",
+ " apart by cohort with `within_study` ([Defaults and\n",
+ " choices](../statistics/aligning_studies.md#aligning-studies-defaults)).\n",
+ "\n",
+ "### A joint model of the stacked cohorts\n",
"\n",
"{api}`CubeCollection.stack` puts the cohorts into one cube for a joint model, with\n",
"every cohort's samples and the cohort as a column of the sample table. A cube holds\n",
@@ -463,7 +490,7 @@
"id": "f905689f",
"metadata": {},
"source": [
- "## The approaches against the truth\n",
+ "### The approaches against the truth\n",
"\n",
"The simulation knows which genes each approach should find. The monocyte genes found\n",
"at a false discovery rate of 5% by cohort_1 alone, by the meta-analyses and by the\n",
@@ -542,31 +569,7 @@
"change in one cohort alone makes the cohorts disagree, which raises τ² and widens the\n",
"interval. The joint model assumes one change for every cohort, as the fixed effect\n",
"does, and finds 76 and 18; it tests only the genes that every cohort measured, which\n",
- "hold 82 of the 100.\n",
- "\n",
- "## Choices\n",
- "\n",
- "- **Meta-analysis or a joint model.** A meta-analysis needs only each cohort's\n",
- " results, keeps the genes some cohorts did not measure, lets each cohort keep its own\n",
- " design, normalization and variances, and measures how far the cohorts disagree. A\n",
- " joint model of the stacked cohorts suits cohorts made alike, with one protocol and\n",
- " platform, that are too small to estimate their variances alone; it estimates one\n",
- " difference for every cohort, so a change in one cohort enters the estimate for all.\n",
- "- **Fixed or random effects.** The fixed-effect model (`method=\"fixed\"`) answers for\n",
- " the cohorts at hand and assumes they estimate one effect; the random-effects model,\n",
- " the default, allows the effect to vary between cohorts. τ² can also be estimated by\n",
- " DerSimonian and Laird's method (`\"dl\"`) or Paule and Mandel's (`\"pm\"`), and\n",
- " `test=\"knapp_hartung\"` gives the t-tests that metafor's documentation recommends for\n",
- " few studies ([Meta-analysis](../statistics/meta_analysis.md#tests-and-heterogeneity)).\n",
- "- **Genes that some cohorts did not test.** `min_studies`, two by default, sets how\n",
- " many cohorts must test a gene; {api}`StudyResults.table` says whether a cohort did\n",
- " not measure a gene or left it out.\n",
- "- **Cell types and ids.** Cell types are aligned by `cell_ontology_id` when every\n",
- " cohort has it, by `group_id` otherwise, or by another column (`align_groups_by`);\n",
- " `group_mapping` names the aligned id of any group, or None to leave it out, and\n",
- " `group_names` gives aligned cell types the names that tables and figures show.\n",
- " Columns of ids other than `sample_id`, such as donors with several samples, are kept\n",
- " apart by cohort with `within_study`."
+ "hold 82 of the 100."
]
}
],
@@ -574,7 +577,7 @@
"datasets": [
"simulated_cohorts"
],
- "executed_at": "2026-10-27T15:22:31+00:00",
+ "executed_at": "2026-10-28T00:19:03+00:00",
"executed_code_sha256": "7ec0f3c884e25df0241e73a4ad5947bd42ebd122147b223ce5086e262df69038",
"executed_datasets": {
"simulated_cohorts": {
@@ -587,8 +590,8 @@
"simulated_cohorts_truth.npz": "sha256:6599ea11dddb183f630183846f7ece9991003a4cc77875e4e7822317f66346ec"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 4.4,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 5.9,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/composition.ipynb b/docs/source/guides/composition.ipynb
index 91e9db9..b218af2 100644
--- a/docs/source/guides/composition.ipynb
+++ b/docs/source/guides/composition.ipynb
@@ -8,14 +8,14 @@
"(composition)=\n",
"# Cell-type composition\n",
"\n",
- "Does a condition change which cells a sample holds? {api}`propeller` tests the\n",
- "proportion of each cell type across samples, as speckle does [@phipson2022], from the\n",
- "number of cells that a cube records for every sample and cell type\n",
- "({api}`Cube.n_cells`); [Composition tests](../statistics/composition.md) gives the\n",
- "steps. The simulated PBMC study knows the answer: 12 donors, each sampled before and\n",
- "after a stimulation that multiplies the ratio of monocytes to every other cell type by\n",
- "$e^{0.7}$, about 2, and changes nothing else (the `simulated_pbmc` dataset of the\n",
- "[Datasets](../reference/datasets.md) page)."
+ "Test whether a condition changes the share of each cell type in the samples with\n",
+ "{api}`propeller`, as speckle does [@phipson2022], from the numbers of cells that a cube\n",
+ "records for every sample and cell type. The simulated PBMC study\n",
+ "knows the answer: 12 donors, each sampled before and after a stimulation that\n",
+ "multiplies the ratio of monocytes to every other cell type by $e^{0.7}$, about 2, and\n",
+ "changes nothing else (the `simulated_pbmc` dataset of the\n",
+ "[Datasets](../reference/datasets.md) page). [Composition\n",
+ "tests](../statistics/composition.md#propeller-steps) gives propeller's steps."
]
},
{
@@ -24,7 +24,7 @@
"id": "e011eb6f",
"metadata": {
"figure_descriptions": [
- "Stacked bars of the proportions of six cell types in each of the 24 samples, the 12 control samples on the left and the 12 stimulated ones on the right, B cells at the top and dendritic cells at the bottom. Monocytes (pink) take about 0.2 to 0.37 of each control sample and about 0.35 to 0.57 of each stimulated one, and every other cell type takes a little less after the stimulation."
+ "Stacked bars of the proportions of six cell types in each of the 24 samples, the 12 control samples on the left and the 12 stimulated ones on the right, B cells at the top and dendritic cells at the bottom. Monocytes (pink) take about 0.2 to 0.37 of each control sample and about 0.32 to 0.56 of each stimulated one, and every other cell type takes a little less after the stimulation."
]
},
"outputs": [
@@ -32,7 +32,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Cube with 24 samples x 6 groups x 2,000 features\n",
" layers: counts (uint32, lazy), detected (uint32, lazy), log_sum (float64,\n",
" lazy), log_sum_sq (float64, lazy), linear_sum (float64, lazy)\n",
@@ -76,7 +76,10 @@
"\n",
"Each donor gives a sample in each condition, so the design pairs them, as for the\n",
"genes ([Genes that change in each cell type](genes_per_cell_type.ipynb)); propeller\n",
- "fits fixed effects, and the donor enters the formula."
+ "fits fixed effects, and the donor enters the formula. It reads the cube's cell numbers\n",
+ "({api}`Cube.n_cells`), and the table of a contrast gives each cell type's baseline\n",
+ "proportion, its mean proportion in each condition, their ratio and the moderated\n",
+ "t-test."
]
},
{
@@ -186,10 +189,30 @@
"Within the lymphocytes nothing changes, as simulated: the rise of the monocytes\n",
"lowered the lymphocytes' share of all cells, not their make-up.\n",
"\n",
- "## The design and the transform\n",
+ "## Choices\n",
"\n",
- "Without the pairing, the differences between donors' compositions count as noise.\n",
- "The arcsine square root is the alternative transform to the logit."
+ "- **The design.** Pair the samples of a donor when each donor gives one in every\n",
+ " condition; propeller refuses random effects, so the donor is a fixed effect. Without\n",
+ " the pairing, the differences between donors' compositions count as noise, as below.\n",
+ "- **The denominator.** Proportions are shares of the cell types tested: a change in one\n",
+ " cell type moves the shares of all the others. Test the cell types of one lineage\n",
+ " with {api}`Cube.select` when the question is about that lineage, as above, and read\n",
+ " the proportion ratios of the result, not only its p-values.\n",
+ "- **The transform.** The logit, speckle's default, or `transform=\"asin\"`; they agree\n",
+ " on clear changes, as below.\n",
+ "- **Robust moderation.** On by default, as in speckle. It changes the prior only with\n",
+ " 10 cell types or more, so with the six cell types here the results are those of\n",
+ " `robust=False` ([Moderation across cell\n",
+ " types](../statistics/composition.md#composition-moderation)).\n",
+ "- **Several conditions.** `f_tests=\"condition\"` tests every level of the condition at\n",
+ " once, and {api}`plot_proportions` then shows the F-test's adjusted p-values.\n",
+ "- **Not yet available.** Precision-weighted mixed models and count models of\n",
+ " composition ([Composition tests](../statistics/composition.md#composition-limits)).\n",
+ "\n",
+ "### The design and the transform\n",
+ "\n",
+ "The paired design and the logit, the defaults above, against the arcsine square root and\n",
+ "a design without the pairing:"
]
},
{
@@ -202,7 +225,14 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "adjusted p-values of the stimulation\n",
+ "adjusted p-values of the stimulation"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
" paired, logit paired, asin unpaired, logit\n",
"B cells 6.4e-05 3.8e-06 0.083\n",
"CD4 T cells 1.7e-07 6.1e-16 0.05\n",
@@ -243,22 +273,7 @@
"Pairing the samples matters more than the transform. Without it, the monocytes still\n",
"stand out, but the falls of the other cell types are no longer clear, with adjusted\n",
"p-values from 0.05 to 0.22. The arcsine square root gives smaller p-values than the\n",
- "logit here, and the same calls.\n",
- "\n",
- "## Choices\n",
- "\n",
- "- **The design.** Pair the samples of a donor when each donor gives one in every\n",
- " condition; propeller refuses random effects, so the donor is a fixed effect.\n",
- "- **The denominator.** Proportions are shares of the cell types tested: a change in one\n",
- " cell type moves the shares of all the others. Test the cell types of one lineage\n",
- " with {api}`Cube.select` when the question is about that lineage, and read the\n",
- " proportion ratios of the result, not only its p-values.\n",
- "- **The transform.** The logit, speckle's default, or `transform=\"asin\"`; they agree\n",
- " on clear changes.\n",
- "- **Several conditions.** `f_tests=\"condition\"` tests every level of the condition at\n",
- " once, and {api}`plot_proportions` then shows the F-test's adjusted p-values.\n",
- "- **Not yet available.** Precision-weighted mixed models and count models of\n",
- " composition ([Composition tests](../statistics/composition.md#limits))."
+ "logit here, and the same calls."
]
}
],
@@ -266,7 +281,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:22:34+00:00",
+ "executed_at": "2026-10-28T00:18:44+00:00",
"executed_code_sha256": "c54aa139f6b76b7f267fca319c49247fe16f21cef0bf07a39883db3a51b7c0f3",
"executed_datasets": {
"simulated_pbmc": {
@@ -274,8 +289,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 2.0,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 2.1,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/export_cubes.ipynb b/docs/source/guides/export_cubes.ipynb
index 43ede5a..310b24d 100644
--- a/docs/source/guides/export_cubes.ipynb
+++ b/docs/source/guides/export_cubes.ipynb
@@ -8,12 +8,13 @@
"(export-cubes)=\n",
"# Export a cube from another tool\n",
"\n",
- "A tool that holds cells, such as an analysis platform or an atlas, can give its users\n",
- "CytePool's tests without moving the cells: it hands over its pseudobulk profiles as a\n",
- "cube file, with the sums of every sample, cell type and gene, the tables that describe\n",
- "them, the number of cells pooled into each profile, and a record of where they came\n",
- "from. This page builds a cube from sums computed outside CytePool, saves it, and checks\n",
- "it against CytePool's own pooling. [Cube files and provenance\n",
+ "Export the pseudobulk profiles of another tool as a cube file: build a {api}`Cube` from\n",
+ "the sums that the tool computed, with a record of where they came from, and save it\n",
+ "with {api}`write_cube`. A tool that holds cells, such as an analysis platform or an\n",
+ "atlas, then gives its users CytePool's tests without moving the cells: the file holds\n",
+ "the sums of every sample, cell type and gene, the tables that describe them and the\n",
+ "number of cells pooled into each profile. This page builds a cube from sums computed\n",
+ "outside CytePool, saves it, and checks it against CytePool's own pooling. [Cube files and provenance\n",
"records](../reference/cube_files.md) describes the file and the record in full, and\n",
"[Save, share and trace cubes](save_and_trace_cubes.ipynb) what its users can then do\n",
"with it.\n",
@@ -34,7 +35,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb, numpy 2.5.4\n",
+ "CytePool 0.1.dev70+g194507939, numpy 2.5.4\n",
"19,151 cells x 2,000 genes in 24 samples\n",
"cell types: B cells, CD4 T cells, CD8 T cells, NK cells, Monocytes, Dendritic cells\n"
]
@@ -294,8 +295,8 @@
"- **The layers.** The `counts` and the cell numbers are what the tests need. The\n",
" number of cells with a positive count (`detected`) and the sums of the cells'\n",
" log-normalized values (`log_sum`) give the dot plot, and layers of normalized values\n",
- " need a `normalization` record that says how the values were computed ([The cube and\n",
- " its additive layers](../statistics/cubes.md)).\n",
+ " need a `normalization` record that says how the values were computed ([The\n",
+ " layers](../statistics/cubes.md#cube-layers)).\n",
"- **The source record.** Record what a reader needs to find the cells again: the tool\n",
" and its version, the dataset, the selection of cells, the annotations that define the\n",
" samples and cell types, and the settings ([Cube files and provenance\n",
@@ -313,7 +314,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:22:37+00:00",
+ "executed_at": "2026-10-28T00:18:24+00:00",
"executed_code_sha256": "85eb2855e72c9727d4b113fe91ac2ee06939e02febe917bbe62cfd7f7d2906b2",
"executed_datasets": {
"simulated_pbmc": {
@@ -321,7 +322,7 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
+ "executed_with": "0.1.dev70+g194507939",
"execution_seconds": 2.9,
"kernelspec": {
"display_name": "Python 3",
diff --git a/docs/source/guides/gene_sets.ipynb b/docs/source/guides/gene_sets.ipynb
index 87874df..57cae96 100644
--- a/docs/source/guides/gene_sets.ipynb
+++ b/docs/source/guides/gene_sets.ipynb
@@ -8,15 +8,16 @@
"(gene-sets)=\n",
"# Gene sets and pathways\n",
"\n",
- "Which pathways respond to a treatment? A gene-set test asks it of a set of genes as a\n",
- "whole, for one contrast, in one of two ways: are the set's genes differentially\n",
- "expressed at all (a self-contained test), or more than the other genes (a competitive\n",
- "test) [@goeman2007]. [Gene-set tests](../statistics/gene_set_tests.md) explains each\n",
- "test. This page asks both of the CD14+ monocytes of the Kang data [@kang2018], first\n",
- "for one set chosen in advance, the Hallmark interferon-alpha response of MSigDB\n",
- "[@liberzon2015], then for a collection of sets (the `kang2018` and\n",
+ "Test which pathways respond to a treatment with {api}`camera`, which asks of every set\n",
+ "of a collection whether its genes change more than the other genes, and allows for the\n",
+ "correlation between them [@wu2012]; {api}`gsea` asks it along a ranking of every gene,\n",
+ "as fgsea does [@korotkevich2021]. This page applies both to the CD14+ monocytes of the\n",
+ "Kang data [@kang2018], with the targets of each transcription factor of CollecTRI as\n",
+ "the sets, then tests one set chosen in advance, the Hallmark interferon-alpha response\n",
+ "of MSigDB [@liberzon2015], and the genes called significant (the `kang2018` and\n",
"`hallmark_interferon_alpha` datasets of the [Datasets](../reference/datasets.md)\n",
- "page).\n",
+ "page). [Gene-set tests](../statistics/gene_set_tests.md) and [Over-representation and\n",
+ "GSEA](../statistics/over_representation_and_gsea.md) explain the tests.\n",
"\n",
"camera, fry and roast work on the linear model of one cell type, so they take a\n",
"result of {api}`limma_voom` and read that cell type's values, weights, design and\n",
@@ -33,8 +34,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
- "92 of the set's 97 genes are among the 5,125 genes tested\n"
+ "CytePool 0.1.dev70+g194507939\n",
+ "5,125 genes tested in the CD14+ Monocytes\n"
]
}
],
@@ -75,71 +76,8 @@
"result = limma_voom(kang, design, groups=monocytes)\n",
"tested = result.table()[\"symbol\"] # the one cell type and contrast\n",
"\n",
- "with open(\"hallmark_interferon_alpha_response.json\") as file:\n",
- " hallmark = json.load(file)\n",
- "# Two genes of the set have earlier symbols in the annotation of these data.\n",
- "interferon = [hallmark[\"earlier_symbols\"].get(gene, gene) for gene in hallmark[\"genes\"]]\n",
"print(f\"CytePool {cytepool.__version__}\")\n",
- "print(\n",
- " f\"{np.isin(interferon, tested).sum()} of the set's {len(interferon)} genes are among the {len(tested):,} genes tested\"\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "1c6f5d90",
- "metadata": {},
- "source": [
- "## One set chosen in advance\n",
- "\n",
- "limma's barcode plot shows where the set's genes fall among all genes ranked by their\n",
- "moderated t-statistic, with a worm above that rises where the set is enriched."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "2265490a",
- "metadata": {
- "figure_descriptions": [
- "limma's barcode plot of the 92 tested genes of the Hallmark interferon-alpha response among the 5,125 genes of the CD14+ monocytes, ranked by moderated t-statistic from -28 on the left to 39 on the right, over a bed shaded blue and red at the two ends. Three of the set's genes fall among the lowest statistics and nearly all the others among the highest, at the right end, where the enrichment worm rises from below 1 to 6.7."
- ]
- },
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAscAAAFLCAYAAAA6bCEQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjIsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvgI3uAAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAObFJREFUeJzt3Xd8FHX+x/H3brJpm0YJHQFpp4AiIHpnARWsyA89PERQVPAUPIUfNs4KyonYTnx4J4LIWVDOfvJDTkFRQUWlKNIUKUlQagLp2Wz5/v7YyZBNAiRLYFNez8djH5v5zndmPkOWzXtnvrPjMMYYAQAAAJAz0gUAAAAAtQXhGAAAALAQjgEAAAAL4RgAAACwEI4BAAAAC+EYAAAAsBCOAQAAAEt0pAuIhEAgoN9++01JSUlyOByRLgcAAABHwRijvLw8tWrVSk7n0R37DSscd+/eXevWrav2vNrit99+U9u2bSNdBgAAAGpQZmam2rRpc1TrcIRzhzyHw6HKFvP7/YqLi5PX6z2qoo61nJwcpaamKjMzU8nJyZEuBwAAAEchNzdXbdu21YEDB5SSknJU66rWkePly5dX+rMUHKrw9ddfq127dkdV0PFQOpQiOTmZcAwAAFBP1MRw2WqF43POOafSn0uLad26tZ555pmjLgoAAACIhGqF46KiIklS06ZNtW/fvtAVRUcrOrpBXt8HAACAeqJaaTYuLk6SlJ+ff0yKAQAAACIp7EO9u3bt0urVq5WdnV1h3siRI4+qKAAAACASwgrHb775pkaNGiVJlV4RWN1wbIzRli1blJCQoFatWh2275o1a+T3+yu0p6Wl1YmLAQEAAFB7hfVVbp06ddLYsWM1ceLEo74qcOHChRo3bpx8Pp8SEhLUtWtXvfLKK2rcuHGl/c8991wVFhba016vV2vXrtU999yjxx57rErbzM3NVUpKinJycvi2CgAAgDquJrNdWOE4KSlJu3fvVkJCwlFt/KuvvlK/fv00Y8YMjRs3TpL00UcfqVWrVurRo0eV1vHmm29q2LBhWr9+vU4++eQqLUM4BgAAqD8iHo7/8Ic/6Nlnn1WfPn2OauMDBw6UMUZLliwJex0XXXSRCgoKKnzv8uEQjgEAAOqP7A2r1KRbnxrJdmGNOb766qt1zTXXaMqUKerUqVOFoRVVCc0lJSX64osv9MQTT6ioqEhbt25Vq1at1KhRoyrXkZGRoSVLluill16q9j4AAACg7vP88Lly/nFnja0vrHA8fvx4SdI111xT6fyqHIzet2+fSkpK9PPPP6tjx45KTU3V9u3bNWDAAL3yyitKTU094jrmzp2rpKQkXXXVVYft5/F45PF47Onc3NwjrhsAAAC1m2ftF8qbPUmq5MsawuUMZ6H9+/cf9lEVUVFRkqQPPvhAK1eu1IYNG7Rt2zatX79ed9111xGXN8Zo7ty5GjFixBHHPk+bNk0pKSn2o23btlWqEQAAALWTZ+0Xypt1j+T31eh6wwrHqamph31URVpamhISEnTVVVfZX9/WvHlzDRs2TEuXLj3i8kuWLFF6erpuuummI/b961//qpycHPuRmZlZpRoBAABQ+5T8uCwkGMecdkGNrTuscCxJe/bs0QsvvKB77rnHbvvyyy/l81UtvTudTl1wwQXauXNnSPuuXbvUpEkTezo9PV3r16+vsPycOXPUp08f9ezZ84jbio2NVXJycsgDAAAAdU/Jj8uVWzYY97lQiSP+WmPrD2vM8erVqzVw4EC1bt1aP/74o6ZPny5JmjdvnjZu3KgxY8ZUaT1TpkzRueeeq2nTpumss87SN998o1dffVXz5s2z+zzyyCNasWKF1q1bZ7dlZWXp/fff17PPPhtO+QAAAKiD7DHGPq+kYDBOun6K8goKj7Bk1YV15PiOO+7QpEmTtHbt2pD2m2++Wc8880yV13Paaafp888/17p163TvvfdqzZo1+uijj/SnP/3J7tO+fXt17949ZLlly5apZ8+eGj58eDjlAwAAoI7xrPxYeS/cfTAY9x6opOunyBEV1rHeQwrre46Tk5P166+/KikpSU6nU4FAQJJUUFCgRo0aqaSkpEaLrGl8zzEAAEDdUfzVB8p/bapkxdbYPhcp8frJdjCuyWwX1pFjl8tV6dehbdy4UU2bNj2qggAAAIBSRUv/rfxXHzkYjM/6HyXeUPNHjEuFFY4vu+wyPfTQQ/L7/fYNQLZu3apbbrlFgwcPrtECAQAA0DAV/neuCt580p6OO+9qJY64Tw5n1DHbZljh+KmnntLKlSvVvHlzBQIBnXTSSerSpYv8fr+mTZtW0zUCAACgATHGqOD951T4n3/abfGX3Cj3VRMr3Jm5poV1PDotLU0rV67UggULtHLlSgUCAfXq1UtDhgyRy+Wq6RoBAADQQBi/T/nz/ibP1/9ntyVccZsSLrzuuGw/rAvy6jouyAMAAKh9jKdIubMnybv+q2CDwyH3sLsU3++qwy5Xk9nuqEYy5+TkVHq76Pbt2x/NagEAANDABPL2K/cfE+RL3xBsiHYp6YZHFNur5u5+VxVh3wRk1KhRITfmKKsBHowGAABAmPx7dyjnufEK7MmQJDniE5V8y5Nydel93GsJKxzfeOON6tOnj+bOnavU1NQaLgkAAAANhS99o3L+OUEmN1uS5ExJU/Jtzyq6daeI1BNWON60aZOWLVumpKSkmq4HAAAADYRnzafKm/ug5PVIkqJadFDybc8qqnGLiNUU1le5de3aVdu3b6/hUgAAANAQGGNU+NG/lDfrHjsYR3c8VSl3zo5oMJbCPHL8+OOP6/rrr9cDDzygjh07Vvi+ue7du9dIcQAAAKhfjM+r/NenyfP1Arsttu8lShx5vxyumAhWFhRWOC4qKtKmTZt0xRVXVDqfC/IAAABQXiD/gHJn3SPf5tV2W8Lltyj+khuP+c09qiqscDxx4kRdf/31uu2227ggDwAAAEfk27Vduf+cqMDezGCDK1ZJox5SbO+BkS2snLDC8e7duzV9+nQlJibWdD0AAACoZzw/fK78fz0kU1wgSXIkN1byLU/J1aH2DcUN64K8k046SZs3b67pWgAAAFCPmEBABf83S3kz77SDcVTrTkq9+1+1MhhLYR45vuKKKzRs2DBNmTJFnTp1qjBGpE+fPjVSHAAAAOqmQFG+8uc+qJIfl9ltMb0HKOnaB+WIjY9gZYfnMGFcPXekAdO1/YK8mrz/NgAAAEL5dm1X3sw75d+dHmxwOJUw5FbFD7z2mFx4V5PZLqwjx/v37z+qjQIAAKB+qjC+OCFZSaP/ppiTz4xwZVUTVjjmGyoAAABQlvH7VPjB8yr6+BW7Lap1ZyXf/Lii0tpEsLLqCSscS9KuXbu0evVqZWdnV5g3cuTIoyoKAAAAdYf/wB7lvXiffFu+t9tieg9U0rUP1OrxxZUJKxy/+eabGjVqlCQpJSWlwnzCMQAAQMNQsvEb5b10v0z+gWCDM0ruK29X3PnDa82NPaojrHB87733aurUqZo4cWKd3GkAAAAcHRPwq3DhiypaNEeyvozB2ai5ksZMk+vEHhGuLnxh3wRk7NixBGMAAIAGKJCbpbyXHpD3p+/sNlf3s5Q0arKciamRK6wGhHUTkB49emjDhg01XQsAAABqOe/m1Trw6MiDwdj6mrbksU/X+WAsVePI8cqVK+2fr776al1zzTXcBAQAAKCBMH6fChe9pKIP50gmIElypjRV0o1T5erSO8LV1Zwq3wSkOkMouAkIAABA/eHP2qm8uQ/It+UHu83V9XQl3fiInMlNIlhZUERuAsKNPwAAABoez6rFyp/3qExRfrDBGaWEy8Yo/uIb5HBGRba4Y6DK4ZgbfwAAADQcprhQ+W89Jc9XH9htziatlHTDw3J1PDWClR1bYV2Qt2fPHj3//PMV2p9//nnt3bv3qIsCAABA5PgyNunAtGtDgnFMnwuVet+8eh2MpTDD8fjx49WoUaMK7Y0aNdKECROOtiYAAABEgAkEVLjkNR14/Ab592QEG2PjlXjdQ0q6caqc8YmRLfA4qPIFeWWlpKQoIyOjwt3xcnJy1KFDh0pvKV2bcEEeAABAqEDOPuW9PEXejSvstuh2JynpxqmKanZCBCs7sohckFdWTEyMduzYUSEcZ2ZmyukM62A0AAAAIqRk3ZfKe3mKTP7BL2CIv/A6JVx+ixzRrghWdvyFlWQvu+wy3Xrrrdq1a5fdtnPnTo0bN06XXXZZjRUHAACAY8d4Pcp/8ynl/mOCHYydKU2VfPtzcl9xW4MLxlKYR44ff/xxXXDBBWrXrp06d+4sY4x++eUXdenSRW+//XZN1wgAAIAa5tu5TXkv3Sf/js12W0yPc5R47QNyJlW8tqyhCCscN2vWTKtWrdJ//vMfrV69Wg6HQ6eddpqGDBkil6vhfcIAAACoK4wx8ix/T/lvPS15PcHG6Bi5/zhecf2uqtaN3+qjsC7Iq+u4IA8AADREgYIc5b/2N5V8v9Rui2p5opJG/03RrTtFsLKjE5EL8j777DNJUv/+/e2fD6V///5HURIAAABqmvfnVcr714MK7N9jt8WdO1TuP46XIyYugpXVLlU+clx6iN0Yc8TD7bX9YDRHjgEAQENh/D4VLpytov/OlayM5nCnKPHaBxR7ar8IV1czInLk2Ov1VvozAAAAaif/3h3Km/ugfNt+tNtcXfoo8YYpikptFsHKaq8qf5VbdHS0oqODWXrmzJn2dGUPAAAARFbxt//VgUdHHgzGziglDLlVyeOfIxgfRlgX5MXExKioqEhRUVHHoqZjjmEVAACgvgoUF6hg/uPyfPOh3eZMa6OkG6fK1b5bBCs7dmoy24V1E5Du3bvru+++O6oNAwAAoGZ5t6/XgUdHhgTj2DMuU+q9r9XbYFzTwhoDMWrUKF199dW6//77dfLJJysmJiZkfp8+fWqkOAAAAByZCQRUtPgVFX4wUwr4JUmOOLfcwycpru/FEa6ubglrWAXfVgEAAFA7+A/sUf6/HpL3p5V2W3T77kq68RFFpbWJYGXHT0S+raKs/fv3H9VGAQAAcPQ8P3yu/FcfkSnICTY4HIq/6AYlDLpJjii+JCEcYf2rpaam1nAZAAAAqCrj86rg3WdVvHS+3eZs1ExJ1z8sV5feEays7gv7I8WuXbu0evVqZWdnV5g3cuTIoyoKAAAAlfPv3aG8OffKl77Rbovp2V+JI++X050Swcrqh7DC8ZtvvqlRo0ZJklJSKv4SCMcAAAA1z7P6k+AwiuKCYEN0jNxDJyju3KFHvCYMVRNWOL733ns1depUTZw4kV8EAADAMWa8HhW8/YyKv3jbbnM2O0HJYx5VdNuuEays/gkrHO/evVtjx44lGAMAABxj/j0Zyn3xXvkzf7LbYvtcJPeIv8oZ545gZfVTWOG4R48e2rBhA99nDAAAcAx5vvtY+a8/enAYhStWiX+6Q7FnDeEg5TESVji++uqrdc0112jKlCnq1KlThV8OoRkAACB8pqRYBW//XcXL3rXbopq3U9KYaYpu0zmCldV/3ASEm4AAAIBaxL93h3JnTwodRnHGpUq8+h454hIiWFntxU1AAAAA6qGSH5crb+6DMkV5wQZXrBKvvluxv7+cYRTHCTcBAQAAiDAT8Ktw4WwVfTjHbotqdoKS/jxd0a07RbCyhsdZnc6TJ08Omf7vf/9boc/1119/NPUAAAA0KIH8A8p9bkJIMI7peZ5SJr1MMI6Aao05djgcIeOJy08fqq22YcwxAACoDbzb1ytv9iQFsncFG5xRShhyq+IHjGQYRTVEfMwxAAAAwmeMUfHy91Tw5pOSzytJciQ1VvKYR+Xq0jvC1TVshGMAAIDjyJQUK/+Nx+RZsdBui+54qpLGTFNUaloEK4NEOAYAADhu/Fm/KXfmXfLv+Nluizt/uNxX3i5HFLGsNqj2b+G555477DQAAAAqKtn0nfJe/KtMQU6wITZeSSPvV2yfCyNbGEJU64K8xMTEKvXLz88Pu6DjgQvyAADA8WKMUfHS+Sp4Z4YU8EuSnM1OUPItTyi65YkRrq5+iNgFebU99AIAANQmxutR/uvTQsYXu7qfpaQbHpEzISmCleFQGNwCAABwDPj371beC3fLl77Bbou/6HolDL5FDmdUBCvD4RCOAQAAaph3yw/KnXW3TG52sCEmTknXPajY3gMjWxiOiHAMAABQg4qXvav8fz8h+X2SJGeTVsHxxW26RLgyVAXhGAAAoAYYn1cFbz6l4mXv2G2urn2UNGaanImpkSsM1UI4BgAAOEqB3Czlzpok35bv7Ta+v7hu4rcFAABwFLzpG5T3wl0K7N8TbIiOUeKIexV35mWRLQxhIRwDAACEqfibD5U/71HJ65EkOVObKenmx+Vq3y3ClSFchGMAAIBqMn6fCt9/TkVL5tlt0R1PVfJNj8mZ0jSCleFoEY4BAACqIVCQq7w598m7cYXdFnf2FXIPu0uOaFcEK0NNIBwDAABUkW/nNuU+f4cCezODDc4ouf90p+L7DY1sYagxhGMAAIAqKPlxmfJeekCmuECS5EhMVfKfp8vVuVeEK0NNIhwDAAAchjFGRf/9lwoXPC8ZI0mKatNZybc8pagmLSNcHWoa4RgAAOAQTEmx8l55WCWrFtttMb0GKOm6B+WIjY9gZThWCMcAAACV8GfvUu7MO+XP/MluSxg8VvEX3yCHwxHBynAsEY4BAADK8W5eo9xZ98jk75ckOWITlHjDw4o9tV+EK8OxRjgGAAAoo3jZu8r/9xOS3ydJcqa1UfItTyq6VccIV4bjgXAMAACg4I09Ct58SsVfvG23uX7XV0ljHpXTnRLBynA8EY4BAECDF8jZp9wX75XvlzV2W9wF18h9xW1yRBGXGhJ+2wAAoEHzbvlBebMnKZCzL9gQ7VLiNfcq7veDIlsYIoJwDAAAGiRjjIo/f0sFbz0tBfySJGejZkr68+Nyte8W4eoQKYRjAADQ4JiSYuW/Pk2ebz6021xd+ihp9N/kTG4cwcoQaYRjAADQoPj37lDurLvl37HZbosfMFIJQ25lfDEIxwAAoOEoWfel8uY+KFOYG2yIjVfStQ8qtveAyBaGWoNwDAAA6j3j96nwP/9U0eJX7baoZico6ebH+f5ihCAcAwCAes2ftVN5c+6Tb9uPdlvMqf2VOOohOeMTI1gZaiPCMQAAqLc8P3yu/JenyBTlBRuiouW+8nbFnXe1HA5HZItDrUQ4BgAA9Y7xelTw3nMqXjrfbnM2ba2kMY/K1e7kCFaG2o5wDAAA6hVf5k/Km/ug/Du32m0xvS5Q4sj7GUaBIyIcAwCAesEE/Cpa/KoKF7wg+X3BxugYuYdOUNy5QxlGgSqpk+E4EAjo448/1qZNmzR06FC1adMm0iUBAIAI8u/dobyXJ8u35Qe7LapNFyXd8DDfRoFqqXPh+N1339Vdd92lFi1a6KuvvlLPnj0JxwAANFDG71Px0n+rYMFMqaQ42OhwKv7C65Qw6M9yRLsiWyDqnDoXjlNTU7V48WLFxMSobdu2kS4HAABEiC/zJ+W/9jf5Mjbabc4mrZR0/RS5OvWMXGGo0+pcOD7//PMlSTt27IhwJQAAIBKMp0iFH76ooiXzpIA/2OhwKO7coUoYcqucce7IFog6rc6F43B4PB55PB57Ojc3N+RZklwul+Lj41VUVCSv12u3x8bGKjY2VgUFBfL7/XZ7XFycYmJilJ+fr0AgYLcnJCQoOjo6ZN2S5Ha75XQ6lZeXF9KelJSkQCCggoKCkPbk5GT5fD4VFhbabU6nU4mJiSopKVFxcbHdHhUVJbfbXWE/2Sf2iX1in9gn9qk+7VNeXp6KV36swv+bpcCBvYqPdira6VBh4zZyD7tbgfbdlF/il9vlrzP7VB9/T5Hapxpj6qjMzEwjySxduvSIfR966CEjqcIjISHBuN1u43a7zbhx44wxxowbN85uc7vdZurUqcYYYwYPHhzSPnfuXGOMMb179w5pX7x4sTHGmBYtWoS0r1+/3uTk5IS0ud1uk5OTY9avXx/S1qJFC2OMMYsXLw5p7927tzHGmLlz54a0Dx482BhjzNSpU0Pa2Sf2iX1in9gn9qm+7FPJtnXmtPYtQ9rfvqK7Kfi/WXV2n+rj7ylS+/TPf/7TSDI5OTnmaDmMMabmovbxs2PHDrVt21ZLly5V//79D9u3siPHbdu2VWZmppKTkyXVvU9H9fETH/vEPrFP7BP7xD6V3yff7gwFPn1N+v4TFXgDClixxdXt90obNlGxrTvWuX2S6t/vKdL7tG/fPqWlpSknJ8fOduFqEOG4vNzcXKWkpNTIPyAAAKh5/n2/qvDDF+VZ8aFkDgatqBbt5R76v4rp9ocIVofapiazXYMYcwwAAOoG329bVLRknjzfLjp4Iw9JjsRGSrjkBsX1u0qOKOILjp069+rasGGDPv74Y+Xk5EiS3n77bX3//fc688wzdeaZZ0a4OgAAUF3GGHk3r1bR4lflXfdlyDxHfJLiB16r+POGyRGXEKEK0ZDUuXCcn5+v7du3S5LGjx8vSdq+fbt+97vfRbAqAABQXYGCXHm++VDFy9+Tf+fWkHmO+CTFnfcnxV8wQs6EpAhViIaozoXjvn37qm/fvpEuAwAAhMEE/PJuXiPPVx/Is+ZTyesJme9s3FLxFwxX7B8G833FiIg6F44BAEDdYoyRb9s6eVZ+JM+qJTK5WRX6RHc8VfH9rlJMrwsYU4yI4tUHAABqnPF65P15lUrWfamStcsUyN5ZoY8jIVmxZ1yquLOHKLpVxwhUCVREOAYAAEfNGCP/7nR5f14l77ovVfLTd1JJccWO0S7FdDtLsb0HKqZnPzlcsce/WOAwCMcAAKDajN8n/85t8v6yRt7Nq+XdvEYmL7vyzs4oubqertjTL1TMqf25wA61GuEYAAAclgn45d+dIV/GRvnSN8iXsUm+zJ8qPzJscSQ3Vky3sxTT/Wy5TuorZ3zicawYCB/hGAAASLKOBu/JlH/XNvl3Bh++Xdvk351e4VslynPEuRXd6TS5OvdSTNc+imrbVQ6n8zhVDtQcwjEAAA2E8XoUyNmnQPYu+bN2yp/1mwJZv1nPOxXYvyfkVs2H42zSStHtTparQ3e5uvRWVJvOcjijjvEeAMce4RjHnTEm+OYbCEgBvxQIyJiDPyvgt6YDwX7GBB/BhSUFp00lbZX1s6eNkamkTSq7TJnnUg7HIaYdB6cP0cdRtk9V11XZfIczeATGGSU5nZIj+LPd5nBKTqcc1nPZfo7y6wRQrxivR4GCHJn8HAXy9yuQmxUMwDn7ZHKzFMjJUiB3nwI5WTJFedXfgDNKUWltFNXyREW3O0nR7U5WdNuuciam1vi+ALUB4biOM8ZIfp+Mr0TyeWV8XslXUu7ZW3G+3yd5S4Ltpct7vTL+g8tVXKev8nX6fVawLQ26pSG3XNAtDb9VPCqBGlIuMDscZcKzM0qOaJcUFS1HlEuKtp6joq320p+jpShX8LtHo11l+gbbg21Wn9K+rhhr2ZiD86NjguuNdtnrPzhdrh+nY9GAGL9Ppihfprgg+CgqkCnOV6AwT6Yg52D4Lcg5OF2Qo0D+gcOO+60OhztFUU1ayZnWWtEtTlRUyw7BR1rb4P9noIFo0OHYGCNTNrDZRzADIUc2jTHBYGc/B4KB0O8LBkX7uTRI+g7+7PdZgdMKkT5v6LS35OC0r0TylQbd8mG0XCgtE4iBwzIByR+Q/L7gZPnZx7+iqnFGlQnirkOE6phgmHbFHAzs0THlQrerzHpiguG9NKSXDe8VwnqZ9Ue7Dh6FdziCHzjsMwaO4AcNKfjBw+5Tpp9k/8yR/COr9KzQwZmHbTeVnQmy+5U/u6Tg+3nAL/n99of8stOm9IO/v8zPpf38pQcCfNb7uUemxBN8by7xyHiLZbweGa/HmvbIeEukkmIZT4ECVgA2RQVHHM971GLi5ExpKmdyUzlTmsiZ2iwYhJu0VFTT1nI2bsEFc4ClQYfj7InnyRvD+KiwlR7ti4ouc4rfKTnKHJUsMwRATmtowGHmB/tEHWyzQoZDjuDIg9IwYj8fus1Rvk1lQkulbUGO0vmSSqOjKT/Uovwf74Ndy0yXH6Zx+H4V/6iX6Vf2w5r94c06Ul96VL7MBzsTcqTeWqZMv4Pz/aEf8vzBD2QVhpZEQsAvlfhlFDwqVgsqqjmlwTnSNURSZf8/asPrrq5wRsnhTpHTnRJ8TkyRw50qZ2KKnMlNrCAcfHakNJUjNoEPZkAVNehwXCc4HGWOXsXI4So9mlXuVHR0JUfFrPkHj4jFSK6DgTY4HVPJ6e7y63RVnB8VzRttPWYC/oNnN8qE59AzIr6Dw24CvopnTUqH75Q/K+L3Hjwr4veWGc7jLXMGxurv91pnV8qcRakPZ02MkYw/0lUgkhxOOeLccsS7refE4LPV5oxLPDivNAQnptrPjjg378HAMdKgw3FUu5MVnRB38GInh+Pg0Urr2VHmYqfgaVWn5HQcPP1aZoxlpeMuo8u0l57atX+OPkQQPRhQ5YziDRDHncMZJcVEqTa/8krH24cMVSo7BKn8WHw7tIcZ3steHGpfEGqCR+TLXhBqAtak1R4od8Go3SdyR0krnAk5/hVIsoaYlJ7BkULPAtkvPkdou93dEXqWp9IzQeXOEIVsQwfPLkVFHzxr5YyWoqIqTDscTqs9Sg5nsP/B6aiDBxxiYuVwBR+ynh0xZaZj4oL9YuN5bwdqqQYdjlMn/FPJycmRLgNAGBwOR/ADZLSrVod4AEDdwuXgAAAAgIVwDAAAAFgIxwAAAICFcAwAAABYCMcAAACAhXAMAAAAWAjHAAAAgIVwDAAAAFgIxwAAAICFcAwAAABYGvTto4FSu3bt0pw5czR69Gi1aNGiwvSx2g4AoPqO13tpVbeza9cuzZgxQ5I0fvx47dmzR3fffbcef/xxnXLKKYddZ7j7Up3a5syZo+bNm+vBBx/Us88+q6FDh4bUXVBQYPd3u90aPny4nn32WS1dulQ9e/bU/v37NW7cOL344ot6/PHH1axZM82YMUN79+7V1q1bdf/99+vrr7/W5ZdfrjfeeEN79+7Vjz/+KJfLpYcffliLFy9WRkaGli1bJkk6/fTTtWPHDnv+119/rdGjR2v58uW65ZZbZIxRixYtFBsbW2F/XC6X2rRpo++++04+n09Op1ONGzdWQkJClf/tjsg0QDk5OUaSycnJiXQpqCXWrFlj3G63WbNmTaXTx2o7AIDqO17vpVXdTmm/0r7z5883brfbzJ8//4jrDHdfqlvbyJEjjdvtNhMmTKi07rKP0vrLPiZMmGDPK7/c9OnTD7lc6bxDPUrnr1mzxt5GOI+EhIQay3YMqwAAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACzRkS4gkt5dvlkJ7sRIl4FaIP2X7ZKkxau26+ec+ArTx2o7ODbObO+IdAmor9aui3QFkLRr2/bg8xefKiNza8S3U9qvtG/Wr79JkrJWf6eMBNdh1xnuvlS3toLfdkiS8rZvUcaCdyvUXVbW6u8qtOVt32LP27X715B5BzatP+RypfMOpXT+ri8+tbcRaRw5BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAQjgGAAAALIRjAAAAwEI4BgAAACyEYwAAAMBCOAYAAAAshGMAAADAEh3pAiLBGCNJKirMj3AlqC2KiwpljFFxUaEKC/IrTB+r7eDYyMtzRLoE1FeFhZGuAJIKiotljFFBcbHyjuHvpKrbKe1X+nORxyNjjIo8ngrLlV9nuPtS3dq8Pp+MMSrxeu3+Zesuq7T+skq8Xnufyi/nKSmx55VfrnTeoZTOLygutrcRjtLlwl2+LIepibXUMVu3blXHjh0jXQYAAABq0JYtW3TiiSce1Toa5JHjxo0bS5IyMjKUkpIS4WrQEOTm5qpt27bKzMxUcnJypMtBA8HrDpHA6w6RkJOToxNOOMHOeEejQYZjpzM41DolJYX/uDiukpOTec3huON1h0jgdYdIKM14R7WOGqgDAAAAqBcIxwAAAIClQYbj2NhYPfTQQ4qNjY10KWggeM0hEnjdIRJ43SESavJ11yC/rQIAAACoTIM8cgwAAABUhnAMAAAAWAjHAAAAgKXBhOMdO3Zo7dq1KigoOGSf3bt3a9WqVdq2bVuN3H4QDVMgENBPP/2kn3/+WV6vt8rLbNu2TatXr1Yht6dFFRw4cEArV67Ujh07qrxMVlaWVq9erb179x6yT1FRkb766iulp6fXRJmop9LT07V27Vrl5eVVqb8xRtu2bdOqVau0e/fuY1wd6jO/368NGzbop59+UklJSaV9SkpKtGHDBq1bt06BQKD6GzH13AcffGC6d+9uWrdubXr06GHcbrd58MEHQ/rk5+ebyy+/3LjdbtOrVy+TlpZmunfvbjZs2BChqlFXPfbYY6Zly5bmd7/7nenQoYNp2bKleffddw+7zKpVq8zJJ59sWrZsaXr06GFSUlLMK6+8cpwqRl30xBNPmLi4OHPSSSeZ+Ph4M2zYMOPxeA7Zf+PGjeaiiy4yLVq0MKeddppJSEgwgwYNMjk5OXaf3bt3mwkTJpiWLVuauLg4c8899xyPXUEds3DhQnPyySebTp06mW7dupn4+PgjvlY2bNhgunfvbtLS0kyvXr2M2+02l19+ucnPzz9OVaO+eOKJJ+y/le3atTPNmzc3b7zxRkif1157zTRp0sR07drVnHjiiaZDhw5m1apV1dpOvQ/HM2bMMOvWrbOnP//8c+Nyucy///1vu+3hhx82TZo0Mb/99psxxhiPx2P69etnLrjgguNeL+q2++67z+zZs8eenjZtmomNjTXp6emV9vf7/aZdu3ZmxIgRxuv1GmOM+eSTT4zL5Qp53QKlPvvsM+NwOMxHH31kjDEmPT3dNGvWzEyZMuWQy3z44Ydm+fLl9vTu3btNmzZtzB133GG3ffvtt+app54yWVlZplu3boRjVGr27Nlm27Zt9vSXX35pJJkFCxYccpnzzz/fnHfeefYHuF9//dU0adLEPPLII8e6XNQz06dPD/lQ/+ijj5ro6GiTnZ1tjDFm06ZNxuVymeeff97uM3nyZHPCCSeY4uLiKm+n3g+ruP3229WtWzd7+txzz1W3bt20fPlyu23v3r3q0KGDWrZsKUmKiYlRnz59DnvqEajM1KlTlZaWZk+PHTtWHo9HK1eurLT/li1blJ6erptvvlnR0cG7uZ9//vnq3Lmz5syZc1xqRt3y0ksv6YwzztCFF14oSTrhhBN03XXX6aWXXjrkMpdcconOOusse7pZs2Y6+eST9euvv9ptp59+uiZOnKjGjRsfu+JR540ZM0bt27e3p88880zFx8eHvJbK27t3r3r37q2YmBhJUqtWrdS+fXv+xqLa7r777pBbkvfv318+n0979uyRJH3xxReSpJtuusnuM27cOGVkZOijjz6q8naia6jeOmP//v3asmWLbrjhBrvtL3/5i95//3098MAD6tevn7Zs2aJ58+Zp5syZEawU9cF3330nSerUqVOl80uDSNk/LCUlJdq7d69WrVp17AtEnbNmzRqde+65IW19+/bVk08+qf3796tRo0aHXHb58uXyeDxavny5vv/+ey1cuPBYl4t6KCcnRz/++KPy8vL0yiuvqEuXLho2bNgh+0+ePFl/+ctf1LlzZ5144olaunSp9u7dq9tuu+04Vo36IiMjQxkZGdq5c6emT5+u6667Tl27dpUU/Jvq9Xq1Z88e+4Bn6d/XVatWafDgwVXaRoMKx8YY/fnPf1ajRo00atQou71jx44aM2aMnn76aS1cuFCZmZm64IILdM4550SwWtR1+/fv19ixYzV48GCdcsoplfZp0qSJRo4cqTvuuEPFxcVq3ry5Zs2aJa/Xq+zs7ONcMeqC7OxsNWnSJKStdDo7O/uw4XjSpEkqLCzUTz/9pDFjxoScVQOqauvWrZo0aZKysrLsgJKamnrI/v369dM555yj++67T23bttXWrVt1xx136MQTTzx+RaPeWLJkiebMmaOMjAwlJCToxhtvtOddcskl6tKli6688kpNmjRJXq9Xf/vb35SYmFitv6n1JhwXFxeHnLpu06ZNyKkfKTjE4tNPP9Wnn36qlJQUu/3+++/Xa6+9pg0bNqhVq1YqLi7W0KFDNXjw4JDhF0Cp3bt3a/PmzfZ0t27dQkJJfn6+LrvsMqWkpOiVV1457Lrmzp2rWbNmacGCBSooKNAVV1yh1NRUrV+//pjVj9rvUO9pLpdLxcXFIX2LiookyT5tfSil72e7du1S//79lZubq7lz59Zw5ajvTjvtNPu19NVXX2nAgAGKi4sLOehU1qBBg9SkSRNlZmYqLi5Ov/32m8444wx5PB5NnTr1eJaOeuDGG2+0A/EzzzyjgQMHav369ercubMSEhL09ddf6+mnn9YLL7yguLg4Pf300xo9erTi4+OrvpEaGyUdYTt27DBnnXWW/XjuuedC5k+YMME0atSo0isWu3fvbm6//faQtnfffddIMllZWce0btRNCxYsCHm9ffnll/a8vLw8c/bZZ5vTTjst7NdPjx49zA033FBT5aIOOtR7Wr9+/cyIESNC+r7wwgvG5XKZkpKSKq9/6tSppnnz5pXO44I8VMeAAQPMsGHDKp23b98+I6nCt/bcfvvtpkePHsejPNRjfr/fxMbGhlyAV96+ffuMw+EwL7/8cpXXW2+OHLdu3fqQR3knTpyol19+WYsXL1avXr0qzE9LS6vwXaGZmZmKiYkJGfgNlBo0aJAGDRpUob2goECXXnqpCgoKtGTJkkovbtq6dav279+v3r17Swoe9Sv7iXb58uX68ccf9eyzzx67HUCtd6j3tIEDB2rGjBnyeDyKjY2VJP3nP/9R//795XK5JAW/z3jjxo3q27evYmJiVFBQILfbHbKeX375pcLwDOBICgsLlZCQYE/7fD5t375dXbp0sdvKvselpKQoJiam0r+xZS9eBo6ksLBQ8fHxcjgcdltmZqY8Hk/Ie1n5v6kzZ85UcnKy/ud//qfK26o34fhQHnzwQc2YMUN///vfVVRUZP+xadq0qX73u99Jkm677Tb98Y9/1F//+ledd9552rx5syZPnqyxY8fa3yAAHInf79egQYO0YcMGvfTSS9qwYYM9r2PHjvbFAU8//bT++9//6pdffpEkPfnkk8rNzdWAAQO0ZcsWTZ48WRMmTFD//v0jsRuo5caNG6dZs2bpyiuv1C233KLPPvtMS5Ys0eeff273Wbp0qa666iplZmaqTZs2GjVqlDp37qzf//73cjqd+vjjj/Xqq6/q9ddft5fxer365ptvJAX/CO3YsUPLly9XcnLyIcfMo2Hx+/06/fTTNXr0aHXr1k25ubmaM2eOsrKyNH78eLtf2fe46OhojR07Vg899JCcTqc6d+6spUuX6v3339c777wTwb1BXbN27VrdddddGjVqlNq3b6/09HQ9+eST6tWrV8iFdiNHjtQ555yj7t2769NPP9XTTz+tefPmhQynPRKHMfX7VnBjxozRpk2bKrSfd955euSRR+zp5cuXa+7cucrIyFDTpk116aWXasSIEXI66/233aGGFBUVaeDAgZXOmzhxoq688kpJ0t///nd98803mj9/vqTgH5x//OMfWrRokZKTk3X11VfriiuuOG51o+759ddfNX36dK1fv14tW7bUbbfdpjPOOMOe/9lnn+n+++/Xe++9p7S0NHk8Hs2ePVtLly6Vx+NRp06ddNNNN4VckJednV3pldzdunXTCy+8cFz2C7Xf7t279dxzz2nVqlWKj4/XqaeeqrFjx4YcBS7/HhcIBDRv3jx9+OGH2rdvn0444QTdcMMNOvvssyO1G6ijfvjhB82aNUubN29WWlqa+vfvr+uuu84+iyYFL4afNm2a1qxZo/bt22vs2LGVjho4nHofjgEAAICq4rAoAAAAYCEcAwAAABbCMQAAAGAhHAMAAAAWwjEAAABgIRwDAAAAFsIxAAAAYCEcA0AVvfXWW9q1a1ekywhLXardGKP58+drz549EVkeQMNGOAZQ573zzjuaP3++cnJyKsxbunSp5s+fr61btx71dq699lp9//33R72eqggEApo/f7727dtXI+s7XO01sa1w11HZcn6/X8OHDw+5BfuxWh4AyiMcA6jzRo0apeHDh+vll18Oac/Pz9fgwYM1fPhwffrppxGqLjwlJSUaPny4Nm3aVCe2Fe46KlvO6XRq2LBhatas2TFfHgDKi450AQBQE84991zNmTNHt99+u902f/58devWrdIjpnv37tWKFSsUHR2tP/zhD0pJSanQZ/v27frhhx/Uvn179ejRo9LtFhQU6Ouvv5bX69Upp5yi1q1b2/O8Xq/eeecdXXTRRdqzZ4/Wr1+vHj16qHPnznrvvffk8XjkdDrVtm1bnXbaaYqLi7OXfe+99yRJn3zyiXbs2KHU1FRdfPHFR9xmdWqvyrbK8/v9+uabb5Sdna3u3burffv2h11HOPt50UUXaciQIWratGlY261seUkqLCzUihUrVFJSojPPPFOpqamH/XcB0DARjgHUC0OHDtUDDzyglStXqk+fPpKkF198UaNHj9Ztt90W0vfll1/W2LFj1adPH3k8Hm3atEnz5s3ToEGD7D7PP/+8/vd//1e///3vlZOTo6ZNmyoQCISs56OPPtLIkSPVtWtXJScn66uvvtKkSZM0adIkScEQO3z4cF166aX6+eef1bNnTyUmJqpz585atGiRcnNz5ff7tX79epWUlOjDDz9Uly5dJEmLFi2SJC1btkwbN25U27ZtdfHFFx9xm1WtvaxDbau8ffv2qV+/fvL7/eratas2btyoyy+/XE899dQh1xHOfg4YMEDDhw/X0qVL1axZs2pvt/zypdu59tpr1aJFC7Vt21a33nqrZs+erfPPP//QLyoADZMBgDrO7Xab2bNnm1tuucWMHTvWGGPMunXrjNvtNrm5uSY2NtbMnj3bGGPMr7/+ahISEszMmTPt5e+//37TrFkzk5uba/eJi4szr776qt3n5ptvNpLMokWLjDHG7N692yQlJZkFCxbYfTZu3GgSEhLMt99+a4wxZv/+/UaSufjii01JSckh6w8EAubGG280Q4YMsduKioqMJLNs2TK7rSrbrErt5VW2rco888wzpnv37sbv99tt7733XpXXUdX99Hq9RpJZunRpWNstv/xvv/1m3G63mTx5st0nOzvbfPHFF4fdXwANE2OOAdQbo0eP1uuvv66ioiLNnj1bV111lZKSkkL6LFiwQAkJCbrpppvstkmTJikrK0tLly6VJH3wwQdq1KiRRowYYfe55557Qtbz9ttvKyYmRsXFxXrrrbf01ltvae3atWrVqpU+++yzkL5jx46Vy+WqUO/PP/+shQsX6t///rcaNWqkb7/99rD7V5VtVqX2cMXHxysnJ0fp6el225AhQ464XHX3s6a2W6r03+3ee++12xo1aqRzzjmnWnUAaBgYVgGg3ujTp4/atWunN954Q6+99pref//9Cn3S09PVvn17OZ0Hjw243W61aNHCDl8ZGRlq166dHA6H3addu3Yhy2zfvl1SMHiV1bt3b7Vq1SqkrWXLliHTPp9PQ4cO1Weffaa+ffsqNTVVe/bsOeJXj1Vlm1WpvSpWrFhhb08KDlu57rrrtGLFCnXv3l2dO3fWwIEDNW7cOHXo0KHSdYS7n+VVd7vlZWRkqEOHDpV+QAGA8gjHAOqV0aNHa+LEiWrevLnOPvvsCvObNm2q7OzsCu379++3L+Bq0qSJ9u/fHzI/JycnZNxucnKyYmJiNH/+/CPWVDaoStK7776r5cuXa9u2bWrUqJEk6bXXXtPy5csPu56qbLMqtVfFqlWrtGzZMnv6iiuuUFxcnF566SX94x//0IoVKzRz5kz16tVLv/zyi9xud4V1hLuf5VV3u+WlpqYqKyurWtsE0HAxrAJAvTJy5Ehdcsklmjx5cqXzzz77bG3bti3kGywWLlyokpISnXHGGXafn3/+OeR7ct99992Q9Vx88cXauXOnPvjgg5D2oqKiCuG0vF27dqlZs2Z2YJQqHg2OjY2Vy+VScXFxtbZZldrLq2xbt956q+bPn28/YmNj9euvv0oKDnM477zzNHPmTB04cEA//fRTpesIdz/Lq+52y7vwwguVnp6uL774IqR97969h/13AdAwceQYQL3SuHFjvfHGG4ec37dvX40cOVKXXXaZ7rzzTnk8Hk2fPl0TJ07UiSeeKEk644wzdOWVV+rSSy/VxIkTdeDAAc2aNStkaEKfPn109913a9iwYbr11lt10kknacuWLXrnnXf01ltvhQTC8i6++GLdfffduvnmm9W3b18tWrRIn3/+eUgfh8OhXr166e9//7t2796tJk2a2MsdbptVqb28Q22rvDfeeEPvv/++Bg8erLS0NL399tvq1KmTevbsedh6q7ufAwYMOKrtll/+9NNP1+23365BgwZp/PjxatOmjRYtWqSLLrpIY8eOPeS/C4CGiSPHAOq8oUOHqmPHjoec/6c//Slk/ty5c/XYY49p3bp12rp1q+bMmaPp06eHLPP6669r4sSJWr16tQKBgL788kuNGDEiZPzw9OnTtWjRIvn9fi1fvlwpKSn67LPPdMopp0iSYmJiNGzYMDVu3Dhk3V26dNHXX3+tuLg4LVu2TGeffbYWLlyoYcOGhfR788031bNnTy1atEiffPJJlbZZ1drLq2xb5d1555164oknlJWVpS+//FIXXHCBvvnmGyUkJFS6jnD3s/xNPKq73cpuAjJjxgzNnz9f2dnZ+uGHHzR69GiCMYBKOYwxJtJFAAAAALUBR44BAAAAC+EYAAAAsBCOAQAAAAvhGAAAALAQjgEAAAAL4RgAAACwEI4BAAAAC+EYAAAAsBCOAQAAAAvhGAAAALAQjgEAAAAL4RgAAACw/D95xuDJ9x7EAQAAAABJRU5ErkJggg==",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "fry, self-contained p-value 5.8e-08\n",
- "roast, self-contained p-value 0.00025\n",
- "camera, competitive p-value 2.5e-35\n"
- ]
- }
- ],
- "source": [
- "table = result.table(monocytes, \"stim_vs_ctrl\")\n",
- "plot_barcode(table, np.isin(table[\"symbol\"], interferon))\n",
- "plt.show()\n",
- "\n",
- "sets = {\"interferon-alpha response\": interferon}\n",
- "rotations = roast(result, interferon, id_column=\"symbol\", rng=1)\n",
- "tests = {\n",
- " \"fry, self-contained\": fry(result, sets, id_column=\"symbol\")[\"p_value\"],\n",
- " \"roast, self-contained\": rotations[\"up_p_value\"],\n",
- " \"camera, competitive\": camera(result, sets, id_column=\"symbol\")[\"p_value\"],\n",
- "}\n",
- "for name, p_value in tests.items():\n",
- " print(f\"{name:<23} p-value {p_value[0]:.2g}\")"
+ "print(f\"{len(tested):,} genes tested in the {monocytes}\")"
]
},
{
@@ -147,18 +85,12 @@
"id": "1b73da96",
"metadata": {},
"source": [
- "Nearly all the set's genes go up with the stimulation. The three tests agree, but\n",
- "answer different questions. fry and roast ask whether the set's genes respond at all:\n",
- "fry's p-value comes from a t distribution on the design's seven residual degrees of\n",
- "freedom, and roast's, 0.00025, is the smallest its 1,999 rotations give for one\n",
- "direction. camera asks whether they respond more than the other genes, a stricter\n",
- "question when thousands of genes respond, and compares them with every other gene.\n",
- "\n",
"## A collection of sets\n",
"\n",
"Any collection of sets works, such as a GMT file of MSigDB, Reactome or the Gene\n",
- "Ontology read by {api}`read_gmt`, under its provider's terms; CytePool bundles none.\n",
- "Here each transcription factor's targets in CollecTRI [@muellerdott2023] form a set.\n",
+ "Ontology read by {api}`read_gmt`, under its provider's terms; CytePool bundles none\n",
+ "([Gene-set collections](../statistics/gene_set_tests.md#gene-set-collections)). Here\n",
+ "each transcription factor's targets in CollecTRI [@muellerdott2023] form a set.\n",
"{api}`collectri` downloads it on first use and returns, by default, the interactions\n",
"whose sources allow commercial use ([Datasets](../reference/datasets.md#networks)\n",
"gives the terms). {api}`Network.to_gene_sets` makes each regulator's targets a set,\n",
@@ -169,7 +101,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 2,
"id": "44896cb2",
"metadata": {
"figure_descriptions": [
@@ -217,32 +149,53 @@
"metadata": {},
"source": [
"The targets of interferon regulatory factors and of STAT1 and STAT2 rise more than\n",
- "the other genes. camera's default inter-gene correlation, 0.01, ranks sets of\n",
- "co-regulated genes higher; `inter_gene_correlation=None` estimates it for each set,\n",
- "which controls the error rate of each set more strictly.\n",
+ "the other genes.\n",
"\n",
- "## Over-representation among the changed genes\n",
+ "## Gene set enrichment analysis\n",
"\n",
- "{api}`over_representation` asks whether the genes called significant hold more of a\n",
- "set's genes than chance would give, separately for the genes up, those down and both,\n",
- "as limma's `kegga` does [@young2010]. Its universe is the genes tested."
+ "{api}`gsea` ranks every tested gene by its moderated t-statistic and asks whether a\n",
+ "set's genes gather at the top or the bottom of the ranking, as fgsea does\n",
+ "[@subramanian2005; @korotkevich2021]. It needs only a statistic for every gene, from any\n",
+ "method. The enrichment plot follows the running score of one set down the ranking, and\n",
+ "fgsea's table shows the top sets with their genes."
]
},
{
"cell_type": "code",
- "execution_count": 4,
- "id": "a403f925",
+ "execution_count": 3,
+ "id": "54cce687",
"metadata": {
"figure_descriptions": [
- "A dot plot of the 20 sets with the smallest over-representation p-values, with a column for the 1,619 genes up, one for the 1,549 down and one for the 3,168 in either direction; a dot's area gives the overlap and its shade of blue the -log10 adjusted p-value, pale grey where the adjusted p-value is 0.05 or more. The targets of STAT1, IRF1, NFKB1, REL and RELA are over-represented among the genes up, STAT1's most; MYC's targets among the genes down; the column of either direction shows weaker signals."
+ "GSEA's enrichment plot of the 135 targets of STAT1 among the genes of the CD14+ monocytes ranked by decreasing moderated t-statistic: the running score rises steeply over the first 500 ranks to its peak of about 0.69, the enrichment score, marked by a dashed line, then falls steadily to about 0 at the last rank, 5,125; ticks below mark the set's genes, dense among the first ranks and sparse after.",
+ "fgsea's table of the 10 sets with positive scores and the 10 with negative scores that have the smallest p-values: for each, its genes as bars at their ranks from 0 to 5,125, its normalized enrichment score (NES), p-value and adjusted p-value. STAT1, IRF1, NFKB1, STAT2, RELA, IRF3, SPI1, NFKB, STAT3 and REL have scores from 1.47 to 2.33 and adjusted p-values from 4.9e-15 to 5.1e-3; of the sets with negative scores, only ID1 has an adjusted p-value below 0.05, 5.4e-3."
]
},
"outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/tmp/ipykernel_1168275/1970039944.py:1: RuntimeWarning: 1 gene set(s) have fewer than 10 random scores of the sign of their enrichment\n",
+ "score among 1000 permutations, but some; their normalized scores and p-values\n",
+ "are NaN. More permutations (n_permutations=10000) may give them enough\n",
+ " enrichment = gsea(result, regulons, id_column=\"symbol\", rng=1)\n"
+ ]
+ },
{
"data": {
- "image/png": "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t2/PxJHIBpg4EhERuaggbzGCvMUwGAzQ6gG94VYvI4ekOidbPE7DkXqe8/LycOXKFQQHV71iNRFZhokjERGRixMEARKuwUQOZvDgwfj555+hVqsxePBgiESmDwtQqVTo1asXWrZsaacIiZwLE0ciIiIicjiLFy9Gbm4u4uPj8cgjj6Bdu3bGfSKRCCEhIejYsaND9ZISNWRMHImIiIjI4fTs2RMA0LhxY0RFRTWYxXqInJWo+ipERERE5OjK5zhauzU0zZs3x2uvvYbi4mIAwHfffYewsDCEh4fjq6++snN0RM6DPY5ERERE5LA+/PBDaLVaeHl5AQBmzJiBxx57DMHBwXjyySeRlpYGmUxm5yiJHB97HImIiIjIYf3000/o378/AODixYtQKpVYsWIF5s6di+DgYFy6dMnOERI5B/Y4EhEREbkAATZ4HAca3lBVQRBQUFAA4NYw1X79+hlXWBUEAQaDoYqjichSTByJiIiIyGENHDgQCxYswNWrV/HBBx/g3XffBQDk5OQgLy8PrVq1snOERM6BiSMREREROaynnnoK2dnZSExMxDPPPIPx48cDALZu3YrFixfDzY2Xu0S2wL8kIiIiIhdgi1VRG+KqqmKxGAsWLKhQ/txzz9khGiLnxcSRiIiIiByWTqerch6jWCxukAkvkaPhqqpERERE5LAGDBgAiURS6ZaYmGjvEImcAnsciYiIiMhhrV692riqarnMzEy8++67aNq0KTp06GCXuIicDRNHIiIiInJYlSWGQ4YMQatWrSCVSus3ICInxaGqRERERC6gfHEcazdH4evri4iICFy4cMHeoRA5BSaOREREROR0Tp48ib///hvBwcH2DoXIKXCoKhERERE5rIkTJ+LYsWMmZaWlpcjIyMD06dMRHR1tp8iInAsTRyIiIiIX4KzPcXzooYdw9913m5R5enqibdu26NSpk52iInI+TByJiIiIyGGNHDnS3iEQuQTOcSQiIiIiIqIqMXEkIiIiIocyYMAAuLm5ITEx0fjvyjYPDw80b94cGzdutHfYRA6NQ1UtVFhYiMGDB+OLL75AVFSUzdp94YUX0KpVKzz55JM2a5OIiIjoTs40x3H58uXIy8tDu3btjP+ujE6nwz///IPZs2djyJAhCA8Pr8dIiZxHg00c4+Pjcf36dWzevNn4es+ePQAAiUSCqKgojBkz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0np3unnzJiQSSYVHY1TmwoUL6Nu3Ly5evAg/P78atXPz5k14enrC19e3wr74+Hjk5ubif//7n0VxALfmOMrlcigUCrNtEhERkWOoj+/08nN0XXgBbu7V32SvirasECcWt+Q1CJELarA9jncuFFPZwjEymcxkIZzK6t0pLCysRvG0bNkSx48fh5ub6Y/MknaqqvPUU08hNDS0RrEQERER0b+uXLmCdevW4fLlyygtNV3UbuXKlWjfvr2dIiNyHhyXUgMxMTHw9va2aZtxcXHw9PS0aZtEREREdxIJttkamvPnz6Ndu3a4cuUK/vjjD3h7e0MsFuPQoUMoKyuDlxcfa0RkC0wciYiIiFyAINhma2jWrFmDWbNmYffu3WjSpAlmzpyJb775Bt9++y2uXLmCmJgYe4dI5BSYOBIRERGRw7py5Qr69+8P4NYUpuLiYgDAfffdB39/f5w9e9ae4RE5DSaORERERC5AJAg22RoalUoFd3d3AEBkZKQxUdTr9VAqldDr9fYMj8hpNNjFcYiIiIjIdmwx1LS2x5eWluLChQuIiIio00UBH3roIYwbNw43btzAhQsXoNfr0bp16zo7H5ErYY8jEREREdWZVatWISQkBI899hh69eqFJ598Elqt1mbtJyQkoEuXLgBuDU9NSEhAeno6wsLCcPDgQWNvJBFZhz2ORERERC7AFqui1vT4tWvXYtGiRfjuu+/Qp08fAMAnn3wClUpV4RFntdW0aVOT1xMmTMCECRNs0jYR/YuJIxEREZELqO+hqlqtFkuWLMHMmTONSSMATJ482bogiMgumDgSERERUY0olUqT1zKZDDKZzKTszJkzyMrKwogRI5CVlYVr164hLi4O/v7+9RkqEdkI5zgSERERuQDBRhsAREdHQy6XG7fly5dXON+NGzcAADt37kT79u0xdepUREREYNq0adDpdHX3RomoTrDHkYiIiMgF2OJxGuXHp6enw9fX11h+Z28jAEgkEgDA2bNnkZKSAg8PD5w/fx7dunVD27Zt8dxzz1kVCxHVLyaORERERFQjvr6+JomjObGxsQBuzWn08PAAALRq1Qr33HMPfvnlF5sljmq1Gn/88QdSU1OhVqvh7++PDh06oFGjRjZpn4huYeJIRERE5ALqe3Gc5s2bIy4uDtevXzcpv379Onr27GldIP9v7dq1ePXVV5Gfn19h34ABA7Bp0yZER0fb5FxEro6JIxEREZELsMfjOFatWoWpU6fC29sbzZo1w65du3Dp0iV89tln1gUCYNeuXXjhhRfw6quvYvTo0YiMjISnpydycnJw8uRJrFq1CsOHD8fp06chWJsxExETRyIiIiKqG+PGjYNcLkdCQgK+/PJLNG/eHH/++WeFZy/WxrZt27BixQo8//zzJuURERGIiIjAfffdh6ioKJw7dw5t2rSx+nxEro6JIxEREZErsMFQVdTi+IEDB2LgwIFWnriikpIShIaGVrpfKpXCz88PxcXFNj83kSvi4ziIiIiIXIAI/w5XrfVm7zdxm759+2LRokX4888/K+wrLi7G0qVLkZubi/bt29shOiLnwx5HIiIiInI4zz33HH799VfjCqqRkZHw8PBAXl4eLly4AKlUiu3bt8Pd3d3eoRI5BSaORERERC5AEASrF4lpSIvMuLu746uvvsLPP/+MQ4cOGR/H0bx5czzzzDMYN24cAgMD7R0mkdNg4khERETkAur7cRz15Z577sE999xj7zCInB4TRyIiIiIXIIL1cxQb0hxHS+h0OohEogbVU0rkqBzt75+IiIiIyCIDBgxAYmKivcMgcgrscSQiIiJyAc46VJWI6gcTRyIiIiIXUP5IDWvbaChmzZqFU6dOVVnnzJkz9RQNkfNj4mihoqIiDBkyBNu2bUNUVJTN2p07dy5at26Nxx9/3GZtEhERETm7S5cuwdPTE40bN660zj///FOPERE5twabOP73v//F9evXsWnTJuPrvXv3AgAkEgmioqIwZswYTJkyBSKRyOS48nq327JlCzp27GhS9tVXXyE+Ph6DBw/G22+/XWU8r7/+OmJiYoxJY15eHtauXYvExERoNBp07doVc+fORWhoqPEYvV6PDRs2YM+ePcjOzsZdd92FV199FTExMcY6EydOxMCBAzFmzBj4+fnV5EdEREREZDFnexzHAw88gN9//x3r1q2rtM6FCxfqMSIi59ZgE8dr167h6tWrJq89PDywadMmaLVaHD16FDNmzMD169fx6quvmq13uzvvRl27dg3PPvssvL29kZaWVmUsRUVFWLt2LQ4cOGAsGzx4MIYNG4aXX34ZYrEYS5cuRa9evXDy5EljAvif//wH27dvx+rVqxEXF4fNmzejV69e+Ouvv4zPFerQoQOaNm2KjRs3Ys6c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EJVecpb1vYiZeQK0ei6HQNVXr169qm3QCAB+fn6Iiopi0EiVrkIDx02bNmHgwIHi68cffwQAcenjDh06FFmuQ4cOOHXqlPg+KCgIX3/9NSZPngyZTIb58+eLx27cuIHMzEyLdanValy/fr3Mn+P69etmE5sXLFiAFi1aICQkBH379kVoaChefPFFTJs2TcwjkUjQtWtXPP3002jevDm+/PJLdO7c2Wz/m6CgIBgMBty8ebPI82s0GqjVapMXVawcrVDiEC6g4IaKyFaMRiM0mrwij+XnW14inag0NKUMBLUG/tYREdV2FTr2pGXLlnjttdfE9w0bNgTwv403PT2L3hfIy8vLbHPOwv1yPD09TTb1tKYuAOI+PGWRk5MDBwcHk7QDBw5g48aNeOutt9CyZUscO3YMK1aswIABA8SnWuPHjzdZverll19G06ZN8cMPP2D8+PFiemGva3FtnD17tsUljMn2rF04VcqHf2RDhQtNFDWDQGKD1dCICpW240IC/tgREdV2FRo4hoaGFrn/Tp06dQAUzEH08DDfLDYuLk7MAxSM7R49ejQmTJiA1atX46uvvhL3aHm4rqIUpluz9G5xfHx8kJ6ebpL29ttv480338Ts2bMBFGxCqlKpMGHCBJw4cQIAzJY8rlevHtq0aWPSmwpArNvHx6fI80+dOhWTJk0S36vVagQGBpb581DJnJRSuDlIoCqhRzHAjasNku1IJBI4ObkiO1tldszZya2IEkRl46qUIl9n3XBVuRRwUFRwg4iIqMqrlEfY3bp1g729PX7//XezY/n5+di6dSsiIyPFtJEjR6JJkyb46quvsHjxYsyYMQOXL18GUBAQtmzZssi6AGDdunUICwsr196Lbdq0QUxMjPjeYDAgKSkJoaGhJvlCQ0OL3ILkYZmZmWZzLC9cuAA3Nzez+goplUq4urqavKjiNfG1/FzFRSlBXXf2ApFtuXv4wN7e0STN0dEZbu5Fj6ogKgtfV+t/u3ycpZByeAURUa1XKXe9Xl5emDJlChYuXIh9+/aJ6TqdDhMmTEBubi6mT58OAFi8eDEOHTqE1atXQyaTYejQoXj++efx+uuviwvNzJs3DwcOHMC3335rcp7vv/8eu3btwn/+859ytfe5557D+fPnkZaWBgCQyWRo06YN1q9fL271kZubiz/++APt27cXy61cudJkMZxf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",
+ "image/png": "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",
"text/plain": [
- ""
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "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KytYWVlBX18f06ZNw5gxY1CiRAnY29tjypQpqFatGjp06JBNV0E3TZo0SboVMPD1u0smT56MX375BYaGhliyZAmioqKkr/DQRHR0NJ49e4YvX74A+Hrzqdu3b0vLtvT09DBjxgwMHToUtra2cHR0xIwZM1C+fPlv9g6suZKsHD16FCNGjED16tVRvHhxPHjwAKdOncLChQthbW2dYfstW7bAwsJCYVOVjLu7O2bOnInbt2/Dyckpw74ePHiAdevWSa+3b9+OGzduwNnZGZ07d9bsxIiIiLJRVt9MKJuNSy9JU1aWOoHRdK9hbitSpAguXLiQYb3p06dr3KZDhw46+8b67t27KFWqFCZMmKBQ/uuvv0p3yCpVqhRMTU2lY8uXL8eqVaswb948xMXFoUKFCrh161amv2dFNnbFihWxbNkyAF835Y8YMQIAMHToUJibm2Pz5s2IiopC7dq1MW3atNzd75DDSpUqhfj4eIWyEiVKKNxauEKFChgwYAB8fHwQERGB+vXrY+PGjVJSqYnLly/j119/BfD1uv/555/4888/0bt3b4wfPx4A0L9/f5iYmMDX1xcRERFwdnbG9OnTs30WJ7foCaHifnXZLDw8HGfPnsWrV69gZWWFevXqKZ1Cff78OQICAjB48GDpGzQDAwORJ08edOzYUWnff/zxB+rUqSNNM964cQMnT57EyJEjVfafWuXKldG8efM05VFRUbC0tERkZGS63+hJRET0o1Hn+7Jkx69cuYLHjx/nRlhE9B3JtWTlW8VkhYiIKPOYrBBRVuT6BnsiIiL6cciWjl25ckXLkRDRt4jJChEREeUY2cyKOntEiIhS4zKwDHAZGBERERGRduTKN9gTERERERFpiskKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5isqKhdu3awdvbW9thEBERERF99wy0HcC35syZM7h9+zZcXFxw4cIFAICPj4+WoyIiIiIi+v4wWcmkwMBAXLlyBQDg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m/n5+SE6OlphM5IQAtOmTUNUVBRmzZoFCwsL+Pn54ePHjyq/dVXVcT8/P1y5cgWjR4/GqVOncOnSJemYpaUlypcvL/3PI3P//n2sXr06zRi1a9dWuvk/KioKlpaWiIyMzNJ0KBER0fckow32qTftHz16FB8/fsy1+H4ksbGxCncyk4mOjkZKSorC98MkJyfj06dPsLS0VHh/pKn4+HhER0enKTc1NU2zPC0pKQnx8fFKY/xWxcfHw9DQEHnypL2xbkxMDJKSkhSSspSUFHz8+DHL1z0hIQFRUVFpyk1MTNJc36SkJMTFxcHc3DzT4+mCHE1WvgdMVoiIiDQnS1SYrBBRVuTK96wQERHRj0WWpOjy8jEi0n25umeFiIiIfiyypEX2XWJERJrgMrAMcBkYEREREZF2cBkYERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK2ratWuXtkMgIiIiIvqhMFlR0+7du7UdAhERERHRD4XJChERERER6SQmKxoKCAjQdghERERERD8EJisaCgwMlJ57e3szeSEiIiIiyiEG2g7gWxYcHIyXL19Kr7t06aLFaIiIiIiIvi+cWckGgYGBCjMuAJeLERERERFlFZOVHCKfvDBxISIiItLMuXPn4OrqqvA4efKktsP67qWkpChcc09PT63Gw2VguSAwMDDNErGAgACpTPZcvoyIiIg0k5KSgjx5NP8cNjExEYaGhhn2raenBz09vcyGRxr66aefMGHCBABAUlISevbsicqVK2s5qu9fSkoKbt68ie3btwMATE1NtRoPZ1a0RH7mRfY89VIyIiIiytju3bvRoEED5MuXDxYWFmjdujVCQ0PTbSOEwJQpU1CwYEEYGxvDyckJp06dSlPv2rVraNKkCYyNjWFlZYWJEyciISEhp06F5BQpUkT6dD8mJgatWrVCwYIFtR3WD+HLly9YuXIlduzYAXNzc63GkmszKykpKbh06RKeP3+O4sWLo0aNGjA2NgYA/PPPP4iNjVXZ1sbGBi1atADw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MDU1xYEDB7T+RTdERJS7mLB9X8LCwrB3714AgIuLi8IxX19faYmTsbGxwjdxDxw4EHFxcfj999/x7t07VK1aFceOHYOVlZVUp23btli3bh2mTp2KFy9ewNbWFsuWLVNIaHRZ6r0PHTt2hJeXF/z9/dGmTRu8ePECU6dOxc8//6wywcksY2NjDBs2DPPmzYOLiwvs7e0xcuRIWFtbo1u3btk6Vm5asmQJZs6ciX379qFJkyZK6/Tt2xc3btzAjRs3cjc4AIaGhvjtt9+wePFi1K1bF46Ojhg7dizy5cuHXr165Xo82SFXkpWRI0fiy5cvaNCgAYoXL44HDx5g/fr16NevX4b3pAa+ri9OTk6WvgRSnru7O5YvX44XL16o9eWOLVu2xP379zFv3jzs3LlTKq9atSqqVq2qyWkRERGRlhUrVkytL9dTtsd1+PDhGD58eLrtunXr9k2/uZY3YMAAfPnyBTNnzkS/fv2QP39+uLq6Yt68eRr3tXXrVoU7dMneo02aNEn68sbZs2cjJSUF7dq1k74U8vDhw9/0ipZZs2bh8+fPab5bZvz48Zg/f36Ojx8cHIwOHTpIr2XPf/vtNyxbtgwAMHXqVCQmJsLDw0P6UsgjR45keMcyXaUnZPN/Oezs2bM4fvw4Xr16BSsrKzRu3Fjp1Nndu3excOFCLF68GIULFwYALF++HPHx8Rg7dmya+snJyfDy8kK3bt2kX5xjx45hx44dWL9+fZr6gwYNQmJiYppyd3f3NN8ACnydAbK0tERkZKTKzVNERERERJT9ci1Z+VYxWSEiIiIi0o5c3WBPRERERESkLiYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5isqKmiRMnajsEIiIiIqIfip4QQmg7CF0WFRUFS0tLWFtbIywsTNvhEBERERH9MDizoqb4+Hhth0BERERE9ENhskJERERERDqJyYqGatWqhYCAAG2HQURERET03TPQdgDfmpCQEAQGBiqUdenSRUvREBER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UK9///7S8i3Z48qVKxnWefLkifjjjz9E4cKFRVJSUpoYtm/fLszMzERUVFSaY1wGRkRE3xNlSwhVLQlM/ZBfXqlLy8BSUlJEYmJimkdKSopUJzk5Wel7gIxktl1Oe/funejTp4+wtLQUefPmFYUKFRK//vqr+Pz5s1SnU6dOom7dugrtjh8/LsqVKyf09fWFubm5GDt2rEhOTtZ4fH9/f2FpaSny5csnAAgzMzNhaWkpZs+eLdWJi4sTgwYNEsbGxsLAwEBUq1ZNXL16NfMnrQMOHjwoateuLczMzISRkZGoVKmS2Lhxo0IdT09PUaVKFem1u7u7ACAsLS0VHuosHUtt3759UnsAwtTUVFhaWiosLUtISBBDhw4VJiYmQl9fX1SuXFlcuHAh0+esbXpCqLnbJof07NkT3bp1Q9u2bRXKlyxZgufPn+OPP/6Q6sXGxqZp36hRI2kJmao6v/zyC/7991/kz58/zaY54Ou3kXp6emLs2LFpZleioqJgaWmJyMhI5MuXL7OnSURE9F3JaIN96hsMHDhwIMubhkm5+Ph4GBkZpSn//PkzUlJSYG5unuZYXFwcjIyMMj2jkpiYqPQ9l7GxcZrlaSkpKUhMTFQa47cqMTERBgYGSq/f58+fkZycDAsLCwBflxIqWyZmYGCg9GeT0bjKrruRkVGaWyGnpKQgISEhzc/jW6P1ZEXXMVkhIiLSnCxRYbJCRFmhc9+zQkRERN8+WZLCGxMQUVbo3AZ7IiIi+n7IkhbZN60TEWmCy8AywGVgRERERETawWVgRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrKhp165d2g6BiIiIiOiHwmRFTbt379Z2CEREREREPxQmK0REREREpJOYrGRCQECAtkMgIiIiIvruGWg7gG9RYGAgLly4ABcXF3Tp0kXb4RARERERfZc4s5JJwcHBCAwMlF57e3trMRoiIiIiou8Pk5VsEhwcrO0QiIiIiIi+K0xWcgH3uBARERFlzr///oslS5Zg9uzZ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ERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrKhp165d2g6BiIiIiOiHwmRFTbt379Z2CEREREREPxQmK0REREREpJOYrGggICBA2yEQEREREf0wDLQdwLckMDBQet6lSxctRkJERERE9P3jzIqGAgMDpaSFMy1ERERERDmHyUoWyCct3t7eWo6GiIiIiOj7wmQlGwQGBiI4OFiaafH29mbyQkRERESURdyzko0CAwPRpUsXBAcHAwB8fHzQrl07ODg4wMfHR7vBEREREX1Dvnz5gokTJ0qvixcvjrFjx2oxoh/P0qVLUaBAAfTt21drMXBmJYedOXNGSl5kUu91kZ+RISIiIiIgPj4e27dvh52dHezs7FCsWDFth/RD2bZtGw4cOIB9+/ZpNQ4mK1ogf1cx+dfyS8nkE5qAgABu5iciIlIhKSkJgYGBaNasGezs7HD9+nW12r19+xa//vorqlSpgnr16mHbtm0Kx2NjYzF9+nTUrl0b5cuXR8uWLbF3796cOAVSQU9PD+/evYO+vj7atGmj7XB+GA8fPsTx48cxdOhQbYfybSwD+/DhA96+fQs7OzuYmpoqHHv69Cnev38PADA0NETJkiVRsGBBhePx8fFwdHRUaBcVFYX79++jdOnSCvW1TbaUTPZfWRmgeLvkgIAAXLhwAS4uLtKxgIAA3lKZiIh+ON7e3nj16hU6d+6MIUOGID4+PsM2b968gbOzM6pWrYp169bB2NgYy5Ytg6OjI2rWrAkAGDJkCM6dO4c1a9agVKlS2Lt3L9zd3XHw4EE0a9Ysp08rXYmJidi4cSP27t2LDx8+oGzZsvj1119Ro0aNdNu9evUK06dPx/Xr11GkSBF4eXmhffv2Go8vhMDx48exdu1ahISEYMOGDdJ7EnlbtmzB5s2bERkZiTp16mDatGnS+65nz55h6dKlSvtfvHgxTExMMGHCBCQnJ+PkyZNYtWoVrl+/DhMTE43jzU6bN29GQEAA3r59CxsbG3Tv3h09e/ZUWf/OnTvo2rVrmnI/Pz9UrlxZ4/H//fdfrFmzBrdu3cLy5cvRpEmTNHV27NgBX19fREREwNnZGdOm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NJo2bYqaNWuqNI420Xiyou2YrBAREeWe7BAD2QZ7JitElBdatwyMiIiICq/M+4JcXFwUjgImIsotJitERERUYNzc3KSji/Py3A4i+joxWSEiIqIClfGhsyEhIRqOhogKEz62nYiIiD6LZcuW5eoYZiIiGc6sEBER0WfDRwUQkSp4GlgOeBoYEREREZFmcBkYERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrubR//35Nh0BERERE9FVhspJL69at03QIRERERERfFSYruRQcHKzpEIiIiIiIvipMVnIpOTlZ0yEQEREREX1VmKyoqHnz5vDy8tJ0GEREREREXzwmKyry9/eHt7c3ExYiIiIiogLGZEVN3t7eACAlLUxeiIiIiIjyF5OVPJIlLbI/ZSZPnqyJcIiIiIiIvhhMVgrIwYMHAXDGhYiIiCiv9u7di9OnT2s6jK+GEAK3b9/G9evXkZaWptFYmKwUsMwzLjJeXl5cQkZERESUg4MHD2LJkiXYtWuXpkP5KiQmJqJLly748ccf8f3336NZs2b48OGDxuJhsqIh3t7eCkvIMiYtGZeRZUxsiIiISNGdO3cwcuRItG/fHo8fP85Vm8TERCxbtgy9e/fGoEGDcPnyZbn7Hz9+xJYtW/DNN9+gd+/emD17Nt68eVMQ4VMWXr16hX379mHixImaDuWrkZSUhPnz5+P69evw9fWFgYEBbt++rbF4inzuAWNjY1GyZEm5srCwsGynmAwMDFC6dGnp+v379zAwMICpqSmAT1NV7969y3ZcY2NjuXHT09Px4cMHFC9eXI1XUTC8vb3h5uYG4NOnCMuWLZPKAUj3evXqhcOHD8PLywtubm5SYrNs2TJMnjxZakdERPQ1mDFjBnx8fNCjRw9s2rQJsbGxObaJjY1F27ZtYWBggMmTJ8PIyAi//vorli1bhoYNGwIABg4ciPLly6Nv377Q1dXFunXrsHnzZty/fx/ly5cv6JeVo5cvX2Ljxo24f/8+pk+fjjZt2mRb//Hjx/jpp58UyleuXAkzMzOVxw8KCsL69evh7++PhQsXolGjRgp1Ll68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tYWRkRGioqJyrJeQkIA9e/bg559/znZvzvjx49WOpU6dOgA+7TsxMzOTyoODg9GnTx+1+9UGL168QPv27dGsWTPs3r1bIcnN7NGjR2jSpImUqABAy5YtkZCQgODgYIW/dxnfp6qqXr160NHRwZMnT1C/fn2p/MmTJ4X2514o9qykp6djy5YtWLt2LQICAqSvZ8+ewdbWFlu3bpWrL9tgnzFRAT596hAaGirXh4ODA7p27YqAgAButici0qC8vFn+HEkEZ1gLlytXrmDcuHH49ddfAQB//vknxo0bBx8fH6nO6dOn5Q7gyUlUVBR69OgBU1NTvH79GuPGjZO+bt++ne+vITtPnz6Vu/b394evr6/c3gkfHx8cOnRIoa2XlxcSExMLdPla7dq1YWVlhV27dkllN27cQFBQkPTcmsLo5cuXaN++PZo2bYo9e/YoPeb3n3/+kZttsbS0xK1bt/Dhwwep7Pz589DX10ft2rXzNb5y5cqhXbt28PDwgBACwKcli0+fPi20/w3TypmVzHx8fPDmzRu4uroqnJzQp08fbN68OVfTsAYGBnJTkcCnxEZZORERUUaFaVYFUC25KqxvYrJTqlQpaXlYxlUZGTeTu7u7ZzsTMXv2bLn9LwYGBli0aJHSupk/IC1o7u7uqFGjBmxsbBAWFoZNmzahY8eOGD58uFRn/fr1CA0NlXsWC/BpCVjPnj1RsWJFtccPDAzE2bNnpf0nR48eRVBQEGxtbdGsWTMAnx7M6eTkhLS0NJiZmWHdunUYNGgQ7O3t1R5X07p164Z3796hZs2amDVrllTevn176cGLsr3RP/74IwDg559/xokTJ2BjY4OuXbvi9evXOHbsGJYtW6bykrjHjx9j+/btSE1NBfDpRLcbN27A3t5e2haxatUqODg4oF27dqhTpw727duHyZMny/07KEwKRbKyadMmtGnTRukRb3369MGMGTNw6dIltG3bVgPRERERaR9VkqvClojlRv369eWWwSjTuXPnbO9/++23ctfFihXDuHHj8hxbfrh06RIOHjyImzdvonjx4ti9eze6du0qV6dLly4Km9kTEhJgbW2d51mVqKgo6bCi0aNHIykpCb6+vnInqXXo0AEPHjzA7t27ERUVheXLl8PV1TVP42raiBEjkJSUpFCeca+Uq6sr2rVrJ12XL18eDx8+xOnTp/H06VM0bdoUS5cuVesgCH19fWmPS8bEOeP49evXR0BAAA4fPozo6GiMGDFC4cjlwkRHyOaIPpP09HRERkbCxMRE4YnyKSkpiI2NVZjliIqKgoGBAYoVK6a0z4iICBgbG6No0aKIj49HWlparj/hkO1ZySqzjY2NhYmJCWJiYrgpn4iISE21atXCs2fPNB0GERUyn31mRVdXN8slV1ktx8r80MjMMk7pqnrSARMQIiIiIiLtVCg22BMREVHhltsHLRIRZcRkhYiIiAqcbLMxEZEqPvuelcKGe1aIiIiIiDSDMytERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrOSBl5eXpkMgIiIiIvpiMVnJA29vb02HQERERET0xSqi6QCIiIiIiLKSmJgIAwMD6OnpaTqUr8aNGzcAACYmJrC0tNRoLJxZ0SAuIyMiIiJSLiIiAv3790eFChVgYmKCefPmaTqkr8bEiRPx3XffYfz48ZoOhcmKJnEZGRERUcFJSEiAh4cHZsyYgVevXqnU9tatW5gxYwZOnjyZr/1S7t2+fRsuLi6IioqCn58f/vrrL/68P5MbN25g/fr1mg4DAJeBfRG8vLzg5uam6TCIiIi0xoYNGzBv3jw0a9YMhw8fhrOzM6pVq5artpGRkRg4cCDCwsIAAI6OjvnSb35LTk7G/fv3ERERgbp166JOnTq5avfgwQMEBwejatWqsLW1zVMMDx48gL+/PxwcHFChQgWF+ykpKbh27RpiYmLQrFkzVK5cWbqXmpqKiIgIpf1WrFhR7udesWJFmJiYoHTp0nmKNz8EBQXh4sWLSE5Ohq2tLVq0aJFjm+TkZJw+fRqBgYGoWLEiunXrBlNTU7XGT0tLw9GjR+Hn5wc3NzfUrVtXoU54eDgOHTqE6OhotGjRAvb29tK9Dx8+4N9//1Xad7NmzbRuuV2+zax07NgR5ubmOHjwoMK9QYMGwdzcHGvWrEFcXBxa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0RGDBg2Srv/9919s374d0dHRaNGiBYYNG4YiRQp0jqLA5GuyEhoaiv/++w9t27aVKw8MDMTr169hZWWF8uXLy92LiopCYGAgjIyMYG5uDmNj4yz7fv78OYoWLYr69etLG7cA4NatWzA1Nc1y+vXixYuoU6cOKleujEuXLiE9PV2hTs2aNaWpuYyYrBAREeVOVgkNkxUiUle+JitfIiYrREREecNkhYjUVWCngREREREBX+6+GCIqeJxZyQFnVoiIiIiINIMzK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaaUimg6AiIiIiIi0R3x8PACgSJEiMDIy0mgsnFkhIiIiIq2TmpqKBQsWwMLCAk2aNMHevXs1HdJXo2LFiihfvjx69Oih6VA4s0JERERfrps3byIwMBCOjo4oW7ZstnWvX7+Op0+fypWVKVMGTk5OSuu/fPkSly5dQoMGDWBjY5NfIdP/d/DgQXz8+BFHjhxBYGAg+vfvDwcHB5QvX17ToX3x4uPjceXKFcybN0/ToTBZISIioi/PsWPH8MsvvyA5ORkBAQG4fv16jsnK+vXrcenSJdjZ2UlltWrVUpqsJCcnw9nZGY8ePcKkSZM0lqyEhYUhLS0NFStWzHWb1NRUvHjxAqVLl0bp0qXzNH5sbCxevnwJMzMzFCtWLMsYY2NjUbNmTejp6eW6bxcXF7i6ugIA6tSpg1KlSqFIEe146/ry5Uvcu3cPjRs3Ro0aNbKtGx0djQsXLiiUt2vXDqVKlVJr/KCgIDx8+BAtWrRApUqVFO6np6fj9u3biI6ORuPGjeUSvPT0dHz48EFpv8WLF1crnoKkHb/x/2/FihV4//49fvvtN6ksPDwckyZNgrm5OebMmYOVK1cq1Mncx7lz5xTKFy1aBEtLS/zzzz/YuXOn3L1ixYoplBEREVHhlZqaiq1b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rm3Xrh1+/vlnqW18bMGCBdi1axcaN26MuLg4vHjxAkuWLClysXFSUhKMjY2RmJhY5H7WRERE/0Wy3ntr6NChXLelZG3btpW6m+l3332HyZMnA/iwkUpcXJzE9stRUVFYsmSJmJBNnTq1xBtJSnPy5EnMmzevUPmUKVMwY8YM8Xjnzp3w9PREUlISHBwcsHjx4kKL7iuSx48fS1383rVrV8ycORPAh2TlxYsX+PPPP8XzJ0+exJkzZ5CamoqmTZti0qRJCr0Od+7cwcqVKwuVOzk5YcKECeLx7du38c8//yAhIQEdO3bE1KlTK+yaFbVKVvz9/ZGWllYoEcnfAaJTp054/fo1IiIiCl1bq1YtODg4FNnGx96/f4+goCDo6+ujWbNmqFatWpF1mawQERGVDpMVIlKUWiUr6ojJChERUekwWSEiRanlAnsiIiKqPCrbehgiKj8cWSkBR1aIiIiIiFSDIytERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK3Ly9vZWdQhERERERP8JTFbkdPDgQSYsRERERETlgMmKAg4ePKjqEIiIiIiIKj1tVQdQWeSPtty+fRtr1qxRcTRERERERBUfR1aU5ODBgzh48CCOHj2q6lCIiIiIiCoFJivliGtdiIiIiGQjCALWr1+Pjh07onv37jh9+rSqQ/rP0NbWhra2Nr788ktVh8JkpTxJW+vycQLj5uYGb29vJjVERERKEhISgpMnTyIhIUGm+llZWfD398f169fx9u1bqXVSUlJw8+ZN+Pn5IS0tTYnR0scOHDiAp0+fYv369ZgwYQKGDh2Kd+/eqTqs/4SMjAxcunQJubm5qg6FyYqqfZzAHD16VJxO9rGCyQuTGSIiouJduXIFXbt2Rc+ePeHk5ISQkJASr/nnn39gY2ODqVOn4scff0T9+vUxZ84c8Xxubi5mzZqFhg0bYs6cOXB1dYWlpSUOHTpUlk+lWFlZWQgPD0dqaqrM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H3VSIZOV8sRkhYiISHFMVoioNCr1mhUiIiJSrYoydYyI1BNHVkrAkRUiIiIiItXgyAoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFa0lZ1AOpOEAQAQFJSkoojISIiqjyqVasGDQ0NVYdBRGqOyUoJ4uPjAQD16tVTcSRERESVx5s3b2Bubq7qMIhIzTFZKUH16tUBABERETA2NlZxNKQMSUlJqFevHiIjI2FkZKTqcEhJ+L5WTnxfK5/891RHR0fVoRBRBcBkpQSamh+W9RgbG/N/lJWMkZER39NKiO9r5cT3tfLhFDAikgUX2BMRERERkVpiskJERERERGqJyUoJdHV1sXjxYujq6qo6FFISvqeVE9/Xyonva+XD95SI5KEh5O/NS0REREREpEY4skJERERERGqJyQoREREREaklJitERERERKSWeJ+VYmRkZCA4OBjVqlVD48aNVR0OFZCXl4d79+5BT08PrVq1klrn3bt3ePnyJSwtLcUbfBaUmJiIFy9eQE9PD9bW1lJvVCZLX1R6ubm5CAsLg6amJqytraGtLfkn6vnz53j9+rVEmb6+Ptq1aye1rcePH6Nq1apo2LCh3H2R8mRmZiI0NBQGBgawsrIS71/1sbS0NDx79gwaGhpo2LAhqlatWmR7YWFhiI2NRdu2bWFoaKhwHVJMdHQ0nj17Vqj8008/LVSWmJiIhw8fwtraGrVr1y50XhAEhIeHIy0tDQ0bNoSenp7CfRFRJSWQVMeOHRNMTU2FRo0aCSYmJkKnTp2E2NhYVYdFgiBkZWUJK1euFKytrQVjY2PB0dGxUJ3AwEChV69eQvXq1YW2bdsK+vr6wvDhw4XU1FSJenPmzBGqVq0qtGnTRmjQoIFgYWEhHDt2TK6+SDmWL18uWFhYCE2bNhWsrKyEOnXqCEePHpWoM23aNKF69epCly5dxMfIkSMLtXX06FGhdu3agpWVldCqVSuhe/fuwps3b+Tqi0ovMzNTmDt3rlCjRg3B1tZWsLCwEKytrQUfHx+JemvXrhWqVasmNG/eXGjatKlg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PHz4EK1atcK2bdtgZ2cncx1SjoLva1JSEu7cuSP3pga5ubkYNWoUbt68CR8fH6kbmSirLyKquJisSNGnTx84Ojpi0KBBWLRoEV6+fInVq1dj8+bNUm9mRuXPz88PGRkZiI2NRWJiorjLV/6Nwo4fP47Ro0dj8uTJaNCggXheV1cXHTp0AAC4urpi/fr1GD58OFxdXZGQkIClS5eic+fOEjcZLKkvKr2cnBz07dsXT548wY4dO/Do0SPxXKNGjWBhYYHc3FzY2dlhwoQJaNmyJV69eoUVK1agdevWEh9cli5dCgcHByxatAifffYZ7t69i507d4rfrsvSFynH0aNH4e3tjUGDBqFOnToIDg6Gu7s7BgwYAFtbWwDA119/jcWLF6N///5wc3ODIAhYvXo1LC0t0bdvXxU/A5LGyckJn3/+OTp06ICkpCSsWbMGVapUkZhqmZKSgsDAQAAfkpLnz5/D19cXNWrUQNOmTQEAkyZNwpEjR/D333/j9evX4g1fP/nkE1hZWcncFxFVbhqCIAiqDkIdpaamYvXq1bhx4waqVauGMWPGwNnZWdVh0f83ZMgQREdHFyrPTyT+/PNPHDp0qND5mjVr4vDhw+Lxs2fPsH79eoSGhqJq1aro1KkTpk2bJvFNXkl9UemlpKSgd+/eUs/98MMPGDBgAAAgJiYG69evh7+/P0xMTPDZZ59h0qRJEnPcASA4OBhr1qxBeHg46tWrhwkTJojJpax9kXJcuXIFXl5eiIyMRO3atdGvXz8MGjRI4oufN2/e4H//+x/u378PDQ0NtG3bFt9//73E9tIfe/HiBUaPHg0PDw/Y2NgoXIcUk5ycjI0bN+LGjRvQ0dFBu3btMH36dImplKGhoZgwYUKha7t164ZffvkFwIc1aNJuBzB06FB89913MvdFRJUbkxUiIiIiIlJLnNNERERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBDRf9Lhw4fx6tUrVYdBRERExeBNIYlILTx48ACPHj0CAGhpaaFOnTqwtbWFvr5+mfRnYmKCbdu2YfDgwWXSPhEREZWetqoDICICgP3792Pt2rXo27cvcnNz8fDhQ7x79w779+9H165dVR0eERERqQCTFSJSG7Vq1cK+ffsAAIIgYMCAAZgyZQpCQ0PFOufPn8e7d++goaGBOnXqoG3btqhWrZpEO4cPH4a9vT20tbURGBgIIyMjdOrUCZqaRc98TUtLw4kTJ9CiRQu0bNmybJ4gERERyYXJChGpJQ0NDfTu3RsnTpxAeno6qlatCgC4evUqnj17BkEQ8PTpU0RFReHw4cPo3LmzeK2rqyscHBwQFhaGZs2awc/PD02bNsXFixehpaVVqK/379+jX79+MDAwwJEjR8rtORIREVHxmKwQkdp69uwZjIyMoKenJ5YtX75cos6yZcswbdo0BAQESJQnJibiwYMHqFq1KmJiYmBtbY3Tp0/DyclJol50dDS+/PJLNG3aFF5eXtDR0Sm7J0RERERyYbJCRGojJSUF+/btQ15eHu7fv4+NGzfit99+g4aGhkS9ly9fIiQkBAkJCdDV1UVQUBAyMzOhq6sr1hk3bpw4GmNhYQEbGxuEhoZKJCtPnz7F3Llz0atXL2zcuLHYaWJERERU/pisEJHaSE1NxdGjR5GdnY1///0Xtra2GDNmjESdqVOnYufOnbC3t4eZmRnS0tIgCALi4uLwySefiPWqV68ucZ2uri4yMjIkypYtW4batWtj3bp1TFSIiIjUEP/vTERqI3+B/aFDh/D48WOkp6fD1dVVPP/vv//i77//xsOHD3H58mUcOHAA8+fPB/BhQb68Vq9eDV1dXQwdOhTZ2dlKex5ERESkHExWiEgtGRoaYtOmTTh8+DAuXrwIAIiJiYG+vj7q1asn1jt48KDCfdSqVQtXrlzBkydPmLAQERGpISYrRKS2OnXqhIEDB+LHH3+EIAjo0qULdHV1MWzYMGzfvh2TJk3Czp07S9VHrVq1cPnyZYSEhDBhISIiUjNMVohILbRu3Rr9+vUrVL5y5Uo0btwYwcHBMDMzw+3bt2FtbY2rV6/CysoKPj4+GDZsmMSd7gcNGiQx+gIAPXv2RIsWLaTWsbCwwJUrV2BkZFSqkRoiIiJSLg1BkYneREREREREZYwjK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqaX/BztKDe1fUkaGAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
]
},
"metadata": {},
@@ -250,8 +203,10 @@
}
],
"source": [
- "enriched = over_representation(result, regulons, id_column=\"symbol\")\n",
- "plot_over_representation(enriched)\n",
+ "enrichment = gsea(result, regulons, id_column=\"symbol\", rng=1)\n",
+ "plot_enrichment(result, regulons[\"STAT1\"], id_column=\"symbol\")\n",
+ "plt.show()\n",
+ "plot_gsea_table(result, regulons, enrichment, id_column=\"symbol\")\n",
"plt.show()"
]
},
@@ -260,53 +215,150 @@
"id": "a7f52f20",
"metadata": {},
"source": [
- "The genes up hold the targets of the same regulators, and the genes down hold MYC's.\n",
- "The lists, and so the result, depend on the cutoff of 0.05 that made them.\n",
+ "GSEA ranks first the targets of the regulators that camera ranked first, STAT1, IRF1\n",
+ "and STAT2 among them, and finds ID1's targets at the bottom. It warns that one set, the\n",
+ "383 targets of MYC, has too few random scores of its sign to be tested; its p-value is\n",
+ "missing, as fgsea leaves it ([p-values from random\n",
+ "sets](../statistics/over_representation_and_gsea.md#gsea-p-values)).\n",
"\n",
- "## Gene set enrichment analysis\n",
+ "## Choices\n",
"\n",
- "{api}`gsea` ranks every tested gene by its moderated t-statistic and asks whether a\n",
- "set's genes gather at the top or the bottom of the ranking, as fgsea does\n",
- "[@subramanian2005; @korotkevich2021]. The enrichment plot follows the running score of\n",
- "one set down the ranking, and fgsea's table shows the top sets with their genes."
+ "- **One set or many.** To rank a collection, use a competitive test: {api}`camera` by\n",
+ " default, or {api}`romer`, which takes a result of {api}`limma_trend`, as it has no\n",
+ " observation weights. For a set chosen in advance, a self-contained test, {api}`fry`\n",
+ " or {api}`roast`, answers whether it responds at all, as below; {api}`mroast` runs\n",
+ " roast on many sets with the same rotations ([Competitive or\n",
+ " self-contained](../statistics/gene_set_tests.md#competitive-and-self-contained)).\n",
+ "- **camera's correlation.** The default inter-gene correlation, 0.01, ranks sets of\n",
+ " co-regulated genes higher; `inter_gene_correlation=None` estimates it for each set,\n",
+ " which controls the error rate of each set more strictly\n",
+ " ([camera](../statistics/gene_set_tests.md#camera)).\n",
+ "- **camera, over-representation or GSEA.** camera uses every gene's statistic and\n",
+ " allows for the correlation between the genes of a set; over-representation needs\n",
+ " only a list of genes, and depends on the cutoff that made it, as below; GSEA needs\n",
+ " only a ranking, from any method. Over-representation and GSEA take genes as\n",
+ " independent, which makes their p-values too small for sets of genes that respond\n",
+ " together ([Genes that respond\n",
+ " together](../statistics/gene_set_tests.md#correlated-genes)).\n",
+ "- **Rotations and permutations.** roast's smallest p-value is $1/(B + 1)$ for the\n",
+ " two-sided and mixed alternatives and $1/(2B + 1)$ for up or down, with $B$ rotations\n",
+ " (`n_rotations`, 1,999 by default): 0.0005 and 0.00025. romer's smallest is\n",
+ " $1/(B + 1)$ ([roast and mroast](../statistics/gene_set_tests.md#roast)). The\n",
+ " rotations and GSEA's random sets are drawn from `rng`: pass a seed to get the same\n",
+ " p-values every time.\n",
+ "- **The collection.** Restrict the sets to the genes tested and to a range of sizes\n",
+ " before testing, and name genes as the data do, with `id_column`: symbols here, or\n",
+ " the Ensembl ids of `feature_id`, the default.\n",
+ "- **Scores per sample.** To compare samples rather than test one contrast, score each\n",
+ " set or regulator in every sample: [Pathway and regulator\n",
+ " activity](pathway_activity.ipynb).\n",
+ "\n",
+ "### One set chosen in advance\n",
+ "\n",
+ "limma's barcode plot shows where the set's genes fall among all genes ranked by their\n",
+ "moderated t-statistic, with a worm above that rises where the set is enriched. fry and\n",
+ "roast test whether the set responds at all, and camera, for comparison, whether it\n",
+ "responds more than the other genes."
]
},
{
"cell_type": "code",
- "execution_count": 5,
- "id": "54cce687",
+ "execution_count": 4,
+ "id": "2265490a",
"metadata": {
"figure_descriptions": [
- "GSEA's enrichment plot of the 135 targets of STAT1 among the genes of the CD14+ monocytes ranked by decreasing moderated t-statistic: the running score rises steeply over the first 500 ranks to its peak of about 0.69, the enrichment score, marked by a dashed line, then falls steadily to about 0 at the last rank, 5,125; ticks below mark the set's genes, dense among the first ranks and sparse after.",
- "fgsea's table of the 10 sets with positive scores and the 10 with negative scores that have the smallest p-values: for each, its genes as bars at their ranks from 0 to 5,125, its normalized enrichment score (NES), p-value and adjusted p-value. STAT1, IRF1, NFKB1, STAT2, RELA, IRF3, SPI1, NFKB, STAT3 and REL have scores from 1.47 to 2.33 and adjusted p-values from 4.9e-15 to 5.1e-3; of the sets with negative scores, only ID1 has an adjusted p-value below 0.05, 5.4e-3."
+ "limma's barcode plot of the 92 tested genes of the Hallmark interferon-alpha response among the 5,125 genes of the CD14+ monocytes, ranked by moderated t-statistic from -28 on the left to 39 on the right, over a bed shaded blue and red at the two ends. Three of the set's genes fall among the lowest statistics and nearly all the others among the highest, at the right end, where the enrichment worm rises from below 1 to 6.7."
]
},
"outputs": [
{
- "name": "stderr",
+ "name": "stdout",
"output_type": "stream",
"text": [
- "/tmp/ipykernel_1042034/1970039944.py:1: RuntimeWarning: 1 gene set(s) have fewer than 10 random scores of the sign of their enrichment\n",
- "score among 1000 permutations, but some; their normalized scores and p-values\n",
- "are NaN. More permutations (n_permutations=10000) may give them enough\n",
- " enrichment = gsea(result, regulons, id_column=\"symbol\", rng=1)\n"
+ "92 of the set's 97 genes are among the 5,125 genes tested\n"
]
},
{
"data": {
- "image/png": "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",
+ "image/png": "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w+EwL7/8cpXXW2+OHLdu3fqQR3knTpyol19+WYsXL1avXr0qzE9LS6vwXaGZmZmKiYkJGfgNlBo0aJAGDRpUob2goECXXnqpCgoKtGTJkkovbtq6dav279+v3r17Swoe9Sv7iXb58uX68ccf9eyzzx67HUCtd6j3tIEDB2rGjBnyeDyKjY2VJP3nP/9R//795XK5JAW/z3jjxo3q27evYmJiVFBQILfbHbKeX375pcLwDOBICgsLlZCQYE/7fD5t375dXbp0sdvKvselpKQoJiam0r+xZS9eBo6ksLBQ8fHxcjgcdltmZqY8Hk/Ie1n5v6kzZ85UcnKy/ud//qfK26o34fhQHnzwQc2YMUN///vfVVRUZP+xadq0qX73u99Jkm677Tb98Y9/1F//+ledd9552rx5syZPnqyxY8fa3yAAHInf79egQYO0YcMGvfTSS9qwYYM9r2PHjvbFAU8//bT++9//6pdffpEkPfnkk8rNzdWAAQO0ZcsWTZ48WRMmTFD//v0jsRuo5caNG6dZs2bpyiuv1C233KLPPvtMS5Ys0eeff273Wbp0qa666iplZmaqTZs2GjVqlDp37qzf//73cjqd+vjjj/Xqq6/q9ddft5fxer365ptvJAX/CO3YsUPLly9XcnLyIcfMo2Hx+/06/fTTNXr0aHXr1k25ubmaM2eOsrKyNH78eLtf2fe46OhojR07Vg899JCcTqc6d+6spUuX6v3339c777wTwb1BXbN27VrdddddGjVqlNq3b6/09HQ9+eST6tWrV8iFdiNHjtQ555yj7t2769NPP9XTTz+tefPmhQynPRKHMfX7VnBjxozRpk2bKrSfd955euSRR+zp5cuXa+7cucrIyFDTpk116aWXasSIEXI66/233aGGFBUVaeDAgZXOmzhxoq688kpJ0t///nd98803mj9/vqTgH5x//OMfWrRokZKTk3X11VfriiuuOG51o+759ddfNX36dK1fv14tW7bUbbfdpjPOOMOe/9lnn+n+++/Xe++9p7S0NHk8Hs2ePVtLly6Vx+NRp06ddNNNN4VckJednV3pldzdunXTCy+8cFz2C7Xf7t279dxzz2nVqlWKj4/XqaeeqrFjx4YcBS7/HhcIBDRv3jx9+OGH2rdvn0444QTdcMMNOvvssyO1G6ijfvjhB82aNUubN29WWlqa+vfvr+uuu84+iyYFL4afNm2a1qxZo/bt22vs2LGVjho4nHofjgEAAICq4rAoAAAAYCEcAwAAABbCMQAAAGAhHAMAAAAWwjEAAABgIRwDAAAAFsIxAAAAYCEcA0AVvfXWW9q1a1ekywhLXardGKP58+drz549EVkeQMNGOAZQ573zzjuaP3++cnJyKsxbunSp5s+fr61btx71dq699lp9//33R72eqggEApo/f7727dtXI+s7XO01sa1w11HZcn6/X8OHDw+5BfuxWh4AyiMcA6jzRo0apeHDh+vll18Oac/Pz9fgwYM1fPhwffrppxGqLjwlJSUaPny4Nm3aVCe2Fe46KlvO6XRq2LBhatas2TFfHgDKi450AQBQE84991zNmTNHt99+u902f/58devWrdIjpnv37tWKFSsUHR2tP/zhD0pJSanQZ/v27frhhx/Uvn179ejRo9LtFhQU6Ouvv5bX69Upp5yi1q1b2/O8Xq/eeecdXXTRRdqzZ4/Wr1+vHj16qHPnznrvvffk8XjkdDrVtm1bnXbaaYqLi7OXfe+99yRJn3zyiXbs2KHU1FRdfPHFR9xmdWqvyrbK8/v9+uabb5Sdna3u3burffv2h11HOPt50UUXaciQIWratGlY261seUkqLCzUihUrVFJSojPPPFOpqamH/XcB0DARjgHUC0OHDtUDDzyglStXqk+fPpKkF198UaNHj9Ztt90W0vfll1/W2LFj1adPH3k8Hm3atEnz5s3ToEGD7D7PP/+8/vd//1e///3vlZOTo6ZNmyoQCISs56OPPtLIkSPVtWtXJScn66uvvtKkSZM0adIkScEQO3z4cF166aX6+eef1bNnTyUmJqpz585atGiRcnNz5ff7tX79epWUlOjDDz9Uly5dJEmLFi2SJC1btkwbN25U27ZtdfHFFx9xm1WtvaxDbau8ffv2qV+/fvL7/eratas2btyoyy+/XE899dQh1xHOfg4YMEDDhw/X0qVL1axZs2pvt/zypdu59tpr1aJFC7Vt21a33nqrZs+erfPPP//QLyoADZMBgDrO7Xab2bNnm1tuucWMHTvWGGPMunXrjNvtNrm5uSY2NtbMnj3bGGPMr7/+ahISEszMmTPt5e+//37TrFkzk5uba/eJi4szr776qt3n5ptvNpLMokWLjDHG7N692yQlJZkFCxbYfTZu3GgSEhLMt99+a4wxZv/+/UaSufjii01JSckh6w8EAubGG280Q4YMsduKioqMJLNs2TK7rSrbrErt5VW2rco888wzpnv37sbv99tt7733XpXXUdX99Hq9RpJZunRpWNstv/xvv/1m3G63mTx5st0nOzvbfPHFF4fdXwANE2OOAdQbo0eP1uuvv66ioiLNnj1bV111lZKSkkL6LFiwQAkJCbrpppvstkmTJikrK0tLly6VJH3wwQdq1KiRRowYYfe55557Qtbz9ttvKyYmRsXFxXrrrbf01ltvae3atWrVqpU+++yzkL5jx46Vy+WqUO/PP/+shQsX6t///rcaNWqkb7/99rD7V5VtVqX2cMXHxysnJ0fp6el225AhQ464XHX3s6a2W6r03+3ee++12xo1aqRzzjmnWnUAaBgYVgGg3ujTp4/atWunN954Q6+99pref//9Cn3S09PVvn17OZ0Hjw243W61aNHCDl8ZGRlq166dHA6H3addu3Yhy2zfvl1SMHiV1bt3b7Vq1SqkrWXLliHTPp9PQ4cO1Weffaa+ffsqNTVVe/bsOeJXj1Vlm1WpvSpWrFhhb08KDlu57rrrtGLFCnXv3l2dO3fWwIEDNW7cOHXo0KHSdYS7n+VVd7vlZWRkqEOHDpV+QAGA8gjHAOqV0aNHa+LEiWrevLnOPvvsCvObNm2q7OzsCu379++3L+Bq0qSJ9u/fHzI/JycnZNxucnKyYmJiNH/+/CPWVDaoStK7776r5cuXa9u2bWrUqJEk6bXXXtPy5csPu56qbLMqtVfFqlWrtGzZMnv6iiuuUFxcnF566SX94x//0IoVKzRz5kz16tVLv/zyi9xud4V1hLuf5VV3u+WlpqYqKyurWtsE0HAxrAJAvTJy5Ehdcsklmjx5cqXzzz77bG3bti3kGywWLlyokpISnXHGGXafn3/+OeR7ct99992Q9Vx88cXauXOnPvjgg5D2oqKiCuG0vF27dqlZs2Z2YJQqHg2OjY2Vy+VScXFxtbZZldrLq2xbt956q+bPn28/YmNj9euvv0oKDnM477zzNHPmTB04cEA//fRTpesIdz/Lq+52y7vwwguVnp6uL774IqR97969h/13AdAwceQYQL3SuHFjvfHGG4ec37dvX40cOVKXXXaZ7rzzTnk8Hk2fPl0TJ07UiSeeKEk644wzdOWVV+rSSy/VxIkTdeDAAc2aNStkaEKfPn109913a9iwYbr11lt10kknacuWLXrnnXf01ltvhQTC8i6++GLdfffduvnmm9W3b18tWrRIn3/+eUgfh8OhXr166e9//7t2796tJk2a2MsdbptVqb28Q22rvDfeeEPvv/++Bg8erLS0NL399tvq1KmTevbsedh6q7ufAwYMOKrtll/+9NNP1+23365BgwZp/PjxatOmjRYtWqSLLrpIY8eOPeS/C4CGiSPHAOq8oUOHqmPHjoec/6c//Slk/ty5c/XYY49p3bp12rp1q+bMmaPp06eHLPP6669r4sSJWr16tQKBgL788kuNGDEiZPzw9OnTtWjRIvn9fi1fvlwpKSn67LPPdMopp0iSYmJiNGzYMDVu3Dhk3V26dNHXX3+tuLg4LVu2TGeffbYWLlyoYcOGhfR788031bNnTy1atEiffPJJlbZZ1drLq2xb5d1555164oknlJWVpS+//FIXXHCBvvnmGyUkJFS6jnD3s/xNPKq73cpuAjJjxgzNnz9f2dnZ+uGHHzR69GiCMYBKOYwxJtJFAAAAALUBR44BAAAAC+EYAAAAsBCOAQAAAAvhGAAAALAQjgEAAAAL4RgAAACwEI4BAAAAC+EYAAAAsBCOAQAAAAvhGAAAALAQjgEAAAAL4RgAAACw/D95xuDJ9x7EAQAAAABJRU5ErkJggg==",
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
"output_type": "display_data"
},
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "fry, self-contained p-value 5.8e-08\n",
+ "roast, self-contained p-value 0.00025\n",
+ "camera, competitive p-value 2.5e-35\n"
+ ]
+ }
+ ],
+ "source": [
+ "with open(\"hallmark_interferon_alpha_response.json\") as file:\n",
+ " hallmark = json.load(file)\n",
+ "# Two genes of the set have earlier symbols in the annotation of these data.\n",
+ "interferon = [hallmark[\"earlier_symbols\"].get(gene, gene) for gene in hallmark[\"genes\"]]\n",
+ "print(\n",
+ " f\"{np.isin(interferon, tested).sum()} of the set's {len(interferon)} genes are \"\n",
+ " f\"among the {len(tested):,} genes tested\"\n",
+ ")\n",
+ "\n",
+ "table = result.table(monocytes, \"stim_vs_ctrl\")\n",
+ "plot_barcode(table, np.isin(table[\"symbol\"], interferon))\n",
+ "plt.show()\n",
+ "\n",
+ "sets = {\"interferon-alpha response\": interferon}\n",
+ "rotations = roast(result, interferon, id_column=\"symbol\", rng=1)\n",
+ "tests = {\n",
+ " \"fry, self-contained\": fry(result, sets, id_column=\"symbol\")[\"p_value\"],\n",
+ " \"roast, self-contained\": rotations[\"up_p_value\"],\n",
+ " \"camera, competitive\": camera(result, sets, id_column=\"symbol\")[\"p_value\"],\n",
+ "}\n",
+ "for name, p_value in tests.items():\n",
+ " print(f\"{name:<23} p-value {p_value[0]:.2g}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1c6f5d90",
+ "metadata": {},
+ "source": [
+ "Nearly all the set's genes go up with the stimulation. The three tests agree, but\n",
+ "answer different questions. fry and roast ask whether the set's genes respond at all:\n",
+ "fry's p-value comes from a t distribution on the design's seven residual degrees of\n",
+ "freedom, and roast's, 0.00025, is the smallest its 1,999 rotations give for one\n",
+ "direction. camera asks whether they respond more than the other genes, a stricter\n",
+ "question when thousands of genes respond, and compares them with every other gene.\n",
+ "\n",
+ "### Over-representation among the changed genes\n",
+ "\n",
+ "{api}`over_representation` asks whether the genes called significant hold more of a\n",
+ "set's genes than chance would give, for the genes up and those down, as limma's\n",
+ "`kegga` tests a fit of limma, and for both together [@young2010]. Its universe is the\n",
+ "genes tested ([The\n",
+ "universe](../statistics/over_representation_and_gsea.md#over-representation-universe))."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "a403f925",
+ "metadata": {
+ "figure_descriptions": [
+ "A dot plot of the 20 sets with the smallest over-representation p-values, with a column for the 1,619 genes up, one for the 1,549 down and one for the 3,168 in either direction; a dot's area gives the overlap and its shade of blue the -log10 adjusted p-value, pale grey where the adjusted p-value is 0.05 or more. The targets of STAT1, IRF1, NFKB1, REL and RELA are over-represented among the genes up, STAT1's most; MYC's targets among the genes down; the column of either direction shows weaker signals."
+ ]
+ },
+ "outputs": [
{
"data": {
- "image/png": "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KytYWVlBX18f06ZNw5gxY1CiRAnY29tjypQpqFatGjp06JBNV0E3TZo0SboVMPD1u0smT56MX375BYaGhliyZAmioqKkr/DQRHR0NJ49e4YvX74A+Hrzqdu3b0vLtvT09DBjxgwMHToUtra2cHR0xIwZM1C+fPlv9g6suZKsHD16FCNGjED16tVRvHhxPHjwAKdOncLChQthbW2dYfstW7bAwsJCYVOVjLu7O2bOnInbt2/Dyckpw74ePHiAdevWSa+3b9+OGzduwNnZGZ07d9bsxIiIiLJRVt9MKJuNSy9JU1aWOoHRdK9hbitSpAguXLiQYb3p06dr3KZDhw46+8b67t27KFWqFCZMmKBQ/uuvv0p3yCpVqhRMTU2lY8uXL8eqVaswb948xMXFoUKFCrh161amv2dFNnbFihWxbNkyAF835Y8YMQIAMHToUJibm2Pz5s2IiopC7dq1MW3atNzd75DDSpUqhfj4eIWyEiVKKNxauEKFChgwYAB8fHwQERGB+vXrY+PGjVJSqYnLly/j119/BfD1uv/555/4888/0bt3b4wfPx4A0L9/f5iYmMDX1xcRERFwdnbG9OnTs30WJ7foCaHifnXZLDw8HGfPnsWrV69gZWWFevXqKZ1Cff78OQICAjB48GDpGzQDAwORJ08edOzYUWnff/zxB+rUqSNNM964cQMnT57EyJEjVfafWuXKldG8efM05VFRUbC0tERkZGS63+hJRET0o1Hn+7Jkx69cuYLHjx/nRlhE9B3JtWTlW8VkhYiIKPOYrBBRVuT6BnsiIiL6cciWjl25ckXLkRDRt4jJChEREeUY2cyKOntEiIhS4zKwDHAZGBERERGRduTKN9gTERERERFpiskKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5isqKhdu3awdvbW9thEBERERF99wy0HcC35syZM7h9+zZcXFxw4cIFAICPj4+WoyIiIiIi+v4wWcmkwMBAXLlyBQDg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m/n5+SE6OlphM5IQAtOmTUNUVBRmzZoFCwsL+Pn54ePHjyq/dVXVcT8/P1y5cgWjR4/GqVOncOnSJemYpaUlypcvL/3PI3P//n2sXr06zRi1a9dWuvk/KioKlpaWiIyMzNJ0KBER0fckow32qTftHz16FB8/fsy1+H4ksbGxCncyk4mOjkZKSorC98MkJyfj06dPsLS0VHh/pKn4+HhER0enKTc1NU2zPC0pKQnx8fFKY/xWxcfHw9DQEHnypL2xbkxMDJKSkhSSspSUFHz8+DHL1z0hIQFRUVFpyk1MTNJc36SkJMTFxcHc3DzT4+mCHE1WvgdMVoiIiDQnS1SYrBBRVuTK96wQERHRj0WWpOjy8jEi0n25umeFiIiIfiyypEX2XWJERJrgMrAMcBkYEREREZF2cBkYERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK2ratWuXtkMgIiIiIvqhMFlR0+7du7UdAhERERHRD4XJChERERER6SQmKxoKCAjQdghERERERD8EJisaCgwMlJ57e3szeSEiIiIiyiEG2g7gWxYcHIyXL19Kr7t06aLFaIiIiIiIvi+cWckGgYGBCjMuAJeLERERERFlFZOVHCKfvDBxISIiItLMuXPn4OrqqvA4efKktsP67qWkpChcc09PT63Gw2VguSAwMDDNErGAgACpTPZcvoyIiIg0k5KSgjx5NP8cNjExEYaGhhn2raenBz09vcyGRxr66aefMGHCBABAUlISevbsicqVK2s5qu9fSkoKbt68ie3btwMATE1NtRoPZ1a0RH7mRfY89VIyIiIiytju3bvRoEED5MuXDxYWFmjdujVCQ0PTbSOEwJQpU1CwYEEYGxvDyckJp06dSlPv2rVraNKkCYyNjWFlZYWJEyciISEhp06F5BQpUkT6dD8mJgatWrVCwYIFtR3WD+HLly9YuXIlduzYAXNzc63GkmszKykpKbh06RKeP3+O4sWLo0aNGjA2NgYA/PPPP4iNjVXZ1sbGBi1atADw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MDU1xYEDB7T+RTdERJS7mLB9X8LCwrB3714AgIuLi8IxX19faYmTsbGxwjdxDxw4EHFxcfj999/x7t07VK1aFceOHYOVlZVUp23btli3bh2mTp2KFy9ewNbWFsuWLVNIaHRZ6r0PHTt2hJeXF/z9/dGmTRu8ePECU6dOxc8//6wywcksY2NjDBs2DPPmzYOLiwvs7e0xcuRIWFtbo1u3btk6Vm5asmQJZs6ciX379qFJkyZK6/Tt2xc3btzAjRs3cjc4AIaGhvjtt9+wePFi1K1bF46Ojhg7dizy5cuHXr165Xo82SFXkpWRI0fiy5cvaNCgAYoXL44HDx5g/fr16NevX4b3pAa+ri9OTk6WvgRSnru7O5YvX44XL16o9eWOLVu2xP379zFv3jzs3LlTKq9atSqqVq2qyWkRERGRlhUrVkytL9dTtsd1+PDhGD58eLrtunXr9k2/uZY3YMAAfPnyBTNnzkS/fv2QP39+uLq6Yt68eRr3tXXrVoU7dMneo02aNEn68sbZs2cjJSUF7dq1k74U8vDhw9/0ipZZs2bh8+fPab5bZvz48Zg/f36Ojx8cHIwOHTpIr2XPf/vtNyxbtgwAMHXqVCQmJsLDw0P6UsgjR45keMcyXaUnZPN/Oezs2bM4fvw4Xr16BSsrKzRu3Fjp1Nndu3excOFCLF68GIULFwYALF++HPHx8Rg7dmya+snJyfDy8kK3bt2kX5xjx45hx44dWL9+fZr6gwYNQmJiYppyd3f3NN8ACnydAbK0tERkZKTKzVNERERERJT9ci1Z+VYxWSEiIiIi0o5c3WBPRERERESkLiYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5isqKmiRMnajsEIiIiIqIfip4QQmg7CF0WFRUFS0tLWFtbIywsTNvhEBERERH9MDizoqb4+Hhth0BERERE9ENhskJERERERDqJyYqGatWqhYCAAG2HQURERET03TPQdgDfmpCQEAQGBiqUdenSRUvREBER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UK9///7S8i3Z48qVKxnWefLkifjjjz9E4cKFRVJSUpoYtm/fLszMzERUVFSaY1wGRkRE3xNlSwhVLQlM/ZBfXqlLy8BSUlJEYmJimkdKSopUJzk5Wel7gIxktl1Oe/funejTp4+wtLQUefPmFYUKFRK//vqr+Pz5s1SnU6dOom7dugrtjh8/LsqVKyf09fWFubm5GDt2rEhOTtZ4fH9/f2FpaSny5csnAAgzMzNhaWkpZs+eLdWJi4sTgwYNEsbGxsLAwEBUq1ZNXL16NfMnrQMOHjwoateuLczMzISRkZGoVKmS2Lhxo0IdT09PUaVKFem1u7u7ACAsLS0VHuosHUtt3759UnsAwtTUVFhaWiosLUtISBBDhw4VJiYmQl9fX1SuXFlcuHAh0+esbXpCqLnbJof07NkT3bp1Q9u2bRXKlyxZgufPn+OPP/6Q6sXGxqZp36hRI2kJmao6v/zyC/7991/kz58/zaY54Ou3kXp6emLs2LFpZleioqJgaWmJyMhI5MuXL7OnSURE9F3JaIN96hsMHDhwIMubhkm5+Ph4GBkZpSn//PkzUlJSYG5unuZYXFwcjIyMMj2jkpiYqPQ9l7GxcZrlaSkpKUhMTFQa47cqMTERBgYGSq/f58+fkZycDAsLCwBflxIqWyZmYGCg9GeT0bjKrruRkVGaWyGnpKQgISEhzc/jW6P1ZEXXMVkhIiLSnCxRYbJCRFmhc9+zQkRERN8+WZLCGxMQUVbo3AZ7IiIi+n7IkhbZN60TEWmCy8AywGVgRERERETawWVgRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrBARERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrKhp165d2g6BiIiIiOiHwmRFTbt379Z2CEREREREPxQmK0REREREpJOYrGRCQECAtkMgIiIiIvruGWg7gG9RYGAgLly4ABcXF3Tp0kXb4RARERERfZc4s5JJwcHBCAwMlF57e3trMRoiIiIiou8Pk5VsEhwcrO0QiIiIiIi+K0xWcgH3uBARERFlzr///oslS5Zg9uzZ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ERERkU5iskJERERERDqJyQoREREREekkJitERERERKSTmKwQEREREZFOYrJCREREREQ6ickKERERERHpJCYrRERERESkk5isEBERERGRTmKyQkREREREOonJChERERER6SQmK0REREREpJOYrKhp165d2g6BiIiIiOiHwmRFTbt379Z2CEREREREPxQmK0REREREpJOYrGggICBA2yEQEREREf0wDLQdwLckMDBQet6lSxctRkJERERE9P3jzIqGAgMDpaSFMy1ERERERDmHyUoWyCct3t7eWo6GiIiIiOj7wmQlGwQGBiI4OFiaafH29mbyQkRERESURdyzko0CAwPRpUsXBAcHAwB8fHzQrl07ODg4wMfHR7vBEREREX1Dvnz5gokTJ0qvixcvjrFjx2oxoh/P0qVLUaBAAfTt21drMXBmJYedOXNGSl5kUu91kZ+RISIiIiIgPj4e27dvh52dHezs7FCsWDFth/RD2bZtGw4cOIB9+/ZpNQ4mK1ogf1cx+dfyS8nkE5qAgABu5iciIlIhKSkJgYGBaNasGezs7HD9+nW12r19+xa//vorqlSpgnr16mHbtm0Kx2NjYzF9+nTUrl0b5cuXR8uWLbF3796cOAVSQU9PD+/evYO+vj7atGmj7XB+GA8fPsTx48cxdOhQbYfybSwD+/DhA96+fQs7OzuYmpoqHHv69Cnev38PADA0NETJkiVRsGBBhePx8fFwdHRUaBcVFYX79++jdOnSCvW1TbaUTPZfWRmgeLvkgIAAXLhwAS4uLtKxgIAA3lKZiIh+ON7e3nj16hU6d+6MIUOGID4+PsM2b968gbOzM6pWrYp169bB2NgYy5Ytg6OjI2rWrAkAGDJkCM6dO4c1a9agVKlS2Lt3L9zd3XHw4EE0a9Ysp08rXYmJidi4cSP27t2LDx8+oGzZsvj1119Ro0aNdNu9evUK06dPx/Xr11GkSBF4eXmhffv2Go8vhMDx48exdu1ahISEYMOGDdJ7EnlbtmzB5s2bERkZiTp16mDatGnS+65nz55h6dKlSvtfvHgxTExMMGHCBCQnJ+PkyZNYtWoVrl+/DhMTE43jzU6bN29GQEAA3r59CxsbG3Tv3h09e/ZUWf/OnTvo2rVrmnI/Pz9UrlxZ4/H//fdfrFmzBrdu3cLy5cvRpEmTNHV27NgBX19fREREwNnZGdOm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NJo2bYqaNWuqNI420Xiyou2YrBAREeWe7BAD2QZ7JitElBdatwyMiIiICq/M+4JcXFwUjgImIsotJitERERUYNzc3KSji/Py3A4i+joxWSEiIqIClfGhsyEhIRqOhogKEz62nYiIiD6LZcuW5eoYZiIiGc6sEBER0WfDRwUQkSp4GlgOeBoYEREREZFmcBkYERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrubR//35Nh0BERERE9FVhspJL69at03QIRERERERfFSYruRQcHKzpEIiIiIiIvipMVnIpOTlZ0yEQEREREX1VmKyoqHnz5vDy8tJ0GEREREREXzwmKyry9/eHt7c3ExYiIiIiogLGZEVN3t7eACAlLUxeiIiIiIjyF5OVPJIlLbI/ZSZPnqyJcIiIiIiIvhhMVgrIwYMHAXDGhYiIiCiv9u7di9OnT2s6jK+GEAK3b9/G9evXkZaWptFYmKwUsMwzLjJeXl5cQkZERESUg4MHD2LJkiXYtWuXpkP5KiQmJqJLly748ccf8f3336NZs2b48OGDxuJhsqIh3t7eCkvIMiYtGZeRZUxsiIiISNGdO3cwcuRItG/fHo8fP85Vm8TERCxbtgy9e/fGoEGDcPnyZbn7Hz9+xJYtW/DNN9+gd+/emD17Nt68eVMQ4VMWXr16hX379mHixImaDuWrkZSUhPnz5+P69evw9fWFgYEBbt++rbF4inzuAWNjY1GyZEm5srCwsGynmAwMDFC6dGnp+v379zAwMICpqSmAT1NV7969y3ZcY2NjuXHT09Px4cMHFC9eXI1XUTC8vb3h5uYG4NOnCMuWLZPKAUj3evXqhcOHD8PLywtubm5SYrNs2TJMnjxZakdERPQ1mDFjBnx8fNCjRw9s2rQJsbGxObaJjY1F27ZtYWBggMmTJ8PIyAi//vorli1bhoYNGwIABg4ciPLly6Nv377Q1dXFunXrsHnzZty/fx/ly5cv6JeVo5cvX2Ljxo24f/8+pk+fjjZt2mRb//Hjx/jpp58UyleuXAkzMzOVxw8KCsL69evh7++PhQsXolGjRgp1Ll68iB07diAmJgZ2dnYYO3YsDA0NAQChoaHYs2eP0r7Hjx8P4NPvdsWKFThx4oTK8RWUwMBAHDlyBO/fv4e5uTn69++PEiVKZNsmKioK3t7eCAwMRMWKFdGvXz/UqFFDrfHj4+Oxc+dO+Pn5YdSoUdLf14yePXuG3bt3Izo6Gi1atEC/fv2go6MDAIiJicH58+eV9t29e3eUKlUKtra2OHjwIEJCQpCcnKz0d/u5fJaZlcTERPz4448wMTFB1apVUapUKQwYMED65KNr166wsbGBjY0NrKysUKlSJVhbW0tlI0aMkPp68uQJKlSoABsbG6Snp0v9y+ra2Nigbt26qFSpklzZwoULAQABAQHo378/SpcujYoVK6Jy5cqF7s39lStXAPxfEnPw4EFpj4zsT5nJkyfnarkZZ26IiKiwmjZtGu7evYvBgwfnus2cOXMQERGBc+fOYcCAAXB2doaPjw/q1Kkj1dm8eTNWr14NFxcX9O3bF/v378fbt29x7ty5gngZKvHw8EC7du2gq6uLY8eO4fXr1zm2iYyMxLFjx/Ddd99hzJgx0leZMmVUHv+PP/5At27dUKRIERw7dgxhYWEKdfbt24dOnTqhYsWKcHR0xPr169GrVy/pfnJyMoKDg5V+CSGwZMkSAMCOHTtw6tQpPH78WONJy4oVK9C/f3+8e/cOpqam2Lx5MywsLBAcHJxlm2vXrqFVq1a4f/8+ypUrh9u3b6NOnTo4duyYyuPv2rULdevWxdWrV7F69Wo8e/ZMoc7169dhbW0NPz8/FC1aFBMmTMA333wj3Y+MjMTWrVuVfqWkpAD49LvZunUrtm/fjtq1a0NPT0/lWPON+AzGjBkjzMzMxKNHj4QQQiQkJIh//vlHLFiwQKGuj4+PACCeP3+utK+pU6cKOzs7UaxYMXHq1CmldVauXCkMDQ2V3lu6dKk4evSoSExMFEIIcfjwYaGnpyf27duntH5MTIwAIExNTYUQQhQvXly4uroKV1dXIYRQ+FPGzMxMablMTn3I2meuK4QQpUqVkqtvZmYm1c/YTnadVYyZ4xFCiEmTJom9e/cKIYT0Z2bK7k+aNCnLvomIiD6HwMBAAUBcv34923rp6emidOnSYtasWSr1f+7cOaGrqyt8fX3zEma+ePv2rfj48aNITU0VAMTu3btzbHP58mUBQHoPlBevX78W6enp4u3btwKA8PHxkbufnp4uatasKSZPniyVPX78WAAQJ06cyNUY27ZtExMmTBATJkwQXbt2FZaWlmLbtm15jj0v/vvvP7nrlJQUUalSJfHrr79m2ebVq1ciISFBrszFxUU4ODioPP6jR49ETEyMiIuLEwDEgQMHFOo0bdpUDBw4ULq+e/euACDOnj2bqzFiYmLkrocNGybWrl2rcqz55bPMrJw+fRrffPMNrKysAHxakuXi4oJZs2ap1E9qaiq2b9+OiRMnon///ti0aZPKsUyZMgXdu3eHkZERAKBnz56oXLkyHj58qHJfX6KDBw9meRyzjLL7mWd0Mst8oEDmmZysZnYylmf3PQ8rICKi3Hrz5g0iIyNhZWWFmTNnwsnJCSNGjMC1a9cU6l65cgXt27dH48aN4ebmhkOHDml0SYxMxYoV1f60e9SoURg4cCAWLFigdEYkNypXriwtK1ImICAAwcHB6Nu3r1RmaWkJKysrnDx5MldjDBkyBMuXL8fy5csxaNAgNG/eHEOGDFEr3vySceYN+LS/IyUlJdvZqapVq8LY2FiuLDY2Vq0ZLSsrK4XtFBm9efMGd+7cwaBBg6SyJk2awMrKCocOHcrVGLdu3YKbmxs8PT2xatUq+Pj4wNraWuVY88tnSVaqVKmCU6dO5XlT2pEjRyCEgLOzM0aPHo1Dhw4hPDxc5X4+fvyI0NBQPH36FL///jsSEhKk/SBUMDIfKJA5EcopMcrp++wOK1BV5sRn8uTJuX5uTm7GZUJFRKRZiYmJAICJEyfCwMAA48ePR/ny5dGmTRuFN3T16tXDvHnz8PPPP8PGxgZTpkzJcZ+sNmvdujXs7OzQpUsXnD9/HhYWFggMDFRad+7cuZg7d65a4zx//hzApzfqGVWrVk26pwpLS0t06dJFrVjyW3h4OMaNG4fhw4ejWbNmGDZsGEaNGpVju9mzZ2P06NFo3rw5ihYtihUrViit5+Hhgb/++kut2GS/y8x7kGrVqpXl7zmzTp06YdiwYfDx8cHjx4/h6emJ1q1bqxVPfvgsycrKlSsRExODqlWrolGjRhg9erRa6/Q2bdqEoUOHQl9fH82bN4elpSV27Nihcj9Pnz6FjY0NrK2tMW/ePPzxxx+oV6+eyv2Q9soq+VGlrbI9Qfkxrre3d5azRNmV5eaejGyvUm76zlyXe5uI6EsnO7TH0dERv/76K7p164b//e9/6Nevn8I+1rJly6J9+/ZwdXXFsWPHkJCQgFWrVmki7DyzsbHBpUuXMGbMGAwbNgxnzpxBrVq1MH36dKX1b968iZs3b6o1lmzvQ+YZBWNjY+meKmxtbeX2XWiSvr4+6tWrh9q1a8PExATnzp3LVQJbu3Zt1KlTB9WrV8fNmzcREBCgtN6JEyewb98+tWKTJeKZN/yXLFlSpeOHnZycsG3bNqxZswbt2rVTK5b88lmSlUaNGiEgIADXrl3D4MGDERISgp49e8pNUeUkJCQEp0+fRs+ePREaGorQ0FC4uLjAw8ND5XgsLCwQGhqKhIQE7N27F2PHjs0x6WnZsiWAT5m9i4sLXFxcAEDhTxlnZ2el5TI59SFrn7kuANjb28vVd3Z2lupnbCe7zirGzPHktn5O8WbVJmO7zH3nNFZO3+f0+1BF5j4y/nxz2zavdYjyW26T4pwSaVXHyu2spLLxcopF1nduEuzcvpbcLlHND7n94CEv/Xt5eUl9ZPXzyssy2qziU/a7y+5DlM+tdOnSqFmzJqpVqyZXXq1aNURERGTZzsDAABUqVCi0MyvFixeHru7/vfXT1dVFt27dcPfuXaX158+fj/nz56s1luzE1sjISLnyiIgI6V5hZWJignHjxuGXX37BtWvXIITAL7/8kmO7oUOHYurUqfD29oarqyuGDh2qtN6IESPU/rcvO+U2OjparjwqKirb5WNaTVObZdasWSMAiICAALnyrDbYz58/XxgbG4sKFSrIfRUpUkRhI112G+yVcXFxEV26dFF6T7bBPvNmIyKiwkTZgRk5lWV1yIYqY6ly+Ebm8XKKRdZ3dnHmdGCIKjHkt+wOPZHJfGiKqv27uroqHMCSedzcHMKianwZ+8p4GEzmA2PyW3Yb7H/77TcxatQouWsLCwsRGRkphBAiLCxMmJmZiXHjxgkhPv3/f/369SItLU1qc+TIEaGnpyf++eefAnsNqlJlg70y48aNE7Vr11Z7/Kw22IeFhQldXV25f0OpqamiTJkyYtGiRWqPp42GDx8uWrZsqVKb7du35+mwg6w22IeHhwsdHR3h5eUlV16zZk0xffp0tcbStM8ysyI7Yjij+vXrA/i0MSknQghs2bIFf//9tzSrIvtyc3NTaaO9EEKhLDIyEkWLFs11H0REhY2yfXk5lam7ly9jO1WOhs88Xk6xyPrOLk7Zvdy+luxiyG+5mWXN7azu1+7w4cNo3749vv32WwDA2LFj0b59e7knnvv7+8vNIPz0009o1qwZatWqhebNm6N27dqwtrbGokWLAABFixbF48ePUalSJTRv3hzm5uZwd3fHokWLCs0M+eLFi+WWeHl5ecnNHP3777/Ytm2b3Cb4/FK2bFl069YNf//9N5KTkwF8Ws4fFxeHAQMG5Pt4n8vp06flriMjI3HmzBnY2tpKZZ6enli8eLF0ffHiRelnAHx6X+zt7Y369etLBz7llzJlyqBTp05Yv3699P77+PHjCA4OLrT7sz/LQyHbt2+Pbt26oW3btqhSpQoCAwPx008/oVGjRlLSkp2zZ8/ixYsXcmdzyzg7O2P48OFYvnx5rh7w2LFjR4wdOxaNGjVCYmIidu7ciYsXL6q1h4aIiEhduXnjkJfngMneUMs2OGe1PDk/ls9qWtOmTTFv3jyF8lq1aknf//LLL3Jr9vX19bFjxw6EhIQgJCQENWrUQKVKleTuL1++HAsXLoS/vz9KlCiBmjVrSg801LR79+5hzpw50oewS5YsgaenJ7p164axY8cCAO7cuYPQ0FCpTZEiRdCiRQuULl0aurq6ePDgAQYPHqzWUq8LFy5g6dKl0pvwWbNmYfny5RgwYIC0zH/dunVwdHRE7dq1UblyZTx69AgbNmxAzZo18/jqNcfb2xvTpk2DjY0NkpOT4ePjAxsbG7mf4ZkzZ+Dr64sZM2YA+LTpffTo0bC2toaxsbF06tzu3btVHv/ff//Fhg0bkJqaCgDYuHEjzpw5AwcHB/Tr1w8AsHr1ajg4OKBZs2YwNzfH8ePHMXv2bDRp0iSvL18jdISyqYZ89vbtW6xZswbnzp3D69evUb58eTg4OGDKlCkKT4C9fPkyXF1dcfv2bWkt6fjx4/Hs2TOlCUV8fDwsLS2xbNkyuLq6AgC2bNmCX3/9VekDegICArBo0SLcvHkT+vr6qF+/PiZNmoQWLVoojT02NhYmJiaIiYkpvGv9iIiICsDkyZOVJlRubm7S3hTZ97Vq1ULTpk0B8LCO/BAWFqZ083uNGjWkY2bv3r2L5ORk2NnZSfdTUlLg5+eHpKQkWFhYoGzZsmqN//r1a9y/f1+hvE6dOrCwsJCu09PTcffuXcTGxqJx48bS4QaFWVBQEG7evAk9PT1YWVkpPEH+7NmzCA8PR//+/aWy8PBwXL58GbGxsTAzM0Pr1q3VOnr6+fPnSt8P29jYSHuagU/vj0+ePIno6Gg0b95c6VPuC4vPkqwUZkxWiIiIVOPl5SW3BI/JChGp67PsWSEiIqKvR8YlboV5eRkRaR6TFSIiIiowssSFhwUQkTqYrBAREVGBW7ZsGWdZiEhln+U0MCIiIqLCenQqEWkON9jngBvsiYiIiIg0g8vAiIiIiIhIKzFZISIiIiIircRkhYiIiIiItBKTFSIiIiIi0kpMVoiIiIiISCsxWSEiIiIiIq3EZIWIiIiIiLQSkxUiIiIiItJKTFaIiIiIiEgrMVkhIiIiIiKtxGSFiIiIiIi0EpMVIiIiIiLSSkxWiIiIiIhIKzFZISIiIiIircRkhYiIiIiItBKTFSIiIiIi0kpMVoiIiIiISCsxWSEiIiIiIq3EZEVFkydP1nQIRERERERfhSKaDqCwOXjwIFq2bAkAcHNz03A0RERERERfLs6sqMHb2xve3t4AAC8vLwBAr169NBkSEREREdEXh8lKHsmSlitXrgDgMjEiIiIiovzCZCWfHTx4UKGMCQwRERGR+o4ePYpLly5pOoyvSmBgIPz9/TUdBpOVz0FZAkNEREREOTt58iR+/vlnbN68WdOhfBWSkpLQp08f9OrVC926dUO7du2QlJSksXiYrGgh2T4YIiIiyh1/f39MmjQJPXr0wJMnT3Ksn56eDm9vb4wcORKurq6YNm2a0k+R79+/j7Fjx8LFxQW//fYb4uLiCiJ8ykJoaCi2bduGqVOnajqUr0ZCQgLGjBkDf39/PHv2DAkJCbh165bG4ik0p4GlpqYiLS0NAFCkSBEUKaI89Iz1MtLV1YWBgYFURwghXWsbb2/vbE8amzx5MpYtW/YZIyIiItJe8+bNw969e9GjRw8cO3YMs2bNyrHNzJkzsWHDBsyaNQvVq1fH0aNH0aRJE1y7dg2NGzcG8Gk/aseOHTFq1Cj07NkTq1evxr59+3Dz5k2Nv4dIT0/HwYMHcfHiRaSlpaF58+b45ptvsnx/lNGDBw+we/duhIeHo2PHjhg4cKBaMbx+/RqbNm2Cv78/Zs2ahQYNGijUuXPnDjw9PRETEwM7OzsMGzZMijEsLAyHDh1S2vewYcOgo6ODadOmYfny5fDx8VErxvx29epVnDhxQq5MX18fc+fOzbHtnTt3cPToUTRo0AAuLi5qx5Camor9+/fDz88P3377LSwtLRXqhIaGYt++fYiOjkaLFi3QqVMn6V58fDzu3LmjtG97e3uUKVMGHTp0wIULFxASEoLExERYWVmpHW9eFZqZlZEjR8LY2BimpqYwNjZGpUqVMHbsWMTGxmZZL+NXq1at5Op06dLlc7+EfMNlZURERP9n5MiR8Pf3x+jRo3PdZufOnZg0aRKmTJkCV1dXbNu2DRUrVsS+ffukOtOnT4ezszNWrlwJd3d3HD9+HP7+/ti+fXtBvAyVtGrVCrt370bt2rVRt25dzJ8/H506dUJqamq27TZs2ICWLVsiJSUFbdq0gY+PT66Su8z+/vtv2NnZISIiAnv37kVoaKhCnWPHjknvv2xsbLBo0SK5N+lxcXG4ceOG0i8hBP766y/o6enhyJEjuHDhAgIDA3H+/HmVY81P169fh4eHB4yMjKQvQ0PDbNu8efMGzZo1w+jRo7Ft27Y8vY87cOAAatWqhV27dmHhwoVKZxF9fX1haWmJo0eP4t27dxg4cCDGjBkj3Q8NDcW8efOUfsmWe3348AHz5s3D/Pnz0aRJExQrVkztmPOq0MysAEDbtm1x4cIFCCFw9+5ddO/eHR8/fsSGDRuU1qO88fLykpvhkS1Pu3HjhsLMDmd7iIhIU6pUqaJym3r16skt+3r37h0iIyOlT5BjY2Nx/fp17N69W6pTtmxZtGvXDidPnsSIESPyHnge/PPPP6hevbp03b17d5ibm+P06dPo3r270jaBgYEYN24c1q5di++++w4A4O7ujrCwMJXH79WrF3744QeEh4dj5cqVSutMnDgRY8aMwfLlywEA7du3h42NDc6ePYuOHTuiVq1a2LRpU5ZjlChRAkWKFMGNGzfw9OlThIWFITAwEA4ODirHm58qVKigUoKnr6+PNWvWoFmzZnB0dMzT2DVq1MDt27dRvHhxlChRQmmdH374Ae3bt8eBAwcAAP3794e9vT0GDRoEe3t7mJubZ/s+OTExESYmJlKdQYMGwdPTEyNHjsxT7OoqNDMrGeno6KBp06b49ttvFabiKP/IjmXOeO3t7a30EwF1PyXw8vLKdo9OVvcyl+fUDxERUUa7d+/Ghw8fUK9ePXTq1Ak2NjZYuHAhvvnmGwBAcHAwhBCoWrWqXLtq1arh+fPnmghZTsZEBQCqVq0KPT09REZGZtlm27ZtMDExwdChQ+XKy5Urp/L4ZmZm2S45+++//xAUFARXV1eprFGjRrCwsMCxY8dyNcaoUaOwadMmbNq0Cd999x3s7OwwatQolWPNb1FRUViyZAmWL18uPboiO+XKlUOzZs3yZewmTZqgYsWKWd5/9+4drl27hmHDhkllrVu3Rt26dbF///5cjXHhwgWMHDkSR48ehaenJ65du4batWvnOXZ1FcpkRebjx4/4+PGjQnl6ejqSkpLkvpTVI83L+IDNrO7npjynfrIiS3BySnTymgjx+GoiIu2yd+9eXLt2DcOHD5eWhy9duhSBgYEAgJSUFACAsbGxXDtjY2PpnjZZu3Yt9PT00K5duyzr+Pr6onnz5rh27RpGjx6N8ePH459//smy/uLFi7F48WK14nn69CkAxaSqRo0aePbsmcr9mZubZ/vaPqdixYohNDQUjx49gqOjIwYMGAAhRL71v2fPHmzcuFGttrJlYebm5nLlderUydXBEwDg5OSEjh07YuPGjTh+/DiWL1+ODh06qBVPfihUyYosCYmPj8fZs2fh6emJQYMGKdS7cuWKwp6V8ePHayBi0nayBCenREedRCgjbd5npEoixtkrIvoSxMTEYMqUKVi8eDF++ukn9O/fH9u2bUO1atUwZ84cAICpqSkAKMxURERESPe0xcWLFzF9+nQsWbJEITnIKD4+Hg8ePMCkSZPQuHFjVK1aFT/88AOGDx+utP6FCxfUXlYv2/tQvHhxufLixYurdQxuy5Yt5WYLNKVPnz54+PAhli1bho0bN+LGjRs4cOAAtmzZkm9jeHt7Y9u2bWq1TUhIAACULFlSrtzExES6lxsDBgzAoUOHsGvXLvTq1UutWPJLodqzIktCPn78iLS0NDg7OyvN+Atyz4qzszNatmwpXcs2itnb20v3lbVRRU4nRKjan7oyxyG7zjwlDqgfU06vNav7WcWm7vjqxpFbn+t3RqSKzPvStE1Bxactr1tb4lCVunFr097GiIgIpKSkKCxtqV27tvSpv5mZGUqUKIEHDx6gY8eOUh1fX1+N75nI6Nq1a+jZsyd++uknTJgwIdu6JiYmeP/+PXx9fVG2bFkAn/buODs7Y86cOahZs6Zc/ZkzZ6odl4mJCYBPyZ5sLODTzz67ZUzaLvPfmQYNGqBVq1a4cOFClkmfqgYOHKhSYpGRLDmMiYmRe78WHR2tkDgWGqKQcHd3F+3atRNCCJGWlib2798vDA0Nxa5du7Ksl5u+chITEyMAiJiYGDWiJiLSXnv37tV0CNkqqPi05XW7urpqOgS1qBu3mZlZPkeiKDAwUAAQ169fV7i3dOlSMXHiRCHEp/cR5cuXF999951IT08XQgjx9u1bUbFiRTF16lSpzZgxY0TdunVFdHS0EEKII0eOCADixo0bBf5acuPatWuiRIkS4ueff85V/dmzZ4uKFSvKlQUEBAgA4urVq2rF8PbtWwFA+Pj4yJW/e/dO6OjoiH379kllHz9+FOXKlRO//fabWmNpqzZt2ggXF5dc1e3atav49ttv8zxmXFycACAOHDggV/7u3TsBQOzfv1+u3NzcXEyZMiXP42pCoVoGJqOrq4s+ffrgp59+wvjx4xETE6PpkIiICh1t/1S/oOLTlted1xlb+j8nTpxAjx49pM3XU6dORY8ePeSW8N6/fx+XL18G8Ol9xK5du3DixAnUqVMHHTp0gIWFBaytraVlYMCnPRvly5dHnTp10KpVK7i6uuL3339HixYtPu8LVOLmzZtwdHTEjz/+iIULFyqts3z5csybN0+6/vbbbxEeHo6rV69KZd7e3ihevLjSZ6TkRfny5dG5c2esXLlS2je8Y8cOREVFoX///vk61ud0/fp1uev79+/jxo0bcrNv+/fvx+rVqz93aAA+/dzbtGmDzZs3S/toLly4gKCgIPTr108jMeWVjhD5uCOoAA0dOhTBwcFyy7vi4+Nhbm6OYcOGYdGiRVK9Z8+e4fTp03LtdXR0pHOws6qjp6cHfX19ubLY2FiYmJggJiZGYf0fERHR18bNzU2t/Wu1atVSa2N1brx8+RL//vuvQrmlpaW0bMfX1xfx8fHSsm0ASE5OxuPHjxEVFYWaNWuiVq1aSvv39fVFWFgYrK2ttWYJU5kyZZCamopu3brJlffv3x99+vQB8CkhDg0NlTuxav369Zg2bRrs7OwQGxuLgIAAeHh4SG1y68qVK1i1ahWSkpJw6NAhtG/fHhUqVEDfvn2lDwSCg4PRuXNn6OjooGrVqrhx4wb+/PNPfP/993l89ZrzzTff4MWLF7C1tUVUVBT27duHvn37YsuWLdJ7yKFDh8LX1xe+vr5Su7lz5yItLQ27d++GsbExevfuDRMTE0ybNk2l8f39/bFz506kpqZiyZIlcHV1Rd26dWFnZyf9XfDz84ODgwMaNmyIOnXq4J9//sHgwYPx999/59vP4XMqNHtW9PX1FZ4WW7x4ccybNw8///wzfvzxR1SqVAn6+vq4deuWwua34sWLIzw8XOpLWZ0uXbrg8OHDBfkyiIiICjVtnBGqXr16thvLgU8PJczM0NBQelq9qm01be3atUhPT1cot7CwkL6fNGmSwmb20aNHo3fv3rh16xZKlCiBJk2aSPtLVFGtWjVpP+aAAQOk8oxPU69ZsyYePXqEq1evIjY2Fjt27FDrmTjaZNeuXbh37x6uXbuGokWLYsqUKQp/P/r16yeXFAOf/q6lp6fLHRKQ08MkldHT05MeRrlgwQKpPOOH7dbW1ggICMDBgwcRHR2NQ4cOoU2bNiqPpS0KzcyKpnBmhYiIKO8KcmaFiL5chXLPChERERERffmYrBAREVGB4xHuRKQOLgPLAZeBERERERFpBmdWiIiIiIhIKzFZISIiIiIircRkhYiIiIiItBKTFSIiIiIi0kpMVoiIiIiISCsxWSEiIiIiIq3EZIWIiIiIiLQSkxUiIiIiItJKTFaIiIiIiEgrMVkhIiIiIiKtxGSFiIiIiIi0EpMVIiIiIiLSSkxWiIiIiIhIKzFZISIiIiIircRkhYiIiIiItBKTFSIiIiIi0kpMVoiIiIiISCsxWSEiIiIiIq3EZIWIiIiIiLQSkxUiIiIiItJKTFaIiIiIiEgrMVkhIiIiIiKtxGSFiIiIiIi0EpMVIiIiIiLSSkU0HQARERERUVaeP38OY2NjVKhQQdOhfDW8vb0BABUqVECbNm00GguTFSIiIiLSOq9evcLAgQPx5s0bREZGwsXFBZs2bdJ0WF+FPXv2ICIiAnp6ejhz5oxGY+EyMCIiIvoihYSE4Ndff8WgQYPw9OlTldqePHkSgwYNwt69exXunTlzBtOmTcPIkSOxcOFCvHr1Kr9CpgyCg4Pxxx9/4NmzZ3j27BkOHjyIFy9eaDqsr4K3tzcWLFig6TAAcGalUPPy8oKbm5umwyAiItI6v//+O9asWQNHR0fs3LkT48aNQ+3atXPVNiQkBKNGjUJCQgKqVq2K/v37S/fmz5+PJUuWYNKkSWjevDmOHTuG33//HdevX0f9+vUL6uUouHLlCq5cuSJXZmxsjB9//DHbdqmpqTh58iSeP38OExMTtG/fHjVq1FArhrS0NBw9ehT+/v4YOHCg0n7Cw8Nx9OhRxMTEwM7ODs2aNZPuJSQkwN/fX2nftra2csuPdHV1UaJECa1YCnbp0iWcP38eycnJsLW1RZ8+faCrm/3n/5GRkdi9ezeePn2KkiVLomPHjmovr4qNjcWOHTvg5+eH77//Ho0aNVKoExgYiF27diE6OhotWrSAm5ubFGNMTAx8fHyU9t27d2/o6+urFVdB0ejMysiRI2FkZAQjIyOUKFECVlZWmDt3LpKSkpTer169Orp164atW7ciLS1N6ic9PR1t27ZVeOOemJiIhg0bYvz48VJZUlISduzYATs7OxgZGWH37t2f58UWANl6wsy8vLw+cyRERETapV+/fnj69CmmTZumUru0tDR88803+OWXX1CuXDmF+xs3bsTkyZOxYMECjBw5Evv370epUqWUzsAUpDNnzmD16tWIjo6WvmJiYrJtExoaCisrK8ycORNPnz7FoUOHYGFhga1bt6o8vre3N2rXro0VK1Zg5syZCAwMVKjz4MEDWFhYYNu2bfD19UXHjh3xyy+/SPefP3+OMWPGKP1KTU2V6sXExMDNzQ2bN2+GkZGRyrHmpx49emDOnDlIT09HsWLFMHnyZHTu3Fku3sxevHgBS0tLeHt7o2rVqoiPj4ejoyPmzp2r8vienp6oV68e7t69i/Xr1+P58+cKda5evYqGDRsiMDAQpUqVwrRp0zBgwADpflRUFPbs2aP0K7vXoTFCg9zd3UWbNm1EYmKiiI2NFceOHRMlSpQQkyZNUrifkJAgnj9/LjZt2iQqVaokOnbsKJKTk6W+Hj9+LIyMjMTOnTulsrFjxwpzc3MRHx8vlS1fvlx8++234vLlywKA2LFjR7YxxsTECAAiJiYmn1993rm6uqpUTkRE9LUJDAwUAMT169dzVX/27NmiV69eQgghLCwsxPTp0+Xu29vbi2HDhknXkZGRwtTUVGzZsiXfYs6NuXPnitatW6vUZtGiRaJMmTIiISFBKpswYYKoUaOGyuNfvnxZvHz5Urx9+1YAED4+Pgp1WrZsKfr06SNdHz16VOjo6AhfX99cj/Pq1SvRpk0bceXKFZVjLAgPHz6Uu37+/LnQ0dER3t7eWbZZsmSJKFOmjEhJSZHKfvvtN1G6dGmVxw8ICBBxcXEiLi5OABAHDhxQqNOkSRPx7bffStf379/P8neUncuXL4uOHTuqHGN+0/ieFV1dXWnmpFu3bnB3d8f+/fsV7hsbG6NmzZr47rvvcOHCBVy+fBkrV66U6llaWmLhwoUYN24c3r59ixMnTmDjxo3w9PREsWLFpHoTJkyAp6cn7O3tP+vrJHmy2Z+cZoE4S0RERJ/LxYsXsXnz5mw3ce/atQuhoaFo0aIFXFxcYGtri+nTp2PIkCGfMdJPIiIisGrVKmzevBl+fn451i9atCj09PRQpMj/7QIwMjKSe5+UW/b29qhWrVqW90NCQnDjxg2MGDFCKuvevTuqVKmS5cqQzJ48eYK2bdtiyJAhMDQ0xJ07d5CQkKByrPkp81K/6tWro1ixYggNDc2yTeXKlZGcnCwXe0REBKpWrary+BYWFihevHiW91+/fo179+7hm2++kcpsbGxQv359HD58ONfjnDx5EhcuXMD79+/h7e2NkJAQlWPNLxpPVjIrUqSI3BIvZerWrQtHR0eFv+wTJ06EtbU1Bg8ejOHDh2PWrFlo0aJFQYZLapL97nL6D1Zu/4OmLZhcEREVTuHh4Rg0aBA2bdqkdPmXzLlz53Dnzh107NgRnTt3RsOGDbFjxw6NvJn7+PEj/Pz8cPLkSTRt2jTH/SqjRo1C165d0aVLF8ybNw8jRozA6dOnsWXLFqX1vby81P7/mmwvioWFhVx53bp1s9ynktmjR49QunRprFu3TloepmzZkybt2bMHCQkJaNeuXZZ1Bg4ciJ9++gl2dnZwd3dHly5dcO/evSx/ths2bMDSpUvVike2HM/MzEyu3MzMDEFBQbnu59ixY/D19UXdunWxZ88ejR5soDUb7IUQuHXrFnbs2IF+/frlWL9evXq4ceOGXJmuri62bNkCCwsLNGjQQG5dJNHn4O3tzUMPiIg+o+PHj2PXrl3S9Zw5c1C3bl2V+9mxYwc+fPgAT09PeHp6AgDevn2Lo0eP4u3bt9i2bRvi4uLw/fffY+nSpRg7diwAYPTo0WjRogVmzZqF7du358+LygVXV1fMnj0benp6AD5t+nZwcICDgwP69OmjtE1qaiqSk5MRHh6OiIgIhIeHIyEhAYmJiUrrb968GQDU+v9afHw8AMDExESu3NTUVLqXk759+6Jv374qj/25yDa4T5s2DQ0aNMiyXnh4OE6dOoVy5crB2toaZcqUwa5du3Dr1i2FZA4ATp8+jdDQUEydOlXlmGS/yxIlSsiVlyxZEq9fv851PxlXL2maxpOVS5cuwcjICOnp6ShTpgz69++PxYsX59hOV1cXQgiF8jNnzkAIgaCgILx8+VIhs/ySuLi4qFSuTWQx5hRrYXgtGRW2eImUyc+TBj/nqYVfwwmJ+f0alfWX2zGyq5dVv4B6b3yzY2ZmBkdHR+m6VKlSavXj6OioMKNy7tw5mJmZoWvXrgCAd+/eITExUWEpkJWVlUqfWueHzDG0bdsWDRs2xNmzZ7NMVubMmYN79+7Bz89P2qi+cOFC9OvXD69evULRokXl6mc8BU1VsqVlsbGxKFu2rFQeExOjkMAURv7+/ujcuTNcXFxyfN86f/58REREwM/PT1qCZ2VlhVGjRqF79+4oXbq0XP3Ro0fjw4cPasVVsmRJAEB0dLTcMrOoqCjpXmGj8WVg9vb2iI6OxocPH/D27VusXr1aIRtU5r///kPNmjXlyoKCgjB58mSsW7cOrVu3xrBhw5QmNF+KrP6DXxj+Zy2LMadYC8NryaiwxUtEhUt+L41V1l9ux8iuXlb9FsTSXktLSwwaNEj6ym4JV2Zr167FnDlzlPYzaNAglCxZEvXr15fW/5uZmaFUqVJye2ujo6Nx9uxZNG7cOH9fmBr09PSynbW4f/8+WrVqJXeiVvv27REREYHg4GCF+sOGDcOwYcPUikU2Y5A5iXv69KlaM1/a5MmTJ+jQoQOcnJywceNG6OjoZFs/KCgI1tbWcnuFGjdujKSkJLx8+VKhfufOndG7d2+1YrO0tISuri4eP34sV/748ePPerR2ftJ4siLbQJ/xF5iTly9f4sSJE3B2dpbKPn78iEGDBqFz584YMWIENm3ahAcPHuDvv/8ugKiJiL5c+Zl0f84E/mv4sICzt7l37tw5DBo0SFpKM2/ePAwaNAhHjx6V6ly9ehXHjx/PdZ96enrYtm0bduzYAVtbW/Tt2xcWFhaoVKkS5s2bl98vIVsPHjyQu7579y4ePHggt3fi4MGD2Llzp3Rtbm6OO3fu4OPHj1LZjRs3oK+vr/azVrJSo0YN2NjYYNu2bVLZ+fPnERwcnOXMT2Hw33//wcHBAU5OTvDw8FD6fJVt27Zh4cKF0nXDhg1x9epVuaOljx07hqJFi6JOnTr5Gl/p0qXRuXNnrFu3TtoDfuTIEbx8+bLQ/jdS48vAVBETE4Nz587hp59+QoMGDTBhwgTp3oIFC/DixQvpP0JVq1bF33//jTFjxqBbt26FPosnIiLK7zcbypKfLyUhqlatmrQ8LONryrg8fOzYsdk+m+SPP/5QOPGqZ8+eCA4Oxt27dxEVFYVZs2ahSZMm+Rx9zqZNmwZ9fX3Y2NggLCwMO3fuhIuLCwYPHizV8fT0RGhoKL799lsAwOzZs9GqVSu0atUKjo6OePXqFXbv3o3FixfD2NhYpfEfPXqEI0eOSDM5e/bswZ07d9C6dWvpYYdr1qxB586d0adPH5iZmWH79u0YO3Ysmjdvnk8/hc+ve/fuiI6ORpEiRaR9S8CnZYSyD9HPnz8PX19fae/0zz//jLNnz8LKygodOnTA69evcfPmTaxdu1blk9gePHiAtWvXSgnnunXrcPLkSXTs2BGurq4AgNWrV8PBwQG2traoXbs2Tp06hXnz5mnF7J86dIQG10kNHToUwcHBuHDhQpb3t2/fDgMDA6Snp8PQ0BANGjSAi4sLxo0bB0NDQwCfPhVo06YNjhw5IrduFQCcnZ3x7t07XLlyBXp6ejh//jycnJwAAMnJydDX14euri6GDRuGtWvXKsQQGxsLExMTxMTEFNq1fkRERPktuz0rbm5uCicdyery1MT8c+7cOdy8eRPFihVDixYtFE5A3blzJ+Li4jBmzBipLD4+HgcOHMDz589hamqKDh06ZLs5PCu+vr7Ys2ePQnmHDh3QpUsX6frly5fYt28fYmNj0apVK7l7hdGWLVuQnJysUN64cWPp53/+/HlERETIJclCCFy7dg1Pnz6FiYkJWrZsiQoVKqg8fnBwME6ePKlQ3rBhQ9jZ2UnXCQkJ8PHxQXR0NJo3bw4rKyuVx9IWGk1WUlNTIYSAgYFBlvdlU1hFihTJcqnYx48fkZaWJiUvGaWnpyMlJQWGhobQ0dGRrjPLqn8mK0RERKpRlsgwWSEidWh0GZi+vn6O93OqA2SfyMj2xGR1TURERPmrsK6NJyLto/EN9kRERPTl+1L2whDR51WoNtgTERFR4cTZFiJSh0b3rBQG3LNCRERERKQZXAZGRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVimg6ACIiIiKirERGRsLIyAjGxsaaDuWrcfLkSQBA2bJl0bRpU43GwmSFiIiIiLROaGgovvvuO9y8eRNJSUkYM2YMli5dqumwvgrLly9HdHQ0ihcvjjNnzmg0Fi4DIyIioi9SVFQU/v77b4wbNw7BwcEqtb18+TLGjRuHw4cPy5ULIXD8+HHMmTMHv/32Gy5fvpyPEVNGjx49wrhx4xAeHo7//vsPHh4eePnypabD+iqcPHlSaxJDzqwQERHRF2flypVYtGgR2rVrhz179mDQoEGoWbNmrtq+f/8egwcPlj5Z7tWrFwAgOTkZTZs2RY0aNdC8eXPExsaiR48ecHd3x4oVKwrw1Si6c+cO7ty5I1dWtGhRuLu7Z9nm8uXLePTokUJ5kSJFMGLECLXiuHjxIvz9/dGzZ09UqVJF4X58fDzOnDmDmJgYtGzZEhYWFtK95ORkvHr1Smm/tWvXRseOHaXrkiVLomTJkihXrpxaceanBw8e4MKFC0hOToatra1cnFn58OEDDhw4gKdPn6JkyZJwcHBAo0aN1Bo/OTkZ3t7e8PPzw5AhQ2BlZaVQ5/Xr1/D29kZ0dDRatGgBR0dH6V5cXByuXr2qtO9OnTqhSBHtSg8+SzQ9evTAjRs3srxfvXp13Lt3DwcOHMDIkSMV7k+ZMgUzZ84EADx+/Bht27aFnZ2dwqcdADB+/Hjs3r0bAKCvr49q1arB1dUVEydOhL6+vlzd7du3Y/LkyejVqxc2b96cl5dIREREWqRdu3YYMWIEXr9+jT179uS6nRACQ4YMwY8//ogNGzbI3dPT04OXlxcsLS2lsrZt26Jnz5744Ycf5N6IF7SjR49i/fr16N27t1RWvHjxbNu8evUKvr6+cmUHDhxA2bJlVU5Wjh07hmnTpsHY2Bh3796Fubm5QrLy5MkTdOjQARUrVkT16tUxduxYzJ07Fz/99BMAwN/fHy4uLkr7f/z4MQwMDAB8eqM/cOBArF69GkWLFlUpzvz2zTffwN/fH23btoWBgQG++eYb2NnZYd++fdDVVb5g6e3bt2jZsiXKly8PJycnPHz4EDNmzMBvv/2GqVOnqjS+t7c3Jk6ciBYtWmD//v1o2bKlQrJy7949ODg4oE2bNqhbty6GDBmCXr16YdOmTQCAsLAwLF++XGn/bdq00bpkBeIziI6OFmFhYSIsLEzcunVLABAHDx6UyiIjI4UQQuzevVsAECEhIdK9sLAwkZCQIPU1adIkUb9+faGjoyOePn2qMJa7u7uws7MTYWFh4s2bN2Lv3r3C2NhYzJgxQ67ekydPRNWqVYWNjY3o169flrHHxMQIACImJiaffhpfpr1792o6BCIiIgWBgYECgLh+/Xqu6v/++++iU6dOIj09XVhYWIjp06dnW//Zs2cCgLhw4UJ+hJtrc+fOFa1bt85TH2FhYcLAwEAsW7ZM5banT58Wjx49Em/fvhUAhI+Pj0Kddu3aCUdHR5GWliaEEMLLy0vo6uqKx48fqxRjhw4dxMmTJ1WOsSBcu3ZN7jogIEB6X5uVpUuXClNTU5GYmCiVzZkzR5QrV07l8e/fvy/ev38v4uLiBABx4MABhTqtWrUSffv2lYsZgLh06ZJKY12+fFl07NhR5Rjz22fZs2JiYoKyZcuibNmyKFWqVJZlMmXKlJHulS1bVjr9ISUlBTt27MD8+fNhb28PDw8PpePp6+ujbNmyqFSpEtzc3DBkyBDs3btXup+cnIz+/fvjjz/+UDplSarz9vbO1/68vLzytT8iIqKc3Lp1C3/99Re2bdsGHR2dXLXZvHkzSpQogcaNGxdwdIpiYmLg6emJ/fv348WLFyq337FjB3R0dDBkyBCV23bu3Fnp8iOZt2/f4uLFi/j++++lGQcXFxdUqFAh1/+Pf/78Odq1awd3d3fUrl0bQUFBSE5OVjnW/NSqVSu567p166J48eJZLmcDgNKlSyM9PR0fP36UylJSUlC6dGmVx7exscl2KVxoaCiuX78utxywVatWsLCwwP79+3M9zuXLl3Hz5k1ERETg5MmTCA0NVTnW/FKoNtgfOnQIenp66NmzJ0aNGoWtW7ciLS0tx3bGxsZISUmRrqdOnQpra2sMGDCgIMOlPMjv5Cc3svuPJ5MnIqIvW2xsLAYMGIBVq1ahcuXKuWpz/PhxLFq0CCtWrEDJkiULOEJF4eHhOHLkCNatW4e6detizpw5KrX38PBAnz59UKZMGaX3jx8/juPHj6sV2+PHjwFALqHR0dFBvXr1lO6bUebGjRtITk7G/Pnz4ejoCEdHR/j7+6sVT0E5dOgQ4uPjYWdnl2WdwYMHY8SIEXBwcMCECRPg5uaGCxcuSNsWMtu5cyfWrVunVjz//fcfAMDc3Fyu3NzcXLqXG1u2bIGPjw8qVKiA5cuX48mTJ2rFkx+0bFHaJ1WrVpW7/u+//1C6dGls2rQJw4YNg76+PlxcXDBhwgScOHECPXr0yLKvhw8fYufOnXBycgIAHD58GMeOHVNYs0nk7e0NNzc3le/llZeXV4H1TUT0pTt79iwOHDggXU+ePBm1atVSuZ8tW7YgNjYW58+fx/nz5wEA7969g4+PDxITE/H3338rjOvi4oKFCxdi6NCheXoN6ujZsydmzJgBIyMjAJ8Six49eqBVq1bSe57s3Lx5E48ePcr2YADZvW7duqkcX2xsLADA1NRUrrxUqVLSvZwMHDgQAwcOVHnszyUoKAgjRozA2LFj0aRJkyzrxcfH48mTJ0hPT0eJEiUQFxeHkJAQvHjxQumM3IEDBxAaGooxY8aoHFNCQgIAKCTPJiYmeP36da770aa93FqZrDx48ACGhobSdalSpfDy5UucPXsWa9asAQAYGRlhyJAh8PDwUEhWrl69irJly+Ljx49IS0tD37598ddff+H169cYMWIE9u/fr5FPQL5kWW2Q05b+8jqmJuIh+trkJXH/HEl/YftgQZV4P8drmzx5MpYtW5bv/ZYpUwb16tWTrtXdgN2uXTvo6enJlRkaGqJMmTIKG+fPnz+PXr16Yfbs2Zg+fbpa4+WVra2t3HW3bt1gbW2NkydP5ipZ8fDwQO3ateHg4JBlne7du6sdn+z3EBcXh7Jly0rlsbGxKFGihNr9aovg4GB07NgRDg4OOZ4EN3/+fAQEBMgdGrB69WoMGjQIISEhCgndoEGDpKRDVbJDFmJiYuQ+/JedbFcYaWWyUqZMGemTApnNmzcjPT0dLVq0kMpSUlKQmJiId+/eoUKFClJ58+bNcejQIejr68PExEQqv3DhAiIjI+Hs7CyVxcbGQkdHB2XLlsX9+/dRrVq1gnthX7D8/p+cJt4QZDdmQcZTmN78EH3NCnKGtSCoEu/neG0HDx4skGTFxsYGNjY2arXdvn073r17h2nTpintZ9WqVWjSpAnGjh0rlV28eBE9evTArFmzpJNKtYWRkRGioqJyrJeQkIA9e/bg559/znZvzvjx49WOpU6dOgA+7TsxMzOTyoODg9GnTx+1+9UGL168QPv27dGsWTPs3r1bIcnN7NGjR2jSpImUqABAy5YtkZCQgODgYIW/dxnfp6qqXr160NHRwZMnT1C/fn2p/MmTJ4X2514o9qykp6djy5YtWLt2LQICAqSvZ8+ewdbWFlu3bpWrL9tgnzFRAT596hAaGirXh4ODA7p27YqAgAButici0qC8vFn+HEkEZ1gLlytXrmDcuHH49ddfAQB//vknxo0bBx8fH6nO6dOn5Q7gyUlUVBR69OgBU1NTvH79GuPGjZO+bt++ne+vITtPnz6Vu/b394evr6/c3gkfHx8cOnRIoa2XlxcSExMLdPla7dq1YWVlhV27dkllN27cQFBQkPTcmsLo5cuXaN++PZo2bYo9e/YoPeb3n3/+kZttsbS0xK1bt/Dhwwep7Pz589DX10ft2rXzNb5y5cqhXbt28PDwgBACwKcli0+fPi20/w3TypmVzHx8fPDmzRu4uroqnJzQp08fbN68OVfTsAYGBnJTkcCnxEZZORERUUaFaVYFUC25KqxvYrJTqlQpaXlYxlUZGTeTu7u7ZzsTMXv2bLn9LwYGBli0aJHSupk/IC1o7u7uqFGjBmxsbBAWFoZNmzahY8eOGD58uFRn/fr1CA0NlXsWC/BpCVjPnj1RsWJFtccPDAzE2bNnpf0nR48eRVBQEGxtbdGsWTMAnx7M6eTkhLS0NJiZmWHdunUYNGgQ7O3t1R5X07p164Z3796hZs2amDVrllTevn176cGLsr3RP/74IwDg559/xokTJ2BjY4OuXbvi9evXOHbsGJYtW6bykrjHjx9j+/btSE1NBfDpRLcbN27A3t5e2haxatUqODg4oF27dqhTpw727duHyZMny/07KEwKRbKyadMmtGnTRukRb3369MGMGTNw6dIltG3bVgPRERERaR9VkqvClojlRv369eWWwSjTuXPnbO9/++23ctfFihXDuHHj8hxbfrh06RIOHjyImzdvonjx4ti9eze6du0qV6dLly4Km9kTEhJgbW2d51mVqKgo6bCi0aNHIykpCb6+vnInqXXo0AEPHjzA7t27ERUVheXLl8PV1TVP42raiBEjkJSUpFCeca+Uq6sr2rVrJ12XL18eDx8+xOnTp/H06VM0bdoUS5cuVesgCH19fWmPS8bEOeP49evXR0BAAA4fPozo6GiMGDFC4cjlwkRHyOaIPpP09HRERkbCxMRE4YnyKSkpiI2NVZjliIqKgoGBAYoVK6a0z4iICBgbG6No0aKIj49HWlparj/hkO1ZySqzjY2NhYmJCWJiYrgpn4iISE21atXCs2fPNB0GERUyn31mRVdXN8slV1ktx8r80MjMMk7pqnrSARMQIiIiIiLtVCg22BMREVHhltsHLRIRZcRkhYiIiAqcbLMxEZEqPvuelcKGe1aIiIiIiDSDMytERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrOSBl5eXpkMgIiIiIvpiMVnJA29vb02HQERERET0xSqi6QCIiIiIiLKSmJgIAwMD6OnpaTqUr8aNGzcAACYmJrC0tNRoLJxZ0SAuIyMiIiJSLiIiAv3790eFChVgYmKCefPmaTqkr8bEiRPx3XffYfz48ZoOhcmKJnEZGRERUcFJSEiAh4cHZsyYgVevXqnU9tatW5gxYwZOnjyZr/1S7t2+fRsuLi6IioqCn58f/vrrL/68P5MbN25g/fr1mg4DAJeBfRG8vLzg5uam6TCIiIi0xoYNGzBv3jw0a9YMhw8fhrOzM6pVq5artpGRkRg4cCDCwsIAAI6OjvnSb35LTk7G/fv3ERERgbp166JOnTq5avfgwQMEBwejatWqsLW1zVMMDx48gL+/PxwcHFChQgWF+ykpKbh27RpiYmLQrFkzVK5cWbqXmpqKiIgIpf1WrFhR7udesWJFmJiYoHTp0nmKNz8EBQXh4sWLSE5Ohq2tLVq0aJFjm+TkZJw+fRqBgYGoWLEiunXrBlNTU7XGT0tLw9GjR+Hn5wc3NzfUrVtXoU54eDgOHTqE6OhotGjRAvb29tK9Dx8+4N9//1Xad7NmzbRuuV2+zax07NgR5ubmOHjwoMK9QYMGwdzcHGvWrEFcXBxa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0RGDBg2Srv/9919s374d0dHRaNGiBYYNG4YiRQp0jqLA5GuyEhoaiv/++w9t27aVKw8MDMTr169hZWWF8uXLy92LiopCYGAgjIyMYG5uDmNj4yz7fv78OYoWLYr69etLG7cA4NatWzA1Nc1y+vXixYuoU6cOKleujEuXLiE9PV2hTs2aNaWpuYyYrBAREeVOVgkNkxUiUle+JitfIiYrREREecNkhYjUVWCngREREREBX+6+GCIqeJxZyQFnVoiIiIiINIMzK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaSUmK0REREREpJWYrBARERERkVZiskJERERERFqJyQoREREREWklJitERERERKSVmKwQEREREZFWYrJCRERERERaickKERERERFpJSYrRERERESklZisEBERERGRVmKyQkREREREWonJChERERERaaUimg6AiIiIiIi0R3x8PACgSJEiMDIy0mgsnFkhIiIiIq2TmpqKBQsWwMLCAk2aNMHevXs1HdJXo2LFiihfvjx69Oih6VA4s0JERERfrps3byIwMBCOjo4oW7ZstnWvX7+Op0+fypWVKVMGTk5OSuu/fPkSly5dQoMGDWBjY5NfIdP/d/DgQXz8+BFHjhxBYGAg+vfvDwcHB5QvX17ToX3x4uPjceXKFcybN0/ToTBZISIioi/PsWPH8MsvvyA5ORkBAQG4fv16jsnK+vXrcenSJdjZ2UlltWrVUpqsJCcnw9nZGY8ePcKkSZM0lqyEhYUhLS0NFStWzHWb1NRUvHjxAqVLl0bp0qXzNH5sbCxevnwJMzMzFCtWLMsYY2NjUbNmTejp6eW6bxcXF7i6ugIA6tSpg1KlSqFIEe146/ry5Uvcu3cPjRs3Ro0aNbKtGx0djQsXLiiUt2vXDqVKlVJr/KCgIDx8+BAtWrRApUqVFO6np6fj9u3biI6ORuPGjeUSvPT0dHz48EFpv8WLF1crnoKkHb/x/2/FihV4//49fvvtN6ksPDwckyZNgrm5OebMmYOVK1cq1Mncx7lz5xTKFy1aBEtLS/zzzz/YuXOn3L1ixYoplBEREVHhlZqaiq1b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rm3Xrh1+/vlnqW18bMGCBdi1axcaN26MuLg4vHjxAkuWLClysXFSUhKMjY2RmJhY5H7WRERE/0Wy3ntr6NChXLelZG3btpW6m+l3332HyZMnA/iwkUpcXJzE9stRUVFYsmSJmJBNnTq1xBtJSnPy5EnMmzevUPmUKVMwY8YM8Xjnzp3w9PREUlISHBwcsHjx4kKL7iuSx48fS1383rVrV8ycORPAh2TlxYsX+PPPP8XzJ0+exJkzZ5CamoqmTZti0qRJCr0Od+7cwcqVKwuVOzk5YcKECeLx7du38c8//yAhIQEdO3bE1KlTK+yaFbVKVvz9/ZGWllYoEcnfAaJTp054/fo1IiIiCl1bq1YtODg4FNnGx96/f4+goCDo6+ujWbNmqFatWpF1mawQERGVDpMVIlKUWiUr6ojJChERUekwWSEiRanlAnsiIiKqPCrbehgiKj8cWSkBR1aIiIiIiFSDIytERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK3Ly9vZWdQhERERERP8JTFbkdPDgQSYsRERERETlgMmKAg4ePKjqEIiIiIiIKj1tVQdQWeSPtty+fRtr1qxRcTRERERERBUfR1aU5ODBgzh48CCOHj2q6lCIiIiIiCoFJivliGtdiIiIiGQjCALWr1+Pjh07onv37jh9+rSqQ/rP0NbWhra2Nr788ktVh8JkpTxJW+vycQLj5uYGb29vJjVERERKEhISgpMnTyIhIUGm+llZWfD398f169fx9u1bqXVSUlJw8+ZN+Pn5IS0tTYnR0scOHDiAp0+fYv369ZgwYQKGDh2Kd+/eqTqs/4SMjAxcunQJubm5qg6FyYqqfZzAHD16VJxO9rGCyQuTGSIiouJduXIFXbt2Rc+ePeHk5ISQkJASr/nnn39gY2ODqVOn4scff0T9+vUxZ84c8Xxubi5mzZqFhg0bYs6cOXB1dYWlpSUOHTpUlk+lWFlZWQgPD0dqaqrM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H3VSIZOV8sRkhYiISHFMVoioNCr1mhUiIiJSrYoydYyI1BNHVkrAkRUiIiIiItXgyAoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFa0lZ1AOpOEAQAQFJSkoojISIiqjyqVasGDQ0NVYdBRGqOyUoJ4uPjAQD16tVTcSRERESVx5s3b2Bubq7qMIhIzTFZKUH16tUBABERETA2NlZxNKQMSUlJqFevHiIjI2FkZKTqcEhJ+L5WTnxfK5/891RHR0fVoRBRBcBkpQSamh+W9RgbG/N/lJWMkZER39NKiO9r5cT3tfLhFDAikgUX2BMRERERkVpiskJERERERGqJyUoJdHV1sXjxYujq6qo6FFISvqeVE9/Xyonva+XD95SI5KEh5O/NS0REREREpEY4skJERERERGqJyQoREREREaklJitERERERKSWeJ+VYmRkZCA4OBjVqlVD48aNVR0OFZCXl4d79+5BT08PrVq1klrn3bt3ePnyJSwtLcUbfBaUmJiIFy9eQE9PD9bW1lJvVCZLX1R6ubm5CAsLg6amJqytraGtLfkn6vnz53j9+rVEmb6+Ptq1aye1rcePH6Nq1apo2LCh3H2R8mRmZiI0NBQGBgawsrIS71/1sbS0NDx79gwaGhpo2LAhqlatWmR7YWFhiI2NRdu2bWFoaKhwHVJMdHQ0nj17Vqj8008/LVSWmJiIhw8fwtraGrVr1y50XhAEhIeHIy0tDQ0bNoSenp7CfRFRJSWQVMeOHRNMTU2FRo0aCSYmJkKnTp2E2NhYVYdFgiBkZWUJK1euFKytrQVjY2PB0dGxUJ3AwEChV69eQvXq1YW2bdsK+vr6wvDhw4XU1FSJenPmzBGqVq0qtGnTRmjQoIFgYWEhHDt2TK6+SDmWL18uWFhYCE2bNhWsrKyEOnXqCEePHpWoM23aNKF69epCly5dxMfIkSMLtXX06FGhdu3agpWVldCqVSuhe/fuwps3b+Tqi0ovMzNTmDt3rlCjRg3B1tZWsLCwEKytrQUfHx+JemvXrhWqVasmNG/eXGjatKlg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PHz4EK1atcK2bdtgZ2cncx1SjoLva1JSEu7cuSP3pga5ubkYNWoUbt68CR8fH6kbmSirLyKquJisSNGnTx84Ojpi0KBBWLRoEV6+fInVq1dj8+bNUm9mRuXPz88PGRkZiI2NRWJiorjLV/6Nwo4fP47Ro0dj8uTJaNCggXheV1cXHTp0AAC4urpi/fr1GD58OFxdXZGQkIClS5eic+fOEjcZLKkvKr2cnBz07dsXT548wY4dO/Do0SPxXKNGjWBhYYHc3FzY2dlhwoQJaNmyJV69eoUVK1agdevWEh9cli5dCgcHByxatAifffYZ7t69i507d4rfrsvSFynH0aNH4e3tjUGDBqFOnToIDg6Gu7s7BgwYAFtbWwDA119/jcWLF6N///5wc3ODIAhYvXo1LC0t0bdvXxU/A5LGyckJn3/+OTp06ICkpCSsWbMGVapUkZhqmZKSgsDAQAAfkpLnz5/D19cXNWrUQNOmTQEAkyZNwpEjR/D333/j9evX4g1fP/nkE1hZWcncFxFVbhqCIAiqDkIdpaamYvXq1bhx4waqVauGMWPGwNnZWdVh0f83ZMgQREdHFyrPTyT+/PNPHDp0qND5mjVr4vDhw+Lxs2fPsH79eoSGhqJq1aro1KkTpk2bJvFNXkl9UemlpKSgd+/eUs/98MMPGDBgAAAgJiYG69evh7+/P0xMTPDZZ59h0qRJEnPcASA4OBhr1qxBeHg46tWrhwkTJojJpax9kXJcuXIFXl5eiIyMRO3atdGvXz8MGjRI4oufN2/e4H//+x/u378PDQ0NtG3bFt9//73E9tIfe/HiBUaPHg0PDw/Y2NgoXIcUk5ycjI0bN+LGjRvQ0dFBu3btMH36dImplKGhoZgwYUKha7t164ZffvkFwIc1aNJuBzB06FB89913MvdFRJUbkxUiIiIiIlJLnNNERERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqSUmK0REREREpJaYrBDRf9Lhw4fx6tUrVYdBRERExeBNIYlILTx48ACPHj0CAGhpaaFOnTqwtbWFvr5+mfRnYmKCbdu2YfDgwWXSPhEREZWetqoDICICgP3792Pt2rXo27cvcnNz8fDhQ7x79w779+9H165dVR0eERERqQCTFSJSG7Vq1cK+ffsAAIIgYMCAAZgyZQpCQ0PFOufPn8e7d++goaGBOnXqoG3btqhWrZpEO4cPH4a9vT20tbURGBgIIyMjdOrUCZqaRc98TUtLw4kTJ9CiRQu0bNmybJ4gERERyYXJChGpJQ0NDfTu3RsnTpxAeno6qlatCgC4evUqnj17BkEQ8PTpU0RFReHw4cPo3LmzeK2rqyscHBwQFhaGZs2awc/PD02bNsXFixehpaVVqK/379+jX79+MDAwwJEjR8rtORIREVHxmKwQkdp69uwZjIyMoKenJ5YtX75cos6yZcswbdo0BAQESJQnJibiwYMHqFq1KmJiYmBtbY3Tp0/DyclJol50dDS+/PJLNG3aFF5eXtDR0Sm7J0RERERyYbJCRGojJSUF+/btQ15eHu7fv4+NGzfit99+g4aGhkS9ly9fIiQkBAkJCdDV1UVQUBAyMzOhq6sr1hk3bpw4GmNhYQEbGxuEhoZKJCtPnz7F3Llz0atXL2zcuLHYaWJERERU/pisEJHaSE1NxdGjR5GdnY1///0Xtra2GDNmjESdqVOnYufOnbC3t4eZmRnS0tIgCALi4uLwySefiPWqV68ucZ2uri4yMjIkypYtW4batWtj3bp1TFSIiIjUEP/vTERqI3+B/aFDh/D48WOkp6fD1dVVPP/vv//i77//xsOHD3H58mUcOHAA8+fPB/BhQb68Vq9eDV1dXQwdOhTZ2dlKex5ERESkHExWiEgtGRoaYtOmTTh8+DAuXrwIAIiJiYG+vj7q1asn1jt48KDCfdSqVQtXrlzBkydPmLAQERGpISYrRKS2OnXqhIEDB+LHH3+EIAjo0qULdHV1MWzYMGzfvh2TJk3Czp07S9VHrVq1cPnyZYSEhDBhISIiUjNMVohILbRu3Rr9+vUrVL5y5Uo0btwYwcHBMDMzw+3bt2FtbY2rV6/CysoKPj4+GDZsmMSd7gcNGiQx+gIAPXv2RIsWLaTWsbCwwJUrV2BkZFSqkRoiIiJSLg1BkYneREREREREZYwjK0REREREpJaYrBARERERkVpiskJERERERGqJyQoREREREaklJitERERERKSWmKwQEREREZFaYrJCRERERERqickKERERERGpJSYrRERERESklpisEBERERGRWmKyQkREREREaonJChERERERqaX/BztKDe1fUkaGAAAAAElFTkSuQmCC",
+ "image/png": "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t2/PxJHIBpg4EhERuaggbzGCvMUwGAzQ6gG94VYvI4ekOidbPE7DkXqe8/LycOXKFQQHV71iNRFZhokjERGRixMEARKuwUQOZvDgwfj555+hVqsxePBgiESmDwtQqVTo1asXWrZsaacIiZwLE0ciIiIicjiLFy9Gbm4u4uPj8cgjj6Bdu3bGfSKRCCEhIejYsaND9ZISNWRMHImIiIjI4fTs2RMA0LhxY0RFRTWYxXqInJWo+ipERERE5OjK5zhauzU0zZs3x2uvvYbi4mIAwHfffYewsDCEh4fjq6++snN0RM6DPY5ERERE5LA+/PBDaLVaeHl5AQBmzJiBxx57DMHBwXjyySeRlpYGmUxm5yiJHB97HImIiIjIYf3000/o378/AODixYtQKpVYsWIF5s6di+DgYFy6dMnOERI5B/Y4EhEREbkAATZ4HAca3lBVQRBQUFAA4NYw1X79+hlXWBUEAQaDoYqjichSTByJiIiIyGENHDgQCxYswNWrV/HBBx/g3XffBQDk5OQgLy8PrVq1snOERM6BiSMREREROaynnnoK2dnZSExMxDPPPIPx48cDALZu3YrFixfDzY2Xu0S2wL8kIiIiIhdgi1VRG+KqqmKxGAsWLKhQ/txzz9khGiLnxcSRiIiIiByWTqerch6jWCxukAkvkaPhqqpERERE5LAGDBgAiURS6ZaYmGjvEImcAnsciYiIiMhhrV692riqarnMzEy8++67aNq0KTp06GCXuIicDRNHIiIiInJYlSWGQ4YMQatWrSCVSus3ICInxaGqRERERC6gfHEcazdH4evri4iICFy4cMHeoRA5BSaOREREROR0Tp48ib///hvBwcH2DoXIKXCoKhERERE5rIkTJ+LYsWMmZaWlpcjIyMD06dMRHR1tp8iInAsTRyIiIiIX4KzPcXzooYdw9913m5R5enqibdu26NSpk52iInI+TByJiIiIyGGNHDnS3iEQuQTOcSQiIiIiIqIqMXEkIiIiIocyYMAAuLm5ITEx0fjvyjYPDw80b94cGzdutHfYRA6NQ1UtVFhYiMGDB+OLL75AVFSUzdp94YUX0KpVKzz55JM2a5OIiIjoTs40x3H58uXIy8tDu3btjP+ujE6nwz///IPZs2djyJAhCA8Pr8dIiZxHg00c4+Pjcf36dWzevNn4es+ePQAAiUSCqKgojBkz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0np3unnzJiQSSYVHY1TmwoUL6Nu3Ly5evAg/P78atXPz5k14enrC19e3wr74+Hjk5ubif//7n0VxALfmOMrlcigUCrNtEhERkWOoj+/08nN0XXgBbu7V32SvirasECcWt+Q1CJELarA9jncuFFPZwjEymcxkIZzK6t0pLCysRvG0bNkSx48fh5ub6Y/MknaqqvPUU08hNDS0RrEQERER0b+uXLmCdevW4fLlyygtNV3UbuXKlWjfvr2dIiNyHhyXUgMxMTHw9va2aZtxcXHw9PS0aZtEREREdxIJttkamvPnz6Ndu3a4cuUK/vjjD3h7e0MsFuPQoUMoKyuDlxcfa0RkC0wciYiIiFyAINhma2jWrFmDWbNmYffu3WjSpAlmzpyJb775Bt9++y2uXLmCmJgYe4dI5BSYOBIRERGRw7py5Qr69+8P4NYUpuLiYgDAfffdB39/f5w9e9ae4RE5DSaORERERC5AJAg22RoalUoFd3d3AEBkZKQxUdTr9VAqldDr9fYMj8hpNNjFcYiIiIjIdmwx1LS2x5eWluLChQuIiIio00UBH3roIYwbNw43btzAhQsXoNfr0bp16zo7H5ErYY8jEREREdWZVatWISQkBI899hh69eqFJ598Elqt1mbtJyQkoEuXLgBuDU9NSEhAeno6wsLCcPDgQWNvJBFZhz2ORERERC7AFqui1vT4tWvXYtGiRfjuu+/Qp08fAMAnn3wClUpV4RFntdW0aVOT1xMmTMCECRNs0jYR/YuJIxEREZELqO+hqlqtFkuWLMHMmTONSSMATJ482bogiMgumDgSERERUY0olUqT1zKZDDKZzKTszJkzyMrKwogRI5CVlYVr164hLi4O/v7+9RkqEdkI5zgSERERuQDBRhsAREdHQy6XG7fly5dXON+NGzcAADt37kT79u0xdepUREREYNq0adDpdHX3RomoTrDHkYiIiMgF2OJxGuXHp6enw9fX11h+Z28jAEgkEgDA2bNnkZKSAg8PD5w/fx7dunVD27Zt8dxzz1kVCxHVLyaORERERFQjvr6+JomjObGxsQBuzWn08PAAALRq1Qr33HMPfvnlF5sljmq1Gn/88QdSU1OhVqvh7++PDh06oFGjRjZpn4huYeJIRERE5ALqe3Gc5s2bIy4uDtevXzcpv379Onr27GldIP9v7dq1ePXVV5Gfn19h34ABA7Bp0yZER0fb5FxEro6JIxEREZELsMfjOFatWoWpU6fC29sbzZo1w65du3Dp0iV89tln1gUCYNeuXXjhhRfw6quvYvTo0YiMjISnpydycnJw8uRJrFq1CsOHD8fp06chWJsxExETRyIiIiKqG+PGjYNcLkdCQgK+/PJLNG/eHH/++WeFZy/WxrZt27BixQo8//zzJuURERGIiIjAfffdh6ioKJw7dw5t2rSx+nxEro6JIxEREZErsMFQVdTi+IEDB2LgwIFWnriikpIShIaGVrpfKpXCz88PxcXFNj83kSvi4ziIiIiIXIAI/w5XrfVm7zdxm759+2LRokX4888/K+wrLi7G0qVLkZubi/bt29shOiLnwx5HIiIiInI4zz33HH799VfjCqqRkZHw8PBAXl4eLly4AKlUiu3bt8Pd3d3eoRI5BSaORERERC5AEASrF4lpSIvMuLu746uvvsLPP/+MQ4cOGR/H0bx5czzzzDMYN24cAgMD7R0mkdNg4khERETkAur7cRz15Z577sE999xj7zCInB4TRyIiIiIXIIL1cxQb0hxHS+h0OohEogbVU0rkqBzt75+IiIiIyCIDBgxAYmKivcMgcgrscSQiIiJyAc46VJWI6gcTRyIiIiIXUP5IDWvbaChmzZqFU6dOVVnnzJkz9RQNkfNj4mihoqIiDBkyBNu2bUNUVJTN2p07dy5at26Nxx9/3GZtEhERETm7S5cuwdPTE40bN660zj///FOPERE5twabOP73v//F9evXsWnTJuPrvXv3AgAkEgmioqIwZswYTJkyBSKRyOS48nq327JlCzp27GhS9tVXXyE+Ph6DBw/G22+/XWU8r7/+OmJiYoxJY15eHtauXYvExERoNBp07doVc+fORWhoqPEYvV6PDRs2YM+ePcjOzsZdd92FV199FTExMcY6EydOxMCBAzFmzBj4+fnV5EdEREREZDFnexzHAw88gN9//x3r1q2rtM6FCxfqMSIi59ZgE8dr167h6tWrJq89PDywadMmaLVaHD16FDNmzMD169fx6quvmq13uzvvRl27dg3PPvssvL29kZaWVmUsRUVFWLt2LQ4cOGAsGzx4MIYNG4aXX34ZYrEYS5cuRa9evXDy5EljAvif//wH27dvx+rVqxEXF4fNmzejV69e+Ouvv4zPFerQoQOaNm2KjRs3Ys6c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EJVecpb1vYiZeQK0ei6HQNVXr169qm3QCAB+fn6Iiopi0EiVrkIDx02bNmHgwIHi68cffwQAcenjDh06FFmuQ4cOOHXqlPg+KCgIX3/9NSZPngyZTIb58+eLx27cuIHMzEyLdanValy/fr3Mn+P69etmE5sXLFiAFi1aICQkBH379kVoaChefPFFTJs2TcwjkUjQtWtXPP3002jevDm+/PJLdO7c2Wz/m6CgIBgMBty8ebPI82s0GqjVapMXVawcrVDiEC6g4IaKyFaMRiM0mrwij+XnW14inag0NKUMBLUG/tYREdV2FTr2pGXLlnjttdfE9w0bNgTwv403PT2L3hfIy8vLbHPOwv1yPD09TTb1tKYuAOI+PGWRk5MDBwcHk7QDBw5g48aNeOutt9CyZUscO3YMK1aswIABA8SnWuPHjzdZverll19G06ZN8cMPP2D8+PFiemGva3FtnD17tsUljMn2rF04VcqHf2RDhQtNFDWDQGKD1dCICpW240IC/tgREdV2FRo4hoaGFrn/Tp06dQAUzEH08DDfLDYuLk7MAxSM7R49ejQmTJiA1atX46uvvhL3aHm4rqIUpluz9G5xfHx8kJ6ebpL29ttv480338Ts2bMBFGxCqlKpMGHCBJw4cQIAzJY8rlevHtq0aWPSmwpArNvHx6fI80+dOhWTJk0S36vVagQGBpb581DJnJRSuDlIoCqhRzHAjasNku1IJBI4ObkiO1tldszZya2IEkRl46qUIl9n3XBVuRRwUFRwg4iIqMqrlEfY3bp1g729PX7//XezY/n5+di6dSsiIyPFtJEjR6JJkyb46quvsHjxYsyYMQOXL18GUBAQtmzZssi6AGDdunUICwsr196Lbdq0QUxMjPjeYDAgKSkJoaGhJvlCQ0OL3ILkYZmZmWZzLC9cuAA3Nzez+goplUq4urqavKjiNfG1/FzFRSlBXXf2ApFtuXv4wN7e0STN0dEZbu5Fj6ogKgtfV+t/u3ycpZByeAURUa1XKXe9Xl5emDJlChYuXIh9+/aJ6TqdDhMmTEBubi6mT58OAFi8eDEOHTqE1atXQyaTYejQoXj++efx+uuviwvNzJs3DwcOHMC3335rcp7vv/8eu3btwn/+859ytfe5557D+fPnkZaWBgCQyWRo06YN1q9fL271kZubiz/++APt27cXy61cudJkMZxf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",
"text/plain": [
- ""
+ ""
]
},
"metadata": {},
@@ -314,10 +366,8 @@
}
],
"source": [
- "enrichment = gsea(result, regulons, id_column=\"symbol\", rng=1)\n",
- "plot_enrichment(result, regulons[\"STAT1\"], id_column=\"symbol\")\n",
- "plt.show()\n",
- "plot_gsea_table(result, regulons, enrichment, id_column=\"symbol\")\n",
+ "enriched = over_representation(result, regulons, id_column=\"symbol\")\n",
+ "plot_over_representation(enriched)\n",
"plt.show()"
]
},
@@ -326,33 +376,9 @@
"id": "e7b6ce28",
"metadata": {},
"source": [
- "GSEA ranks the same regulators first, now from the whole ranking rather than a\n",
- "cutoff, and finds ID1's targets at the bottom. It warns that one set, the 383 targets\n",
- "of MYC, has too few random scores of its sign to be tested; its p-value is missing,\n",
- "as fgsea leaves it.\n",
- "\n",
- "## Choices\n",
- "\n",
- "- **One set or many.** For a set chosen in advance, a self-contained test, {api}`fry`\n",
- " or {api}`roast`, answers whether it responds at all; to rank a collection, use a\n",
- " competitive test, {api}`camera` or {api}`romer`, which takes a result of\n",
- " {api}`limma_trend`, as it has no observation weights. {api}`mroast` runs roast on\n",
- " many sets with the same rotations.\n",
- "- **camera, over-representation or GSEA.** camera uses every gene's statistic and\n",
- " allows for the correlation between the genes of a set; over-representation needs\n",
- " only a list of genes, and depends on the cutoff that made it; GSEA needs only a\n",
- " ranking, from any method. Over-representation and GSEA take genes as independent,\n",
- " which makes their p-values too small for sets of genes that respond together\n",
- " ([Gene-set tests](../statistics/gene_set_tests.md#choices)).\n",
- "- **Rotations and permutations.** roast's smallest p-value is about one over its\n",
- " number of rotations (`n_rotations`, 1,999 by default), and GSEA's random sets are\n",
- " drawn from `rng`: pass a seed to get the same p-values every time.\n",
- "- **The collection.** Restrict the sets to the genes tested and to a range of sizes\n",
- " before testing, and name genes as the data do, with `id_column`: symbols here, or\n",
- " the Ensembl ids of `feature_id`, the default.\n",
- "- **Scores per sample.** To compare samples rather than test one contrast, score each\n",
- " set or regulator in every sample: [Pathway and regulator\n",
- " activity](pathway_activity.ipynb)."
+ "The genes up hold the targets of the regulators that camera and GSEA ranked first, and\n",
+ "the genes down hold MYC's. The lists, and so the result, depend on the cutoff of 0.05\n",
+ "that made them."
]
}
],
@@ -361,8 +387,8 @@
"kang2018",
"hallmark_interferon_alpha"
],
- "executed_at": "2026-10-27T15:23:08+00:00",
- "executed_code_sha256": "645668ea574e82f35b283420bd774178657944718fadfc5c23633fb60f2d8054",
+ "executed_at": "2026-10-28T00:18:49+00:00",
+ "executed_code_sha256": "90b3447df9ecdc605a8c6942967d78b9acda52d73506de30c042ef43f33f047b",
"executed_datasets": {
"hallmark_interferon_alpha": {
"hallmark_interferon_alpha_response.json": "sha256:c5ef439a3f9b6e7faef95eb6b9f7c82ed1a48ede2af78629c85be3b873a8f1c8"
@@ -371,8 +397,8 @@
"kang2018.cube.zarr": "sha256:a30af3dabe17f21a6c75f41b64a279a1423640b9aa919022cf67897f9cc77a51"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 30.3,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 5.0,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/genes_per_cell_type.ipynb b/docs/source/guides/genes_per_cell_type.ipynb
index c12100f..2def6a6 100644
--- a/docs/source/guides/genes_per_cell_type.ipynb
+++ b/docs/source/guides/genes_per_cell_type.ipynb
@@ -8,14 +8,15 @@
"(genes-per-cell-type)=\n",
"# Genes that change in each cell type\n",
"\n",
- "Which genes does a treatment change, and in which cell types? This page answers it for\n",
- "the interferon-beta stimulation of Kang et al. [@kang2018]: blood cells of eight lupus\n",
- "patients, each donor's cells split into a culture stimulated with interferon-beta for\n",
- "six hours and an untreated one. The cells of each sample and cell type are pooled into\n",
- "one profile, so that every test compares donors, not cells ([Choosing a\n",
- "method](../statistics/choosing_a_method.md) says why). The cube is the `kang2018`\n",
- "dataset ([Datasets](../reference/datasets.md)), a download of 77 MB that\n",
- "`python docs/datasets.py prepare kang2018` makes."
+ "Test which genes a treatment changes in each cell type with {api}`limma_voom`, which runs\n",
+ "limma-voom on the pseudobulk profiles of every cell type of a cube, so that every test\n",
+ "compares donors, not cells. This page applies it to the interferon-beta stimulation of\n",
+ "Kang et al. [@kang2018]: blood cells of eight lupus patients, each donor's cells split\n",
+ "into a culture stimulated with interferon-beta for six hours and an untreated one. The\n",
+ "cube is the `kang2018` dataset ([Datasets](../reference/datasets.md)), a download of\n",
+ "77 MB that `python docs/datasets.py prepare kang2018` makes. [Why\n",
+ "pseudobulk](../why_pseudobulk.ipynb) says why the tests compare donors, and [limma-voom\n",
+ "and limma-trend](../statistics/limma_voom.md) explains the method."
]
},
{
@@ -32,7 +33,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Cube with 16 samples x 8 groups x 35,635 features\n",
" layers: counts (uint32, lazy), detected (uint32, lazy)\n",
" filled: 128 of 128 sample x group entries have cells (100.0%)\n",
@@ -78,17 +79,20 @@
"source": [
"A profile pooled from a handful of cells is noisy, so {api}`limma_voom` leaves the\n",
"profiles of fewer than 10 cells out of their cell type's fit (`min_cells=10`); they\n",
- "are pale in the heatmap. Two samples of the dendritic cells and four of the\n",
- "megakaryocytes are left out, and every other profile enters.\n",
+ "are pale in the heatmap ([Samples with few cells or\n",
+ "counts](../statistics/normalization_and_filtering.md#samples-with-few-cells)). Two\n",
+ "samples of the dendritic cells and four of the megakaryocytes are left out, and every\n",
+ "other profile enters.\n",
"\n",
"## A paired design\n",
"\n",
"Every donor gives a control and a stimulated sample. The design `~ donor + condition`\n",
"compares each donor's two samples, so that the differences between donors do not count\n",
- "as noise, and its coefficient `conditionstim` is the log2 fold change of the\n",
- "stimulation. Name the levels of the condition: the first is the reference.\n",
- "{api}`Design.check` looks for designs that cannot answer the question before anything\n",
- "is fitted ([Designs, linear models and contrasts](../statistics/linear_models.md))."
+ "as noise ([A paired design](../statistics/linear_models.md#paired-design)), and its\n",
+ "coefficient `conditionstim` is the log2 fold change of the stimulation. Name the levels\n",
+ "of the condition: the first is the reference. {api}`Design.check` looks for designs that\n",
+ "cannot answer the question before anything is fitted ([Design\n",
+ "checks](../statistics/linear_models.md#design-checks))."
]
},
{
@@ -128,10 +132,11 @@
"source": [
"## Test every cell type\n",
"\n",
- "{api}`limma_voom` runs limma-voom in each cell type in turn: it leaves out the profiles\n",
- "with too few cells, keeps the genes with enough counts for the design, normalizes the\n",
- "libraries by TMM, gives every observation a voom precision weight, fits the design to\n",
- "every gene and moderates the contrast by empirical Bayes [@law2014; @ritchie2015]. The\n",
+ "{api}`limma_voom` fits each cell type in turn, with the steps of limma's workflow for\n",
+ "RNA-seq: it leaves out the profiles with too few cells, keeps the genes with enough\n",
+ "counts for the design, normalizes the libraries by TMM, gives every observation a voom\n",
+ "precision weight, fits the design to every gene and moderates the contrast by empirical\n",
+ "Bayes ([What runs in each cell type](../statistics/limma_voom.md#limma-voom-steps)). The\n",
"figure counts the genes up and down at an adjusted p-value below 0.05 in each cell\n",
"type, the first figure to look at in a study of many cell types."
]
@@ -186,12 +191,11 @@
"metadata": {},
"source": [
"A cell type left with too few samples to estimate a variance is not fitted, and both\n",
- "the printed summary and the figure say so ([Designs, linear models and\n",
- "contrasts](../statistics/linear_models.md#designs-that-cannot-answer-the-question) says\n",
- "when); `result.summary` gives the\n",
- "reason and `result.samples` lists every profile with the reason it was left out. Each fit keeps\n",
- "the rows of the design for its own samples, so the megakaryocytes, which have no\n",
- "profile of donor 107, are fitted without that donor's column.\n",
+ "the printed summary and the figure say so ([What runs in each cell\n",
+ "type](../statistics/limma_voom.md#limma-voom-steps) says when); `result.summary` gives\n",
+ "the reason, and `result.samples` lists every profile with the reason it was left out.\n",
+ "Each fit keeps the rows of the design for its own samples, so the megakaryocytes, which\n",
+ "have no profile of donor 107, are fitted without that donor's column.\n",
"\n",
"## One cell type in detail\n",
"\n",
@@ -385,13 +389,41 @@
"id": "c7466605",
"metadata": {},
"source": [
- "## voom or trend\n",
+ "The responding genes feed the next questions: which pathways and regulators they\n",
+ "belong to ([Gene sets and pathways](gene_sets.ipynb)) and which regulators are active\n",
+ "([Pathway and regulator activity](pathway_activity.ipynb)).\n",
+ "\n",
+ "## Choices\n",
+ "\n",
+ "- **voom or trend.** {api}`limma_voom` by default, the safer choice when library sizes\n",
+ " vary with the number of cells pooled; {api}`limma_trend` suits libraries within about\n",
+ " threefold of each other ([limma-trend](../statistics/limma_voom.md#limma-trend)). The\n",
+ " two are compared below.\n",
+ "- **Within or across cell types.** Adjust within each cell type to rank its genes, the\n",
+ " default; `adjust_across_groups=True` adds a column adjusted across cell types, for\n",
+ " when the study's list of genes, of every cell type, is the result ([Adjusting within\n",
+ " or across cell\n",
+ " types](../statistics/multiple_testing.md#adjusting-across-cell-types)). Below, both\n",
+ " on these data.\n",
+ "- **Profiles with few cells or counts.** `min_cells` (10) and `min_counts` (0) decide\n",
+ " which profiles enter each fit; raise them when small pools look noisy on an MDS plot,\n",
+ " or keep every profile and weight it by its number of cells with\n",
+ " `prior_weights=\"n_cells\"` ([Prior weights from the number of\n",
+ " cells](../statistics/limma_voom.md#prior-weights-from-cells)). `quality_weights=True`\n",
+ " down-weights the samples that follow the model less closely instead, as\n",
+ " [Normalization, filtering and sample quality](normalization_and_quality.ipynb) shows.\n",
+ "- **A variable of the donors.** The paired design cannot test a variable that is\n",
+ " constant within each donor, such as sex or disease: [Repeated measures and\n",
+ " donors](repeated_measures.ipynb) tests it with {api}`dream_voom`, which gives the\n",
+ " donor a random effect.\n",
+ "- **Other models.** edgeR's quasi-likelihood F-tests and DESeq2 run on one cell type's\n",
+ " counts at a time ({api}`Cube.layer`), as on [bulk RNA-seq](bulk_rna_seq.ipynb).\n",
+ "\n",
+ "### voom or trend\n",
"\n",
"{api}`limma_trend` fits the log-CPM values without precision weights and lets the prior\n",
- "variance follow the expression level. It suits libraries of similar sizes, within\n",
- "about threefold of each other by the limma User's Guide ([Choosing a\n",
- "method](../statistics/choosing_a_method.md)). A pseudobulk library grows with the\n",
- "number of cells pooled, and the libraries of each cell type here differ far more:"
+ "variance follow the expression level. A pseudobulk library grows with the number of\n",
+ "cells pooled, and the libraries of each cell type here differ far more than threefold:"
]
},
{
@@ -457,12 +489,12 @@
"least, as in the dendritic cells and the CD14+ monocytes, the two agree more closely.\n",
"voom's weights give the small libraries less say.\n",
"\n",
- "## Adjusting within or across cell types\n",
+ "(guide-adjusting-across-cell-types)=\n",
+ "### Adjusting within or across cell types\n",
"\n",
"The p-values of each cell type and contrast are adjusted over its own genes, as limma's\n",
"`topTable` adjusts them. `adjust_across_groups=True` adds a column adjusted over the\n",
- "genes of every cell type for the same contrast, one family of tests for the study\n",
- "([Multiple testing](../statistics/multiple_testing.md))."
+ "genes of every cell type for the same contrast, one family of tests for the study."
]
},
{
@@ -506,31 +538,7 @@
"Adjusting across cell types is not stricter for every cell type. The many small\n",
"p-values of the monocytes lower the adjusted p-values of the other cell types' genes,\n",
"which gain calls, while the monocytes, now adjusted with genes that respond less, lose\n",
- "some.\n",
- "\n",
- "## Choices\n",
- "\n",
- "- **voom or trend.** {api}`limma_voom` is the default, and the safer choice when library\n",
- " sizes vary with the number of cells pooled; {api}`limma_trend` is faster and suits\n",
- " libraries of similar sizes.\n",
- "- **Profiles with few cells or counts.** `min_cells` (10) and `min_counts` (0) decide\n",
- " which profiles enter each fit; raise them when small pools look noisy on an MDS plot,\n",
- " or keep every profile and weight it by its number of cells with\n",
- " `prior_weights=\"n_cells\"` [@hoffman2026a]. `quality_weights=True` down-weights the\n",
- " samples that follow the model less closely instead. [Normalization, filtering and\n",
- " sample quality](normalization_and_quality.ipynb) shows both.\n",
- "- **Within or across cell types.** Adjust within each cell type to rank its genes;\n",
- " adjust across cell types when the study's list of genes, of every cell type, is the\n",
- " result.\n",
- "- **A variable of the donors.** The paired design cannot test a variable that is\n",
- " constant within each donor, such as sex or disease: [Repeated measures and\n",
- " donors](repeated_measures.ipynb) tests it with a random effect for the donor.\n",
- "- **Other models.** edgeR's quasi-likelihood F-tests and DESeq2 run on one cell type's\n",
- " counts at a time ({api}`Cube.layer`), as on [bulk RNA-seq](bulk_rna_seq.ipynb).\n",
- "\n",
- "The responding genes feed the next questions: which pathways and regulators they\n",
- "belong to ([Gene sets and pathways](gene_sets.ipynb)) and which regulators are active\n",
- "([Pathway and regulator activity](pathway_activity.ipynb))."
+ "some."
]
}
],
@@ -538,15 +546,15 @@
"datasets": [
"kang2018"
],
- "executed_at": "2026-10-27T15:23:13+00:00",
+ "executed_at": "2026-10-28T00:18:38+00:00",
"executed_code_sha256": "c06caf3991b426d7951d3ef1577da56188a5c05f25406d8f79f63fea4f567c13",
"executed_datasets": {
"kang2018": {
"kang2018.cube.zarr": "sha256:a30af3dabe17f21a6c75f41b64a279a1423640b9aa919022cf67897f9cc77a51"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 5.0,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 5.8,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/normalization_and_quality.ipynb b/docs/source/guides/normalization_and_quality.ipynb
index 2e5d88f..39df32c 100644
--- a/docs/source/guides/normalization_and_quality.ipynb
+++ b/docs/source/guides/normalization_and_quality.ipynb
@@ -8,20 +8,23 @@
"(normalization-and-quality)=\n",
"# Normalization, filtering and sample quality\n",
"\n",
- "Before testing, look at the data: do the samples group as the design expects, which\n",
- "genes have enough counts to be tested, do the libraries need rescaling, and does any\n",
- "sample or gene behave unlike the rest? {api}`limma_voom` takes these steps inside,\n",
- "with defaults; this page takes them one at a time, so that each choice can rest on\n",
- "what the data show. It uses the CD14+ monocytes of the Kang data [@kang2018] and the\n",
- "simulated bulk experiment, whose noisier sample is known (the `kang2018` and\n",
- "`simulated_bulk` datasets of the [Datasets](../reference/datasets.md) page).\n",
+ "Check the samples, genes and libraries of a cube before testing them. By default\n",
+ "{api}`limma_voom` leaves out the samples of fewer than 10 cells in each cell type, keeps\n",
+ "the genes that edgeR's filter passes with the design, and scales the libraries by TMM\n",
+ "factors. This page takes these steps one at a time, with the figures that show whether\n",
+ "the samples group as the design expects and whether a sample or gene behaves unlike\n",
+ "the rest, so that each choice can rest on what the data show. It uses the CD14+\n",
+ "monocytes of the Kang data [@kang2018] and the simulated bulk experiment, whose noisier\n",
+ "sample is known (the `kang2018` and `simulated_bulk` datasets of the\n",
+ "[Datasets](../reference/datasets.md) page). [Normalization and\n",
+ "filtering](../statistics/normalization_and_filtering.md) explains each step.\n",
"\n",
"## Do the samples group as expected?\n",
"\n",
- "limma's multidimensional scaling plot ({api}`plot_mds`) places the samples so that\n",
- "their distances are the typical log2 fold changes between them, the root mean square\n",
- "of the 500 largest. From a cube it takes one cell type's counts, filtered and\n",
- "normalized as edgeR prepares them for its own plot."
+ "limma's multidimensional scaling plot ({api}`plot_mds`) places the samples of one cell\n",
+ "type so that their distances are the typical log2 fold changes between them ([Samples\n",
+ "at a glance](../statistics/normalization_and_filtering.md#samples-at-a-glance) says how\n",
+ "it prepares a cube's counts)."
]
},
{
@@ -38,7 +41,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n"
+ "CytePool 0.1.dev70+g194507939\n"
]
},
{
@@ -101,7 +104,7 @@
"A sample far from its group on such a plot deserves a look before any test.\n",
"\n",
"DESeq2's PCA plot ({api}`plot_pca`) places the same samples on the principal components\n",
- "of their 500 most variable genes, each labelled with its share of the variance\n",
+ "of their most variable genes, each labelled with its share of the variance\n",
"[@love2014]. {api}`principal_components` also returns the loadings of the genes, which\n",
"say what each component is made of."
]
@@ -155,22 +158,21 @@
"IFITM3. The second sets the two samples of donor 1039 apart, as the second dimension\n",
"of the MDS plot does, and the gene with by far the largest loading on it is HBB,\n",
"whose reads come from red cells that contaminate some samples more than others; the\n",
- "genes with outlying variances, below, return to it. The two plots agree here: MDS measures each pair of samples by the\n",
- "genes that differ most between those two, PCA every sample by the same 500 genes.\n",
+ "genes with outlying variances, below, return to it. The two plots agree here, although\n",
+ "they measure the distances between samples differently.\n",
"\n",
"## Which genes to test\n",
"\n",
- "voom estimates how the variance of the log counts falls with their level, and weights\n",
- "every observation by it [@law2014]; its plot ({api}`plot_mean_variance`) shows the\n",
- "trend. Genes whose counts are mostly zeros have log-CPM\n",
- "values near the floor in most samples: their spread is small for want of counts, not\n",
- "because they are precise, and they bend the trend down at its low end, where voom\n",
- "would then weight noisy observations most. {api}`filter_by_expression` keeps the\n",
- "genes with enough counts in enough samples, edgeR's `filterByExpr` rule [@chen2016],\n",
- "and the number of samples it asks for comes from the design. Below, voom of every gene\n",
- "with counts in at least two samples, on the left, against the trend that\n",
- "{api}`limma_voom` fitted to the genes its filter kept, drawn from its result, on the\n",
- "right."
+ "voom's plot ({api}`plot_mean_variance`) shows how the variance of the log counts falls\n",
+ "with their level, the trend from which voom weights every observation [@law2014].\n",
+ "Genes with too few counts bend the trend down at its low end, where voom would then\n",
+ "weight noisy observations most ([Filtering by\n",
+ "expression](../statistics/normalization_and_filtering.md#filtering-by-expression) says\n",
+ "why). {api}`filter_by_expression` keeps the genes with enough counts in enough samples,\n",
+ "by edgeR's `filterByExpr` rule [@chen2016], and the number of samples it asks for comes\n",
+ "from the design. Below, voom of every gene with counts in at least two samples, on the\n",
+ "left, against the trend that {api}`limma_voom` fitted to the genes its filter kept,\n",
+ "drawn from its result, on the right."
]
},
{
@@ -179,7 +181,7 @@
"id": "872a24f8",
"metadata": {
"figure_descriptions": [
- "Two voom mean-variance plots of the CD14+ monocytes: the square root of each gene's residual standard deviation against its average log2 count, with the LOWESS trend in orange. Left, the 12,966 genes with counts in at least two samples: the trend rises from about 0.8 at the lowest counts to a peak near 0.95 at an average log2 count of about 1, then falls to near 0 at 16. Right, the 5,125 genes kept by the filter: the trend falls steadily from about 1.2 at an average log2 count of 0 to about 0.3 at 16."
+ "Two voom mean-variance plots of the CD14+ monocytes: the square root of each gene's residual standard deviation against its average log2 count, with the LOWESS trend in orange. Left, the 12,966 genes with counts in at least two samples: the trend rises from about 0.8 at the lowest counts to a peak near 0.95 at an average log2 count of about 1, then falls to near 0 at 16. Right, the 5,125 genes kept by the filter: the trend falls steadily from about 1.2 at an average log2 count of 0 to about 0.2 at 16."
]
},
"outputs": [
@@ -245,10 +247,12 @@
"## Normalization factors\n",
"\n",
"Library sizes account for sequencing depth, not for composition: when a few genes take\n",
- "a larger share of a library, every other gene looks lower in it. TMM factors\n",
+ "a larger share of a library, every other gene looks lower in it ([Library sizes and\n",
+ "composition](../statistics/normalization_and_filtering.md#library-sizes)). TMM factors\n",
"({api}`tmm_factors`) rescale the libraries so that most genes do not change between\n",
- "samples [@robinson2010a], and [Count models](../statistics/count_models.md) describes\n",
- "the alternatives."
+ "samples [@robinson2010a]; here they are compared with the other rules ([Normalization\n",
+ "factors and size\n",
+ "factors](../statistics/normalization_and_filtering.md#normalization-and-size-factors))."
]
},
{
@@ -262,13 +266,7 @@
"output_type": "stream",
"text": [
"The 20 genes that gain most take 4% of the control libraries and 22% of the stimulated ones.\n",
- "\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
+ "\n",
"median factor control stimulated\n",
"TMM 1.19 0.83\n",
"RLE 1.19 0.84\n",
@@ -322,7 +320,8 @@
"and empirical Bayes moderation shrinks each towards a common prior\n",
"([Empirical Bayes moderation](../statistics/empirical_bayes.md)). limma's `plotSA`\n",
"({api}`plot_residual_variances`) shows them, and `robust=True` estimates the prior so\n",
- "that genes with outlying variances do not distort it [@phipson2016].\n",
+ "that genes with outlying variances do not distort it [@phipson2016] ([Estimating the\n",
+ "prior](../statistics/empirical_bayes.md#estimating-the-prior)).\n",
"{api}`DifferentialExpressionResult.fit` computes the fit of a cell type of a result\n",
"again, from the cube and the design that the result keeps: a {api}`GroupFit`, with the\n",
"samples, genes, values and weights fitted and the moderated tests, from which the\n",
@@ -391,8 +390,8 @@
"\n",
"A sample that follows the model less closely than the others, such as a degraded one,\n",
"adds noise to every gene. Sample quality weights estimate how closely each sample\n",
- "follows the model and down-weight it, instead of removing it [@liu2015]; [Precision\n",
- "weights](../statistics/precision_weights.md) gives the model. In the simulated bulk\n",
+ "follows the model and down-weight it, instead of removing it [@liu2015]; [limma-voom and\n",
+ "limma-trend](../statistics/limma_voom.md#sample-quality-weights) gives the model. In the simulated bulk\n",
"experiment, sample `drug_b_3` has four times the dispersion of the others."
]
},
@@ -507,26 +506,26 @@
"metadata": {},
"source": [
"The check also finds aliased and confounded columns, levels without samples, contrasts\n",
- "the design cannot estimate and numeric codes that look like batches ([Designs, linear\n",
- "models and contrasts](../statistics/linear_models.md#designs-that-cannot-answer-the-question)).\n",
+ "the design cannot estimate and numeric codes that look like batches ([Design\n",
+ "checks](../statistics/linear_models.md#design-checks)).\n",
"\n",
"## Choices\n",
"\n",
- "- **The filter.** {api}`limma_voom` filters with the design, as above; a paired design\n",
- " gives every sample a high leverage, so a gene needs counts in only a few samples.\n",
+ "- **The filter.** {api}`limma_voom` filters each cell type with its design, as above.\n",
" `expression_filter` changes the thresholds, and\n",
" `expression_filter={\"sample_groups\": \"condition\"}` groups the samples by a column\n",
- " instead of the design, as {api}`filter_by_expression` does with `sample_groups=`: a\n",
- " gene then needs counts in as many samples as the smallest group has.\n",
- "- **Normalization factors.** TMM is the default of {api}`limma_voom` and of edgeR.\n",
- " TMM, RLE and DESeq2's median ratios assume that most genes do not change between\n",
- " samples, and the upper quartile that one quantile of the counts does not; when a\n",
- " response changes many genes, check that the factors make sense, as above.\n",
+ " instead, as {api}`filter_by_expression` does with `sample_groups=`, so that designs of\n",
+ " the same samples test the same genes ([Filtering by\n",
+ " expression](../statistics/normalization_and_filtering.md#filtering-by-expression)).\n",
+ "- **Normalization factors.** TMM by default, and `normalization=None` for library sizes\n",
+ " alone. When a response changes many genes, check that the factors make sense, as\n",
+ " above ([Normalization factors and size\n",
+ " factors](../statistics/normalization_and_filtering.md#normalization-and-size-factors)).\n",
"- **Robust moderation.** Pass `robust=True` to {api}`limma_voom` when `plotSA` shows\n",
" outlying variances, as here.\n",
"- **A poor sample.** Quality weights keep it at a lower weight; leaving it out loses\n",
- " its information, and in a paired design its partner's ([Precision\n",
- " weights](../statistics/precision_weights.md)). `prior_weights=\"n_cells\"` instead\n",
+ " its information, and in a paired design its partner's ([limma-voom and\n",
+ " limma-trend](../statistics/limma_voom.md#sample-quality-weights)). `prior_weights=\"n_cells\"` instead\n",
" weights pseudobulk profiles by their number of cells; the two cannot be combined.\n",
"- **What to adjust for.** [Repeated measures and donors](repeated_measures.ipynb)\n",
" partitions the variance of every gene among the variables of the design."
@@ -538,7 +537,7 @@
"kang2018",
"simulated_bulk"
],
- "executed_at": "2026-10-27T15:23:17+00:00",
+ "executed_at": "2026-10-28T00:18:32+00:00",
"executed_code_sha256": "4273edc8fc8913273299dacca602d6a6f46975f14b5a355a815399853e7a4b85",
"executed_datasets": {
"kang2018": {
@@ -549,8 +548,8 @@
"simulated_bulk_truth.npz": "sha256:a8a0275532e3cafb02401846176a7ddfa037579aceb804c5f9c007700f022977"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 4.0,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 4.1,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/pathway_activity.ipynb b/docs/source/guides/pathway_activity.ipynb
index 3c4fa79..06eb2e1 100644
--- a/docs/source/guides/pathway_activity.ipynb
+++ b/docs/source/guides/pathway_activity.ipynb
@@ -8,21 +8,20 @@
"(pathway-activity)=\n",
"# Pathway and regulator activity\n",
"\n",
- "A gene-set test answers one question of one contrast. Scores answer it for every\n",
- "profile: how active is a regulator or a pathway in each sample, or in each cell type's\n",
- "response? A transcription factor raises some of its targets and lowers others, and a\n",
- "signalling pathway leaves a footprint of genes that respond to it. CollecTRI\n",
- "[@muellerdott2023] and PROGENy [@schubert2018] give each such source signed weights\n",
- "on its targets, and {api}`ulm` scores the sources from them as decoupler does\n",
- "[@badiaimompel2022]; GSVA scores gene sets, whose genes count alike\n",
- "[@haenzelmann2013]. [Gene-set tests](../statistics/gene_set_tests.md#activities-of-regulators-and-pathways)\n",
- "explains the methods. This page scores the Kang data [@kang2018] (the `kang2018`\n",
- "dataset of the [Datasets](../reference/datasets.md) page).\n",
+ "Score how active each transcription factor and pathway is, in each sample or in each\n",
+ "cell type's response, with {api}`ulm`, which reads a source's activity from the signed\n",
+ "weights of a network on its targets, as decoupler does [@badiaimompel2022]. CollecTRI\n",
+ "[@muellerdott2023] gives the regulons of transcription factors and PROGENy\n",
+ "[@schubert2018] the footprints of signalling pathways; {api}`gsva` scores gene sets,\n",
+ "whose genes carry no signs [@haenzelmann2013]. This page scores the Kang data [@kang2018]\n",
+ "(the `kang2018` dataset of the [Datasets](../reference/datasets.md) page). [Activities\n",
+ "from networks](../statistics/activities.md) and [Pathway scores per\n",
+ "sample](../statistics/pathway_scores.md) explain the methods.\n",
"\n",
- "{api}`collectri` and {api}`progeny` download their networks on first use, check them\n",
- "against pinned digests and keep them in a cache. By default `collectri()` keeps the\n",
- "interactions whose sources allow commercial use, the licence level a company may use\n",
- "([Datasets](../reference/datasets.md#networks) gives the terms)."
+ "{api}`collectri` and {api}`progeny` download their networks on first use and keep them\n",
+ "in a cache; by default `collectri()` keeps the interactions whose sources allow\n",
+ "commercial use ([Networks of regulators and\n",
+ "pathways](../statistics/activities.md#regulator-networks))."
]
},
{
@@ -35,7 +34,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Network: 1,171 sources, 39,896 edges, 6,505 target genes\n",
" name: CollecTRI; version: 2.0 (Zenodo record 8192729); licence: CC BY 4.0\n",
" (Zenodo record 8192729); level 'commercial': the interactions of\n",
@@ -98,7 +97,7 @@
"metadata": {
"figure_descriptions": [
"Bars of the 25 transcription factors with the largest absolute activities in the response of the CD14+ monocytes, the ULM t-statistics of CollecTRI's regulons on the moderated t-statistics, in decreasing order from the top and coloured by their value: STAT1 at about 11.5, IRF1 at 10.5, STAT2 at 8, then NFKB1, IRF9 and RELA near 7, down to FOXO3 at about 3.3, all in shades of red; TRERF1 and ZBTB4, at about -3.4, are pale blue.",
- "Heatmap of the activities of 25 transcription factors (rows) in the response of each of eight cell types (columns), coloured from blue for negative through grey to red for positive, up to about 17. STAT1, IRF1, STAT2 and IRF9 are active in every cell type, most in the CD4 T cells; NFKB1, RELA, IRF2 and IRF3 are moderately active in most. Dark grey cells, frequent in the CD8 T cells and the megakaryocytes, mark regulators with fewer than 5 targets among a cell type's tested genes."
+ "Heatmap of the activities of 25 transcription factors (rows) in the response of each of eight cell types (columns), coloured from blue for negative through grey to red for positive, up to about 17. STAT1, IRF1, STAT2 and IRF9 are active in every cell type, most in the CD4 T cells; NFKB1, RELA, IRF2 and IRF3 are moderately active in most. Dark grey cells, most of them in the CD8 T cells, the NK cells and the megakaryocytes, mark regulators with fewer than 5 targets among a cell type's tested genes."
]
},
"outputs": [
@@ -140,8 +139,8 @@
"([Gene sets and pathways](gene_sets.ipynb)), now with each target counted with its\n",
"sign. The same regulators are active in the response of every cell type, most in the\n",
"CD4 T cells. A regulator with too few targets among a cell type's tested genes has no\n",
- "score there, drawn dark grey: fewer genes pass the filter where the profiles are\n",
- "small, as in the CD8 T cells and the megakaryocytes.\n",
+ "score there, drawn dark grey: the cell types differ in the genes that pass the filter,\n",
+ "and the CD8 T cells, the NK cells and the megakaryocytes lack the most scores.\n",
"\n",
"## Pathways in every sample\n",
"\n",
@@ -150,7 +149,8 @@
"of a gene. PROGENy's footprints are those of 14 pathways in human cells. {api}`limma`\n",
"tests a contrast of the design on the activities of every pathway, as limma tests\n",
"genes: it matches the design's samples to the profiles scored by their ids, so the\n",
- "design of the cube serves."
+ "design of the cube serves ([Testing scores with a\n",
+ "design](../statistics/pathway_scores.md#testing-scores))."
]
},
{
@@ -222,7 +222,7 @@
"\n",
"{api}`gsva` scores each gene set in every sample from how the set's genes rank against\n",
"the sample's other genes, each gene measured against its own distribution across the\n",
- "samples. Here the sets are the regulators' targets, without their signs\n",
+ "samples ([GSVA](../statistics/pathway_scores.md#gsva)). Here the sets are the regulators' targets, without their signs\n",
"({api}`Network.to_gene_sets`); as GSVA's guide does, the scores are tested with limma,\n",
"with a prior that follows the sets' sizes (`trend=scores.n_genes`), and the heatmap\n",
"shows the 20 sets that differ most between the conditions."
@@ -234,7 +234,7 @@
"id": "0c325c73",
"metadata": {
"figure_descriptions": [
- "Heatmap of the GSVA scores of the target sets of 20 transcription factors (rows), those that differ most between the conditions, in the 16 CD14+ monocyte samples (columns), the 8 controls on the left and the 8 stimulated samples on the right, coloured from blue (about -0.6) through grey to red (about 0.6). The sets of STAT1, IRF1, REL, IRF4, IRF3, NFKB1 and RELA score low in every control and high in most stimulated samples; those of NRF1 and DNMT1 score high in most controls and low after the stimulation."
+ "Heatmap of the GSVA scores of the target sets of 20 transcription factors (rows), those that differ most between the conditions, in the 16 CD14+ monocyte samples (columns), the 8 controls on the left and the 8 stimulated samples on the right, coloured from blue (about -0.6) through grey to red (about 0.6). The sets of STAT1, IRF1, REL, IRF4, IRF3, NFKB1 and RELA score low in nearly every control and high in most stimulated samples; those of NRF1 and DNMT1 score high in most controls and low after the stimulation."
]
},
"outputs": [
@@ -276,16 +276,18 @@
"\n",
"- **A contrast or the samples.** Score a contrast's statistics to see which regulators\n",
" drive a response, in each cell type; score the samples to compare them, correlate\n",
- " activities with other measurements, or test them with any design.\n",
- "- **ULM, MLM or the weighted aggregate.** {api}`ulm` is decoupler's default for\n",
- " regulators and pathways alike; {api}`mlm` fits all the sources at once, which\n",
- " separates regulators whose targets overlap; {api}`weighted_aggregate` gives the\n",
- " weighted mean or sum of the targets' values, the sum being PROGENy's original score,\n",
- " with permutation p-values.\n",
+ " activities with other measurements, or test them with any design ([Activities or\n",
+ " scores](../statistics/activities.md#activities-or-scores)).\n",
+ "- **ULM, MLM or the weighted aggregate.** {api}`ulm` by default, decoupler's default\n",
+ " for regulators and pathways alike; {api}`mlm` fits all the sources at once, which\n",
+ " separates regulators whose targets overlap ([MLM](../statistics/activities.md#mlm));\n",
+ " {api}`weighted_aggregate` gives the weighted mean or sum of the targets' values, with\n",
+ " permutation p-values ([The weighted\n",
+ " aggregate](../statistics/activities.md#weighted-aggregate)).\n",
"- **Activities or scores.** Activities weigh each target by its sign and weight, so\n",
" that a repressed target counts against its regulator; GSVA and the other pathway\n",
- " scores ({api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`) treat a set's genes\n",
- " alike.\n",
+ " scores ({api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`) take a set's genes\n",
+ " without signs ([How the scores differ](../statistics/pathway_scores.md#how-pathway-scores-differ)).\n",
"- **The networks.** `collectri(licence=\"academic\")` adds the interactions that only a\n",
" resource listed for academic use supports. `progeny()` keeps each pathway's genes\n",
" with a p-value below 0.05, as decoupler does, and `progeny(top=100)` the 100 most\n",
@@ -302,15 +304,15 @@
"datasets": [
"kang2018"
],
- "executed_at": "2026-10-27T15:23:27+00:00",
+ "executed_at": "2026-10-28T00:18:53+00:00",
"executed_code_sha256": "02276c359f48a48739d2082eb79908d8fe1d9609bd2840eb972779f0205719c8",
"executed_datasets": {
"kang2018": {
"kang2018.cube.zarr": "sha256:a30af3dabe17f21a6c75f41b64a279a1423640b9aa919022cf67897f9cc77a51"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 9.9,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 3.8,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/pool_cells.ipynb b/docs/source/guides/pool_cells.ipynb
index 2a20e7d..70373cf 100644
--- a/docs/source/guides/pool_cells.ipynb
+++ b/docs/source/guides/pool_cells.ipynb
@@ -8,17 +8,17 @@
"(pool-cells)=\n",
"# Pool cells into pseudobulk profiles\n",
"\n",
- "Tests of conditions compare samples, so the cells of every sample and cell type are\n",
- "first pooled into one profile ([Why pseudobulk](../why_pseudobulk.ipynb)). A cube keeps\n",
- "those profiles: for every gene, sums over the cells of each sample and cell type, with\n",
- "the number of cells pooled into each. Because every value is a sum, cell types can be\n",
- "merged and samples pooled later without the cells, and the same sums give what\n",
- "describes the cells: the fraction that express each gene and their mean expression, the\n",
- "values of a dot plot. This page pools the cells of the simulated PBMC\n",
- "study (the `simulated_pbmc` dataset of the [Datasets](../reference/datasets.md) page),\n",
- "looks at the markers of its cell types and at its samples, and merges a rare cell type\n",
- "into its lineage. [The cube and its additive layers](../statistics/cubes.md) defines\n",
- "every layer.\n",
+ "Pool the cells of every sample and cell type into one pseudobulk profile with\n",
+ "{api}`from_cells`, the first step of a test of conditions, which compares samples, not\n",
+ "cells. The cube it returns holds sums over the cells, with the number of cells pooled\n",
+ "into each profile. Because every value is a sum, cell types can be merged and samples\n",
+ "pooled later without the cells, and the same sums give the fraction of the cells that\n",
+ "express each gene and their mean expression, the values of a dot plot. This page pools\n",
+ "the cells of the simulated PBMC study (the `simulated_pbmc` dataset of the\n",
+ "[Datasets](../reference/datasets.md) page), looks at the markers of its cell types and\n",
+ "at its samples, and merges a rare cell type into its lineage. [Why\n",
+ "pseudobulk](../why_pseudobulk.ipynb) says why the tests compare samples, and\n",
+ "[Pseudobulk profiles and cubes](../statistics/cubes.md) defines every layer.\n",
"\n",
"## Samples, cell types and layers\n",
"\n",
@@ -38,7 +38,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Cube with 24 samples x 6 groups x 2,000 features\n",
" layers: counts (uint32), detected (uint32), log_sum (float64),\n",
" log_sum_sq (float64), linear_sum (float64)\n",
@@ -97,22 +97,20 @@
"id": "14996e17",
"metadata": {},
"source": [
- "The cube holds five layers: the `counts`, which the tests read, and four that describe\n",
- "the cells: `detected`, the number of cells with a positive count, and the sums of the\n",
- "cells' log-normalized values (`log_sum`), of their squares (`log_sum_sq`) and of the\n",
- "normalized values themselves (`linear_sum`). Each cell is normalized to 10,000 counts\n",
- "and log-transformed, as scanpy's `normalize_total` and `log1p` do.\n",
+ "The cube holds the `counts`, which the tests read, and four layers that describe the\n",
+ "cells: the number of cells that detect each gene, and the sums of the cells' normalized\n",
+ "values, of their log-normalized values and of the squares of these ([The\n",
+ "layers](../statistics/cubes.md#cube-layers)).\n",
"\n",
"## The markers of each cell type\n",
"\n",
- "A dot plot shows, for chosen genes and cell types, the fraction of the cells that\n",
- "express each gene, as the area of a dot, and their mean expression, as its colour, as\n",
- "scanpy draws it [@wolf2018]. {api}`plot_dotplot` reads both from the cube, pooled over\n",
- "every sample, and gives the values scanpy computes from the cells\n",
- "([Validation](../validation.md#principal-components-and-dot-plots)). The simulation\n",
- "gave each cell type 20 marker genes; here are three of each, with the means of each\n",
- "gene scaled from 0 to 1 over the cell types, as scanpy's `standard_scale=\"var\"` scales\n",
- "them."
+ "{api}`plot_dotplot` draws scanpy's dot plot from the cube [@wolf2018]: for chosen genes\n",
+ "and cell types, the fraction of the cells that express each gene, as the area of a dot,\n",
+ "and their mean expression, as its colour, pooled over every sample. The sums give the\n",
+ "values that scanpy computes from the cells ([The dot plot](../statistics/cubes.md#dot-plot)).\n",
+ "The simulation gave each cell type 20 marker genes; here are three of each, with the\n",
+ "means of each gene scaled from 0 to 1 over the cell types, as scanpy's\n",
+ "`standard_scale=\"var\"` scales them."
]
},
{
@@ -223,11 +221,10 @@
"\n",
"## The samples of each cell type\n",
"\n",
- "DESeq2's PCA plot places the samples of one cell type on the principal components of\n",
- "their 500 most variable genes [@love2014], prepared as for an MDS plot: profiles of\n",
- "fewer than 10 cells are left out, genes are filtered by expression, and the values are\n",
- "log-CPM ({api}`plot_pca`, {api}`principal_components`). It shows at a glance whether\n",
- "the samples group as the design expects."
+ "{api}`plot_pca` draws DESeq2's PCA plot of the samples of one cell type [@love2014],\n",
+ "which shows at a glance whether they group as the design expects. [Samples at a\n",
+ "glance](../statistics/normalization_and_filtering.md#samples-at-a-glance) says which\n",
+ "samples, genes and values {api}`principal_components` takes from a cube."
]
},
{
@@ -351,7 +348,8 @@
"- **Cell types.** Annotate at the level the question needs. {api}`Cube.select` keeps\n",
" some cell types, {api}`Cube.regroup` merges them, and the minimum of cells\n",
" (`min_cells` of {api}`limma_voom`, 10 by default) leaves out the profiles of too few\n",
- " cells, with the reason in the result.\n",
+ " cells, with the reason in the result ([Samples with few cells or\n",
+ " counts](../statistics/normalization_and_filtering.md#samples-with-few-cells)).\n",
"- **Layers.** `layers=\"counts\"` keeps only what the tests read: 4 bytes per sample,\n",
" cell type and gene, against 32 for all five layers, which matters for atlases of\n",
" thousands of samples. The dot plot needs `detected` and `log_sum`, and\n",
@@ -370,7 +368,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:23:31+00:00",
+ "executed_at": "2026-10-28T00:18:17+00:00",
"executed_code_sha256": "a3ce492792f0724a29e182158218c75a1d51d25c8b1048f4e92d707eddc57bed",
"executed_datasets": {
"simulated_pbmc": {
@@ -378,8 +376,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 3.2,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 2.9,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/repeated_measures.ipynb b/docs/source/guides/repeated_measures.ipynb
index 5480953..727fb4f 100644
--- a/docs/source/guides/repeated_measures.ipynb
+++ b/docs/source/guides/repeated_measures.ipynb
@@ -8,49 +8,54 @@
"(repeated-measures)=\n",
"# Repeated measures and donors\n",
"\n",
- "When a study samples each donor more than once, before and after a treatment, at\n",
- "several time points or in several tissues, the samples of a donor are correlated. Two\n",
- "models allow for it: a fixed effect for each donor, which compares each donor's samples\n",
- "with each other, and a random effect for the donor, which dream fits by a mixed model\n",
- "for every gene [@hoffman2021]. This page compares the two on the Kang data\n",
- "[@kang2018], tests a variable of the donors and several time points on a simulated\n",
- "study whose truth is known, and measures how much of each gene's variation the donors\n",
- "explain. [Mixed models, REML and dream](../statistics/mixed_models.md) explains the\n",
- "models.\n",
+ "Test which genes change when donors give several samples each, at several time points\n",
+ "or in several tissues, with {api}`dream_voom`. It fits every gene with a linear mixed\n",
+ "model that gives each donor a random intercept, as variancePartition's dream does\n",
+ "[@hoffman2021], and so tests both the changes within donors and the variables of the\n",
+ "donors, such as disease. This page tests a variable of the donors and several time\n",
+ "points on a simulated study whose truth is known, measures how much of each gene's\n",
+ "variation the donors explain in the Kang data [@kang2018], and compares dream with a\n",
+ "fixed donor effect. [Mixed models and dream](../statistics/mixed_models.md) explains\n",
+ "the models.\n",
"\n",
- "## A fixed or a random donor effect\n",
+ "## A variable of the donors\n",
"\n",
- "In the Kang data every donor gives one control and one stimulated sample. The paired\n",
- "design `~ donor + condition` and the mixed model `~ condition` with `(1 | donor)` both\n",
- "compare the stimulated samples with the controls of the same donors. By default each\n",
- "keeps the genes with enough counts for its own fixed effects, and the paired design\n",
- "asks for counts in fewer samples than the condition alone, so that the two would test\n",
- "different genes, 5,125 against 3,651 in these monocytes ([Normalization, filtering and\n",
- "sample quality](normalization_and_quality.ipynb#which-genes-to-test)).\n",
- "`expression_filter={\"sample_groups\": \"condition\"}` filters both by the condition\n",
- "instead, so that they test the same genes and their results compare gene for gene."
+ "A study of patients and healthy donors, each sampled at three time points, asks two\n",
+ "kinds of question: what changes over time within donors, and how patients differ from\n",
+ "healthy donors. The simulation below has three patients and three healthy donors, each\n",
+ "sampled at 0, 6 and 24 hours, in one cell type: time changes 400 of 4,000 genes, the\n",
+ "disease 150 others, and every donor shifts every gene a little in all its samples. A\n",
+ "paired design cannot hold the disease, which is constant within each donor, and\n",
+ "{api}`Design.check` says why ([A paired design](../statistics/linear_models.md#paired-design))."
]
},
{
"cell_type": "code",
"execution_count": 1,
- "id": "06e77e7a",
+ "id": "97b13b7f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "fixed donor effect 3,651 genes tested 2,536 called\n",
- "random donor effect 3,651 genes tested 2,542 called\n",
- "Of the 3,651 genes both test, the log2 fold changes correlate at 0.999 and 2,456 are called by both.\n"
+ "CytePool 0.1.dev70+g194507939\n",
+ "Design check: 18 samples, 9 columns, rank 8, 10 residual degrees of freedom.\n",
+ "2 errors:\n",
+ " 1. [rank_deficient] the design matrix has 9 columns but rank 8, so 1\n",
+ " coefficient is not estimable. Aliased columns: diseasepatient = donorP1 +\n",
+ " donorP2 + donorP3. The columns in each relation cannot be estimated\n",
+ " separately; remove or recode the variables that make up these columns (see\n",
+ " the other findings), or drop terms from the formula\n",
+ " 2. [confounded] every level of donor holds a single level of disease (disease\n",
+ " 'healthy': donor 'H1', 'H2' and 'H3'; and disease 'patient': donor 'P1',\n",
+ " 'P2' and 'P3'), so the effect of disease cannot be separated from the donor\n",
+ " effects. Remove donor from the formula; if donor groups repeated samples\n",
+ " (donors, batches), the right model has it as a random effect, random='(1 |\n",
+ " donor)'; fit_mixed_model estimates this random intercept by REML;\n",
+ " fit_linear_model fits it with a known correlation, given block= (the donor\n",
+ " of each sample) and correlation=; or pool the samples of each donor with\n",
+ " Cube.collapse_samples('donor')\n"
]
}
],
@@ -73,92 +78,8 @@
" plot_variance_fractions,\n",
")\n",
"\n",
- "kang = read_cube(\"kang2018.cube.zarr\")\n",
"print(f\"CytePool {cytepool.__version__}\")\n",
- "monocytes = \"CD14+ Monocytes\"\n",
- "levels = {\"condition\": [\"ctrl\", \"stim\"]}\n",
- "contrasts = {\"stim_vs_ctrl\": \"conditionstim\"}\n",
- "paired = Design(kang.samples, \"~ donor + condition\", levels=levels, contrasts=contrasts)\n",
- "mixed = Design(\n",
- " kang.samples,\n",
- " \"~ condition\",\n",
- " random=\"(1 | donor)\",\n",
- " levels=levels,\n",
- " contrasts=contrasts,\n",
- ")\n",
- "by_condition = {\"sample_groups\": \"condition\"}\n",
- "fixed = limma_voom(\n",
- " kang, paired, groups=monocytes, expression_filter=by_condition\n",
- ").table(monocytes, \"stim_vs_ctrl\")\n",
- "random = dream_voom(\n",
- " kang, mixed, groups=monocytes, expression_filter=by_condition\n",
- ").table(monocytes, \"stim_vs_ctrl\")\n",
"\n",
- "for name, table in ((\"fixed donor effect\", fixed), (\"random donor effect\", random)):\n",
- " called = np.count_nonzero(table[\"adjusted_p_value\"] < 0.05)\n",
- " print(f\"{name:<20} {table['feature_id'].size:>6,} genes tested {called:>6,} called\")\n",
- "shared, i, j = np.intersect1d(\n",
- " fixed[\"feature_id\"], random[\"feature_id\"], return_indices=True\n",
- ")\n",
- "correlation = np.corrcoef(fixed[\"log2_fold_change\"][i], random[\"log2_fold_change\"][j])\n",
- "both = (fixed[\"adjusted_p_value\"][i] < 0.05) & (random[\"adjusted_p_value\"][j] < 0.05)\n",
- "print(\n",
- " f\"Of the {shared.size:,} genes both test, the log2 fold changes correlate at \"\n",
- " f\"{correlation[0, 1]:.3f} and {both.sum():,} are called by both.\"\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f26ba559",
- "metadata": {},
- "source": [
- "Both models find the same response: on the same genes, their log2 fold changes agree\n",
- "almost exactly, and nearly every gene that one calls the other calls too. When every\n",
- "donor gives a sample in every condition and the question is within donors, the paired\n",
- "design is the simpler of the two.\n",
- "\n",
- "## A variable of the donors\n",
- "\n",
- "A study of patients and healthy donors, each sampled at three time points, asks two\n",
- "kinds of question: what changes over time within donors, and how patients differ from\n",
- "healthy donors. The simulation below has three patients and three healthy donors, each\n",
- "sampled at 0, 6 and 24 hours, in one cell type: time changes 400 of 4,000 genes, the\n",
- "disease 150 others, and every donor shifts every gene a little in all its samples. A\n",
- "paired design cannot hold the disease, which is constant within each donor, and\n",
- "{api}`Design.check` says why."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "97b13b7f",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Design check: 18 samples, 9 columns, rank 8, 10 residual degrees of freedom.\n",
- "2 errors:\n",
- " 1. [rank_deficient] the design matrix has 9 columns but rank 8, so 1\n",
- " coefficient is not estimable. Aliased columns: diseasepatient = donorP1 +\n",
- " donorP2 + donorP3. The columns in each relation cannot be estimated\n",
- " separately; remove or recode the variables behind them (see the other\n",
- " findings), or drop terms from the formula\n",
- " 2. [confounded] every level of donor holds a single level of disease (disease\n",
- " 'healthy': donor 'H1', 'H2' and 'H3'; disease 'patient': donor 'P1', 'P2'\n",
- " and 'P3'), so the effect of disease cannot be separated from the donor\n",
- " effects. Remove donor from the formula; if donor groups repeated samples\n",
- " (donors, batches), the right model has it as a random effect, random='(1 |\n",
- " donor)'; fit_mixed_model estimates this random intercept by REML;\n",
- " fit_linear_model fits it with a known correlation, given block= (the donor\n",
- " of each sample) and correlation=; or pool the samples of each donor with\n",
- " Cube.collapse_samples('donor')\n"
- ]
- }
- ],
- "source": [
"rng = np.random.default_rng(20261026)\n",
"n_genes, times = 4_000, [\"0h\", \"6h\", \"24h\"]\n",
"donors = [\"P1\", \"P2\", \"P3\", \"H1\", \"H2\", \"H3\"]\n",
@@ -203,13 +124,14 @@
"The random donor effect keeps the disease testable. {api}`dream_voom` tests each\n",
"contrast, and `f_tests` tests several at once: here whether a gene changes at any time\n",
"point, by one F-test instead of a test per time point. With fewer than 20 samples it\n",
- "uses Kenward and Roger's degrees of freedom, which allow for variance components\n",
- "estimated from few donors [@kenward1997]. The simulation knows which genes changed."
+ "uses Kenward-Roger's degrees of freedom, which allow for variance components estimated\n",
+ "from few donors ([dream](../statistics/mixed_models.md#dream)). The simulation knows\n",
+ "which genes changed."
]
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 2,
"id": "fd3055f8",
"metadata": {},
"outputs": [
@@ -267,15 +189,181 @@
"false, where an adjustment at 5% allows for about 5 on average. With three donors in\n",
"each group, a difference between patients and healthy donors is measured against the\n",
"variation between six donors, which every gene's model estimates from those six alone.\n",
- "The p-values of the 3,850 genes that the disease leaves unchanged should be uniform.\n",
- "Pooling each donor's samples into one profile ({api}`Cube.collapse_samples`) gives\n",
- "six profiles, whose residuals hold the variation between donors, and\n",
- "{api}`limma_voom` compares them."
+ "Pooling each donor's samples first holds the error rate, as the choices below show.\n",
+ "\n",
+ "## What drives the variation between samples\n",
+ "\n",
+ "{api}`variance_partitioning` fits every gene with a mixed model of every variable of the\n",
+ "design and reports the share of its variance that each explains, as variancePartition\n",
+ "does [@hoffman2016] ([Variance partitioning](../statistics/mixed_models.md#variance-fractions)).\n",
+ "It shows which variables dominate, and which genes follow each. It runs on every cell\n",
+ "type of a cube, after the same steps as {api}`dream_voom`; here on the CD14+ monocytes of\n",
+ "the Kang data, whose donors each gave a control and a stimulated sample."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "893fa455",
+ "metadata": {
+ "figure_descriptions": [
+ "Two panels of variance partitioning in the CD14+ monocytes. Left, violins of the percentage of each gene's variance that the condition, the donor and the residuals explain, with a narrow box plot inside each: the condition spreads from 0 to 100% with a median near 43%, the donor lies mostly below 25% with a median near 8% and a tail of genes up to 97%, and the residuals spread from 0 to 100% with a median near 34%. Right, stacked bars for eight genes: ISG15, CD14 and S100A8 are mostly condition; RPS26 is about 38% condition and 58% donor; HBB about 54% donor and 46% residuals; HLA-DQA1, HLA-DRA and LYZ are mostly donor."
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "median share of a gene's variance: donor 8%, condition 43%, residuals 34%\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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G1+Tf5sxYwYuXLiA4cOH49KlS7hx4wY2btyIrl276tpMnDgRO3fuxPLlyxEREYFly5bhp59+eul+XVxc0LJlS+zevTvfuoyMDIwePRrff/+9rhdTmzZtsHz5cty6dQurV6/WG+h49+7d8PDwQOXKlQt8vkRk/FhcKWTPRl3/8MMPERUVhYyMDGzatAn//e9/dd0/lUolevTogdGjR6Np06YSJyaiwuDr64v09HQEBwfrljk5OeG3335DnTp10LlzZ8TFxcHExAQtWrSAt7c33NzcJExMRFRyqFQqDB8+HBqNJt/A30OGDMGoUaMwatQotGnTBuHh4Zg+fbreuCJKpRJ2dnb5xrhSKBSws7PT3WL0tvuSyWSws7PT9XqwsrLCqVOn4OzsjD59+qBnz56QyWTw9vYG8HT8kdDQUDRo0ABDhw5F69atsXr1akybNg3Ozs7PvQav2iY5ORlTpkzB119/DXd3dwBPizzdu3fH+PHjodVqAQB169ZFz549MXToUPTs2RP169dHQECA7jimpqawtbXVPV60aBHc3NzQtWtXfPjhh3Bzc0O/fv1QpkyZF25jZmamd82e/Qzt7e11j93d3XH69GmkpKSgW7du6NGjB06cOIGlS5fq2rRs2RIBAQFYvXo1OnTogJMnT2LevHl6x3qeSZMmYd26dfluIVq8eDE+/PBDNGrUSLfsv//9L+RyOTp06AA3NzdMmDABwNMi1Pr16/H555+/9FhEVHrIxIv6FtJbu3HjBgYPHozQ0FCYmJigVatWWLly5SsHSSMi4+bp6QlHR8d8g1bn5uYiOzsbZmZmEiUjIiJ6tfnz52PLli26gdVLsp49e6JXr17o27fvW20fGBiIdevWvXUvKCIqeVhcKULJyclQKBQcS4GolFi9ejXGjRuH+Ph4WFtbSx2HiIjojZSm4goRUWHjvGFF6N/dHYmoZOvbty+aN2/OwgoRERml592uQ0REr4c9V4iIiIiIiIiICoAD2hIRERERERERFQCLK0REREREREREBcDiChERERERERFRAbC4QkRERERERERUACyuEBEREREREREVAIsrREREREREREQFwOIKEREREREREVEBsLhCRERERERERFQALK4QERERERERERUAiytERERERERERAXA4goRERERERERUQGwuEJEREREREREVAD/B+41Av2l5gdkAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "kang = read_cube(\"kang2018.cube.zarr\")\n",
+ "monocytes = \"CD14+ Monocytes\"\n",
+ "partition_design = Design(\n",
+ " kang.samples, \"~ 1\", random=[\"(1 | donor)\", \"(1 | condition)\"]\n",
+ ")\n",
+ "partition = variance_partitioning(kang, partition_design, groups=monocytes)\n",
+ "chosen = [\"ISG15\", \"CD14\", \"S100A8\", \"RPS26\", \"HBB\", \"HLA-DQA1\", \"HLA-DRA\", \"LYZ\"]\n",
+ "\n",
+ "fractions = partition.table(monocytes)\n",
+ "shares = [f\"{name} {np.median(fractions[name]):.0%}\" for name in partition.variables]\n",
+ "print(\"median share of a gene's variance:\", \", \".join(shares))\n",
+ "\n",
+ "figure, (left, right) = plt.subplots(1, 2, figsize=(11, 4.5), layout=\"constrained\")\n",
+ "plot_variance_fractions(partition, ax=left)\n",
+ "plot_feature_variance_fractions(partition, chosen, ax=right)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8e408f64",
+ "metadata": {},
+ "source": [
+ "The stimulation explains much of the variance of most genes, and the donors a little\n",
+ "of most, but the genes differ widely. ISG15 and CD14 vary almost only with the\n",
+ "stimulation; the HLA class II genes HLA-DQA1 and HLA-DRA, which differ between people,\n",
+ "and LYZ vary mostly between donors, and RPS26 with both. HBB, whose reads come from\n",
+ "contaminating red cells, varies between donors and between the two samples of each\n",
+ "donor, which the residuals hold.\n",
+ "\n",
+ "## Choices\n",
+ "\n",
+ "- **A fixed or a random donor effect.** {api}`dream_voom` by default for a variable of\n",
+ " the donors, such as disease, sex or age, and for donors with unequal numbers of\n",
+ " samples. With every donor in every condition and a question within donors, the\n",
+ " paired design `~ donor + condition` and {api}`limma_voom` are simpler and find nearly\n",
+ " the same genes, as below ([A fixed or a random donor\n",
+ " effect](../statistics/mixed_models.md#fixed-or-random-donor)).\n",
+ "- **Few donors, a question between donors.** With three donors in each group,\n",
+ " dream_voom's test between donors called too many genes above. When every donor was\n",
+ " sampled alike, compare the profiles pooled by donor ({api}`Cube.collapse_samples`)\n",
+ " with {api}`limma_voom`, as below, and test the changes within donors with\n",
+ " {api}`dream_voom`.\n",
+ "- **Degrees of freedom.** `df_method=\"adaptive\"`, the default, uses Kenward-Roger's\n",
+ " below 20 samples and Satterthwaite's from 20; `\"kenward_roger\"` and `\"satterthwaite\"`\n",
+ " fix one ([dream](../statistics/mixed_models.md#dream)).\n",
+ "- **Several time points or groups.** `f_tests` asks whether anything changes among\n",
+ " them; the contrasts then say what changes when.\n",
+ "- **What to adjust for.** A variable that explains much of the variance of many genes\n",
+ " and is not the question, such as a batch, is worth a term in the design.\n",
+ " [Normalization, filtering and sample quality](normalization_and_quality.ipynb) shows\n",
+ " how to look for outlying samples.\n",
+ "\n",
+ "### A fixed donor effect\n",
+ "\n",
+ "In the Kang data every donor gives one control and one stimulated sample. The paired\n",
+ "design `~ donor + condition` and the mixed model `~ condition` with `(1 | donor)` both\n",
+ "compare the stimulated samples with the controls of the same donors. By default each\n",
+ "keeps the genes with enough counts for its own fixed effects, and the paired design\n",
+ "asks for counts in fewer samples than the condition alone, so that the two would test\n",
+ "different genes, 5,125 against 3,651 in these monocytes ([Normalization, filtering and\n",
+ "sample quality](normalization_and_quality.ipynb#which-genes-to-test)).\n",
+ "`expression_filter={\"sample_groups\": \"condition\"}` filters both by the condition\n",
+ "instead, so that they test the same genes and their results compare gene for gene."
]
},
{
"cell_type": "code",
"execution_count": 4,
+ "id": "06e77e7a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "fixed donor effect 3,651 genes tested 2,536 called\n",
+ "random donor effect 3,651 genes tested 2,542 called\n",
+ "Of the 3,651 genes both test, the log2 fold changes correlate at 0.999 and 2,456 are called by both.\n"
+ ]
+ }
+ ],
+ "source": [
+ "levels = {\"condition\": [\"ctrl\", \"stim\"]}\n",
+ "contrasts = {\"stim_vs_ctrl\": \"conditionstim\"}\n",
+ "paired = Design(kang.samples, \"~ donor + condition\", levels=levels, contrasts=contrasts)\n",
+ "mixed = Design(\n",
+ " kang.samples,\n",
+ " \"~ condition\",\n",
+ " random=\"(1 | donor)\",\n",
+ " levels=levels,\n",
+ " contrasts=contrasts,\n",
+ ")\n",
+ "by_condition = {\"sample_groups\": \"condition\"}\n",
+ "fixed = limma_voom(\n",
+ " kang, paired, groups=monocytes, expression_filter=by_condition\n",
+ ").table(monocytes, \"stim_vs_ctrl\")\n",
+ "random = dream_voom(\n",
+ " kang, mixed, groups=monocytes, expression_filter=by_condition\n",
+ ").table(monocytes, \"stim_vs_ctrl\")\n",
+ "\n",
+ "for name, table in ((\"fixed donor effect\", fixed), (\"random donor effect\", random)):\n",
+ " called = np.count_nonzero(table[\"adjusted_p_value\"] < 0.05)\n",
+ " print(f\"{name:<20} {table['feature_id'].size:>6,} genes tested {called:>6,} called\")\n",
+ "shared, i, j = np.intersect1d(\n",
+ " fixed[\"feature_id\"], random[\"feature_id\"], return_indices=True\n",
+ ")\n",
+ "correlation = np.corrcoef(fixed[\"log2_fold_change\"][i], random[\"log2_fold_change\"][j])\n",
+ "both = (fixed[\"adjusted_p_value\"][i] < 0.05) & (random[\"adjusted_p_value\"][j] < 0.05)\n",
+ "print(\n",
+ " f\"Of the {shared.size:,} genes both test, the log2 fold changes correlate at \"\n",
+ " f\"{correlation[0, 1]:.3f} and {both.sum():,} are called by both.\"\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f26ba559",
+ "metadata": {},
+ "source": [
+ "Both models find the same response: on the same genes, their log2 fold changes agree\n",
+ "almost exactly, and nearly every gene that one calls the other calls too.\n",
+ "\n",
+ "### Few donors, a question between donors\n",
+ "\n",
+ "Pooling each donor's samples into one profile ({api}`Cube.collapse_samples`) gives the\n",
+ "simulated study six profiles, whose residuals hold the variation between donors, and\n",
+ "{api}`limma_voom` compares them. The p-values of the 3,850 genes that the disease\n",
+ "leaves unchanged should be uniform."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
"id": "2695cd04",
"metadata": {
"figure_descriptions": [
@@ -349,99 +437,20 @@
"each. {api}`variance_partitioning` runs it on every cell type of a cube, after the\n",
"same steps as {api}`dream_voom`."
]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "id": "893fa455",
- "metadata": {
- "figure_descriptions": [
- "Two panels of variance partitioning in the CD14+ monocytes. Left, violins of the percentage of each gene's variance that the condition, the donor and the residuals explain, with a narrow box plot inside each: the condition spreads from 0 to 100% with a median near 43%, the donor lies mostly below 25% with a median near 8% and a tail of genes up to 97%, and the residuals spread from 0 to 100% with a median near 34%. Right, stacked bars for eight genes: ISG15, CD14 and S100A8 are mostly condition; RPS26 is about 38% condition and 58% donor; HBB about 54% donor and 46% residuals; HLA-DQA1, HLA-DRA and LYZ are mostly donor."
- ]
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "median share of a gene's variance: donor 8%, condition 43%, residuals 34%\n"
- ]
- },
- {
- "data": {
- "image/png": "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G1+Tf5sxYwYuXLiA4cOH49KlS7hx4wY2btyIrl276tpMnDgRO3fuxPLlyxEREYFly5bhp59+eul+XVxc0LJlS+zevTvfuoyMDIwePRrff/+9rhdTmzZtsHz5cty6dQurV6/WG+h49+7d8PDwQOXKlQt8vkRk/FhcKWTPRl3/8MMPERUVhYyMDGzatAn//e9/dd0/lUolevTogdGjR6Np06YSJyaiwuDr64v09HQEBwfrljk5OeG3335DnTp10LlzZ8TFxcHExAQtWrSAt7c33NzcJExMRFRyqFQqDB8+HBqNJt/A30OGDMGoUaMwatQotGnTBuHh4Zg+fbreuCJKpRJ2dnb5xrhSKBSws7PT3WL0tvuSyWSws7PT9XqwsrLCqVOn4OzsjD59+qBnz56QyWTw9vYG8HT8kdDQUDRo0ABDhw5F69atsXr1akybNg3Ozs7PvQav2iY5ORlTpkzB119/DXd3dwBPizzdu3fH+PHjodVqAQB169ZFz549MXToUPTs2RP169dHQECA7jimpqawtbXVPV60aBHc3NzQtWtXfPjhh3Bzc0O/fv1QpkyZF25jZmamd82e/Qzt7e11j93d3XH69GmkpKSgW7du6NGjB06cOIGlS5fq2rRs2RIBAQFYvXo1OnTogJMnT2LevHl6x3qeSZMmYd26dfluIVq8eDE+/PBDNGrUSLfsv//9L+RyOTp06AA3NzdMmDABwNMi1Pr16/H555+/9FhEVHrIxIv6FtJbu3HjBgYPHozQ0FCYmJigVatWWLly5SsHSSMi4+bp6QlHR8d8g1bn5uYiOzsbZmZmEiUjIiJ6tfnz52PLli26gdVLsp49e6JXr17o27fvW20fGBiIdevWvXUvKCIqeVhcKULJyclQKBQcS4GolFi9ejXGjRuH+Ph4WFtbSx2HiIjojZSm4goRUWHjvGFF6N/dHYmoZOvbty+aN2/OwgoRERml592uQ0REr4c9V4iIiIiIiIiICoAD2hIRERERERERFQCLK0REREREREREBcDiChERERERERFRAbC4QkRERERERERUACyuEBEREREREREVAIsrREREREREREQFwOIKEREREREREVEBsLhCRERERERERFQALK4QERERERERERUAiytERERERERERAXA4goRERERERERUQGwuEJEREREREREVAD/B+41Av2l5gdkAAAAAElFTkSuQmCC",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "partition_design = Design(\n",
- " kang.samples, \"~ 1\", random=[\"(1 | donor)\", \"(1 | condition)\"]\n",
- ")\n",
- "partition = variance_partitioning(kang, partition_design, groups=monocytes)\n",
- "chosen = [\"ISG15\", \"CD14\", \"S100A8\", \"RPS26\", \"HBB\", \"HLA-DQA1\", \"HLA-DRA\", \"LYZ\"]\n",
- "\n",
- "fractions = partition.table(monocytes)\n",
- "shares = [f\"{name} {np.median(fractions[name]):.0%}\" for name in partition.variables]\n",
- "print(\"median share of a gene's variance:\", \", \".join(shares))\n",
- "\n",
- "figure, (left, right) = plt.subplots(1, 2, figsize=(11, 4.5), layout=\"constrained\")\n",
- "plot_variance_fractions(partition, ax=left)\n",
- "plot_feature_variance_fractions(partition, chosen, ax=right)\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "8e408f64",
- "metadata": {},
- "source": [
- "The stimulation explains much of the variance of most genes, and the donors a little\n",
- "of most, but the genes differ widely. ISG15 and CD14 vary almost only with the\n",
- "stimulation; the HLA class II genes HLA-DQA1 and HLA-DRA, which differ between people,\n",
- "and LYZ vary mostly between donors, and RPS26 with both. HBB, whose reads come from\n",
- "contaminating red cells, varies between donors and between the two samples of each\n",
- "donor, which the residuals hold.\n",
- "\n",
- "## Choices\n",
- "\n",
- "- **A fixed or a random donor effect.** With every donor in every condition and a\n",
- " question within donors, the paired design and {api}`limma_voom` are the simplest. A\n",
- " variable of the donors, such as disease, sex or age, needs the random effect and\n",
- " {api}`dream_voom`, and so do donors with unequal numbers of samples.\n",
- "- **Few donors, a question between donors.** With three donors in each group,\n",
- " dream_voom's test between donors called too many genes above. When every donor was\n",
- " sampled alike, compare the profiles pooled by donor ({api}`Cube.collapse_samples`)\n",
- " with {api}`limma_voom` instead, and test the changes within donors with\n",
- " {api}`dream_voom`.\n",
- "- **Degrees of freedom.** `df_method=\"adaptive\"`, the default, uses Kenward and\n",
- " Roger's below 20 samples and Satterthwaite's from 20; `\"kenward_roger\"` and\n",
- " `\"satterthwaite\"` fix one ([Mixed models, REML and dream](../statistics/mixed_models.md)).\n",
- "- **Several time points or groups.** `f_tests` asks whether anything changes among\n",
- " them; the contrasts then say what changes when.\n",
- "- **What to adjust for.** A variable that explains much of the variance of many\n",
- " genes and is not the question, such as a batch, is worth a term in the design.\n",
- " [Normalization, filtering and sample quality](normalization_and_quality.ipynb)\n",
- " shows how to look for outlying samples."
- ]
}
],
"metadata": {
"datasets": [
"kang2018"
],
- "executed_at": "2026-10-27T15:23:34+00:00",
- "executed_code_sha256": "10cc8beac825f403ee5850b95a78213b817ff1fbf7d2f770da7ad48090039480",
+ "executed_at": "2026-10-28T00:18:41+00:00",
+ "executed_code_sha256": "3c52bc712125c6e7957b94f7b13282f90972aa23014bdbdfbdea76a0c802f940",
"executed_datasets": {
"kang2018": {
"kang2018.cube.zarr": "sha256:a30af3dabe17f21a6c75f41b64a279a1423640b9aa919022cf67897f9cc77a51"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
+ "executed_with": "0.1.dev70+g194507939",
"execution_seconds": 2.7,
"kernelspec": {
"display_name": "Python 3",
diff --git a/docs/source/guides/replication_across_studies.ipynb b/docs/source/guides/replication_across_studies.ipynb
index 9aa849c..340f4a4 100644
--- a/docs/source/guides/replication_across_studies.ipynb
+++ b/docs/source/guides/replication_across_studies.ipynb
@@ -8,16 +8,19 @@
"(replication-across-studies)=\n",
"# Replication across studies\n",
"\n",
- "Most genes that one study finds are not found by the next: of the genes that one\n",
- "dataset of Alzheimer's disease found differentially expressed, over 85% were not\n",
- "found in any of 16 others [@nakatsuka2025]. Some are changes the other studies were\n",
- "too small to detect, others belong to one study alone. This page measures how well\n",
- "six cohorts replicate each other, and which of their genes replicate: the genes found\n",
- "in k cohorts, π₁ between cohorts, combined p-values against the p-value that a change\n",
- "replicates, SumRank [@nakatsuka2025], and the prediction of cases in a cohort left\n",
- "out. [Meta-analysis](../statistics/meta_analysis.md) explains the statistics.\n",
+ "Find the changes that replicate across studies with {api}`sumrank`, which ranks the\n",
+ "genes within each study and sums each gene's ranks across the studies, so that a gene\n",
+ "must rank high in study after study [@nakatsuka2025]. Most genes that one study finds\n",
+ "are not found by the others ([Why findings fail to\n",
+ "replicate](../statistics/aligning_studies.md#why-findings-fail-to-replicate)): some\n",
+ "changes are too small for the other studies to detect, and others belong to one study\n",
+ "alone. This page finds the genes that replicate in six cohorts with SumRank, checks how\n",
+ "well they predict the cases of a cohort left out, and then, under Choices, measures how\n",
+ "well the cohorts replicate each other: the genes found in k cohorts, π₁ between\n",
+ "cohorts, and combined p-values against the p-value that a change replicates.\n",
+ "[Replication](../statistics/replication.md) explains the statistics.\n",
"\n",
- "The cohorts are the six simulated ones of [Combine studies](combine_studies.ipynb),\n",
+ "The cohorts are the six simulated ones of [Align and combine studies](combine_studies.ipynb),\n",
"aligned and analysed as there: {api}`analyse_each_study` tests each by\n",
"{api}`limma_voom` on its own, and the functions below take the {api}`StudyResults` it\n",
"returns. The simulation knows which monocyte genes the disease changes in every\n",
@@ -34,7 +37,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb, numpy 2.5.4\n",
+ "CytePool 0.1.dev70+g194507939, numpy 2.5.4\n",
"monocyte genes, adjusted p < 0.05 every cohort one cohort unchanged\n",
"cohort_1 (60 donors) 62 10 1\n",
"cohort_2 (32 donors) 38 10 4\n",
@@ -132,226 +135,20 @@
"Each cohort finds 17 to 62 of the 100 genes that the disease changes in every cohort,\n",
"the most in the largest, and 2 to 10 of the 10 genes it changes alone.\n",
"\n",
- "## Genes found in k cohorts\n",
- "\n",
- "{api}`replication` reads one cell type and contrast of every cohort, lined up gene by\n",
- "gene, and counts the cohorts that find each gene (adjusted p-value below 0.05) and in\n",
- "which direction. {api}`plot_replicated_genes` draws how many genes each number of\n",
- "cohorts finds, as the paper that introduced SumRank counts them [@nakatsuka2025]."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "779274c0",
- "metadata": {
- "figure_descriptions": [
- "Bars of the monocyte genes found at an adjusted p-value below 0.05 by exactly k of the six cohorts: 73 by one, 32 by two, 15 by three, 9 by four, 4 by five and none by all six. Every gene is found in one direction by the cohorts that find it; no bar has a part for genes found up in some cohorts and down in others."
- ]
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "monocyte genes found: 133\n",
- " by one cohort only: 73\n",
- " changed in every cohort: 22\n",
- " changed in that cohort alone: 43\n",
- " unchanged: 8\n"
- ]
- },
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAtsAAAGbCAYAAAAVwFxZAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjIsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvgI3uAAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAX01JREFUeJzt3XdYFFfbBvB7KaIgRQ2KIMWCURFsGBVBEcXEEnvBWLEbu8Yoid0YjLEFC2o0xgSx997BFsWGRsACFlApImUVlLbz/eHrfG5oy7LLsnr/rovr3Tkzc+aeWd74OJ45IxEEQQAREREREamcjqYDEBERERF9rFhsExERERGpCYttIiIiIiI1YbFNRERERKQmLLaJiIiIiNSExTYRERERkZqw2CYiIiIiUhMW20REREREasJim4iISp0dO3Zg1qxZmo5BRFRsepoOQFSSpFIpAgMDcf36dWRlZaFmzZro3r076tevDwB48eIFxowZAwCQSCQwMjJCtWrV0KpVK7Rr1w46OvJ/PxUEAadOncKWLVtgbGyMlStXFnj877//Hg8fPsTy5cthbW2tnpPUAg8fPsT333+Pv/76C4aGhpqOQyqg6u80PDwcZ8+eVUEyIiLN4p1t+mRcvHgRtWrVwtatW9GgQQO0bdsWOTk5GDRoEPz9/QEAaWlp2L17NxwdHdG3b1+0bdsWMpkMw4YNQ5MmTRAdHS3XZ/369fHLL78gPj4eR48eLfD4q1atws6dO7F7926kpqaq7Ty1QVJSEnbv3o3MzExNRyEV4XdKRJQ33tmmT0JSUhK6du2KTp06YfPmzXLr5syZg9jYWLm21q1bw93dXVyeMWMGPDw80KtXL4SEhIjthw4dQvXq1TF37lw8ePAg3+PfunULixYtgr+/P7p06VJg1rt372LmzJlYuHAhtm/fjsjISNSpUweTJk1CXFwc1q1bh9jYWLRo0QKjRo2Su9uelpaG9evX4/r16zAxMUHv3r3Rpk2bXH2vWLECmzZtQlRUFGrXro2JEyfCyMhI4X4AIDs7G9u2bUNQUBAMDAzg5eUFNzc3yGQyDB06FEOGDJG7hsnJyRg5ciQmTpyIOXPmAAAGDx4MfX19uLq6YtKkScjOzkZgYCDOnj0LAwMDtG7dGv369ZM77vHjx3Hw4EG8efMGLi4uGDJkCHR1dQu8poVlBoCUlBQMHz4cAKCvr4/q1avD29sb9vb2Ch9fkfx5kclkGDJkCIYNG4bWrVuL7ampqRg+fDjmzp0LBwcHpc+9oPMGiv978+LFC0yfPh2A/Hf61VdfYebMmVi2bBnWrFmDx48fY9asWXBwcMCDBw/w+++/4+nTp7Czs8Po0aNhY2NT6LkQEWkb3tmmT8LmzZuRmpqKxYsX51onkUhgaWlZ4P4mJib46aefcPXqVVy7dk1sr169eqHHTktLg5eXF1atWoWqVasWun1iYiJ2796Njh07omzZsnB1dYW/vz+++uordOjQAebm5mjRogXmzZuHn376SdwvOzsbrVu3xl9//QU3NzdUqFABX375JTZt2pSrbw8PD+jp6aFVq1bYunUrevbsWaR+srOz0alTJ/zwww+oU6cO6tevj1mzZuHs2bPQ0dGBRCLBsmXL5M4rMDAQ58+fh5OTE7788ksAQM+ePeHl5YUWLVogJycHX3/9NdasWYPmzZujYcOGmD9/vlgAA8DWrVvRp08fWFtbo1WrVggJCcHo0aMLvaaFZQaAcuXKwcvLC15eXvj6668hlUrRsGFD/PvvvwodX5H8+dHR0UFGRgZWr14t175r1y6cOXMG9vb2Sp97Yeetit8bIyOjPL/T9/u1bt0a5cuXR8+ePVG5cmXcuXMHjRs3xrNnz9CuXTtERESgYcOGePToUaHnQ0SkdQSiT4CXl5dQs2bNQrd79OiRAEA4e/ZsrnXJyckCAGHDhg251s2ZMyff/ocMGSKMGDFCEARBuHr1qgBA+Pfff/PNcP78eQGAcPDgQbFt3bp1AgDh5MmTYtvSpUuFzz//XFzesGGDYGxsLCQlJYltixYtEszNzYU3b97I9X3gwAFxm0uXLgkAhMTERIX72bhxo1CuXDkhJiZG3EYmkwkvX74UBEEQLl68KOjp6QmxsbHi+saNGwvTpk2Tuw7Jycni+s2bNwu2trbiMQRBEKKjowWJRCJEREQIgiAI/fv3F7799lu56/XixYt8r+WHCsucl6FDhwre3t7ickHHVyR/Qfbv3y+ULVtWSE1NFdvatGkjjBo1qtBjF6Sw81bV701e3+n7/QIDA+Uyde7cWejcubNcHhcXF2HAgAFi25w5c4SWLVsWen5ERKUd72zTJyEtLQ2VKlUqVh/vh1mkpaUpvE9gYCAuXryI5cuXF/l4H/4zv52dXZ5tz58/F5cvXLgADw8PVKhQQWzr3bs3Xrx4gfv378v13apVK/FzzZo1AQDPnj1TuJ8TJ07Aw8MD1apVE7eRSCSoWLEiAMDFxQW1a9fGX3/9BQC4ffs2bty4gaFDh+Z7vseOHROHU/Tt2xd9+vTBlClToK+vL95dbty4MXbv3o0//vhDHPrz2Wef5dvnhwrLDPz/cImBAweiV69euHr1qty1K+j4iuQvSIcOHWBkZIQ9e/YAePd9BAcHY8CAAcU698LOW1W/NwVp27at3PLFixfl/jVFIpGgZ8+euHjxYqF9ERFpG47Zpk+ChYWF3FhrZcTFxQEAqlSpovA+S5YsgZGREQYPHgzg3bhlAJgyZQq6du2KsWPH5ruvgYGB+Pn9uOz/tslkMnH55cuXuf5C8b6gevnyZaF9v+9LkX5SU1NRuXLlfLMDwLBhw/D777/j+++/x8aNG9GiRQvUqVMn3+1TUlJQvXp19OrVS669b9++aNq0KQBg8uTJqFixIrZt24aJEyeiRo0aWLx4sTiEoSCFZb58+TLc3d3h7e2NNm3awMTEBPv27cOdO3fEbQo6viL5C6Kvr4/evXtjy5YtGDJkCLZu3QobGxu0bNmyWOde2Hmr6vemICYmJuLn7OxspKamyhX374/53+MREX0MWGzTJ+HLL7/E77//josXL4rFS1EdOHAAurq6cHV1VXifxYsXIyUlRVx++PAhzpw5gw4dOqBZs2ZK5ciPnZ0dQkND5dqioqLEdarsx9bWFrdu3Sqwn0GDBsHHxwdBQUHYsmULfvnlF3GdRCLJtb21tTUuX76cq1j9kEQiwZAhQzBkyBBkZWVh3rx56NmzJ5KTk6Gvr19gnsIyb9myBR07dhRnpgGAU6dOKXx8RfIXZsCAAWjVqhViY2OxZcsW9O/fX7xWyp57Yeetqt+bvL7TvOjp6cHKyko8xofHLMrxiIi0BYeR0Cehe/fuaNmyJUaNGoUnT57IrQsPD8fx48cL3P/EiRP48ccf8e2338LKykrh47Zr1w69evUSfzw8PAAAnp6ecHZ2LvqJFMDLywsXL17E+fPnAbx7YM/X1xfNmzdX6EHOovQzaNAgXLlyBbt27RL3i4yMxO3bt8Xlzz77DN26dYO3tzfevn2Lvn37yq0D3s1r/p63tzfu3LmDtWvXim2CIGDr1q14/fo1AGDbtm3ivw7o6+ujSZMmyMzMRHZ2NgBg/Pjx2LhxY57nVVjmMmXKIC4uDoIgAADu3buHwMBAuT4KOr4i+Qvj4uICW1tbzJ49G6GhoeIQEkXOPT+Fnbeqfm/y+k7z069fP6xduxZJSUkAgNjYWGzcuFGhmVuIiLQN72zTJ0FHRweHDx/G2LFjUadOHTg7O8PU1BRRUVEoW7ZsrpfRzJkzB+bm5nj79i3u3r2L1NRUTJ06FT4+PnLbzZo1CxEREQgPD0dcXJx4V3P9+vVyY4FLgouLC2bOnImvvvoKzZo1w7Nnz/D27VscPnxY5f24uLhg5cqVGDx4MBYvXgxDQ0NxnuUPjRgxAjt27MCQIUNQvnx5sf398IiOHTvCyckJbm5umDRpEn7//XdMmjQJ/v7+qFq1KsLDw9GqVStxfG9aWhoaNGgAOzs7lC1bFleuXIGvry/KlSsH4N1bBz8cm/zf8yoo87hx47B161bUr18flpaWuHnzJhwdHeXG6Bd0/ObNmxeavzASiQTffPMNFi5ciCZNmsgNuyns3PNT2Hmr6vcmr+80v79Qzpw5E1euXEHdunXRsGFDXLt2Dc2bN8ekSZOKdEwiIm0gEd7fxiH6RCQmJiI0NFR8g6S9vb34T+Dp6ek4cuQIgHeFT7ly5WBtbY26detCTy/3302DgoKQmJiYq71Tp055FkHJyck4ffo02rdvLzeO9UMvX77E2bNn0aNHD3FcbEJCAs6dOyc3ROH58+cICQlBt27d5PaPiYnBrVu3YGxsjObNm8uNs82r78zMTBw4cACenp4wNTVVqJ/3kpKSEBISAmNjYzg7O+faJiYmBjY2Njh37pzcw53AuzuoV65cQVxcHKysrMRhNa9fv0ZISAiysrLg5OSUa7rEtLQ03LhxAxkZGXBychLHI9+6dQsdO3bE/fv35eYML0rm169f48qVK8jKyoKzszOSkpIQHR2Ndu3aFXr8D/soKH9h4uPjcf78edSuXRtOTk4KnbsiFPmuivt789/vtFatWrn2+9CNGzfEebb/e67h4eFISEiQm6udiEgbsdgmIrX58ccfcfDgQbnhJery5MkTpKSkoEGDBmo/FhERkaI4jISIVO706dNYunQpTp8+jf3795fIMW1tbWFra1six1KGn58fzp07l+e6Vq1aYcKECaWqXyIiUg3e2SYilXv8+LE45rlWrVqajlMqXL16NdfDue/Z2toqND1gSfZLRESqwWKbiIiIiEhNOPUfEREREZGasNgmIiIiIlITFttERERERGryScxGIpPJ8Pz5cxgbGyv8SmEiIiLSLEEQ8OrVK1haWuY5VzuRNvgkiu3nz5/D2tpa0zGIiIhICTExMfm+HZaotPskim1jY2MA7/7Pmt9b+4iIiKh0kUqlsLa2Fv8cJ9JGGi22k5OT4ePjgyNHjiA5ORk2NjYYPXo0xo8fL7fdypUrsWLFCsTHx8PR0RHLli1DixYtFD7O+6EjJiYmLLaJiIi0DIeAkjbT6ACokSNH4ty5czh8+DCeP3+OuXPnYurUqQgMDBS32bRpE6ZPn46lS5ciKioKLi4uaN++PWJiYjSYnIiIiIiocBottq9du4Y+ffrA0dERxsbG6N27N+rWrYurV6+K2/z6668YOnQounXrhipVqmDJkiUwMTGBv7+/BpMTERERERVOo8V2nz59sHfvXkRFRSE7OxtHjhzBw4cP0aNHDwDvhplERESgTZs24j4SiQRt2rTBpUuXNBWbiIiIiP7nwYMHaN68OV6/fg0AuH37Npo3b47MzEyN5Fm5ciUmT56skWPnRaNjtn/++WdER0ejVq1aAIAyZcpg/fr1cHNzAwDExsYCACpXriy3n7m5Oa5du5ZvvxkZGcjIyBCXpVKpqqMTERERFdvDhw+xatUqhIeHo2zZsnBxccGoUaNgamqq6WgKS0tLw5UrV5CdnQ3gXd115coVyGQytR97xYoVePr0KZYsWSK2PXnyBBEREWo/tqI0Wmx7e3vjzp07uHHjBmrWrIkTJ05g0KBBMDY2Fu9u50VHRweCIOS73tfXF/PmzVNHZCIiItIiDj4lV3SF+dYt0vb37t3DF198gU6dOmHChAkAgIsXL6JFixYIDw9XR8QS0aBBA/zzzz8wMDBQ+7EeP36MyMhIubYJEyYgPT1d7cdWlMaGkSQmJuLvv//G3Llz0ahRI5iYmKBXr17o0aMHli9fDgCwsLAAALx48UJu34SEBFSpUiXfvn18fJCamir+8GFKIiIiKm02b94Mc3NzBAYGomPHjujYsSMWLlyIkJAQcZunT5+iefPmaN68OVq1agVvb2+5Z9sA4PLly3B1dcWlS5cwbNgweHh4YMKECUhOTsbFixfh5eWFtm3bYt68ecjKypLbNzU1FfPmzUO7du3QpUsX/Pbbb+Id6vxERkZi8ODBaNeuHSZMmCCORHjv0aNHmDRpknis9/lu3bqFb775Bq6urrh79y4AICwsDCNGjIC7uzv69++PU6dO5TpeeHg4Ro8ejTZt2mDYsGF48OABAODPP//E9u3bcfHiRfEaXbx4Efv378e6devk+ggKCoKXl5d4De/cuSO3fvHixfjhhx+wbds29O3bF1999RX8/PxUcndeY8X2+zdB/Xc6Hx0dHejq6gIAKlasiNq1ayM4OFhcLwgCgoKC4OLikm/fBgYG4jR/nO6PiIiISqPs7Gykp6fnugtbvnx58bO5uTlWrFiBFStWYMGCBahRowbc3NwQGhoqbpOSkoKLFy9i5MiR+PLLLzF58mQcPnwYbdq0wdixY9GrVy+MHz8eGzZswIIFC8T90tLS4OrqirCwMEydOhXDhg1DQEAABg4cmG/m1NRUuLq6Ij09Hd999x2srKzQt29fuW3+O4zkfb4+ffqgY8eOWLJkCaytrXH58mW4ubnBxsYGs2fPhpubG/r06YPt27eLfV24cAHOzs6QyWSYPn06XF1dMWDAAACAp6cnWrduDQcHB/EaOTg45BpGcurUKbRv3x61a9fGjz/+CF1dXTRv3lzujvjDhw+xfPlybNu2Dd7e3ujXrx9mz56NNWvWKPJVFkhjw0gqVqyIVq1aYcGCBahXrx5q1KiBEydOYOfOnfD19RW3mzp1KqZMmYKOHTuiefPmWLx4MZKSkjB69GhNRSciIiIqtuHDh2PDhg2oU6cOunbtiubNm8PDwwNVq1YVtzEwMEDz5s3F5datW+Phw4fw9/fPdfd206ZNaNq0KYB3L/IbO3Ys/v33X9SvXx8AcP/+fWzduhXz588HAKxZswZlypTB9u3bxZufTZo0gbW1NRYsWCA+U/ehVatWwdDQENu3b4eOjg6++uorJCYmyo2Zzs/atWvlJr2YOnUqJk2ahFmzZgEAPDw8kJ2djQULFogF/NSpU9G9e3esX79e3K9fv34AACsrK1hYWOD169dy1+i/fvjhB3h7e4vn/eWXX+LevXuYP38+/vrrL3G7KlWqYOfOndDX1wfw7kHP/fv3Y9y4cYWeW0E0OmZ7+/btmD59Otzd3ZGUlAQbGxvMnTsXEydOFLcZOXIkUlJS4O3tjYSEBNSvXx9HjhyBnZ2d5oITERERFVPt2rVx7949/P333zhz5gy2bduGly9fYuTIkVizZo04CuDkyZP4+++/ER0djbdv3+Lp06eoW1d+fLiOjg4aN24sLletWhUGBgZiof2+LS4uTlw+e/Ysnj17Bjc3NwiCIP7o6Ojg7t27eRbb165dg4eHh5gNANq3b69Qsf3hCwnfvn2Ly5cvIyUlBSdOnBCPnZKSgsjISAiCgDdv3uDq1auYNm2aXD9ly5Yt9Fjv5eTkIDQ0FD4+PnLtHTp0QEBAgFxbgwYNxEIbAKpVq4YTJ04ofKz8aLTYtrCwwObNmwvd7vvvv8f3339fAomIiIiISo65uTmmTJmCKVOmQCaT4Y8//sCIESPg5uaG/v37Y9++fejfvz/mzJmDAQMGwMTEBGvXrs31UKBEIhGH4b5f1tPTy7XNhxNMvL8jPGPGjFy5ateunWdeqVQKGxsbuTYjI6NCz1NXV1euSE5PT4dMJsPw4cPzfCu4RCLB27dvIQhCsYYDv3nzBllZWXJDc4B3Q3VevXol1/Zhof0+Q0ETcihKo8U2EREREb2jo6OD4cOHY/r06bh37x4AYPfu3ejXr5/cTcelS5eq5Hi1atVCaGhogUMw/qtGjRpitvfeP+xYFBUqVEClSpUglUrzPf77bW7fvo327dvnuU1hM9SVL18elStXRkREBDw9PcX2iIgI1KxZs8i5laHRl9oQERERfao2b96Mo0ePyhWLe/fuRVJSEpo1awbgXcEZFhYmzuxx6tQp7N27VyXHHzNmDG7fvo1ffvlFzJCeno6ffvop3xfSeHt74+TJkzh79iwA4OXLlwoNIfkviUSCb7/9FsuXL8eVK1fE9ocPH2L16tXiNqNGjcLSpUsRFhYGAJDJZFi5cqW4fZUqVfDs2bMCjzVs2DD89ttv4qwpt2/fRkBAAIYNG1bk3MpgsU1ERESkAfXr18eaNWtgamoKJycn2NraYtiwYfj555/RqVMnAMC0adOQlJQEa2tr1KlTB0OGDEHbtm1VcvymTZti+/bt8PPzg4WFBRwcHGBtbY3MzMxcQyrec3FxwaxZs/Dll1+ibt26sLe3z3MYiCLmzJmD4cOHo02bNrC3t4ednR2++uor2NraitvMnTsX3bt3R5MmTVCnTh1UrlwZT58+Fdf37dsX8fHxsLe3F6f++6+ZM2eiYcOGqFmzJurVq4dmzZph6NCh4qwm6iYRVDEYpZSTSqUwNTVFamoqpwEkIiLSEp/Kn9/p6emIioqCkZERqlWrhjJlysitz87OxoMHDyCTyfD5558jLi4OUqkU9erVA/BuOr67d++Kd8MBIDk5GZGRkeLsJMC7d5xER0fLPUgJvLtbfP/+fQiCAHt7+1xjvfPy4sULxMXFwd7eHjKZDLdv30bTpk2hq6uLV69eISwsDM2aNYNEIskz34fS0tLw4MEDVKpUCdbW1nluk5KSgsePH6NGjRq5fhcyMjIQFRUFqVSKOnXqQCqVIj09HXXq1JHbLjY2FrGxsbCzs0PFihXl1j169AjZ2dmwt7cX2+Li4pCQkAAnJ6dCr0dBWGwTERFRqcQ/v+ljwGEkRERERERqwmKbiIiIiEhNWGwTEREREakJ59kuRQ4cOCD3OlIA+OKLLzB79mwAwNWrV7Fhwwa8efMGX331Fb755htNxCQiIiIiBbHYLkUcHR0xevRocXnWrFmoUKECgHeF9owZM9C/f3/IZDL4+PggIyMD3t7emopLRERERIVgsV2KVK9eHdWrVwcAJCQkICoqCv379wcA1KlTB6dOnYJEIgHwrvh+9OiRxrISERERUeFYbJdSmzdvRufOncV5II2NjZGQkIChQ4fixYsXqFSpEn755RcNpyQiIiKigvAByVJq48aNuV4jamxsjNGjR2PIkCEIDw8XX5VKRERERKUTi+1S6Pz588jIyICHh4dce7ly5dC5c2eMGTMGs2fPxpo1azSUkIiIiD5VcXFxmDt3Lt6+favWfT4WLLZLoQ0bNmDo0KHi+GwAOHv2LJKTk8XlW7duwdLSUhPxiIiISAXCw8PzLUCXLFmCc+fOaSBV4eLi4jBv3rwiF9tF3edjwTHbpYxUKsWePXsQHh4u166vr4/mzZujatWqiI+PR05ODo4fP66hlERERNohcUzTEjvWZ/5Xi7R9eHg45s2bh0mTJqFs2bJy65YsWYJx48ahVatWqoxIGsBiu5TJzMzEwYMHYW1tLdfu6uqKmzdv4vr16zA2NoajoyN0dXU1lJKIiIhKSmRkJAIDAzF16lQcPnwYjx49QqNGjdC+fft890lKSoKfnx8AoEyZMqhZsya+/vprGBoa5ur3+++/F/utV68eOnbsKNdXdnY2du7ciWfPnqFevXqoWrVqoZkV2UcQBBw9ehS3b9+GmZkZunbtKrfdsmXL0K5dOzg5OQEA9u3bhzt37uDHH3+ERCKBTCbDwoULMWjQIGRlZSl0LprAYSSlzGeffQZ3d/c81xkaGsLNzQ0NGzZkoU1ERPSJiIyMxNy5c/HFF19gz549ePLkCXr37o0pU6YotP+rV6/g5+eH+vXrIzExMVe/Li4u2L9/P54+fYoBAwbgu+++E7eRyWRo164dfHx88PTpUyxZsgQDBgwo8HiK7CMIAjp16oSRI0ciNjYW+/fvR+3atXHx4kVxm9OnT+Ovv/4Sl+fMmYNZs2YhLCwMwLshtXPnzoWJiYlC56IpvLNNREREVMoJgoB+/fph5syZAIA+ffqgbdu2GDZsGBwcHHJtX7FiRcydO1eurW3btli+fDkWLlwo1++0adPQr18/AO/+Jf2bb76Br68v9PX1sWXLFty4cQORkZGoXLkyBEFA165dcw13/ZAi+2zZsgXBwcG4d+8eqlWrBgAYNmwYxowZg1u3bkEikcDd3R3bt28H8O5OfUREBNq1a4egoCDUr18fQUFBcHJyEl8AWNi5aArvbBMRERFpgQ/fGu3u7o7q1avjxIkT+W4vlUoRGBiIRYsWYe7cucjKysLt27dzbdetWzfxc8OGDZGVlYXnz58DAI4ePYpOnTqhcuXKAACJRIKhQ4cWmFORfY4ePYoOHTqIhTYAjBo1Cv/++y+ePXsmnuPNmzeRkpKC4OBgNGzYEF26dBGnPg4KCso1GqCgc9EUFttEREREGqCj864My8nJybUuJycn15BRCwsLueWqVavmW0iGhYXB1tYWGzZsQEJCgni8D2c2AwBdXV2UK1dOXNbTezfoISsrCwAQGxub53ELosg+z58/z9X2fpa198V248aNUb58eZw7d04srN3d3REcHIycnBycP39ertgu7Fw0hcNIiIiIiDTg/WQIcXFxqFSpktielZWFly9f5posIS4uDlZWVuJybGxsvtMAr1q1Cq1atcL+/fvFtpEjRxY4/CMvVatWRVxcnFxbbGxssfextLTM1fZ++f056urqwtXVFUFBQQgKCsKiRYtQv359SCQSBAQEIDU1VStma2GxrSQHnwhNR1CrMN+6mo5ARET0UXNwcEDlypURGBgoN45669at0NfXh4uLi9z2mzZtEsdsBwcH4+HDh/D09Myz7/T0dLnpBBMSErBnzx7UqVOnSBm/+uorjB8/HgkJCeL46z/++KPY+3To0AGjRo3C8+fPxb8wrF+/Ho6OjnJ/oXB3d8fatWvx9OlTuLq6QiKRoFWrVliwYIHceO3SjMU2ERERkQYYGhri999/x6BBgxAaGor69evjyZMnOHjwIJYsWYIaNWqI20okEgQGBiIsLAwVKlTAli1bMH78eNSvXz/Pvr29vfHll1/im2++QeXKlbF7926YmpoWOWP//v2xYcMGfPHFF+jevTtu3bqF+Pj4Yu/Tv39/BAYGolmzZujZsycePHiA4OBgHD9+XO6lfu7u7vj+++/RrFkzGBsbi2179uzBpEmTinw+msBim4iIiEhDunTpgvv37+P06dOIiYlBvXr18PPPP8sV2sC78dZXr17FoUOH8PjxY+zYsQNffvllvv26u7sjNDQUx44dg0wmw759+/Dq1Ss8fPhQ3KZWrVqYPXu23H5mZmaYM2cOKlasCODdUI7Tp0+Lc2a3a9cOjRo1wvr163O9iOc9RfaRSCQ4fPgwjh49ilu3buHzzz/Hhg0bco3jbty4MebNm4fGjRuLbV27dsXLly/RvXv3Ip2LpkgEQRA0mqAESKVSmJqaIjU1FSYmJirpk8NIiIiI1Esdf35ro2PHjqFz587Izs7WdBRSAmcjISIiIiJSExbbRERERKVYXkMkSHuw2CYiIiIqxVhsazeNPiDZvn17pKen52rv2rUrpk2bJi5funQJq1evRnx8PBwdHTFjxgxUqVKlJKMSERERERWZRovtefPmyb01KSIiAiNHjsSECRPEtnPnzqFdu3aYMmUK+vTpg1WrVqFly5YIDQ1F+fLlNRGbiIiIiEghGi22W7RoIbe8d+9emJuby73X/scff0S3bt2waNEiAEC7du1QtWpVrF+/HlOmTCnJuERERERERVJqxmxnZmbi77//xuDBg1GmTBkA795+dOnSJXTp0kXczsjICO3atcPJkyc1FZWIiIiISCGlptg+cOAAXrx4geHDh4ttMTExkMlkcq/tBAArKytER0fn21dGRgakUqncDxERERFRSSs1xfbGjRvRqlUrfP7552JbVlYWAOR6Q1G5cuWQmZmZb1++vr4wNTUVf6ytrdUTmoiIiIioAKWi2H769ClOnDiBESNGyLW/f73my5cv5dpfvnxZ4Ks3fXx8kJqaKv7ExMSoPjQRERERUSFKRbG9adMmmJiYoFevXnLtlpaWqFKlCq5fvy7XHhISgkaNGuXbn4GBAUxMTOR+iIiIiIhKmsaLbUEQsGnTJgwcODDXcBEA8Pb2xoYNGxAbGwsA2LdvH+7cuQNvb++SjkpEREREVCQanfoPAM6cOYNHjx7JPRj5oTlz5uDevXuoVasWbG1t8fjxY/z2229o1qxZCSclIiIiIioajRfb9vb2+Oeff+Dk5JTn+rJly2LPnj2Ijo5GfHw8ateuDVNT0xJOSURERERUdBovtm1sbGBjY6Oy7YiIiIiISguNj9kmIiIiIvpYsdgmIiIiIlITFttERERERGrCYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNWGxTURERESkJiy2iYiIiIjUhMU2EREREZGasNgmIiIiIlITFttERERERGrCYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNWGxTURERESkJnrF2fnly5cICAhAZGQkLC0tMXjwYFhaWqoqGxERERGRVitSsV2rVi1ERkYCAO7fv49WrVohLS0NdnZ2ePz4MZYsWYIrV66gVq1aaglLRERERKRNijSMJCoqSvw8ffp0NG3aFLGxsfj333/x/PlztGjRAjNnzlR5SCIiIiIibaT0MJJLly7h/PnzKF++PADA2NgYy5YtQ7t27VQWjoiIiIhImyn9gGRqaiqqVq0q12ZpaYmUlJTiZiIiIiIi+igUudju3LkzOnfuDF1dXQQFBcmt+/fff9GkSRNVZSMiIiIi0mpFGkYyceJE8XOtWrXw8OFDufULFy7EDz/8oJpkRERERERarkjF9ooVKwpcf+jQoeJkISIiIiL6qPClNkREREREalLk2UgiIyOxevVqXLp0CfHx8RAEARYWFnBxccHYsWM5xzYRERER0f8Uqdg+e/YsOnXqBAsLC3h4eMDDwwMSiQQJCQnYv38/1q1bhyNHjsDd3V1NcYmIiIiItEeRiu2pU6fiu+++w/z58/NcP3v2bEyZMgU3btxQSTgiIiIiIm1WpDHbYWFhmDRpUr7rJ02ahPDw8OJmIiIiIiL6KBTpznbVqlVx7tw5dOvWLc/1wcHBuV50o4hLly7hzJkzMDQ0RN++fWFlZSW3/sWLF9i2bRvi4+Ph6OiIXr16QVdXt8jHISIiIiIqSUW6s/3dd9/By8sLI0aMQGBgIE6ePImTJ08iMDAQI0eORL9+/TB16lSF+5PJZPD29kaXLl2QnJyM5ORkdOzYEXfv3hW3iYqKgqOjI/bt24ecnBz4+PigY8eOyMnJKUp0IiIiIqISV6Q72+PGjYOZmRmWLFmCjRs3QhAEAIBEIoGTkxM2btyI/v37K9zfmjVrsGPHDoSGhsLe3h4AMG3aNLx580bcZvr06ahVqxZOnjwJHR0djBo1CrVr18a2bduKdCwiIiIiopImEd5XzEWUlpaG+Ph4AECVKlVgZGRU5D7q1KkDNzc3/P7773muz87OhrGxMZYtW4YxY8aI7Z6enjAzM8POnTsVOo5UKoWpqSlSU1NhYmJS5Jx5cfCJUEk/pVWYb11NRyAiok+cOv78JippRZ5n+z0jIyPUqFFD6QO/evUK9+7dww8//IB9+/bh+vXrsLS0RI8ePVClShUAwJMnT/D27VvUrFlTbt+aNWvin3/+ybfvjIwMZGRkiMtSqVTpnEREREREylLpGyRr1aql8EttUlNTAQBLly6Fv78/9PX1cejQIdjb2yMkJAQAkJ6eDgC5/jZramqKtLS0fPv29fWFqamp+GNtba3M6RARERERFYvSd7bz8tVXXym8rbGxMQCgYsWKOH78uNjeoUMH+Pj44PTp0yhfvjwAICUlRW7flJQUcf+8+Pj4YMqUKeKyVCplwU1EREREJU6lxfaqVasU3tbU1BSWlpZo2rSpXHvTpk3x999/AwBsbGxgZGSEe/fuyRXyd+/eRd26+Y8pNjAwgIGBQRHTExERERGplkqHkRSVl5cXgoODIZPJAACCICAoKAhOTk4AAF1dXfTo0QObN28Wx2DfuXMHFy5cQO/evTWWm4iIiIhIEUWejSQyMhKrV6/GpUuXEB8fD0EQYGFhARcXF4wdO1bhMdvAu3Hbbdu2RU5ODlq0aIGQkBC8ePECZ86cER+KjI2NhZubG8qXL4/GjRvj0KFDaN++PQICAhQ+DmcjKTrORkJERJrG2UjoY1CkYvvs2bPo1KkTLCws4OHhAXNzc0gkEiQkJODMmTOIi4vDkSNH4O7urnCArKwsHDt2DE+ePIGNjQ3at2+PsmXLym2Tnp6OQ4cOiW+QLEr/AIttZbDYJiIiTWOxTR+DIhXbjRs3RufOnTF//vw818+ePRuHDh3CjRs3VBZQFVhsFx2LbSIi0jQW2/QxKNKY7bCwMEyaNCnf9ZMmTUJ4eHhxMxERERERfRSKVGxXrVoV586dy3d9cHAwqlatWuxQREREREQfgyJN/ffdd9/By8sLAwcORJs2bWBubg4AePHiBYKCgvDXX39hyZIlaglKRERERKRtilRsjxs3DmZmZliyZAk2btyI98O9JRIJnJycsHHjRvTv318tQYmIiIiItE2RX2ozYMAADBgwAGlpaYiPjwcAVKlSBUZGRioPR0RERESkzZR+g6SRkRFq1KihyixERERERB8Vjb5BkoiIiIjoY8Zim4iIiIhITVhsExERERGpCYttIiIiIiI1UarYDg8PR2pqaq721NRUvkGSiIiIiOh/lCq2x48fjx9//DFX+w8//IAJEyYUOxQRERER0cdAqWJ72LBhCAwMREZGhtiWkZGBrVu3YtiwYSoLR0RERESkzZQqtnv06AFBELB//36xbd++fQCA7t27qyQYEREREZG2U6rYLlu2LPr3749NmzaJbZs2bUL//v1RtmxZlYUjIiIiItJmSr9BctiwYXB2dsazZ88AACdPnoSvr6/KghERERERaTuli+1GjRqhQYMG+PvvvyEIAho2bIhGjRqpMhsRERERkVZTutgG3t3d9vPzAwBMnDhRJYGIiIiIiD4WxXqpTf/+/REdHY3o6Gh88803qspERERERPRRKNadbTMzM/zyyy+QSCQwMzNTUSQiIiIioo9DsYptAHyJDRERERFRPoo1jISIiIiIiPLHYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNSl2sS0IAmQymSqyEBERERF9VJQutv/88080aNAA5cqVg6GhIRo2bIi//vpLldmIiIiIiLSaUlP/zZ8/H0uWLMG3336L2bNnAwCuXr2KcePGISYmBj/++KNKQxIRERERaSOliu2VK1di27Zt6Nixo9jWs2dPtGrVCt7e3goX2zdv3kRUVJRcm6mpKTw9PeXaBEHAtWvXEB8fj/r168POzk6Z2EREREREJUqpYjsnJweurq652l1dXZGdna1wP+vWrcOhQ4fQvHlzsc3Gxkau2JZKpejYsSOioqJQp04dhISEYPLkyfjpp5+UiU5EREREVGKUKrZbtmyJrVu3YtSoUXLtW7duRcuWLYvUV/PmzbFr165818+cORPx8fGIiIiAmZkZgoOD4e7uDg8PD3h4eCgTn4iIiIioRChVbNeoUQNjxozB/v370bRpU3GYx7FjxzBhwgQsWbJE3Pa7774rsK/U1FQcOXIE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B4+JiIgKUaQx21ZWVrhz506+6//991/xxTREpH2ePHmCBw8eYPLkybn+4jx58mTx+YzHjx/jzZs3GkpJRESkPYp0Z7tDhw6YPXs2Tp06BR0d+To9JycH8+bNk5s1hIi0y9KlS/H69WusWbMmz/X/vbtNREREBStSse3j44NGjRqhUaNGmDhxImrXrg1BEHD//n389ttviIuLw/r169WVlYg0bPPmzdDT04O7uzusrKw0HYeIiKjUK9IwkmrVquHcuXMwNjbGsGHD4ObmhlatWmH48OEwNTXF+fPnYWlpqa6sRKRBEydOxI0bN7Br1y7Uq1cPp0+f1nQkIiKiUq/Ib5CsW7cuLly4gJiYGNy/fx8AULt2bT4oRfSR+3AIyapVq+Dr6ytO0UlERER5U/p17dbW1iywiT5RpqamCs88RERE9ClTutgmoo9PQEAAnj9/DuDd3WsrKyv069cP9+/fx5EjRwC8m7Fk06ZNWLdunSajEhERaQUW20QkevbsGWJjYzFx4kQ8ffpUnHUoLS0Njx8/hkQiQeXKlXHu3Dk4OTlpOC0REVHpx2KbiETTp0/Ps/39LERERERUNEWajYSIiIiIiBTHYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNeEDkkRayMEnQtMR1CrMt66mIxAREakE72wTEREREakJi20iIiIiIjVhsU1EREREpCYstomIiIiI1ITFNhERERGRmrDYJiIiIiJSExbbRERERERqwmKbiIiIiEhNWGwTEREREakJi20iIjV5+fIlTp06hdDQUE1HISIiDeHr2omI1ODYsWP45ptv0LhxYzx8+BDu7u74448/NB2LiIhKGO9sExGpgY+PD1auXIlTp07h7t27uHDhAq5du6bpWEREVMJYbBMRqUFSUhJq1qwJAChTpgysra1x+vRpDaciIqKSxmEkRERq0L17d4wfPx4TJ07E/fv3cevWLTg5OWk6FhERlTCN39nOycnBoUOHsGTJEsTExOS5TXp6Onbu3IlVq1YhODi4hBMSERXdr7/+ikGDBuHo0aMwNDRE586dYW5urulYRERUwjR6Z3vXrl2YNm0arK2tcf78eTg7O8Pa2lpum9jYWLRq1QpGRkZo1KgR5s+fj/bt2yMgIEBDqYmICqevr4/x48dj/PjxePPmDapXr47BgwdrOhYREZUwjRbb5ubmCAoK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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "replicated = replication(studies, monocytes, contrast)\n",
- "found = replicated.features\n",
- "once = found.filter(n_found=1)[\"feature_id\"]\n",
- "every, own, none = (np.isin(once, kind).sum() for kind in truth_sets)\n",
- "print(f\"monocyte genes found: {np.count_nonzero(found['n_found'])}\")\n",
- "print(f\" by one cohort only: {once.size}\")\n",
- "print(f\" changed in every cohort: {every}\")\n",
- "print(f\" changed in that cohort alone: {own}\")\n",
- "print(f\" unchanged: {none}\")\n",
- "plot_replicated_genes(replicated)\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f898c8d6",
- "metadata": {},
- "source": [
- "Most genes are found by one cohort only, 73 of the 133 found, and none by all six. Of\n",
- "those 73, 43 belong to that cohort alone, but 22 change in every cohort and the other\n",
- "cohorts missed them: a gene that one cohort finds and the next does not is not\n",
- "necessarily a change of one cohort.\n",
- "\n",
- "## How well each cohort replicates the others\n",
- "\n",
- "A gene that one cohort finds and another does not may still change in the other, by\n",
- "too little for it to detect. π₁ of the second cohort's p-values for the genes the\n",
- "first finds, one minus Storey's estimate of the proportion of them without an effect\n",
- "[@storey2003], estimates the share that change in the second cohort, whether it finds\n",
- "them or not. {api}`plot_study_agreement` draws it for every pair of cohorts. Each\n",
- "entry is computed from the few dozen genes that one cohort finds; on so few p-values\n",
- "qvalue's default estimate of π₀, a smoother read at λ = 0.95, is unsteady, so the\n",
- "analysis estimates it at a single λ of 0.5, which is steadier; `null_proportion={}`\n",
- "asks for qvalue's smoother."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "ebdc14ed",
- "metadata": {
- "figure_descriptions": [
- "Heatmap of the six cohorts, discovery cohorts in rows and replication cohorts in columns, each entry with its value and the number of genes it is computed from, 20 to 73: pi1 of the genes the discovery cohort finds, in the replication cohort's p-values, from 0.47 (the genes of cohort_3 in cohort_4 and in cohort_6) to 1.00 (the genes of cohort_5 in cohort_2 and in cohort_3), most entries between 0.6 and 0.9."
- ]
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "pi1 by qvalue's smoother: 0.00 to 1.00, mean 0.72\n",
- "pi1 by lambda 0.5: 0.47 to 1.00, mean 0.73\n",
- "share of the genes found by both cohorts: mean 0.32\n",
- "share with the same sign: 0.73 to 1.00\n"
- ]
- },
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "smoother = replication(studies, monocytes, contrast, null_proportion={})\n",
- "pairs = replicated.study_pairs\n",
- "for name, pi1 in (\n",
- " (\"qvalue's smoother\", smoother.study_pairs[\"pi1\"]),\n",
- " (\"lambda 0.5\", pairs[\"pi1\"]),\n",
- "):\n",
- " low, high, mean = pi1.min(), pi1.max(), pi1.mean()\n",
- " print(f\"pi1 by {name}: {low:.2f} to {high:.2f}, mean {mean:.2f}\")\n",
- "both = pairs[\"n_features_found_in_both\"] / pairs[\"n_features_found\"]\n",
- "signs = pairs[\"sign_agreement\"]\n",
- "print(f\"share of the genes found by both cohorts: mean {both.mean():.2f}\")\n",
- "print(f\"share with the same sign: {signs.min():.2f} to {signs.max():.2f}\")\n",
- "figure, ax = plt.subplots(figsize=(7.5, 5.8), layout=\"constrained\")\n",
- "plot_study_agreement(replicated, ax=ax)\n",
- "plt.show()"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "45c69a6b",
- "metadata": {},
- "source": [
- "π₁ lies between 0.47 and 1.00, 0.73 on average: of the genes one cohort finds, most\n",
- "change in each other cohort, although the other cohort finds only a third of them\n",
- "itself, on average, and their signs agree for 73% to 100% of them. qvalue's smoother\n",
- "gave π₁ from 0 to 1 on the same genes. π₁ is not symmetric: it reads the genes of the\n",
- "discovery cohort in the p-values of the replication cohort, and the genes of the small\n",
- "cohort_5, its clearest changes, replicate best.\n",
- "\n",
- "## Combined p-values and replicated changes\n",
- "\n",
- "{api}`combine_p_values` combines each gene's p-values over the cohorts that tested it\n",
- "into the p-value of the hypothesis that no cohort changes it: Fisher's method responds\n",
- "to the strongest cohort [@fisher1932], and Stouffer's, weighted by the square roots of\n",
- "the cohorts' sizes, to the evidence they share [@stouffer1949; @zaykin2011]; the\n",
- "replication result holds the latter. A small combined p-value says that some cohort\n",
- "changes the gene, not that the change replicates. {api}`partial_conjunction` tests\n",
- "instead whether at least u cohorts change it, in one direction [@benjamini2008]; the\n",
- "replication result holds it for u = 2. Both take the p-values of every cohort as\n",
- "{api}`StudyResults.matrix` lines them up, cohorts x genes, with NaN where a cohort did\n",
- "not test a gene. The monocyte genes each finds at a false discovery rate of 5%, with\n",
- "the random-effects meta-analysis (REML) of [Combine studies](combine_studies.ipynb)\n",
- "for comparison:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "be8696bb",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "genes at adjusted p < 0.05 every cohort one cohort unchanged\n",
- "Fisher 94 36 3\n",
- "Stouffer, weighted 93 19 2\n",
- "in at least 2 cohorts 41 0 0\n",
- "in at least 3 cohorts 7 0 0\n",
- "meta-analysis (REML) 80 0 2\n"
- ]
- }
- ],
- "source": [
- "p_values = studies.matrix(\"p_value\", monocytes, contrast)\n",
- "log2_fc = studies.matrix(\"log2_fold_change\", monocytes, contrast)\n",
- "ids, found_ids = collection.features[\"feature_id\"], found[\"feature_id\"]\n",
- "fisher = combine_p_values(p_values, method=\"fisher\", directions=log2_fc)\n",
- "in_three = partial_conjunction(p_values, 3, directions=log2_fc)\n",
- "meta = meta_analysis(studies).table(monocytes, contrast)\n",
- "methods = {\n",
- " \"Fisher\": (ids, adjust_p_values(fisher.p_value, \"bh\")),\n",
- " \"Stouffer, weighted\": (found_ids, found[\"adjusted_p_value\"]),\n",
- " \"in at least 2 cohorts\": (found_ids, found[\"replication_adjusted_p_value\"]),\n",
- " \"in at least 3 cohorts\": (ids, adjust_p_values(in_three.p_value, \"bh\")),\n",
- " \"meta-analysis (REML)\": (meta[\"feature_id\"], meta[\"adjusted_p_value\"]),\n",
- "}\n",
- "row(\"genes at adjusted p < 0.05\", columns)\n",
- "for name, (feature_ids, adjusted) in methods.items():\n",
- " row(name, by_truth(feature_ids, adjusted))"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "1e06f3e4",
- "metadata": {},
- "source": [
- "Fisher's method finds 94 of the 100 genes changed in every cohort, and 36 of the 60\n",
- "genes of single cohorts: one cohort's strong change is enough. Stouffer's method,\n",
- "which weighs the evidence of every cohort and its direction, keeps 19 of those. The\n",
- "partial conjunction tests find no gene of a single cohort, at a cost in power: 41\n",
- "genes in at least 2 cohorts and 7 in at least 3, since the change must be clear in\n",
- "that many cohorts on its own. The random-effects meta-analysis finds 80, none of a\n",
- "single cohort.\n",
- "\n",
"## SumRank\n",
"\n",
- "SumRank asks instead that a gene rank high in most cohorts [@nakatsuka2025]. Each\n",
- "cohort ranks the genes that every cohort tested by their signed -log10 p-values, and a\n",
- "gene's normalized ranks are summed over the cohorts, for genes up and, from the other\n",
- "end, for genes down. If the cohorts ranked the genes at random, the sum of a gene's\n",
- "ranks over every cohort would follow the Irwin-Hall distribution\n",
- "[@irwin1927; @hall1927], which gives its p-value. {api}`sumrank` computes it for one\n",
- "cell type and contrast, and {api}`plot_sumrank` draws every gene's p-value, the genes\n",
- "in alphabetical order, as the authors of SumRank draw it."
+ "Each cohort ranks the genes that every cohort tested by their signed -log10 p-values,\n",
+ "and a gene's normalized ranks are summed over the cohorts, every cohort by default, for\n",
+ "genes up and, from the other end, for genes down. If the cohorts ranked the genes at\n",
+ "random, the sum over every cohort would follow the Irwin-Hall distribution, which gives\n",
+ "its p-value ([SumRank](../statistics/replication.md#sum-of-ranks)). {api}`sumrank`\n",
+ "computes it for one cell type and contrast, and {api}`plot_sumrank` draws every gene's\n",
+ "p-value, the genes in alphabetical order, as the authors of SumRank draw it."
]
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 2,
"id": "a34e9df8",
"metadata": {
"figure_descriptions": [
@@ -396,7 +193,7 @@
},
{
"cell_type": "markdown",
- "id": "1069af8b",
+ "id": "f898c8d6",
"metadata": {},
"source": [
"SumRank finds 56 genes, 29 up and 27 down, all but one changed in every cohort, and no\n",
@@ -407,21 +204,18 @@
"## Summing the best ranks\n",
"\n",
"A cohort can miss a change for reasons of its own, so SumRank can sum each gene's best\n",
- "ranks only, those of the cohorts that rank it highest: the paper summed every study of\n",
- "Parkinson's disease, Huntington's disease and schizophrenia, of which there were 3 to\n",
- "6, and 65% of the 17 studies of Alzheimer's disease and 55% of the 16 of COVID-19,\n",
- "proportions it chose by cross-validation [@nakatsuka2025]. A sum of the best ranks is\n",
- "smaller than a sum of uniform ranks, so its Irwin-Hall p-values are too small, as\n",
- "{api}`sumrank` warns. Its p-values come instead from permutations: the case and\n",
- "control labels are permuted within each cohort, each cohort's analysis is run again,\n",
- "and the statistics of every gene in every permutation make one null distribution\n",
- "[@phipson2010]. Here the best 4 of the 6 cohorts are summed (`n_studies_summed=4`),\n",
- "with 30 permutations."
+ "ranks only, those of the cohorts that rank it highest, as the paper did where a disease\n",
+ "had many studies. A sum of the best ranks is smaller than a sum of uniform ranks, so\n",
+ "its Irwin-Hall p-values are too small, as {api}`sumrank` warns, and its p-values come\n",
+ "from permutations instead: the case and control labels are permuted within each\n",
+ "cohort, each cohort's analysis is run again, and the statistics of every gene in every\n",
+ "permutation make one null distribution. Here the best 4 of the 6 cohorts are summed\n",
+ "(`n_studies_summed=4`), with 30 permutations."
]
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 3,
"id": "d6b26d0c",
"metadata": {},
"outputs": [
@@ -468,7 +262,7 @@
},
{
"cell_type": "markdown",
- "id": "b3cc6d7f",
+ "id": "45c69a6b",
"metadata": {},
"source": [
"Summed over the best 4 of the 6 cohorts, the Irwin-Hall p-values find 16 unchanged\n",
@@ -481,18 +275,17 @@
"Genes that replicate should tell cases from controls in a cohort that had no part in\n",
"choosing them. {api}`held_out_prediction` leaves each cohort out in turn, chooses 20\n",
"genes up and 20 down in every cell type from the other five, by SumRank and by a\n",
- "random-effects meta-analysis with the Knapp-Hartung adjustment, as the paper compares\n",
- "them, scores every donor of the held-out cohort by how high its profiles rank those\n",
- "genes, as UCell scores them [@andreatta2021], and reports the area under the ROC\n",
- "curve (AUC) of its cases against its controls [@hanley1982], the two levels of the\n",
- "condition that the contrast compares. {api}`plot_held_out_auc` draws it for every\n",
- "cohort. The paper chose the proportion of studies summed by cross-validation; the mean\n",
- "AUC for a few proportions comes first."
+ "random-effects meta-analysis, as the paper compares them, scores every donor of the\n",
+ "held-out cohort by how high its profiles rank those genes, and reports the area under\n",
+ "the ROC curve (AUC) of its cases against its controls ([Prediction in held-out\n",
+ "studies](../statistics/replication.md#held-out-prediction)). {api}`plot_held_out_auc`\n",
+ "draws it for every cohort. The paper chose the proportion of studies summed by\n",
+ "cross-validation; the mean AUC for a few proportions comes first."
]
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 4,
"id": "e0583f28",
"metadata": {
"figure_descriptions": [
@@ -540,7 +333,7 @@
},
{
"cell_type": "markdown",
- "id": "0e48d69a",
+ "id": "1e06f3e4",
"metadata": {},
"source": [
"SumRank's genes predict the held-out cases a little better here, with a mean AUC of\n",
@@ -548,24 +341,19 @@
"proportion summed moves the mean AUC between 0.87 and 0.92 only. In the 17 studies of\n",
"Alzheimer's disease of the paper, SumRank's genes predicted the left-out datasets with\n",
"a mean AUC of 0.78, as did those of the datasets merged into one model with the\n",
- "dataset as a covariate; those of the inverse-variance meta-analysis 0.74, and the\n",
- "genes of single datasets 0.67 (its Table 1) [@nakatsuka2025].\n",
+ "dataset as a covariate; those of the paper's inverse-variance meta-analysis, a\n",
+ "random-effects model, 0.74, and the genes of single datasets 0.67 (its Table 1)\n",
+ "[@nakatsuka2025].\n",
"\n",
"## Choices\n",
"\n",
- "- **Which genes replicate.** A meta-analysis estimates each gene's change with its\n",
- " uncertainty and weighs the cohorts by their precision; with random effects, their\n",
- " disagreement widens the interval. A partial conjunction test claims a change in at\n",
- " least u cohorts, with a false discovery rate over the genes. SumRank asks that a\n",
- " gene rank high in most cohorts, and weighs every cohort alike.\n",
- "- **Which combination of p-values.** Fisher's method finds genes that any cohort\n",
- " changes; Stouffer's, weighted by the square roots of the cohorts' sizes, the default\n",
- " of {api}`replication`, rewards evidence the cohorts share; neither says that a change\n",
- " replicates, which {api}`partial_conjunction` tests (`at_least`, and\n",
- " `conjunction_method` for Simes's, Fisher's or Stouffer's combination).\n",
- "- **π₁ on few genes.** π₀ is estimated at λ = 0.5, which few genes allow;\n",
- " `null_proportion` chooses qvalue's smoother (`{}`) or its bootstrap\n",
- " (`{\"method\": \"bootstrap\"}`) instead, for cohorts that find many genes.\n",
+ "- **Which genes replicate.** {api}`sumrank` by default, which asks that a gene rank\n",
+ " high in study after study and weighs every cohort alike; {api}`partial_conjunction`\n",
+ " to claim a change in at least u cohorts, with a false discovery rate over the genes;\n",
+ " a meta-analysis to estimate each gene's change with its uncertainty ([Defaults and\n",
+ " choices](../statistics/replication.md#replication-defaults)). {api}`replication`\n",
+ " gives the partial conjunctions with the tables below: the genes found in each number\n",
+ " of cohorts and the agreement between pairs of cohorts.\n",
"- **SumRank's proportion and p-values.** Summing every cohort, the Irwin-Hall p-values\n",
" serve; summing fewer (`n_studies_summed`, or `proportion_of_studies`) needs\n",
" permutations (`n_permutations`), of the labels of the factor that the contrast\n",
@@ -573,9 +361,223 @@
" analysis again, and designs with random effects cannot be permuted. SumRank ranks\n",
" only the genes that every cohort tested, so a cohort with a short panel of genes or\n",
" a strict expression filter shortens the list for all.\n",
+ "- **Which combination of p-values.** Fisher's method finds genes that any cohort\n",
+ " changes; Stouffer's, weighted by the square roots of the cohorts' sizes, the default\n",
+ " of {api}`replication`, rewards evidence the cohorts share; neither says that a change\n",
+ " replicates, which {api}`partial_conjunction` tests (`at_least`, and\n",
+ " `conjunction_method` for Simes's, Fisher's or Stouffer's combination), as below.\n",
+ "- **π₁ on few genes.** π₀ is estimated at λ = 0.5, which few genes allow;\n",
+ " `null_proportion` chooses qvalue's smoother (`{}`) or its bootstrap\n",
+ " (`{\"method\": \"bootstrap\"}`) instead, for cohorts that find many genes ([How well one\n",
+ " study replicates another](../statistics/replication.md#pi1-between-studies)).\n",
"- **Held-out prediction.** `n_features` sets how many genes up and down are chosen in\n",
- " each cell type, and `groups` the cell types scored; an AUC below 0.5 says the genes\n",
- " predict the wrong direction."
+ " each cell type, and `groups` the cell types scored.\n",
+ "\n",
+ "### Genes found in k cohorts\n",
+ "\n",
+ "{api}`replication` reads one cell type and contrast of every cohort, lined up gene by\n",
+ "gene, and counts the cohorts that find each gene (adjusted p-value below 0.05) and in\n",
+ "which direction. {api}`plot_replicated_genes` draws how many genes each number of\n",
+ "cohorts finds, as the paper that introduced SumRank counts them [@nakatsuka2025]."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "779274c0",
+ "metadata": {
+ "figure_descriptions": [
+ "Bars of the monocyte genes found at an adjusted p-value below 0.05 by exactly k of the six cohorts: 73 by one, 32 by two, 15 by three, 9 by four, 4 by five and none by all six. Every gene is found in one direction by the cohorts that find it; no bar has a part for genes found up in some cohorts and down in others."
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "monocyte genes found: 133\n",
+ " by one cohort only: 73\n",
+ " changed in every cohort: 22\n",
+ " changed in that cohort alone: 43\n",
+ " unchanged: 8\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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WuxQREREREQfgyJN/ffdd9/By8sLAwcORJs2bWBubg4AePHiBYKCgvDXX39hyZIlaglKRERERKRtilRsjxs3DmZmZliyZAk2btyI98O9JRIJnJycsHHjRvTv318tQYmIiIiItE2RX2ozYMAADBgwAGlpaYiPjwcAVKlSBUZGRioPR0RERESkzZR+g6SRkRFq1KihyixERERERB8Vjb5BkoiIiIjoY8Zim4iIiIhITVhsExERERGpCYttIiIiIiI1UarYDg8PR2pqaq721NRUvkGSiIiIiOh/lCq2x48fjx9//DFX+w8//IAJEyYUOxQRERER0cdAqWJ72LBhCAwMREZGhtiWkZGBrVu3YtiwYSoLR0RERESkzZQqtnv06AFBELB//36xbd++fQCA7t27qyQYEREREZG2U6rYLlu2LPr3749NmzaJbZs2bUL//v1RtmxZlYUjIiIiItJmSr9BctiwYXB2dsazZ88AACdPnoSvr6/KghERERERaTuli+1GjRqhQYMG+PvvvyEIAho2bIhGjRqpMhsRERERkVZTutgG3t3d9vPzAwBMnDhRJYGIiIiIiD4WxXqpTf/+/REdHY3o6Gh88803qspERERERPRRKNadbTMzM/zyyy+QSCQwMzNTUSQiIiIioo9DsYptAHyJDRERERFRPoo1jISIiIiIiPLHYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNSl2sS0IAmQymSqyEBERERF9VJQutv/88080aNAA5cqVg6GhIRo2bIi//vpLldmIiIiIiLSaUlP/zZ8/H0uWLMG3336L2bNnAwCuXr2KcePGISYmBj/++KNKQxIRERERaSOliu2VK1di27Zt6Nixo9jWs2dPtGrVCt7e3goX2zdv3kRUVJRcm6mpKTw9PeXaBEHAtWvXEB8fj/r168POzk6Z2EREREREJUqpYjsnJweurq652l1dXZGdna1wP+vWrcOhQ4fQvHlzsc3Gxkau2JZKpejYsSOioqJQp04dhISEYPLkyfjpp5+UiU5EREREVGKUKrZbtmyJrVu3YtSoUXLtW7duRcuWLYvUV/PmzbFr165818+cORPx8fGIiIiAmZkZgoOD4e7uDg8PD3h4eCgTn4iIiIioRChVbNeoUQNjxozB/v370bRpU3GYx7FjxzBhwgQsWbJE3Pa7774rsK/U1FQcOXIEpqamcHR0hImJibhOEAQEBATg+++/h5mZGQCgdevWaNq0KQICAlhsExEREVGpplSxHRwcDCcnJzx//hz79+8X252cnBAUFCS3bWHF9vXr17FixQo8e/YMz58/h5+fHwYOHAgAePr0KZKTk+Hk5CS3j5OTE0JDQ/PtMyMjAxkZGeKyVCpV8MyIiIiIiFRHqWK7oEK3KPr06YNly5bB0NAQALBkyRIMGzYMjRo1Qv369ZGamgoAqFChgtx+lSpVQkpKSr79+vr6Yt68eSrJSERERESkLI2+QdLDw0MstAFg6tSpMDExwdGjRwEABgYGAID09HS5/V6/fo2yZcvm26+Pjw9SU1PFn5iYGDWkJyIiIiIqmFJ3ttVFIpHA2NgYCQkJAABra2vo6ekhOjpabrvo6GjUqFEj334MDAzEQp2IiIiISFM0dmdbJpMhMTFRru3mzZt48uQJnJ2dAQBly5ZFmzZt5GYrefnyJU6fPo0OHTqUaF4iIiIioqLS2J3t93N1d+nSBQ4ODoiOjsZvv/2GL7/8Er169RK3W7RoEdzc3ODt7Y0WLVrg999/R+3atTF06FBNRSciIiIiUojG7mzr6+vj6tWrqFatGoKCgpCcnIzff/8dR48eha6urrhd48aNcf36dVSoUAHBwcHo2bMnzp8/z2EiRERERFTqSQRBEBTZcMaMGQp3umjRIqUDqYNUKoWpqSlSU1Pl5vEuDgefCJX0U1qF+dbVdAQiIvrEqePPb6KSpvAwkg+n+8vIyEBQUBBMTU1Rt+67oiw8PBxSqRTu7u6qzkhEREREpJUULraPHTsmfp40aRJsbW2xevVqGBkZAQDS0tIwduzYXHNiExERERF9qpQas7179278+uuvYqENAEZGRli8eDF2796tsnBERERERNpMqWI7KSkp17R9AJCYmIiXL18WOxQRERER0cdAqWL766+/hpeXF86cOQOpVAqpVIozZ86gX79+6Nq1q6ozEhERERFpJaWK7XXr1sHR0RGenp4wNTWFqakpPD094eTkhLVr16o6IxERERGRVlLqpTampqYICAjAkiVLcO/ePQBAnTp1UKVKFZWGIyIiIiLSZsV6g6SFhQUsLCxUlYWIiIiI6KOi9BskT548iX79+qFZs2Zim5+fH1JSUlSRi4iIiIhI6ylVbG/fvh09e/aEubk5QkJCxPbMzEwsXrxYZeGIiIiIiLSZUsX2woULsWPHDvj5+cm1d+/eHX///bdKghERERERaTuliu0HDx6gdevWAACJRCK2V65cGQkJCapJRkRERESk5ZQqts3NzREZGQlAvtg+c+YMbG1tVZOMiIiIiEjLKVVse3t7Y+TIkQgNDYVEIkF8fDw2b96MESNGYPjw4arOSERERESklZSa+m/WrFlITEyEs7MzcnJyYGFhAR0dHYwZMwbfffedqjMSEREREWklpYrtlJQUrF69GnPnzkVoaChkMhkaNGgACwsLJCYm4rPPPlN1TiIiIiIiraNUsW1ubg5BEGBubg5PT8881xERERERfeqUfqlNXt68eYOyZcuqsksiIiIiIq1VpDvbP/30U56fAUAmk+HatWtwcnJSTTIiIiIiIi1XpGL70KFDeX4GAH19fdjZ2cHX11c1yYiIiIiItFyRiu3Lly8DADp37pyr2CYiIiIiInlKjdk+ePAgoqOjxeXo6GgsWrQIO3fuVFkwIiIiIiJtp9RsJP7+/oiIiMDKlSuRmZkJd3d36Ojo4MWLF3j69CkmT56s6pxERERERFpHqTvbfn5+mDhxIgDg7NmzMDAwwL1793D48GH4+/urNCARERERkbZSqth+8uQJrKysALwrtrt06QJdXV04Ozvj6dOnKg1IRERERKStlCq2a9Sogb179+LVq1fYvn27+GKbqKgo1KhRQ6UBiYiIiIi0lVLF9uzZszF48GCYmZmhWrVqaNOmDQBgw4YNGD58uEoDEhERERFpK6UekOzbty9cXV3x7NkzNGrUCLq6ugCA9u3bo23btioNSERERESkrZQqtgHAyspKHLf9XocOHZQOkpWVhfj4eBgbG8PU1DTX+szMTEilUlSqVAkSiUTp4xARERERlRSliu0ZM2YUuH7RokVF7nPkyJH4888/MXHiRKxYsUJsl8lkmDZtGvz9/aGjowNTU1OsWrUK3bt3L/IxiIiIiIhKklLF9rVr1+SWZTIZoqKiEB0dDTc3tyL3t3XrVoSFhcHBwSHXuqVLl2LTpk24dOkSnJycsGrVKvTt2xe3bt1C3bp1lYlPRERERFQilCq2T506lavt/R3oMmXKFKmvqKgoTJ06FcHBwejdu3eu9atXr8bw4cPRsGFDAMCECRPg5+eH9evXY/ny5crEJyIiIiIqEUrNRpJnRzo6+OGHH7Bt2zaF98nMzISXlxfmz58Pe3v7XOsTEhLw5MkTtGzZUq7d1dUVISEhxc5MRERERKROKiu2AeDVq1dISkpSeHsfHx9Uq1Yt3+kCX7x4AQD47LPP5No/++wzcV1eMjIyIJVK5X6IiIiIiEqaUsNI/vzzz1xtycnJ+OOPP9CuXTuF+ggKCsKmTZtw5swZ8a2TWVlZeP36NZ4+fYpq1apBR+fd3wWys7Pl9s3KyhKnG8yLr68v5s2bp+DZEBERERGph1LF9syZM3O1VahQAW5ubliwYIFCfTx+/BiGhobo3Lmz2JaQkIDo6GgcO3ZM7pXwcXFxcvvGx8fnmnbwQz4+PpgyZYq4LJVKYW1trVAuIiIiIiJVUarYfn8nujiGDBmCIUOGyLU1bNgQ7u7u4tR/JiYmaNiwIU6ePIm+ffsCeHeX+/Tp0xg/fny+fRsYGMDAwKDYGYmIiIiIikPpl9qUlFmzZqFv375o2rQpWrRogSVLlgAAxowZo+FkREREREQFU7jYnjt3rvi/7z/nx9DQEHXr1kWnTp3EcdeKqFKlCszMzOTaevTogYCAAPz2229YvHgxHB0dERwcDHNzc4X7JSIiIiLSBIkgCIIiG7q6ugIALly4IH7OT1ZWFu7evYtevXph48aNxU9ZTFKpFKampkhNTYWJiYlK+nTwiVBJP6VVmC9fGERERJqljj+/iUqawne2L1y4kOfn/Dx58gQNGjQoFcU2EREREZEmqHSe7Q/Z2tpiw4YN6uqeiIiIiKjUU/jO9owZMxTudNGiRQCAXr16FT0REREREdFHQuFiOzQ0VPyckZGBoKAgmJqaom7dd2N7w8PDIZVK4e7uruqMRERERERaSeFi+9ixY+LnSZMmwdbWFqtXr4aRkREAIC0tDWPHjkWFChVUn5KIiIiISAspNWZ79+7d+PXXX8VCGwCMjIywePFi7N69W2XhiIiIiIi0mVLFdlJSEhITE3O1JyYm4uXLl8UORURERET0MVCq2P7666/h5eWFM2fOQCqVQiqV4syZM+jXrx+6du2q6oxERERERFpJqWJ73bp1cHR0hKenJ0xNTWFqagpPT084OTlh7dq1qs5IRERERKSVFH5A8kOmpqYICAjAkiVLcO/ePQBAnTp1UKVKFZWGIyIiIiLSZkoV2+9ZWFjAwsJCVVmIiIiIiD4qxSq2nz9/jujoaGRnZ8u1u7q6FisUEREREdHHQKliOyYmBl5eXrh06VKe6wVBKFYoImWdPn0aK1euREpKCtq0aQMfHx+UKVMGABAQEICAgAAIgoB+/fphyJAhmg1LREREHz2liu1JkybBzs4Ou3fvRtWqVZGcnIyQkBCMGzcOkydPVnVGIoVERETAz88Po0aNQpkyZfDjjz8iJycH8+fPx+bNm3HmzBlMnjwZr1+/xrfffgsjIyP07t1b07GJiIjoIyYRlLgNbW5ujlu3bsHS0hISiQRZWVnQ09NDSEgIBg4cKD40WVpIpVKYmpoiNTUVJiYmKunTwSdCJf2UVmG+dTUdociysrKgr68vLk+aNAkSiQTLly9HRkYGDAwMxHWTJ0+Gvr4+Fi9erImoRESkAHX8+U1U0pS6s52YmAhLS0sAQMWKFZGQkABLS0s4ODjg8ePHqsxHpDB9fX28efMGLVq0QHJyMmxtbXH48GEAkCu0MzIycPLkSSxdulRTUYmIiOgTodQ82x9q1KgRVq9ejZSUFPj7+8PW1lYVuYiUYmBggD///BPr1q1Deno6Vq9eLbc+Ozsb/fv3R+fOnfHll19qKCURERF9KpS6s92zZ0/x88KFC9GxY0f8/PPPKFeuHLZs2aKycERFpaOjg4YNGwIAZDIZfvjhB8yYMQMAkJmZiT59+sDe3h6LFi3SYEoiIiL6VChVbO/atUv83KxZM8TExOD+/fuwtbVFhQoVVBaOqChOnDgBa2tr1K1bFzKZDMePH0fNmjUBAG/fvkX37t3h5OSEX375RcNJiYiI6FNRrHm23zM0NBTvJhJpSo0aNdCvXz+8fPkSr169gp2dHXbu3AkAWLduHU6ePInY2FgcP34cANClSxfMnz9fk5GJiIjoI6eSYpuoNKhVqxauXr2KJ0+ewMDAQO7tpt988w1at24tt33FihVLOiIRERF9Ylhs00cnr4d0zc3NYW5uroE0RERE9ClTajaSxMREpdYREREREX1KlCq2C7pDyLuHRERERETvFHue7Q+9efMGZcuWVWWXRERERERaq0hjtn/66ac8PwPv5jS+du0anJycVJOMiIiIiEjLFanYPnToUJ6fgXevyrazs4Ovr69qktFHzcEnQtMR1CrMt66mIxAREVEpUKRi+/LlywCAzp075yq2iYiIiIhInlJjtlloExEREREVrljzbD9//hzR0dHIzs6Wa3d1dVVo/+TkZCxfvhzHjx/H69ev4eDggO+//x7Ozs5y2+3YsQO//fYb4uPj4ejoiJ9//hl16/Kf6YmIiIiodFOq2I6JiYGXlxcuXbqU53pBEBTqZ9KkSXB0dMTatWuhp6eHtWvXonXr1ggNDYW9vT0AYO/evejfvz9WrlyJFi1aYOnSpWjdujXCwsI4zSARERERlWpKDSOZNGkS7OzsEBsbC+DdHerjx4/D3t4ea9asUbifTZs24bvvvkOjRo3g6OgIPz8/ZGZm4vz58+I2CxYswODBgzF69Gg0aNAAf/zxBwRBgL+/vzLRiYiIiIhKjFLF9rlz5/Drr7/CwsICAFC+fHm0b98eAQEBWLFiheIH15E//Pbt26Grq4sWLVoAAKRSKW7evAlPT09xGz09PbRt2xbnzp1TJjoRERERUYlRahhJYmIiLC0tAQAVK1ZEQkICLC0t4eDggMePHxepr6CgIAwYMABSqRS6uro4cOCAOB772bNnACAW9e9VqVIFt27dyrfPjIwMZGRkiMtSqbRImYiIiIiIVKHYb5Bs1KgRVq9ejZSUFPj7+8PW1rZI+7do0QKXL1/GuXPn0K9fP3h5eSE8PBzAuxflAO/uZn9IX18fOTk5+fbp6+sLU1NT8cfa2rqIZ0VEREREVHxKFds9e/YUPy9cuBBr165FhQoVMHv2bPzyyy9F6svAwADVqlVDw4YNsWbNGlhaWorjvt8/AJmYmCi3T2JiYoEPR/r4+CA1NVX8iYmJKVImIiIiIiJVUGoYya5du8TPzZo1Q0xMDO7fvw9bW1tUqFChWIEMDQ2Rnp4OAKhcuTJsbW1x8eJFdO3aVdzmwoUL+Prrr/Ptw8DAAAYGBsXKQURERERUXMUeRgK8K5AbNmxYpEI7PT0dkydPRlxcHAAgOzsba9aswbVr19CrVy9xu7Fjx2Ljxo24desWZDIZ/Pz8EB0djZEjR6oiOhERERGR2hTrpTbFUa5cOXz++edwcXFBSkoK0tPTYW9vj23btqFjx47idlOnTkVsbCyaN28OXV1dmJiYYNu2bXypDRERERGVehJB0TfQqFFqaioMDQ2hr6+f7zaZmZlITU3FZ599BolEUqT+pVIpTE1NkZqaChMTk+LGBQA4+ESopJ/SKsxXvX+Z4fUjIqLCqOPPb6KSprE72x8yNTUtdJsyZcrwjZFEREREpFWUGrMtCAKio6PF5ejoaCxatAg7d+5UWTAiIiIiIm2n1J1tf39/REREYOXKlcjMzIS7uzt0dHTw4sULPH36FJMnT1Z1TiIiIiIiraPUnW0/Pz9MnDgRAHD27FkYGBjg3r17OHz4MPz9/VUakIiIiIhIWylVbD958gRWVlYA3hXbXbp0ga6uLpydnfH06VOVBiQiIiIi0lZKFds1atTA3r178erVK2zfvh2enp4AgKioKNSoUUOlAYmIiIiItJVSxfbs2bMxePBgmJmZoVq1amjTpg0AYMOGDRg+fLhKAxIRERERaSulHpDs27cvXF1d8ezZMzRq1Ai6uroAgPbt26Nt27YqDUhEREREpK2UnmfbyspKHLf9XocOHYodiIiIiIjoY6HUMBIAOHnyJPr164dmzZqJbX5+fkhJSVFFLiIiIiIiradUsb19+3b07NkT5ubmCAkJEdszMzOxePFilYUjIiIiItJmShXbCxcuxI4dO+Dn5yfX3r17d/z9998qCUZEREREpO2UKrYfPHiA1q1bAwAkEonYXrlyZSQkJKgmGRERERGRllOq2DY3N0dkZCQA+WL7zJkzsLW1VU0yIiIiIiItp1Sx7e3tjZEjRyI0NBQSiQTx8fHYvHkzRowYwXm2iYiIiIj+R6mp/2bNmoXExEQ4OzsjJycHFhYW0NHRwZgxY/Ddd9+pOiMRERERkVZSqtjW09PD6tWrMXfuXISGhkImk6FBgwawsLBQdT4iIiIiIq2l9EttgHdjtz09PVWVhYiIiIjoo6J0sb1//35cvHgRSUlJudZt2LChWKGIiIiIiD4GShXbPj4+WL58OVq3bo0KFSqoOhMRERER0UdBqWL7jz/+wPHjx8W5tomIiIiIKDelpv7LycmBs7OzqrMQEREREX1UlCq2W7dujePHj6s6CxERERHRR0WpYSQ2Njbo378/Bg0ahFq1asm9RRIA59omIiIiIoKSxfbZs2fx+eef48qVK7hy5Uqu9Sy2iYiIiIiULLZDQ0NVHIOIiIiI6OOj1JhtIiIiIiIqnNLF9smTJ9GvXz80a9ZMbPPz80NKSooqchERERERaT2liu3t27ejZ8+eMDc3R0hIiNiemZmJxYsXqywcEREREZE2U6rYXrhwIXbs2AE/Pz+59u7du+Pvv/9WuJ+MjAxs3rwZo0aNwtixY7F161bIZLJc2929exdTp07FgAED8Msvv+D169fKxCYiIiIiKlFKFdsPHjwQ3x754bR/lStXRkJCgkJ9yGQy1KtXD0FBQWjSpAns7e0xbdo09OjRA4IgiNvduHEDTZo0QVJSEtzc3LBr1y64ubkhIyNDmehERERERCVGqdlIzM3NERkZCUdHR7li+8yZM7C1tVWoD4lEgnPnzsHKykpsa9asGVxcXHD9+nXxDZU+Pj5wd3fHpk2bAAC9evWCtbU1Nm3ahNGjRysTn4iIiIioRCh1Z9vb2xsjR45EaGgoJBIJ4uPjsXnzZowYMQLDhw9XqA+JRCJXaANAtWrVAADJyckA3g0zOXPmDHr16iVuU6lSJbRt2xZHjhxRJjoRERERUYlR6s72rFmzkJiYCGdnZ+Tk5MDCwgI6OjoYM2ZMsV5os3LlSpiZmYkznERHRyM7Oxs2NjZy29nY2CA4ODjffjIyMuSGmUilUqUzEREREREpS6liW09PD6tXr8bcuXMRGhoKmUyGBg0awMLCQukgO3bswNKlS7F161aYmJgAgFgwGxoaym1bvnx5vH37Nt++fH19MW/ePKWzEBERERGpglLF9nvm5ubw9PQsdoj9+/dj4MCBWL16Nfr06SO2m5qaAvj/YSXvvXz5EmZmZvn25+PjgylTpojLUqkU1tbWxc5JRERERFQURSq2MzIycPDgQbkx1EOGDBE/SyQSLF26FBUrVlS4z4MHD6Jv375Yvnx5rgceq1WrhgoVKuD27dvo2LGj2H779m04OTnl26eBgQEMDAwUzkBEREREpA5FekAyICAAJ06ckGvbvHkzHj9+jMePH+P8+fPYuHGjwv0dPnwYvXv3xrJly/Dtt9/mWi+RSNC/f39s3LhRfDNlcHAwrl69igEDBhQlOhERERFRiSvSne3Nmzdj0aJFudqDgoIAAJcuXcL48eMxbdq0QvuSSqXo2bMnTExMcObMGZw5c0ZcN2bMGLRt2xbAuxfo3LhxA3Xr1kXdunVx5coV/PDDD/Dw8ChKdCIiIiKiElekYvvevXuoXbt2vuvr16+Pe/fuKdRX2bJlERAQkOe6mjVrip9NTExw4cIFXL16FfHx8ahfvz6qV69elNhERERERBpRpGI7JSUFenryu3z4tscyZcogKytLob7KlCkjN/a7IBKJBF988YXiQYlIKX5+fpDJZACA8ePHQ1dXV1y3YsUKuW179uzJB4+JiIgKUaQx21ZWVrhz506+6//991/xxTREpH2ePHmCBw8eYPLkybn+4jx58mTx+YzHjx/jzZs3GkpJRESkPYp0Z7tDhw6YPXs2Tp06BR0d+To9JycH8+bNk5s1hIi0y9KlS/H69WusWbMmz/X/vbtNREREBStSse3j44NGjRqhUaNGmDhxImrXrg1BEHD//n389ttviIuLw/r169WVlYg0bPPmzdDT04O7uzusrKw0HYeIiKjUK9IwkmrVquHcuXMwNjbGsGHD4ObmhlatWmH48OEwNTXF+fPnYWlpqa6sRKRBEydOxI0bN7Br1y7Uq1cPp0+f1nQkIiKiUq/Ib5CsW7cuLly4gJiYGNy/fx8AULt2bT4oRfSR+3AIyapVq+Dr6ytO0UlERER5U/p17dbW1iywiT5RpqamCs88RERE9ClTutgmoo9PQEAAnj9/DuDd3WsrKyv069cP9+/fx5EjRwC8m7Fk06ZNWLdunSajEhERaQUW20QkevbsGWJjYzFx4kQ8ffpUnHUoLS0Njx8/hkQiQeXKlXHu3Dk4OTlpOC0REVHpx2KbiETTp0/Ps/39LERERERUNEWajYSIiIiIiBTHYpuIiIiISE1YbBMRERERqQmLbSIiIiIiNeEDkkRayMEnQtMR1CrMt66mIxAREakE72wTEREREakJi20iIiIiIjVhsU1EREREpCYstomIiIiI1ITFNhERERGRmrDYJiIiIiJSExbbRERERERqwmKbiIiIiEhNWGwTEREREakJi20iIjV5+fIlTp06hdDQUE1HISIiDeHr2omI1ODYsWP45ptv0LhxYzx8+BDu7u74448/NB2LiIhKGO9sExGpgY+PD1auXIlTp07h7t27uHDhAq5du6bpWEREVMJYbBMRqUFSUhJq1qwJAChTpgysra1x+vRpDaciIqKSxmEkRERq0L17d4wfPx4TJ07E/fv3cevWLTg5OWk6FhERlTCN39nOycnBoUOHsGTJEsTExOS5TXp6Onbu3IlVq1YhODi4hBMSERXdr7/+ikGDBuHo0aMwNDRE586dYW5urulYRERUwjR6Z3vXrl2YNm0arK2tcf78eTg7O8Pa2lpum9jYWLRq1QpGRkZo1KgR5s+fj/bt2yMgIEBDqYmICqevr4/x48dj/PjxePPmDapXr47BgwdrOhYREZUwjRbb5ubmCAoK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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "replicated = replication(studies, monocytes, contrast)\n",
+ "found = replicated.features\n",
+ "once = found.filter(n_found=1)[\"feature_id\"]\n",
+ "every, own, none = (np.isin(once, kind).sum() for kind in truth_sets)\n",
+ "print(f\"monocyte genes found: {np.count_nonzero(found['n_found'])}\")\n",
+ "print(f\" by one cohort only: {once.size}\")\n",
+ "print(f\" changed in every cohort: {every}\")\n",
+ "print(f\" changed in that cohort alone: {own}\")\n",
+ "print(f\" unchanged: {none}\")\n",
+ "plot_replicated_genes(replicated)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1069af8b",
+ "metadata": {},
+ "source": [
+ "Most genes are found by one cohort only, 73 of the 133 found, and none by all six. Of\n",
+ "those 73, 43 belong to that cohort alone, but 22 change in every cohort and the other\n",
+ "cohorts missed them: a gene that one cohort finds and the next does not is not\n",
+ "necessarily a change of one cohort.\n",
+ "\n",
+ "### How well each cohort replicates the others\n",
+ "\n",
+ "π₁ of the second cohort's p-values for the genes the first finds estimates the share\n",
+ "of them that change in the second cohort, whether it finds them or not ([How well one\n",
+ "study replicates another](../statistics/replication.md#pi1-between-studies)).\n",
+ "{api}`plot_study_agreement` draws it for every pair of cohorts. Each entry is computed\n",
+ "from the few dozen genes that one cohort finds; on so few p-values qvalue's default\n",
+ "estimate of π₀, a smoother read at λ = 0.95, is unsteady, so the analysis estimates it\n",
+ "at a single λ of 0.5, which is steadier; `null_proportion={}` asks for qvalue's\n",
+ "smoother."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "ebdc14ed",
+ "metadata": {
+ "figure_descriptions": [
+ "Heatmap of the six cohorts, discovery cohorts in rows and replication cohorts in columns, each entry with its value and the number of genes it is computed from, 20 to 73: pi1 of the genes the discovery cohort finds, in the replication cohort's p-values, from 0.47 (the genes of cohort_3 in cohort_4 and in cohort_6) to 1.00 (the genes of cohort_5 in cohort_2 and in cohort_3), most entries between 0.6 and 0.9."
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "pi1 by qvalue's smoother: 0.00 to 1.00, mean 0.72\n",
+ "pi1 by lambda 0.5: 0.47 to 1.00, mean 0.73\n",
+ "share of the genes found by both cohorts: mean 0.32\n",
+ "share with the same sign: 0.73 to 1.00\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "smoother = replication(studies, monocytes, contrast, null_proportion={})\n",
+ "pairs = replicated.study_pairs\n",
+ "for name, pi1 in (\n",
+ " (\"qvalue's smoother\", smoother.study_pairs[\"pi1\"]),\n",
+ " (\"lambda 0.5\", pairs[\"pi1\"]),\n",
+ "):\n",
+ " low, high, mean = pi1.min(), pi1.max(), pi1.mean()\n",
+ " print(f\"pi1 by {name}: {low:.2f} to {high:.2f}, mean {mean:.2f}\")\n",
+ "both = pairs[\"n_features_found_in_both\"] / pairs[\"n_features_found\"]\n",
+ "signs = pairs[\"sign_agreement\"]\n",
+ "print(f\"share of the genes found by both cohorts: mean {both.mean():.2f}\")\n",
+ "print(f\"share with the same sign: {signs.min():.2f} to {signs.max():.2f}\")\n",
+ "figure, ax = plt.subplots(figsize=(7.5, 5.8), layout=\"constrained\")\n",
+ "plot_study_agreement(replicated, ax=ax)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b3cc6d7f",
+ "metadata": {},
+ "source": [
+ "π₁ lies between 0.47 and 1.00, 0.73 on average: of the genes one cohort finds, most\n",
+ "change in each other cohort, although the other cohort finds only a third of them\n",
+ "itself, on average, and their signs agree for 73% to 100% of them. qvalue's smoother\n",
+ "gave π₁ from 0 to 1 on the same genes. π₁ is not symmetric: it reads the genes of the\n",
+ "discovery cohort in the p-values of the replication cohort, and the genes of the small\n",
+ "cohort_5, its clearest changes, replicate best.\n",
+ "\n",
+ "### Combined p-values and replicated changes\n",
+ "\n",
+ "{api}`combine_p_values` combines each gene's p-values over the cohorts that tested it\n",
+ "into the p-value of the hypothesis that no cohort changes it, by Fisher's method, which\n",
+ "responds to the strongest cohort, or by Stouffer's, weighted by the square roots of the\n",
+ "cohorts' sizes, which responds to the evidence they share ([Combined\n",
+ "p-values](../statistics/replication.md#combined-p-values)); the replication result\n",
+ "holds the latter. A small combined p-value says that some cohort changes the gene, not\n",
+ "that the change replicates. {api}`partial_conjunction` tests instead whether at least u\n",
+ "cohorts change it, in one direction ([An effect in at least u\n",
+ "studies](../statistics/replication.md#partial-conjunction)); the replication result\n",
+ "holds it for u = 2. Both take the p-values of every cohort as\n",
+ "{api}`StudyResults.matrix` lines them up, cohorts x genes, with NaN where a cohort did\n",
+ "not test a gene. The monocyte genes each finds at a false discovery rate of 5%, with\n",
+ "the random-effects meta-analysis (REML) of [Align and combine studies](combine_studies.ipynb)\n",
+ "for comparison:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "be8696bb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "genes at adjusted p < 0.05 every cohort one cohort unchanged\n",
+ "Fisher 94 36 3\n",
+ "Stouffer, weighted 93 19 2\n",
+ "in at least 2 cohorts 41 0 0\n",
+ "in at least 3 cohorts 7 0 0\n",
+ "meta-analysis (REML) 80 0 2\n"
+ ]
+ }
+ ],
+ "source": [
+ "p_values = studies.matrix(\"p_value\", monocytes, contrast)\n",
+ "log2_fc = studies.matrix(\"log2_fold_change\", monocytes, contrast)\n",
+ "ids, found_ids = collection.features[\"feature_id\"], found[\"feature_id\"]\n",
+ "fisher = combine_p_values(p_values, method=\"fisher\", directions=log2_fc)\n",
+ "in_three = partial_conjunction(p_values, 3, directions=log2_fc)\n",
+ "meta = meta_analysis(studies).table(monocytes, contrast)\n",
+ "methods = {\n",
+ " \"Fisher\": (ids, adjust_p_values(fisher.p_value, \"bh\")),\n",
+ " \"Stouffer, weighted\": (found_ids, found[\"adjusted_p_value\"]),\n",
+ " \"in at least 2 cohorts\": (found_ids, found[\"replication_adjusted_p_value\"]),\n",
+ " \"in at least 3 cohorts\": (ids, adjust_p_values(in_three.p_value, \"bh\")),\n",
+ " \"meta-analysis (REML)\": (meta[\"feature_id\"], meta[\"adjusted_p_value\"]),\n",
+ "}\n",
+ "row(\"genes at adjusted p < 0.05\", columns)\n",
+ "for name, (feature_ids, adjusted) in methods.items():\n",
+ " row(name, by_truth(feature_ids, adjusted))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0e48d69a",
+ "metadata": {},
+ "source": [
+ "Fisher's method finds 94 of the 100 genes changed in every cohort, and 36 of the 60\n",
+ "genes of single cohorts: one cohort's strong change is enough. Stouffer's method,\n",
+ "which weighs the evidence of every cohort and its direction, keeps 19 of those. The\n",
+ "partial conjunction tests find no gene of a single cohort, at a cost in power: 41\n",
+ "genes in at least 2 cohorts and 7 in at least 3, since the change must be clear in\n",
+ "that many cohorts on its own. The random-effects meta-analysis finds 80, none of a\n",
+ "single cohort."
]
}
],
@@ -583,8 +585,8 @@
"datasets": [
"simulated_cohorts"
],
- "executed_at": "2026-10-27T15:23:44+00:00",
- "executed_code_sha256": "b01614ea664c39f2c57bbacd88649c2c3117f28848f5fd86b09a25c7da56c2cb",
+ "executed_at": "2026-10-28T00:19:14+00:00",
+ "executed_code_sha256": "d0230134796f2031e6d50f6e8a353a9859ad0f7c03e108e3d33fa1760120d4a5",
"executed_datasets": {
"simulated_cohorts": {
"simulated_cohort_1.cube.zarr": "sha256:e97a0a97353376a5ac3a5c1e34f7e5f6fad6b0ac07c09b738b808fba6ccedb9e",
@@ -596,8 +598,8 @@
"simulated_cohorts_truth.npz": "sha256:6599ea11dddb183f630183846f7ece9991003a4cc77875e4e7822317f66346ec"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 10.0,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 10.4,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/guides/save_and_trace_cubes.ipynb b/docs/source/guides/save_and_trace_cubes.ipynb
index 9ab3b61..6e9fd26 100644
--- a/docs/source/guides/save_and_trace_cubes.ipynb
+++ b/docs/source/guides/save_and_trace_cubes.ipynb
@@ -8,13 +8,15 @@
"(save-and-trace-cubes)=\n",
"# Save, share and trace cubes\n",
"\n",
- "A pseudobulk analysis passes through several hands: one person or tool pools the cells,\n",
- "another tests the profiles, and the results are read months later, often on another\n",
- "machine. Each step should be checkable: which values did a result come from, and is the\n",
- "file at hand still the one analysed? A cube answers both with three records. Its file\n",
- "opens without reading its counts and checks every value it reads; its fingerprint, a\n",
- "digest of every value it holds, names it; and its provenance record says how it was\n",
- "made, and goes into every result computed from it. This page saves and opens a cube,\n",
+ "Save a cube with {api}`write_cube`, open it on any machine with {api}`read_cube`, and\n",
+ "trace every result back to the values it came from. A pseudobulk analysis passes\n",
+ "through several hands: one person or tool pools the cells, another tests the profiles,\n",
+ "and the results are read months later. Each step should be checkable: which values did\n",
+ "a result come from, and is the file at hand still the one analysed? A cube answers both\n",
+ "with three records. Its file opens without reading its counts and checks every value it\n",
+ "reads; its fingerprint, a digest of every value it holds, names it; and its provenance\n",
+ "record says how it was made, and goes into every result computed from it ([What a cube\n",
+ "records](../statistics/cubes.md#what-a-cube-records)). This page saves and opens a cube,\n",
"derives one from another, shows a changed file being caught, and traces a result back\n",
"to the profiles it came from. [Cube files and provenance\n",
"records](../reference/cube_files.md) describes the file and the records for programs\n",
@@ -39,7 +41,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Cube with 24 samples x 6 groups x 2,000 features\n",
" layers: counts (uint32, lazy), detected (uint32, lazy), log_sum (float64,\n",
" lazy), log_sum_sq (float64, lazy), linear_sum (float64, lazy)\n",
@@ -228,7 +230,7 @@
"of samples lists. From these, the profiles of a cell type can be read back by their\n",
"ids. The result also keeps the cube and the design it analysed, so\n",
"{api}`DifferentialExpressionResult.fit` computes a cell type's fit again: the values,\n",
- "weights and moderated tests behind its table ({api}`GroupFit`)."
+ "weights and moderated tests from which its table was computed ({api}`GroupFit`)."
]
},
{
@@ -380,7 +382,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:23:47+00:00",
+ "executed_at": "2026-10-28T00:18:20+00:00",
"executed_code_sha256": "a8691bd0ef96c1c887705507e18a45f1269fe2fa089b1b0bbedd6db74c4a6e7f",
"executed_datasets": {
"simulated_pbmc": {
@@ -388,8 +390,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 2.2,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 2.1,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/index.md b/docs/source/index.md
index 1654471..4cae3de 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -1,5 +1,5 @@
---
-description: CytePool brings the sample-level statistics of limma, edgeR, DESeq2, dream, propeller and metafor to Python for pseudobulk and bulk omics data, with numpy and numba as its only required dependencies.
+description: Test which genes, cell types and pathways change between conditions in pseudobulk and bulk omics data, with the sample-level statistics of limma, edgeR, DESeq2, dream, propeller and metafor, in Python.
---
# CytePool
@@ -10,17 +10,16 @@ Studies that compare conditions, donors or treatments ask questions about sample
cells of one donor are not independent replicates, and a test that treats them as
such finds differences that other donors do not share [@squair2021; @zimmerman2021].
CytePool pools the cells of each sample and cell type into a *cube* of pseudobulk
-profiles, which can be saved, shared and traced back to the cells, and tests them with
-the methods that are standard in R: limma, edgeR, DESeq2, dream, propeller and
-metafor. Its only required dependencies are numpy and numba, and every method is
-compared with the package that defined it ([Validation](validation.md)).
+profiles, which can be saved and shared, and tests them with the methods that are
+standard in R: limma, edgeR, DESeq2, dream, propeller and metafor. Its only required
+dependencies are numpy and numba, and every method is checked against an independent
+reference, usually the package that defined it ([Validation](validation.md)).
Start with [Installation](installation.md) and the [Quick start](quickstart.ipynb),
which goes from cells to results in a few minutes. [Why pseudobulk](why_pseudobulk.ipynb)
shows why the tests compare samples, [Choosing a method](statistics/choosing_a_method.md)
-which function answers which question, and the [Capability
-matrix](reference/capabilities.md) and the [API reference](/api/index.html) list every
-method.
+which function answers which question and when to choose another, and the [API
+reference](/api/index.html) documents every function.
:::{note} Early development
Nothing is released yet, and the interface may still change.
@@ -28,39 +27,43 @@ Nothing is released yet, and the interface may still change.
## The questions CytePool answers
-Studies of many samples, across immune and brain cohorts, population genetics,
-pathways, interactions, pharmacology and atlases, come back to ten questions. This is
-what CytePool offers for each today.
+Each question has a function that answers it by default, and a guide that runs it on
+data. [Choosing a method](statistics/choosing_a_method.md) gives the alternatives for
+each, and what is not implemented yet.
-| Question | What CytePool offers | Where to read |
+| Question | Start with | Guide |
| --- | --- | --- |
-| Which genes change in each cell type? | limma-voom, limma-trend and dream on every cell type of a cube; edgeR's quasi-likelihood tests and DESeq2 on one matrix at a time | [Genes that change in each cell type](guides/genes_per_cell_type.ipynb), [Bulk RNA-seq](guides/bulk_rna_seq.ipynb), [Choosing a method](statistics/choosing_a_method.md) |
-| Which cell types expand or contract? | propeller. Not yet: precision-weighted mixed models or count models of composition | [Cell-type composition](guides/composition.ipynb), [Composition tests](statistics/composition.md) |
-| Which pathways and regulators respond? | camera, fry, roast, mroast and romer on any contrast; gene set enrichment analysis (GSEA) of ranked genes; over-representation analysis of the genes that change; scores of pathways in every sample (GSVA, ssGSEA, combined z-scores, PLAGE); activities of transcription factors and pathways from the networks CollecTRI and PROGENy (decoupler's ULM, MLM and weighted aggregate); gene sets read from GMT files | [Gene sets and pathways](guides/gene_sets.ipynb), [Pathway and regulator activity](guides/pathway_activity.ipynb), [Gene-set tests](statistics/gene_set_tests.md) |
-| What drives the variation between donors, and what must be adjusted for? | Design checks for confounded factors, sample quality weights, mixed models that estimate a variance per random effect for every gene, and variance partitioning: the share of each gene's variance that each variable explains, in every cell type | [Normalization, filtering and sample quality](guides/normalization_and_quality.ipynb), [Repeated measures and donors](guides/repeated_measures.ipynb), [Designs, linear models and contrasts](statistics/linear_models.md), [Mixed models, REML and dream](statistics/mixed_models.md) |
-| Does it replicate, and how should studies be combined? | Studies aligned by sample, gene and cell type, each analysed on its own and lined up gene by gene; meta-analysis of every gene across studies, with the between-study variance, Q and I²; combined p-values, the p-value that a change replicates in at least u studies, sign agreement, pi1 between studies and the genes found in k studies; SumRank, which asks that a gene rank high in most studies, and the prediction of cases in held-out studies. Not yet: shrinkage across studies | [Combine studies](guides/combine_studies.ipynb), [Replication across studies](guides/replication_across_studies.ipynb), [Meta-analysis](statistics/meta_analysis.md), [The cube and its additive layers](statistics/cubes.md) |
-| Can patients be stratified from their profiles? | Not yet; cubes convert to AnnData and MuData for other tools | [Capability matrix](reference/capabilities.md) |
-| Which cell-cell interactions change? | Not yet | [Related software](reference/related_software.md) |
-| Do effects depend on genotype, time, sex or context? | Interactions and F-tests in designs, likelihood ratio tests, random intercepts for repeated measures, with F-tests and small-sample (Kenward-Roger) tests in mixed models. Not yet: random slopes. QTL mapping is left to dedicated tools | [Repeated measures and donors](guides/repeated_measures.ipynb), [Designs, linear models and contrasts](statistics/linear_models.md), [Count models](statistics/count_models.md), [Mixed models, REML and dream](statistics/mixed_models.md) |
-| How do cells respond to drugs and doses? | A dose as a factor or a covariate of a design. Not yet: dose-response fits, benchmark doses, synergy | [Capability matrix](reference/capabilities.md) |
-| Can public studies be pooled into one cube? | The cubes of several studies stacked into one, of the genes and cell types that every study has, with sample and donor ids kept apart by study; cubes import the pseudobulk layouts of scanpy, decoupler and LIANA | [Combine studies](guides/combine_studies.ipynb), [The cube and its additive layers](statistics/cubes.md), [Export a cube from another tool](guides/export_cubes.ipynb) |
+| Which genes change in each cell type? | {api}`limma_voom` | [Genes that change in each cell type](guides/genes_per_cell_type.ipynb) |
+| Which genes change in bulk RNA-seq? | {api}`limma_voom` on a cube from {api}`from_bulk` | [Bulk RNA-seq](guides/bulk_rna_seq.ipynb) |
+| Which genes change, with several samples of each donor? | {api}`dream_voom` | [Repeated measures and donors](guides/repeated_measures.ipynb) |
+| What drives the variation between samples? | {api}`variance_partitioning` | [Repeated measures and donors](guides/repeated_measures.ipynb) |
+| Which cell types expand or contract? | {api}`propeller` | [Cell-type composition](guides/composition.ipynb) |
+| Do the genes of a pathway respond to a contrast? | {api}`camera` | [Gene sets and pathways](guides/gene_sets.ipynb) |
+| Do the genes of a pathway gather along a ranking? | {api}`gsea` | [Gene sets and pathways](guides/gene_sets.ipynb) |
+| How active is a pathway in each sample? | {api}`gsva` | [Pathway and regulator activity](guides/pathway_activity.ipynb) |
+| Which regulators and pathways are active? | {api}`ulm` | [Pathway and regulator activity](guides/pathway_activity.ipynb) |
+| What do several studies say together? | {api}`meta_analysis` | [Align and combine studies](guides/combine_studies.ipynb) |
+| Which changes replicate across studies? | {api}`sumrank` | [Replication across studies](guides/replication_across_studies.ipynb) |
-The [Literature](reference/literature.md) page collects studies behind these questions,
-and the [Capability matrix](reference/capabilities.md) lists every method with the
-reference it is checked against, and what is not implemented yet.
+Grouping patients by their profiles, changes in cell-cell interactions and the response
+to drugs and doses are not answered yet ([Not implemented
+yet](statistics/choosing_a_method.md#not-implemented-yet)). Cubes convert to AnnData and
+MuData for other tools ([Work with AnnData and MuData](guides/anndata_and_mudata.ipynb)).
## Principles
- **Samples are the unit of replication.** Cells are pooled into samples before any
test, and a cube records how many cells each profile holds.
- **Traceable results.** A cube's fingerprint names its values, its provenance record
- says how it was made, and every result keeps both.
+ says how it was made, and the results of the analyses of a cube, such as those of
+ `limma_voom` and `propeller`, keep both.
- **Checked against the reference implementations.** Each area has a script in
[`validation/`](https://github.com/NygenAnalytics/CytePool/tree/master/validation)
- that runs the reference package as a separate program on the same inputs.
-- **Implemented from the literature.** Methods whose reference packages carry copyleft
- licences are written from their papers and documentation, never from their source
- code, so that CytePool can stay BSD-licensed.
+ that runs an independent reference, usually the package that defined the method, as a
+ separate program on the same inputs.
+- **Implemented from the literature.** CytePool is written from the papers and
+ documentation of the methods, and stays BSD-licensed ([Related
+ software](reference/related_software.md)).
- **Two required dependencies,** numpy and numba. Numerical routines are compiled the
first time they run and cached on disk.
diff --git a/docs/source/installation.md b/docs/source/installation.md
index bf5a083..0c0e187 100644
--- a/docs/source/installation.md
+++ b/docs/source/installation.md
@@ -5,9 +5,9 @@ description: Install CytePool from its repository with Python 3.13 or newer, set
(installation)=
# Installation
-CytePool needs Python 3.13 or newer. Its only required dependencies, numpy and numba,
-are installed with it. Nothing is released on PyPI yet, so install it from the
-repository.
+Install CytePool from its repository with uv or pip: it needs Python 3.13 or newer, and
+its only required dependencies, numpy and numba, are installed with it. Nothing is
+released on PyPI yet.
## With uv
@@ -43,7 +43,10 @@ uv pip install "cytepool[mudata] @ git+https://github.com/NygenAnalytics/CytePoo
```
The `mudata` extra installs anndata too. CytePool imports them only when a
-conversion runs, and says which extra to install when one is missing.
+conversion runs, and says which extra to install when one is missing. anndata needs
+h5py and numcodecs, which have no wheels for Python 3.15 yet, so on Python 3.15 the
+`test` extra leaves anndata and mudata out and the tests of these conversions are
+skipped.
The figures of `cytepool.plotting` need matplotlib, which the `plot` extra installs;
it is imported only when a figure is drawn.
@@ -58,10 +61,12 @@ temporary directory. Any change to the installed CytePool source starts a new ca
Parallel routines use numba's threads: `from_cells`, the fits of linear models in
which each gene needs its own fit (with observation weights, as from voom, or missing
-values), which `limma_voom` runs, and the fits of negative binomial models. Set
-`NUMBA_NUM_THREADS` before Python starts to cap them, for example on a shared machine,
-or pass `n_threads` to a function that takes it; results do not depend on the number
-of threads. With numba's OpenMP threading layer, a process that has run a parallel
+values), which `limma_voom` runs, the fits of negative binomial models and DESeq2's, the
+rotation gene-set tests, GSEA, the pathway scores and the activities. The fits of mixed
+models run on Python threads instead, in compiled code that releases the GIL, as many
+as numba's setting allows. Set `NUMBA_NUM_THREADS` before Python starts to cap them,
+for example on a shared machine, or pass `n_threads` to a function that takes it;
+results do not depend on the number of threads. With numba's OpenMP threading layer, a process that has run a parallel
routine cannot fork worker processes that run one again: start workers with the
"spawn" or "forkserver" method of `multiprocessing` instead. With one thread
(`n_threads=1`, or `numba.set_num_threads(1)`), the fits of linear and negative
diff --git a/docs/source/myst.yml b/docs/source/myst.yml
index ee74694..515f273 100644
--- a/docs/source/myst.yml
+++ b/docs/source/myst.yml
@@ -24,7 +24,7 @@ project:
# To add a page, add its file here. A notebook page also needs a saved
# execution: make -C docs execute PAGES="". MyST needs a file first, the
# Overview; the six groups after it are the ones docs/page_index.py lists, in that
- # order.
+ # order, and the API reference comes last.
toc:
- file: index.md
title: Overview
@@ -60,63 +60,86 @@ project:
title: Gene sets and pathways
- file: guides/pathway_activity.ipynb
title: Pathway and regulator activity
- - title: Other designs and data
- children:
- file: guides/bulk_rna_seq.ipynb
title: Bulk RNA-seq
+ - title: Combine studies
+ children:
- file: guides/combine_studies.ipynb
- title: Combine studies
+ title: Align and combine studies
- file: guides/replication_across_studies.ipynb
title: Replication across studies
- - title: How the statistics work
+ # The chapters, in reading order: the map of questions, then the parts.
+ - title: Understand the methods
children:
- file: statistics/choosing_a_method.md
title: Choosing a method
- - file: statistics/cubes.md
- title: The cube and its additive layers
- - file: statistics/linear_models.md
- title: Designs, linear models and contrasts
- - file: statistics/empirical_bayes.md
- title: Empirical Bayes moderation and TREAT
- - file: statistics/precision_weights.md
- title: Precision weights
- - file: statistics/count_models.md
- title: Count models
- - file: statistics/mixed_models.md
- title: Mixed models, REML and dream
- - file: statistics/gene_set_tests.md
- title: Gene-set tests
+ - title: Foundations
+ children:
+ - file: statistics/cubes.md
+ title: Pseudobulk profiles and cubes
+ - file: statistics/linear_models.md
+ title: Designs, linear models and contrasts
+ - file: statistics/empirical_bayes.md
+ title: Empirical Bayes moderation
+ - file: statistics/multiple_testing.md
+ title: Multiple testing
+ - title: Differential expression
+ children:
+ - file: statistics/normalization_and_filtering.md
+ title: Normalization and filtering
+ - file: statistics/limma_voom.md
+ title: limma-voom and limma-trend
+ - file: statistics/count_models.md
+ title: edgeR and DESeq2
+ - file: statistics/mixed_models.md
+ title: Mixed models and dream
- file: statistics/composition.md
title: Composition tests
- - file: statistics/meta_analysis.md
- title: Meta-analysis
- - file: statistics/multiple_testing.md
- title: Multiple testing
+ - title: Gene sets and pathways
+ children:
+ - file: statistics/gene_set_tests.md
+ title: Gene-set tests
+ - file: statistics/over_representation_and_gsea.md
+ title: Over-representation and GSEA
+ - file: statistics/pathway_scores.md
+ title: Pathway scores per sample
+ - file: statistics/activities.md
+ title: Activities from networks
+ - title: Several studies
+ children:
+ - file: statistics/aligning_studies.md
+ title: Aligning studies
+ - file: statistics/meta_analysis.md
+ title: Meta-analysis
+ - file: statistics/replication.md
+ title: Replication
- title: Reference
children:
- - title: API reference
- url: /api/index.html
- open_in_same_tab: true
- - file: reference/cube_files.md
- title: Cube files and provenance records
- - file: reference/capabilities.md
- title: Capability matrix
- file: validation.md
title: Validation
- - file: reference/datasets.md
- title: Datasets
- - file: reference/related_software.md
- title: Related software
- - file: reference/literature.md
- title: Literature
- - file: reference/bibliography.md
- title: Bibliography
- file: reference/glossary.md
title: Glossary
- - file: reference/citing.md
- title: How to cite
+ - title: Files and datasets
+ children:
+ - file: reference/cube_files.md
+ title: Cube files and provenance records
+ - file: reference/datasets.md
+ title: Datasets
+ - title: Citing and related software
+ children:
+ - file: reference/citing.md
+ title: How to cite
+ - file: reference/bibliography.md
+ title: Bibliography
+ - file: reference/related_software.md
+ title: Related software
- url: https://github.com/NygenAnalytics/CytePool/issues
title: Report a bug
+ # build_docs.py gives this entry the pages of the API reference as children, in
+ # the groups of its index (docs/source/reference/api/index.md).
+ - title: API reference
+ url: /api/index.html
+ open_in_same_tab: true
site:
template: https://github.com/myst-templates/book-theme/archive/23f2df5493e23dfb1d9433c8aa64c87beb4232a3.zip
parts:
diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb
index d15622c..caf7c3d 100644
--- a/docs/source/quickstart.ipynb
+++ b/docs/source/quickstart.ipynb
@@ -9,11 +9,9 @@
"# Quick start\n",
"\n",
"Which genes does a stimulation change in each cell type, in a way that holds for other\n",
- "donors and not only for the cells that were measured? This page answers that question\n",
- "for a simulated study, from the cells to a result whose origin can be checked. It pools\n",
- "the cells of every sample and cell type into a cube of pseudobulk profiles, tests the\n",
- "stimulation in every cell type with limma-voom, and traces a result back to the\n",
- "profiles and the cube it came from. Each step agrees with the package it follows\n",
+ "donors and not only for the cells measured? This page answers it for a simulated study:\n",
+ "{api}`from_cells` pools the cells into a cube, {api}`limma_voom` tests every cell type,\n",
+ "and a result is traced to its cube. Each step agrees with the package it follows\n",
"([Validation](validation.md)), and [Why pseudobulk](why_pseudobulk.ipynb) shows why the\n",
"tests compare samples, not cells.\n",
"\n",
@@ -49,7 +47,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"Cube with 24 samples x 6 groups x 2,000 features\n",
" layers: counts (uint32), detected (uint32), log_sum (float64),\n",
" log_sum_sq (float64), linear_sum (float64)\n",
@@ -108,17 +106,19 @@
"source": [
"The profiles pool from 4 to 444 cells. A profile of a few cells is noisy, so\n",
"{api}`limma_voom` leaves the profiles of fewer than 10 cells out of their cell type's\n",
- "fit: the six pale entries, all of them dendritic cells.\n",
+ "fit: the six pale entries, all of them dendritic cells ([Samples with few cells or\n",
+ "counts](statistics/normalization_and_filtering.md#samples-with-few-cells)).\n",
"\n",
"## Test every cell type\n",
"\n",
"Every donor gives a control and a stimulated sample. The design `~ donor + condition`\n",
"compares the two samples of each donor, so that the differences between donors do not\n",
- "count as noise, and its coefficient `conditionstim` is the log2 fold change of the\n",
- "stimulation. {api}`limma_voom` then takes one cell type at a time: it keeps the genes\n",
- "with enough counts, normalizes the libraries by TMM, gives every observation a voom\n",
- "precision weight, fits the design to every gene and moderates the contrast by empirical\n",
- "Bayes [@law2014; @ritchie2015]."
+ "count as noise ([A paired design](statistics/linear_models.md#paired-design)), and its\n",
+ "coefficient `conditionstim` is the log2 fold change of the stimulation.\n",
+ "{api}`limma_voom` then takes one cell type at a time: it keeps the genes with enough\n",
+ "counts, normalizes the libraries by TMM, gives every observation a voom precision\n",
+ "weight, fits the design to every gene and moderates the contrast by empirical Bayes\n",
+ "([What runs in each cell type](statistics/limma_voom.md#limma-voom-steps))."
]
},
{
@@ -192,7 +192,7 @@
"id": "6572bdf4",
"metadata": {
"figure_descriptions": [
- "Volcano plot of the monocyte genes: -log10 p-value, up to about 300, against the log2 fold change of the stimulation, from about -2 to 2.5. The 95 genes up, in red, and the 62 down, in blue, lie at fold changes beyond about 0.7 in either direction; the 1,840 genes that are not significant, in grey, gather near zero. The ten genes with the smallest p-values are named, all of them up, among them gene_1493, gene_0766 and gene_0640."
+ "Volcano plot of the monocyte genes: -log10 p-value, up to about 300, against the log2 fold change of the stimulation, from about -2 to 2.5. Of the 95 genes up, in red, and the 62 down, in blue, 139 lie at fold changes beyond about 0.7 in either direction, and 18, all but one of them down, lie near zero, at -log10 p-values below 5; the 1,840 genes that are not significant, in grey, gather near zero. The ten genes with the smallest p-values are named, all of them up, among them gene_1493, gene_0766 and gene_0640."
]
},
"outputs": [
@@ -382,7 +382,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:23:52+00:00",
+ "executed_at": "2026-10-28T00:18:11+00:00",
"executed_code_sha256": "0ded631f07098d277adda0deb810874bcd1d2153bc220c1ee0cae53624f4577c",
"executed_datasets": {
"simulated_pbmc": {
@@ -390,8 +390,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 4.2,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 3.7,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/docs/source/reference/api/activity.md b/docs/source/reference/api/activity.md
index e72154c..ffcaec2 100644
--- a/docs/source/reference/api/activity.md
+++ b/docs/source/reference/api/activity.md
@@ -6,16 +6,8 @@ myst:
# Pathway and regulator activity
-{func}`~cytepool.ulm`, {func}`~cytepool.mlm` and {func}`~cytepool.weighted_aggregate`
-score every source of a network, a transcription factor or a pathway, in every
-profile: the samples of an expression matrix or of a cell type of a cube, or the
-contrasts of a differential-expression result, as decoupler's `mt.ulm`, `mt.mlm` and
-`mt.waggr` compute them. The activities, an {class}`~cytepool.Activities`, are profiles
-x sources, so that {func}`~cytepool.limma` tests the activities of samples with the
-design built for the cube; {func}`~cytepool.plotting.plot_activities` draws them as a
-heatmap and {func}`~cytepool.plotting.plot_top_activities` the top sources of a profile
-as bars. The networks are {class}`~cytepool.Network` objects, such as those of
-{func}`~cytepool.collectri` and {func}`~cytepool.progeny`.
+Activities of regulators and pathways by ULM, MLM and the weighted aggregate;
+[Activities from networks](../statistics/activities/#ulm) explains them.
```{eval-rst}
.. automodule:: cytepool.activity
diff --git a/docs/source/reference/api/anndata.md b/docs/source/reference/api/anndata.md
index 52bdf1d..c284d4a 100644
--- a/docs/source/reference/api/anndata.md
+++ b/docs/source/reference/api/anndata.md
@@ -6,16 +6,9 @@ myst:
# AnnData and MuData
-scverse keeps pseudobulk profiles in two layouts, and CytePool reads and writes both.
-{func}`~cytepool.to_anndata` and {func}`~cytepool.from_anndata` convert a cube to and
-from a long AnnData, with a row per sample and cell type, as `scanpy.get.aggregate`
-and `decoupler.pp.pseudobulk` make it; {func}`~cytepool.to_mudata` and
-{func}`~cytepool.from_mudata` convert it to and from a MuData with a modality per cell
-type and the samples as its rows, as LIANA's `adata_to_views` makes it. An import
-takes only sums and counts of cells, so a cube made from aggregated profiles holds
-what one made from the cells would hold, and a cube exported and read back has the
-same fingerprint. anndata and mudata are optional: install CytePool's `anndata` or
-`mudata` extra.
+Conversions of cubes to and from the AnnData and MuData layouts of scverse;
+[Pseudobulk profiles and cubes](../statistics/cubes/#profiles-from-other-tools)
+explains what an import takes.
```{eval-rst}
.. automodule:: cytepool.io.anndata
diff --git a/docs/source/reference/api/combined_p_values.md b/docs/source/reference/api/combined_p_values.md
index 6ea39e6..8cfe08b 100644
--- a/docs/source/reference/api/combined_p_values.md
+++ b/docs/source/reference/api/combined_p_values.md
@@ -6,16 +6,8 @@ myst:
# Combined p-values
-{func}`~cytepool.stats.combined_p_values.combine_p_values` combines, for every gene,
-the p-values of the studies that tested it into one p-value of the hypothesis that no
-study has an effect: by Fisher's, Stouffer's (with weights, such as the square roots of
-the studies' sample sizes), Tippett's, Wilkinson's, Pearson's, Mudholkar and George's
-or Simes's method, as scipy's `combine_pvalues` and the R package metap compute them.
-With the directions of the effects, two-sided p-values become one-sided ones, and
-effects in opposite directions count against each other.
-{func}`~cytepool.stats.combined_p_values.partial_conjunction` gives the p-value that at
-least u of the studies have an effect, by combining the largest p-values (Benjamini and
-Heller 2008). A study that did not test a gene is left out of its combination.
+Combined p-values of several studies, and partial conjunction tests that an effect
+replicates; [Replication](../statistics/replication/#combined-p-values) explains them.
```{eval-rst}
.. automodule:: cytepool.stats.combined_p_values
diff --git a/docs/source/reference/api/composition.md b/docs/source/reference/api/composition.md
index 8049348..ead686b 100644
--- a/docs/source/reference/api/composition.md
+++ b/docs/source/reference/api/composition.md
@@ -6,16 +6,8 @@ myst:
# Cell-type composition
-{func}`~cytepool.propeller` tests whether the proportions of cell types differ between
-conditions, as speckle's `propeller`, `propeller.ttest` and `propeller.anova` do. It
-reads the number of cells of every sample in every group of a cube (or a table of
-them), transforms the proportions (logit or arcsine square root), fits a linear model
-with a {class}`~cytepool.Design` matched to the samples by `sample_id`, moderates the
-variances of the cell types by empirical Bayes, and tests the design's named contrasts
-by moderated t-statistics and the levels of a factor by a moderated F-statistic. The
-result, a {class}`~cytepool.CompositionResult`, holds speckle's columns for every cell
-type, the samples and cell types left out, the proportions, and a record that
-identifies the cell numbers by the cube's fingerprint.
+propeller's tests of cell-type composition; [Composition
+tests](../statistics/composition/) explains them.
```{eval-rst}
.. automodule:: cytepool.composition
diff --git a/docs/source/reference/api/cube_files.md b/docs/source/reference/api/cube_files.md
index a22cec5..3909bc3 100644
--- a/docs/source/reference/api/cube_files.md
+++ b/docs/source/reference/api/cube_files.md
@@ -6,11 +6,10 @@ myst:
# Cube files
-A cube file is a Zarr v3 group that any Zarr v3 reader can open.
-{func}`~cytepool.write_cube` saves a cube and {func}`~cytepool.read_cube` opens one,
-leaving its layers on disk until they are used; a read then opens only the chunks that
-hold the selected groups and features. `cytepool.io.zarr` is the minimal Zarr v3
-reader and writer behind them.
+Cube files, and the Zarr v3 reader and writer they use; [Cube files and provenance
+records](../reference/cube-files/#cube-file-format) specifies the format, and
+[Pseudobulk profiles and cubes](../statistics/cubes/#cubes-larger-than-memory)
+explains how a cube larger than memory is read.
```{eval-rst}
.. autofunction:: cytepool.write_cube
diff --git a/docs/source/reference/api/cubes.md b/docs/source/reference/api/cubes.md
index b0cde68..86af7f2 100644
--- a/docs/source/reference/api/cubes.md
+++ b/docs/source/reference/api/cubes.md
@@ -6,15 +6,9 @@ myst:
# Pseudobulk cubes and tables
-A cube holds additive statistics of the cells of each sample and group (such as a
-cell type), for every feature: counts, detected cells, and sums of normalized and
-log-normalized expression. Because the statistics add up, groups can be merged and
-samples pooled without going back to the cells. {func}`~cytepool.from_cells` pools a
-cell-level count matrix in one pass, and {func}`~cytepool.from_bulk` wraps a samples x
-features count matrix as a cube with one group. Sample, group and feature metadata are
-{class}`~cytepool.Table` objects. {class}`~cytepool.CubeCollection` aligns the cubes of
-several studies by sample, gene and cell type, with what each study measured, for
-analyses of each study and for one cube of them all.
+Cubes, their tables, and the collections of the cubes of several studies;
+[Pseudobulk profiles and cubes](../statistics/cubes/) explains cubes, and [Aligning
+studies](../statistics/aligning-studies/) their collections.
```{eval-rst}
.. autoclass:: cytepool.Cube
@@ -44,12 +38,9 @@ analyses of each study and for one cube of them all.
## Fingerprints and provenance
-A cube's fingerprint identifies its values, tables and cell numbers included, so a
-result can name the cube it was computed from, and its provenance records how the cube
-was made: the caller's record of the source, the settings of the function that built
-it, and every operation that derived it, with what it kept or pooled and the
-fingerprint of the cube it was applied to. {meth}`~cytepool.Cube.history` reads the
-record back in plain terms.
+The fingerprint that names a cube's values and the record of how it was made; [Cube
+files and provenance records](../reference/cube-files/#fingerprint-encoding) specifies
+both.
```{eval-rst}
.. automodule:: cytepool.cube.fingerprints
@@ -61,7 +52,8 @@ record back in plain terms.
:module: cytepool.cube.fingerprints
:value: 1
- The version of the encoding that this module documents and computes.
+ The version of the encoding of fingerprints that this module computes,
+ https://docs.nygen.io/CytePool/reference/cube-files#fingerprint-encoding.
```
```{eval-rst}
diff --git a/docs/source/reference/api/deseq2.md b/docs/source/reference/api/deseq2.md
index bcefce8..e55bdd3 100644
--- a/docs/source/reference/api/deseq2.md
+++ b/docs/source/reference/api/deseq2.md
@@ -6,13 +6,8 @@ myst:
# DESeq2
-{func}`~cytepool.stats.deseq2.fit_deseq2` estimates DESeq2's dispersions and fits every
-gene as `DESeq()` does; {func}`~cytepool.stats.deseq2.deseq2_wald_test` tests a
-coefficient or a contrast and {func}`~cytepool.stats.deseq2.deseq2_likelihood_ratio_test`
-a reduced design against the full one, such as all the levels of a factor at once, each
-with the columns of `results()`. {func}`~cytepool.stats.deseq2.deseq2_dispersions` gives
-the dispersion estimates of every stage, and takes given values for each stage, as
-DESeq2's lower-level functions do.
+DESeq2's dispersions, fits, Wald tests and likelihood ratio tests; [edgeR and
+DESeq2](../statistics/count-models/#deseq2) explains them.
```{eval-rst}
.. automodule:: cytepool.stats.deseq2
diff --git a/docs/source/reference/api/designs.md b/docs/source/reference/api/designs.md
index ef193b4..bbac77f 100644
--- a/docs/source/reference/api/designs.md
+++ b/docs/source/reference/api/designs.md
@@ -6,26 +6,45 @@ myst:
# Designs and contrasts
-A {class}`~cytepool.Design` joins a sample table, the fixed effects as an R formula,
-optional random effects in lme4 notation and named contrasts. Its model matrix has the
-columns and column names that R's `model.matrix` gives for the same level order.
-Without explicit levels, factor levels are sorted by code point, as R sorts them with
-`LC_COLLATE=C`; R's default follows the session's locale.
-{meth}`~cytepool.Design.check` reports problems such as aliased columns before anything
-is fitted.
+Designs, their formulas, model matrices, contrasts and checks; [Designs, linear models
+and contrasts](../statistics/linear-models/) explains them.
```{eval-rst}
.. autoclass:: cytepool.Design
+
+.. automodule:: cytepool.design
+ :no-members:
```
-The parts of a design are in `cytepool.design`: its model matrix, its contrasts, its
-formula and random effects as they are read, and the report of its check.
+## Formulas
```{eval-rst}
-.. automodule:: cytepool.design
+.. automodule:: cytepool.design.formula
+ :no-members:
+ :no-index:
+
+.. autofunction:: cytepool.design.parse_formula
+
+.. autoclass:: cytepool.design.FormulaTerms
+
+.. autofunction:: cytepool.design.parse_random_effects
+
+.. autoclass:: cytepool.design.RandomEffect
+
+.. autofunction:: cytepool.design.formula.quote_name
```
+## Model matrices
+
```{eval-rst}
+.. automodule:: cytepool.design.model_matrix
+ :no-members:
+ :no-index:
+
+.. autofunction:: cytepool.design.model_matrix
+
+.. autoclass:: cytepool.design.ModelMatrix
+
.. py:data:: INTERCEPT
:module: cytepool.design
:value: '(Intercept)'
@@ -33,10 +52,26 @@ formula and random effects as they are read, and the report of its check.
The name of the intercept column of a model matrix, as R names it.
```
-Column names and term labels write a variable name that is not syntactic in R between
-backticks, as R does: a factor `cell type` gives columns such as `` `cell type`B ``,
-and a contrast refers to them by the same names.
+## Contrasts
```{eval-rst}
-.. autofunction:: cytepool.design.formula.quote_name
+.. automodule:: cytepool.design.contrasts
+ :no-members:
+ :no-index:
+
+.. autofunction:: cytepool.design.make_contrasts
+
+.. autoclass:: cytepool.design.Contrasts
+```
+
+## Design checks
+
+```{eval-rst}
+.. automodule:: cytepool.design.checks
+ :no-members:
+ :no-index:
+
+.. autoclass:: cytepool.design.DesignReport
+
+.. autoclass:: cytepool.design.DesignIssue
```
diff --git a/docs/source/reference/api/differential_expression.md b/docs/source/reference/api/differential_expression.md
index 5afacaf..e116947 100644
--- a/docs/source/reference/api/differential_expression.md
+++ b/docs/source/reference/api/differential_expression.md
@@ -1,26 +1,15 @@
---
myst:
html_meta:
- description: Differential expression of every cell type of a pseudobulk cube by limma-voom, limma-trend and dream in CytePool, with options for pseudobulk data, the samples behind each fit and a provenance record.
+ description: Differential expression of every cell type of a pseudobulk cube by limma-voom, limma-trend and dream in CytePool, with options for pseudobulk data, the samples of each fit and a provenance record.
---
# Differential expression
-{func}`~cytepool.limma_voom` and {func}`~cytepool.limma_trend` test the contrasts of a
-{class}`~cytepool.Design` in every group (cell type) of a cube, one group at a time, as
-the limma and edgeR User's Guides do for RNA-seq counts, and
-{func}`~cytepool.dream_voom` does so with the random intercepts of the design, such as
-a donor's, as variancePartition's dream does. The design is matched to the cube by
-`sample_id`; samples with too few cells are left out of a group's fit, and a group
-whose design becomes rank deficient is fitted on its column space, with the contrasts
-its samples can estimate. Options for pseudobulk data, each off by default, add sample
-quality weights, prior weights from the number of cells, a minimum total count per
-sample, and p-values adjusted across groups. The result holds a table per group and
-contrast, a summary of every group, the samples used and left out with the weights of
-each fit, and a record that traces every number to the profiles of the cube that
-produced it. It keeps the cube and design it analysed, and
-{meth}`~cytepool.DifferentialExpressionResult.fit` computes a group's fit again, with
-the values, weights and moderated tests behind its tables.
+{func}`~cytepool.limma_voom`, {func}`~cytepool.limma_trend` and
+{func}`~cytepool.dream_voom` test the contrasts of a design in every cell type of a
+cube; [limma-voom and limma-trend](../statistics/limma-voom/) and [Mixed models and
+dream](../statistics/mixed-models/) explain them.
```{eval-rst}
.. automodule:: cytepool.differential_expression
diff --git a/docs/source/reference/api/dream.md b/docs/source/reference/api/dream.md
index 8c11af7..404b51e 100644
--- a/docs/source/reference/api/dream.md
+++ b/docs/source/reference/api/dream.md
@@ -6,16 +6,8 @@ myst:
# dream
-{func}`~cytepool.stats.dream.voom_with_dream_weights` gives voom's precision weights
-from mixed models of every gene, fitted by maximum likelihood, as
-variancePartition's `voomWithDreamWeights` does.
-{func}`~cytepool.stats.dream.moderated_fixed_effect_tests` moderates the
-Satterthwaite or Kenward-Roger t-tests of
-{func}`~cytepool.stats.mixed_models.fixed_effect_tests` by empirical Bayes, as
-variancePartition's `eBayes` does for `dream` fits, adds `topTable`'s z-statistic, and
-tests the contrasts at once by the moderated F-statistic.
-{func}`~cytepool.dream_voom` runs these steps on every cell type of a cube, with
-dream's choice of the degrees of freedom.
+dream's precision weights and moderated tests of mixed models; [Mixed models and
+dream](../statistics/mixed-models/#dream) explains them.
```{eval-rst}
.. automodule:: cytepool.stats.dream
diff --git a/docs/source/reference/api/empirical_bayes.md b/docs/source/reference/api/empirical_bayes.md
index d83b512..a6c5af7 100644
--- a/docs/source/reference/api/empirical_bayes.md
+++ b/docs/source/reference/api/empirical_bayes.md
@@ -6,6 +6,9 @@ myst:
# Empirical Bayes moderation
+Moderated t and F statistics, B-statistics and TREAT; [Empirical Bayes
+moderation](../statistics/empirical-bayes/) explains them.
+
```{eval-rst}
.. automodule:: cytepool.stats.empirical_bayes
```
diff --git a/docs/source/reference/api/gene_set_tests.md b/docs/source/reference/api/gene_set_tests.md
index cc94722..cccdde8 100644
--- a/docs/source/reference/api/gene_set_tests.md
+++ b/docs/source/reference/api/gene_set_tests.md
@@ -6,10 +6,8 @@ myst:
# Gene-set tests
-{func}`~cytepool.camera` and {func}`~cytepool.fry` test gene sets on one contrast of a
-linear model: of a group of a result of {func}`~cytepool.limma_voom` or
-{func}`~cytepool.limma_trend`, whose fit they compute again, or of values with a design
-and a contrast.
+limma's camera and fry on one contrast of a linear model; [Gene-set
+tests](../statistics/gene-set-tests/#camera) explains them.
```{eval-rst}
.. automodule:: cytepool.gene_set_tests
diff --git a/docs/source/reference/api/gene_sets.md b/docs/source/reference/api/gene_sets.md
index 2ab4277..2ca556f 100644
--- a/docs/source/reference/api/gene_sets.md
+++ b/docs/source/reference/api/gene_sets.md
@@ -1,18 +1,14 @@
---
myst:
html_meta:
- description: Gene-set collections in CytePool, read from and written to the GMT files of MSigDB, Reactome, WikiPathways and the Gene Ontology, with each collection's name, version and licence, matched to the genes of an analysis and filtered by size.
+ description: Gene-set collections in CytePool, read from and written to the GMT files of MSigDB, Reactome and WikiPathways and the GMT exports of the Gene Ontology, with each collection's name, version and licence, matched to the genes of an analysis and filtered by size.
---
# Gene-set files
-{func}`~cytepool.read_gmt` reads a GMT file, one gene set per line, as MSigDB,
-Reactome, WikiPathways and the GMT exports of the Gene Ontology write them, into a
-{class}`~cytepool.GeneSets`: a mapping of set name to gene ids that every gene-set
-function of CytePool takes as its `gene_sets`, with each set's description and the
-name, version and licence of the collection, which you state when you read it, and the
-name and SHA-256 digest of the file. CytePool bundles no collection.
-{func}`~cytepool.write_gmt` writes a collection back.
+Gene-set collections, read from and written to GMT files; [Gene-set
+tests](../statistics/gene-set-tests/#gene-set-collections) explains how they are
+matched to an analysis.
```{eval-rst}
.. automodule:: cytepool.gene_sets
diff --git a/docs/source/reference/api/gsea.md b/docs/source/reference/api/gsea.md
index 9802724..439fd32 100644
--- a/docs/source/reference/api/gsea.md
+++ b/docs/source/reference/api/gsea.md
@@ -6,15 +6,8 @@ myst:
# Gene set enrichment analysis
-{func}`~cytepool.gsea` ranks genes by a statistic, of a group and contrast of a
-differential-expression result, of a table of results or of a mapping of gene id to
-statistic, and asks of each gene set whether its genes gather at the top or at the
-bottom of the ranking, as fgsea's `fgsea` computes it: the enrichment score and its
-leading edge, the score normalized by those of random sets of the same size, and
-p-values by fgsea's multilevel method or by permutations of the genes.
-{func}`~cytepool.plotting.plot_enrichment` draws the running enrichment score of one
-set, and {func}`~cytepool.plotting.plot_gsea_table` the top sets with their genes on
-the ranking.
+Gene set enrichment analysis of genes ranked by a statistic; [Over-representation
+and GSEA](../statistics/over-representation-and-gsea/#gsea) explains it.
```{eval-rst}
.. automodule:: cytepool.gene_set_enrichment
diff --git a/docs/source/reference/api/held_out_prediction.md b/docs/source/reference/api/held_out_prediction.md
index 718cb97..8d6f783 100644
--- a/docs/source/reference/api/held_out_prediction.md
+++ b/docs/source/reference/api/held_out_prediction.md
@@ -6,15 +6,8 @@ myst:
# Held-out prediction
-{func}`~cytepool.held_out_prediction` leaves each study of a
-{class}`~cytepool.StudyResults` out in turn, chooses genes up and down in each cell
-type from the results of the other studies, by SumRank and by a random-effects
-meta-analysis, scores every case and control of the held-out study by how high its
-profiles rank those genes, as UCell scores them, and gives the area under the ROC
-curve of cases against controls, by study and method, as Nakatsuka et al. (2025)
-compare methods. The cases and controls are the two levels that the contrast compares.
-{func}`~cytepool.plotting.plot_held_out_auc` draws the AUC of every held-out study by
-method.
+The prediction of cases in held-out studies by genes chosen from the others;
+[Replication](../statistics/replication/#held-out-prediction) explains it.
```{eval-rst}
.. automodule:: cytepool.held_out_studies
diff --git a/docs/source/reference/api/index.md b/docs/source/reference/api/index.md
index cb10966..bf07f13 100644
--- a/docs/source/reference/api/index.md
+++ b/docs/source/reference/api/index.md
@@ -32,22 +32,52 @@ differential-expression analysis.
```
```{toctree}
+:caption: Cubes and files
:maxdepth: 1
cubes
cube_files
anndata
+```
+
+```{toctree}
+:caption: Designs and linear models
+:maxdepth: 1
+
designs
-normalization
linear_models
-mixed_models
+empirical_bayes
+multiple_testing
+```
+
+```{toctree}
+:caption: Differential expression
+:maxdepth: 1
+
+differential_expression
+normalization
+principal_components
voom
-dream
sample_weights
negative_binomial_models
quasi_likelihood
deseq2
-empirical_bayes
+mixed_models
+dream
+variance_partitioning
+```
+
+```{toctree}
+:caption: Composition
+:maxdepth: 1
+
+composition
+```
+
+```{toctree}
+:caption: Gene sets and pathways
+:maxdepth: 1
+
gene_sets
gene_set_tests
rotation_gene_set_tests
@@ -57,18 +87,31 @@ pathway_scores
networks
activity
score_tests
-multiple_testing
-combined_p_values
-differential_expression
-variance_partitioning
-principal_components
-composition
-meta_analysis
+```
+
+```{toctree}
+:caption: Several studies
+:maxdepth: 1
+
study_results
+meta_analysis
+combined_p_values
replication
sumrank
held_out_prediction
+```
+
+```{toctree}
+:caption: Figures
+:maxdepth: 1
+
plotting
+```
+
+```{toctree}
+:caption: Building blocks on arrays
+:maxdepth: 1
+
distributions
smoothing
```
diff --git a/docs/source/reference/api/linear_models.md b/docs/source/reference/api/linear_models.md
index 2f4d140..47af622 100644
--- a/docs/source/reference/api/linear_models.md
+++ b/docs/source/reference/api/linear_models.md
@@ -1,11 +1,14 @@
---
myst:
html_meta:
- description: Fit one linear model to every gene by ordinary, weighted or generalised least squares, and test contrasts, as limma's lmFit and contrasts.fit do.
+ description: Fit one linear model to every gene by ordinary, weighted or generalized least squares, and test contrasts, as limma's lmFit and contrasts.fit do.
---
# Linear models
+The linear model fitted to every gene, and its contrasts; [Designs, linear models and
+contrasts](../statistics/linear-models/#one-model-per-gene) explains them.
+
```{eval-rst}
.. automodule:: cytepool.stats.linear_models
```
diff --git a/docs/source/reference/api/meta_analysis.md b/docs/source/reference/api/meta_analysis.md
index 7d39601..12d4f8a 100644
--- a/docs/source/reference/api/meta_analysis.md
+++ b/docs/source/reference/api/meta_analysis.md
@@ -6,23 +6,8 @@ myst:
# Meta-analysis
-{func}`~cytepool.meta_analysis` combines, for every gene, the log2 fold changes of
-several studies and their standard errors into an estimate of a common effect, as
-metafor's `rma.uni` does without moderators: the fixed-effect (inverse-variance)
-estimate, or a random-effects estimate with the between-study variance by
-DerSimonian-Laird, REML or Paule-Mandel, with Cochran's Q, I-squared, H-squared and
-z-tests or the Knapp-Hartung adjustment. It takes the results of
-{func}`~cytepool.limma_voom` or {func}`~cytepool.limma_trend` on several cubes, such as
-the same cell types in several cohorts, kept together as a
-{class}`~cytepool.StudyResults`: it matches groups, contrasts and genes by their ids,
-reads each table's log2 fold changes and moderated standard errors, and returns a table
-per cell type and contrast with p-values adjusted over its genes, the studies used and
-left out with the reason, and the provenance of every study. A gene measured in some
-studies only is analysed over those. {meth}`StudyResults.table
-` lines up the studies' results gene by gene, with the
-reason a study has no result for a gene, and
-{func}`~cytepool.stats.meta_analysis.fit_meta_analysis` meta-analyses arrays of effects
-and standard errors.
+Meta-analysis of every gene across studies by fixed and random effects, as metafor
+computes it; [Meta-analysis](../statistics/meta-analysis/) explains it.
```{eval-rst}
.. automodule:: cytepool.meta_analysis_of_studies
diff --git a/docs/source/reference/api/mixed_models.md b/docs/source/reference/api/mixed_models.md
index f2a093b..2181232 100644
--- a/docs/source/reference/api/mixed_models.md
+++ b/docs/source/reference/api/mixed_models.md
@@ -6,6 +6,10 @@ myst:
# Mixed models
+Linear mixed models with random intercepts fitted to every gene, and their t-tests and
+F-tests; [Mixed models and dream](../statistics/mixed-models/#mixed-model-reml) explains
+them.
+
```{eval-rst}
.. automodule:: cytepool.stats.mixed_models
```
diff --git a/docs/source/reference/api/multiple_testing.md b/docs/source/reference/api/multiple_testing.md
index 7561c32..bf85b72 100644
--- a/docs/source/reference/api/multiple_testing.md
+++ b/docs/source/reference/api/multiple_testing.md
@@ -6,6 +6,9 @@ myst:
# Multiple testing
+Adjusted p-values and the proportion of true nulls; [Multiple
+testing](../statistics/multiple-testing/) explains them.
+
```{eval-rst}
.. automodule:: cytepool.stats.multiple_testing
```
diff --git a/docs/source/reference/api/negative_binomial_models.md b/docs/source/reference/api/negative_binomial_models.md
index d8c4acb..b27f1af 100644
--- a/docs/source/reference/api/negative_binomial_models.md
+++ b/docs/source/reference/api/negative_binomial_models.md
@@ -6,11 +6,8 @@ myst:
# Negative binomial models
-{func}`~cytepool.stats.negative_binomial_models.fit_negative_binomial_model` fits the
-same negative binomial log-linear model to every gene, with known dispersions, as
-edgeR's `glmFit` does: one-way layouts group by group, other designs by Newton's method
-with Levenberg damping. It is the first step of edgeR's quasi-likelihood pipeline;
-dispersion estimation and the tests come next.
+Negative binomial models of every gene, as edgeR's `glmFit` fits them; [edgeR and
+DESeq2](../statistics/count-models/#negative-binomial-model) explains them.
```{eval-rst}
.. automodule:: cytepool.stats.negative_binomial_models
diff --git a/docs/source/reference/api/networks.md b/docs/source/reference/api/networks.md
index 723e99c..111d0d0 100644
--- a/docs/source/reference/api/networks.md
+++ b/docs/source/reference/api/networks.md
@@ -6,15 +6,10 @@ myst:
# Networks
-A {class}`~cytepool.Network` maps each source, a transcription factor or a pathway, to
-its target genes with signed weights, and records its name, version, licence, citation
-and the file it was read from. {func}`~cytepool.collectri` returns CollecTRI's regulons
-of human transcription factors, at the licence level you choose, and
-{func}`~cytepool.progeny` the pathway signatures of PROGENy for human or mouse; both
-download their files on first use, check them against pinned SHA-256 digests and keep
-them in a cache. The activity functions, such as {func}`~cytepool.ulm`, take a network,
-or a table with the columns `source`, `target` and `weight`, and
-{meth}`~cytepool.Network.to_gene_sets` gives each source's targets as a gene set.
+Networks of regulators and pathways with signed weights, CollecTRI and PROGENy
+among them; [Activities from networks](../statistics/activities/#regulator-networks)
+explains them, and [Datasets](../reference/datasets/#networks) gives their files
+and licences.
```{eval-rst}
.. automodule:: cytepool.networks
diff --git a/docs/source/reference/api/normalization.md b/docs/source/reference/api/normalization.md
index 9dc9ee4..60f408a 100644
--- a/docs/source/reference/api/normalization.md
+++ b/docs/source/reference/api/normalization.md
@@ -6,6 +6,10 @@ myst:
# Normalization and filtering
+Normalization factors, size factors, counts per million and the filter by expression;
+[Normalization and filtering](../statistics/normalization-and-filtering/#normalization-and-size-factors)
+explains them.
+
```{eval-rst}
.. automodule:: cytepool.normalization
```
diff --git a/docs/source/reference/api/over_representation.md b/docs/source/reference/api/over_representation.md
index 3208ea9..2807dd2 100644
--- a/docs/source/reference/api/over_representation.md
+++ b/docs/source/reference/api/over_representation.md
@@ -6,14 +6,9 @@ myst:
# Over-representation analysis
-{func}`~cytepool.over_representation` tests each gene set for over-representation
-among the genes called significant, by the one-sided hypergeometric test of limma's
-`kegga` and `goana`: for the genes that go up, those that go down and those that change
-either way in a group and contrast of a differential-expression result or in a table
-of results, or for a list of genes against a universe. Its table gives each set's genes
-in the universe, the overlap, the expected overlap, the p-value, the adjusted p-value
-and the genes of the overlap. {func}`~cytepool.plotting.plot_over_representation`
-draws the top sets.
+Over-representation analysis of the genes called significant; [Over-representation
+and GSEA](../statistics/over-representation-and-gsea/#over-representation)
+explains it.
```{eval-rst}
.. automodule:: cytepool.over_representation_analysis
diff --git a/docs/source/reference/api/pathway_scores.md b/docs/source/reference/api/pathway_scores.md
index 20d2800..305de94 100644
--- a/docs/source/reference/api/pathway_scores.md
+++ b/docs/source/reference/api/pathway_scores.md
@@ -6,13 +6,8 @@ myst:
# Pathway scores
-{func}`~cytepool.gsva`, {func}`~cytepool.ssgsea`, {func}`~cytepool.combined_z_scores`
-and {func}`~cytepool.plage` turn the expression values of every sample into one score
-per gene set, as the four methods of the R package GSVA compute them, from a samples x
-genes matrix or a cell type of a cube. The scores, a {class}`~cytepool.PathwayScores`,
-are profiles x sets, so that {func}`~cytepool.limma` tests them with the design built
-for the cube, and {func}`~cytepool.plotting.plot_pathway_scores` draws them as a
-heatmap.
+Scores of gene sets in every sample by GSVA, ssGSEA, combined z-scores and PLAGE;
+[Pathway scores per sample](../statistics/pathway-scores/#gsva) explains them.
```{eval-rst}
.. automodule:: cytepool.pathway_scores
diff --git a/docs/source/reference/api/plotting.md b/docs/source/reference/api/plotting.md
index 071b8f9..decd2b7 100644
--- a/docs/source/reference/api/plotting.md
+++ b/docs/source/reference/api/plotting.md
@@ -1,15 +1,14 @@
---
myst:
html_meta:
- description: Figures of CytePool's results in one call each, drawn as limma, edgeR, DESeq2, speckle and metafor draw them, and of the replication of studies, with matplotlib as an optional dependency.
+ description: Figures of CytePool's cubes and results in one call each, each drawn as the tool whose method it shows draws it, from limma, edgeR and DESeq2 to scanpy, decoupler and metafor, with matplotlib as an optional dependency.
---
# Figures
-{mod}`cytepool.plotting` draws, in one call each, the figures that limma, edgeR,
-DESeq2, speckle and metafor draw, from CytePool's cubes and results. It needs
-matplotlib: install CytePool's `plot` extra (`pip install "cytepool[plot]"`), or
-matplotlib itself.
+{mod}`cytepool.plotting` draws, in one call each, the figures of CytePool's cubes and
+results, each as the tool whose method it shows draws it. It needs matplotlib: install
+CytePool's `plot` extra (`pip install "cytepool[plot]"`), or matplotlib itself.
```{eval-rst}
.. automodule:: cytepool.plotting
diff --git a/docs/source/reference/api/principal_components.md b/docs/source/reference/api/principal_components.md
index c54d604..adc934f 100644
--- a/docs/source/reference/api/principal_components.md
+++ b/docs/source/reference/api/principal_components.md
@@ -6,14 +6,8 @@ myst:
# Principal components
-{func}`~cytepool.principal_components` places samples on the principal components of
-their most variable genes, as DESeq2's `plotPCA` does: the `top` genes of largest
-variance over the samples, each centred, decomposed by singular values. It takes a
-samples x genes matrix of log-expression values or a cell type of a cube, whose samples
-are prepared as edgeR prepares counts for `plotMDS`. The result, a
-{class}`~cytepool.PrincipalComponents`, holds the samples' coordinates, each
-component's share of the variance, the features used and their loadings, and
-{func}`~cytepool.plotting.plot_pca` draws it.
+Principal components of samples, as DESeq2's `plotPCA` computes them; [Normalization and
+filtering](../statistics/normalization-and-filtering/#samples-at-a-glance) explains them.
```{eval-rst}
.. automodule:: cytepool.principal_component_analysis
diff --git a/docs/source/reference/api/quasi_likelihood.md b/docs/source/reference/api/quasi_likelihood.md
index 92bf582..e412abb 100644
--- a/docs/source/reference/api/quasi_likelihood.md
+++ b/docs/source/reference/api/quasi_likelihood.md
@@ -1,21 +1,14 @@
---
myst:
html_meta:
- description: edgeR's quasi-likelihood F-tests in CytePool, as glmQLFit and glmQLFTest compute them by default in edgeR 4, with bias-adjusted deviances, the common dispersion of the top genes, average log CPM and exact moments of the negative binomial unit deviance.
+ description: edgeR's quasi-likelihood F-tests in CytePool, as glmQLFit and glmQLFTest compute them by default in edgeR 4.4, with bias-adjusted deviances, the common dispersion of the top genes, average log CPM and exact moments of the negative binomial unit deviance.
---
# Quasi-likelihood F-tests
-{func}`~cytepool.stats.quasi_likelihood.fit_quasi_likelihood` and
-{func}`~cytepool.stats.quasi_likelihood.quasi_likelihood_f_test` are edgeR's
-`glmQLFit` and `glmQLFTest` as edgeR 4 runs them by default: the negative binomial
-dispersion is the Cox-Reid common dispersion of the most highly expressed genes
-({func}`~cytepool.stats.quasi_likelihood.common_dispersion`), each gene's
-quasi-dispersion comes from deviances adjusted for small counts, and limma's prior for
-unequal degrees of freedom moderates them, towards a trend in the average log CPM
-({func}`~cytepool.stats.quasi_likelihood.average_log2_cpm`). The tests give edgeR's
-columns logFC, logCPM, F, PValue and FDR for coefficients or named contrasts of a
-design.
+edgeR's quasi-likelihood F-tests, as `glmQLFit` and `glmQLFTest` run them by default in
+edgeR 4.4; [edgeR and DESeq2](../statistics/count-models/#quasi-likelihood) explains
+them.
```python
from cytepool import Design
diff --git a/docs/source/reference/api/replication.md b/docs/source/reference/api/replication.md
index e5aa5e9..8a5594e 100644
--- a/docs/source/reference/api/replication.md
+++ b/docs/source/reference/api/replication.md
@@ -4,17 +4,11 @@ myst:
description: How well the studies of a CubeCollection replicate each other's differential expression in CytePool, gene by gene and study by study, with combined and partial conjunction p-values, sign agreement, heterogeneity, pi1 between studies and the genes found in at least k studies.
---
-# Replication across studies
+# Replication tables
-{func}`~cytepool.replication` reads one cell type and contrast of the studies' results,
-a {class}`~cytepool.StudyResults`, lined up gene by gene by {meth}`StudyResults.table
-`, and gives for every gene its combined p-value, its
-partial conjunction p-value of an effect in at least u studies in one direction, the
-studies that find it and the heterogeneity of its log2 fold changes; for every pair of
-studies, pi1 of one study's p-values among the genes the other finds, and their sign
-agreement; and the number of genes found in at least k studies.
-{func}`~cytepool.plotting.plot_study_agreement` and
-{func}`~cytepool.plotting.plot_replicated_genes` draw them.
+The replication tables of one cell type and contrast across the studies of a
+collection; [Replication](../statistics/replication/#replication-tables) explains what
+each table answers.
```{eval-rst}
.. automodule:: cytepool.replication_across_studies
diff --git a/docs/source/reference/api/rotation_gene_set_tests.md b/docs/source/reference/api/rotation_gene_set_tests.md
index bb3a89d..a67ee68 100644
--- a/docs/source/reference/api/rotation_gene_set_tests.md
+++ b/docs/source/reference/api/rotation_gene_set_tests.md
@@ -6,11 +6,8 @@ myst:
# Rotation gene-set tests
-{func}`~cytepool.roast`, {func}`~cytepool.mroast` and {func}`~cytepool.romer` test gene
-sets by rotations of the residuals on one contrast of a linear model: of a group of a
-result of {func}`~cytepool.limma_voom` or {func}`~cytepool.limma_trend`, whose fit they
-compute again, or of values with a design and a contrast. romer takes sample weights
-only, so of the two results it tests those of limma-trend.
+limma's rotation tests roast, mroast and romer on one contrast of a linear model;
+[Gene-set tests](../statistics/gene-set-tests/#roast) explains them.
```{eval-rst}
.. automodule:: cytepool.rotation_gene_set_tests
diff --git a/docs/source/reference/api/sample_weights.md b/docs/source/reference/api/sample_weights.md
index d5cec60..8941b66 100644
--- a/docs/source/reference/api/sample_weights.md
+++ b/docs/source/reference/api/sample_weights.md
@@ -6,6 +6,9 @@ myst:
# Sample quality weights
+Sample quality weights of a matrix of log-expression values; [limma-voom and
+limma-trend](../statistics/limma-voom/#sample-quality-weights) explains them.
+
```{eval-rst}
.. automodule:: cytepool.stats.sample_weights
```
diff --git a/docs/source/reference/api/score_tests.md b/docs/source/reference/api/score_tests.md
index 542a84f..cd24bd9 100644
--- a/docs/source/reference/api/score_tests.md
+++ b/docs/source/reference/api/score_tests.md
@@ -6,14 +6,9 @@ myst:
# Tests of pathway scores and activities
-Pathway scores and the activities of regulators give every sample of a cell type one
-value per gene set or source, and differ between conditions as expression values do.
-{func}`~cytepool.limma` tests a contrast of a {class}`~cytepool.Design` on them as
-GSVA's and decoupler's guides do with limma: a linear model of every set's scores,
-moderated t-tests of the contrast and Benjamini-Hochberg adjusted p-values, with the
-design built for the cube matched to the scored samples by `sample_id`. It takes
-{class}`~cytepool.PathwayScores`, {class}`~cytepool.Activities` of samples, or an array
-of values from another platform.
+limma on the pathway scores and activities of samples, with the cube's design;
+[Pathway scores per sample](../statistics/pathway-scores/#testing-scores) explains
+it.
```{eval-rst}
.. automodule:: cytepool.score_tests
diff --git a/docs/source/reference/api/study_results.md b/docs/source/reference/api/study_results.md
index 3a23765..abe1aa7 100644
--- a/docs/source/reference/api/study_results.md
+++ b/docs/source/reference/api/study_results.md
@@ -6,17 +6,9 @@ myst:
# Results of several studies
-Studies of one question are compared or combined once each is analysed on the
-collection's axes: its aligned cube, with a design built from its own samples.
-{func}`~cytepool.analyse_each_study` runs {func}`~cytepool.limma_voom`,
-{func}`~cytepool.limma_trend` or {func}`~cytepool.dream_voom` on every study of a
-{class}`~cytepool.CubeCollection` that way, with one formula, its levels and contrasts.
-{class}`~cytepool.StudyResults` keeps the results with their collection: it lines them
-up gene by gene, with the reason a study has no value for a gene, and gives one column
-of every study, such as the p-values, as a studies x genes matrix, the form that
-combined p-values and SumRank take. {func}`~cytepool.meta_analysis`,
-{func}`~cytepool.replication`, {func}`~cytepool.sumrank` and
-{func}`~cytepool.held_out_prediction` take it.
+Every study of a collection analysed alike, with the results kept together and lined up
+gene by gene; [Aligning studies](../statistics/aligning-studies/#one-analysis-per-study)
+explains them.
```{eval-rst}
.. automodule:: cytepool.study_results
diff --git a/docs/source/reference/api/sumrank.md b/docs/source/reference/api/sumrank.md
index cfbf614..aae36ab 100644
--- a/docs/source/reference/api/sumrank.md
+++ b/docs/source/reference/api/sumrank.md
@@ -6,17 +6,8 @@ myst:
# SumRank
-{func}`~cytepool.sumrank` ranks the genes of every study by a signed statistic, sums
-each gene's best normalized ranks across the studies and caps the sum at its mean, for
-genes up and genes down, as SumRank does (Nakatsuka et al. 2025), for one cell type
-and contrast of the studies' results, a {class}`~cytepool.StudyResults`, with p-values
-from the Irwin-Hall distribution or from permutations: it permutes the variable that
-the contrast compares among the samples of each study's fit, re-runs the study's
-analysis on its aligned cube, and pools the null distribution.
-{func}`~cytepool.stats.sumrank.fit_sumrank` does it for a matrix of statistics, with
-the statistics of permuted studies. {func}`~cytepool.plotting.plot_sumrank` draws the
-p-value of every gene, the genes in alphabetical order, as the authors of SumRank draw
-it.
+SumRank, every gene's best ranks summed across studies, with Irwin-Hall and permutation
+p-values; [Replication](../statistics/replication/#sum-of-ranks) explains it.
```{eval-rst}
.. automodule:: cytepool.sumrank_across_studies
diff --git a/docs/source/reference/api/variance_partitioning.md b/docs/source/reference/api/variance_partitioning.md
index 157b0e4..64b21b0 100644
--- a/docs/source/reference/api/variance_partitioning.md
+++ b/docs/source/reference/api/variance_partitioning.md
@@ -6,18 +6,9 @@ myst:
# Variance partitioning
-{func}`~cytepool.variance_partitioning` partitions the variance of every gene among the
-variables of a {class}`~cytepool.Design` and the residuals, in every cell type of a
-cube, as variancePartition's `fitExtractVarPartModel` does after the steps of
-{func}`~cytepool.dream_voom` up to the precision weights: each gene is fitted with a
-linear mixed model, by maximum likelihood, whose random intercepts are the categorical
-variables (donor, condition, batch) and whose fixed effects are the continuous ones
-(age), with voom's precision weights, and the fraction of a variable is its variance
-component, or the variance of its fitted effect, over their sum. It records the samples
-each fit used. {func}`~cytepool.stats.variance_fractions.variance_fractions` does it
-for a matrix of expression values. {func}`~cytepool.plotting.plot_variance_fractions`
-and {func}`~cytepool.plotting.plot_feature_variance_fractions` draw the fractions as
-variancePartition's `plotVarPart` and `plotPercentBars` do.
+The share of each gene's variance that each variable of a design explains, in every cell
+type of a cube or in a matrix; [Mixed models and
+dream](../statistics/mixed-models/#variance-fractions) explains it.
```{eval-rst}
.. automodule:: cytepool.variance_components
diff --git a/docs/source/reference/api/voom.md b/docs/source/reference/api/voom.md
index 56b8919..61ce0c5 100644
--- a/docs/source/reference/api/voom.md
+++ b/docs/source/reference/api/voom.md
@@ -6,6 +6,9 @@ myst:
# Precision weights for counts
+voom's precision weights for log counts, alone and with sample quality weights;
+[limma-voom and limma-trend](../statistics/limma-voom/#voom) explains them.
+
```{eval-rst}
.. automodule:: cytepool.stats.voom
```
diff --git a/docs/source/reference/bibliography.md b/docs/source/reference/bibliography.md
index a48ae2d..b9c8dcf 100644
--- a/docs/source/reference/bibliography.md
+++ b/docs/source/reference/bibliography.md
@@ -1,88 +1,104 @@
---
-description: Every work that the CytePool documentation and the References sections of its modules cite, grouped by area, with full references.
+description: Every work that the CytePool documentation and the References sections of its modules cite, grouped by the chapter that uses it, with full references.
---
# Bibliography
These are the works that the pages of this site cite and that CytePool's modules cite in
-their References sections, which `help()` shows. The groups below say where each is
-used; the list at the end of the page gives every work in full, with its DOI where it
-has one. [How to cite](citing.md) says which to cite for each function.
+their References sections, which `help()` shows. The groups below follow the chapters of
+[Understand the methods](../statistics/choosing_a_method.md) and say where each work is
+used, so that a work can appear in more than one; the list at the end of the page gives
+every work in full, with its DOI where it has one. [How to cite](citing.md) says which
+to cite for each function.
## Pseudobulk analysis and single-cell studies
Cells or samples [@squair2021; @zimmerman2021; @murphy2022; @junttila2022; @crowell2020;
@lun2017; @hafner2025; @nguyen2023; @heumos2023]; studies and atlases [@kang2018;
-@perez2022; @stephenson2021; @yazar2022; @murphy2023; @nakatsuka2025; @garg2021;
-@srivatsan2020; @czi2025; @sikkema2023].
+@perez2022; @stephenson2021; @murphy2023; @nakatsuka2025; @garg2021].
-## Linear models, empirical Bayes and precision weights
+## Foundations
-limma and its methods [@smyth2004; @smyth2005; @ritchie2015; @phipson2016;
-@mccarthy2009; @loennstedt2002; @law2014; @liu2015; @ritchie2006; @smyth2002;
-@smyth2025]; estimability, leverage and models in R [@searle1971; @hoaglin1978;
-@wilkinson1973; @chambers1992; @venables2025; @rcoreteam2025; @becker1988].
+Pseudobulk profiles and cubes [@wolf2018; @virshup2024; @bredikhin2022; @miles2025;
+@nist2015].
-## Counts: normalization, negative binomial models, quasi-likelihood and DESeq2
+Designs, linear models and contrasts [@wilkinson1973; @chambers1992; @becker1988;
+@venables2025; @rcoreteam2025; @searle1971; @hoaglin1978; @golub2013; @smyth2004;
+@smyth2005; @ritchie2015; @bates2015].
-[@robinson2010a; @robinson2010b; @bullard2010; @anders2010; @chen2016; @mccarthy2012;
-@robinson2008; @cox1987; @lund2012; @chen2025; @baldoni2024; @phipson2013; @dunn2018;
-@love2014; @bourgon2010; @cook1977; @muzellec2023; @levenberg1944; @marquardt1963]
+Empirical Bayes moderation [@smyth2004; @loennstedt2002; @phipson2016; @mccarthy2009;
+@chen2025].
-## Mixed models and dream
+Multiple testing [@holm1979; @hochberg1988; @hommel1988; @simes1986; @benjamini1995;
+@benjamini2001; @bourgon2010; @crowell2020; @storey2002; @storey2003; @storey2004].
-[@bates2015; @patterson1971; @harville1977; @henderson1975; @satterthwaite1946;
-@giesbrecht1985; @kuznetsova2017; @bertsekas1982; @hoffman2016; @hoffman2021;
-@hoffman2026a]; Kenward and Roger's tests and F-tests [@kenward1997; @halekoh2014;
-@fai1996]
+## Differential expression
-## Gene sets, pathways and interactions
+Normalization and filtering [@robinson2010a; @bullard2010; @anders2010; @chen2016;
+@heumos2023; @gower1966; @pearson1901].
-[@wu2010; @wu2012; @giner2016; @majewski2010; @langsrud2005; @phipson2010; @goeman2007;
-@barry2008; @subramanian2005; @holland2020; @badiaimompel2022; @liberzon2015;
-@dimitrov2022; @dimitrov2024; @routledge1994; @bailey1980; @hill1970; @brophy1987;
-@zar1999]
+limma-voom and limma-trend [@law2014; @ritchie2015; @cleveland1979; @scott1979;
+@freedman1981; @ritchie2006; @liu2015; @smyth2002; @smyth2025; @hoffman2026a;
+@kang2018; @perez2022].
-Gene-set files and over-representation analysis [@gsea2026; @liberzon2011;
-@young2010; @fisher1922; @rivals2007; @tavazoie1999; @timmons2015; @wijesooriya2022;
-@yu2012].
+edgeR and DESeq2 [@robinson2010b; @mccarthy2012; @lund2012; @chen2025; @baldoni2024;
+@love2014; @muzellec2023; @cox1987; @robinson2008; @bourgon2010; @cook1977;
+@phipson2013; @dunn2018; @levenberg1944; @marquardt1963].
-Gene set enrichment analysis [@subramanian2005; @mootha2003; @korotkevich2021] and the
-random numbers of its tests [@matsumoto1998; @durstenfeld1964].
+Mixed models and dream [@bates2015; @patterson1971; @harville1977; @henderson1975;
+@bertsekas1982; @satterthwaite1946; @giesbrecht1985; @kuznetsova2017; @kenward1997;
+@halekoh2014; @fai1996; @hoffman2016; @hoffman2021; @hoffman2026a; @hintze1998;
+@silverman1986].
-Scores of gene sets in every sample [@haenzelmann2013; @barbie2009; @lee2008;
-@tomfohr2005].
+## Composition tests
-Activities of regulators and pathways, and their networks [@badiaimompel2022;
-@muellerdott2023; @schubert2018; @holland2020b; @tuerei2021], and the running mean and
-variance of their permutations [@welford1962].
+[@phipson2022; @buttner2021; @mangiola2023; @hoffman2026b; @dann2022; @yi2024;
+@stephenson2021]
-## Composition
+## Gene sets and pathways
-[@phipson2022; @buttner2021; @mangiola2023; @hoffman2026b; @dann2022; @yi2024]
+Gene-set tests [@goeman2007; @wu2012; @wu2010; @langsrud2005; @giner2016;
+@majewski2010; @phipson2010; @routledge1994; @barry2008; @liberzon2011; @liberzon2015;
+@gsea2026; @subramanian2005], with the z-scores of t-statistics that they use
+[@bailey1980; @hill1970; @brophy1987; @zar1999].
-## Meta-analysis and multiple testing
+Over-representation and GSEA [@subramanian2005; @mootha2003; @tavazoie1999;
+@fisher1922; @rivals2007; @young2010; @yu2012; @timmons2015; @wijesooriya2022;
+@korotkevich2021], and the random numbers of GSEA [@matsumoto1998; @durstenfeld1964].
-[@dersimonian1986; @viechtbauer2005; @viechtbauer2010; @paule1982; @cochran1954;
-@higgins2002; @hartung1999; @sidik2002; @knapp2003; @urbut2019; @benjamini1995;
-@benjamini2001; @holm1979; @hochberg1988; @hommel1988; @simes1986]
+Pathway scores per sample [@haenzelmann2013; @barbie2009; @lee2008; @tomfohr2005;
+@smyth2004].
-Combined p-values, partial conjunctions and replication [@fisher1932; @stouffer1949;
-@liptak1958; @tippett1931; @wilkinson1951; @pearson1933; @george1983; @zaykin2011;
-@owen2009; @benjamini2008; @heller2014; @storey2002; @storey2003; @storey2004].
+Activities from networks [@muellerdott2023; @schubert2018; @holland2020b; @holland2020;
+@badiaimompel2022; @tuerei2021; @benjamini1995], and the running mean and variance of
+their permutations [@welford1962].
-SumRank and the prediction of held-out studies [@nakatsuka2025; @irwin1927; @hall1927;
-@andreatta2021; @hanley1982; @robin2011].
+## Several studies
+
+Aligning studies [@nakatsuka2025; @diehl2016; @czi2025; @sikkema2023].
+
+Meta-analysis [@viechtbauer2010; @dersimonian1986; @viechtbauer2005; @paule1982;
+@hartung1999; @sidik2002; @knapp2003; @cochran1954; @higgins2002; @lewis2001;
+@urbut2019].
+
+Replication [@nakatsuka2025; @irwin1927; @hall1927; @phipson2010; @hanley1982;
+@andreatta2021; @robin2011; @fisher1932; @stouffer1949; @liptak1958; @tippett1931;
+@wilkinson1951; @pearson1933; @george1983; @simes1986; @zaykin2011; @owen2009;
+@benjamini2008; @storey2003; @heller2014].
+
+## Questions not answered yet
+
+Cell-cell interactions, responses to drugs and doses, and genetic effects on
+expression [@dimitrov2022; @dimitrov2024; @srivatsan2020; @yazar2022].
## Smoothing, distributions and numerical methods
[@cleveland1979; @cleveland1981; @deboor1978; @hastie1992; @scott1979; @freedman1981;
@abramowitz1964; @johnson1995; @olver2026; @wichura1988; @paulson1942; @temme1979;
-@temme1982;
-@lentz1976; @thompson1986; @loader2000; @maechler2012; @dekker1971; @knuth1997;
-@ogita2005; @neumaier1974; @kendall1945; @golub2013; @giraud2005; @berrut2004;
-@brent1973; @dowell1971; @graham1969; @briggs1993; @goldberg1991; @hoare1961;
-@wirth1976]
+@temme1982; @lentz1976; @thompson1986; @loader2000; @maechler2012; @dekker1971;
+@knuth1997; @ogita2005; @neumaier1974; @kendall1945; @golub2013; @giraud2005;
+@berrut2004; @brent1973; @dowell1971; @graham1969; @briggs1993; @goldberg1991;
+@hoare1961; @wirth1976]
## Figures
diff --git a/docs/source/reference/capabilities.md b/docs/source/reference/capabilities.md
deleted file mode 100644
index 220d710..0000000
--- a/docs/source/reference/capabilities.md
+++ /dev/null
@@ -1,78 +0,0 @@
----
-description: Every method CytePool implements, with its function, the reference it is checked against and its version, the tolerance tier, and the validation script; then what is not implemented yet.
----
-
-# Capability matrix
-
-Every method below is compared with an independent reference, usually the package that
-defined it, run as a separate program on the same inputs. The tier says how closely:
-**exact**, a relative difference of at most 1e-10, for closed-form results; **close**,
-at most 1e-6, for iterative fits; **calibrated**, within simulation error with the same
-calls, for methods that draw random rotations. Where CytePool differs from the reference
-on purpose, the [Validation](../validation.md) page reports each difference with its
-size; its sections give every check.
-
-## Implemented
-
-| Method | CytePool | Checked against | Tier | Script |
-| --- | --- | --- | --- | --- |
-| Pseudobulk profiles pooled from cells | {api}`from_cells` | scanpy 1.12.4, decoupler 2.2.0 | exact | [`pool_cells.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pool_cells.py) |
-| Bulk counts as a cube | {api}`from_bulk` | decoupler 2.2.0 | exact | [`pool_cells.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pool_cells.py) |
-| Selecting, merging groups, pooling samples; means, variances, detection | {api}`Cube` | scanpy 1.12.4 | exact | [`cube.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube.py) |
-| Cube files | {api}`write_cube`, {api}`read_cube` | zarr-python 3.4.1 | exact | [`cube_files.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_files.py) |
-| Reads of selected features from cube files | {api}`read_cube`, {api}`Cube.select` | zarr-python 3.4.1 | exact | [`cube_reads.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_reads.py) |
-| Fingerprints of cubes | {api}`Cube.fingerprint`, {api}`Cube.digests` | the documented encoding, with zarr-python 3.4.1 | exact | [`cube_fingerprints.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_fingerprints.py) |
-| AnnData and MuData layouts of scanpy, decoupler and LIANA | {api}`to_anndata`, {api}`from_anndata`, {api}`to_mudata`, {api}`from_mudata` | scanpy 1.12.4, decoupler 2.2.0, LIANA 1.10.0, anndata 0.13.4, mudata 0.4.1 | exact | [`anndata_conversion.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/anndata_conversion.py) |
-| Several studies' cubes aligned by sample, gene and cell type, with what each study measured; stacked for a joint model; each study's results lined up gene by gene | {api}`CubeCollection`, {api}`StudyResults.table` | anndata 0.13.4, pandas 3.1.0, limma 3.62.1, edgeR 4.4.0 | exact | [`cube_collection.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_collection.py) |
-| TMM, upper-quartile and RLE factors; CPM and log-CPM; `filterByExpr` | {api}`tmm_factors`, {api}`upper_quartile_factors`, {api}`rle_factors`, {api}`cpm`, {api}`log2_cpm`, {api}`filter_by_expression` | edgeR 4.4.0 | exact | [`normalization.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/normalization.py) |
-| DESeq2's median-of-ratios size factors | {api}`median_ratio_size_factors` | DESeq2 1.46.0 | exact | [`normalization.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/normalization.py) |
-| Model matrices from R formulas, named contrasts, design checks | {api}`Design`, {api}`model_matrix`, {api}`make_contrasts`, {api}`Design.check` | R 4.4.3, limma 3.62.1, lme4 2.0.6 | exact | [`design.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/design.py) |
-| Linear models of every gene, contrasts (limma's `lmFit`, `contrasts.fit`) | {api}`fit_linear_model`, {api}`contrasts_fit` | limma 3.62.1 | exact | [`linear_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/linear_models.py) |
-| Mixed models with random intercepts by REML or ML, Satterthwaite's t-tests | {api}`fit_mixed_model`, {api}`fixed_effect_tests` | lme4 2.0.6, lmerTest 3.2.1 | close | [`mixed_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/mixed_models.py) |
-| Kenward-Roger's t-tests and F-tests of mixed models, Satterthwaite's F-tests (pbkrtest's `vcovAdj` and `KRmodcomp`, lmerTest's `contestMD`); dream's choice of degrees of freedom and its F-tests | {api}`fixed_effect_tests`, {api}`fixed_effect_f_test`, {api}`dream_voom`, {api}`moderated_fixed_effect_tests` | pbkrtest 0.5.3, lmerTest 3.1-3, lme4 1.1-38, variancePartition 1.36.2 | exact at given estimates; close at the optimum and for dream | [`kenward_roger_and_f_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/kenward_roger_and_f_tests.py) |
-| voom precision weights, limma's span rule | {api}`voom`, {api}`choose_lowess_span` | limma 3.62.1, edgeR 4.4.0 | exact; close on rarefied counts | [`voom.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/voom.py) |
-| Sample quality weights (`arrayWeights`, `voomWithQualityWeights`) | {api}`sample_quality_weights`, {api}`voom_with_quality_weights` | limma 3.62.1, edgeR 4.4.0 | close | [`sample_quality_weights.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sample_quality_weights.py) |
-| Negative binomial models of every gene (edgeR's `glmFit`), deviances | {api}`fit_negative_binomial_model`, {api}`negative_binomial_deviance`, {api}`negative_binomial_unit_deviance` | edgeR 4.4.0, mpmath 1.3.0 | close | [`negative_binomial_fits.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/negative_binomial_fits.py) |
-| Quasi-likelihood F-tests (edgeR 4's `glmQLFit`, `glmQLFTest`) | {api}`fit_quasi_likelihood`, {api}`quasi_likelihood_f_test`, {api}`common_dispersion`, {api}`average_log2_cpm` | edgeR 4.4.0, limma 3.62.1, mpmath 1.3.0 | close | [`quasi_likelihood.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/quasi_likelihood.py) |
-| DESeq2's dispersions, Wald tests and likelihood ratio tests | {api}`fit_deseq2`, {api}`deseq2_dispersions`, {api}`deseq2_wald_test`, {api}`deseq2_likelihood_ratio_test` | DESeq2 1.46.0, PyDESeq2 0.5.4 | close | [`deseq2_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/deseq2_tests.py) |
-| Empirical Bayes moderation, moderated t and F, B-statistics, TREAT | {api}`moderated_statistics`, {api}`treat`, {api}`squeeze_variances`, {api}`fit_f_distribution`, {api}`fit_f_distribution_robust`, {api}`fit_f_distribution_likelihood` | limma 3.62.1 | exact; close for the robust and likelihood priors | [`empirical_bayes.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/empirical_bayes.py) |
-| Gene-set tests camera and fry | {api}`camera`, {api}`fry` | limma 3.62.1 | exact; close with robust moderation; 1e-3 for fry's default standardization, which differs on purpose | [`gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_tests.py) |
-| Rotation gene-set tests roast, mroast and romer | {api}`roast`, {api}`mroast`, {api}`romer` | limma 3.62.1 | calibrated; exact given limma's rotations | [`rotation_gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/rotation_gene_set_tests.py) |
-| Gene-set files (GMT) read and written; collections matched to an analysis (limma's `ids2indices`) and filtered by size | {api}`read_gmt`, {api}`write_gmt`, {api}`GeneSets` | R 4.4.3, limma 3.62.1, decoupler 2.2.0 | exact | [`gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py) |
-| Over-representation analysis (limma's `kegga` and `goana`) of the genes up, down or either way, and its dot plot | {api}`over_representation`, {api}`plot_over_representation` | limma 3.62.1, R 4.4.3 | exact | [`gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py) |
-| Gene set enrichment analysis of ranked genes (fgsea's `fgsea`: `fgseaMultilevel` and `fgseaSimple`), with its enrichment plot and table (`plotEnrichment`, `plotGseaTable`) | {api}`gsea`, {api}`plot_enrichment`, {api}`plot_gsea_table` | fgsea 1.32.2 | calibrated; exact given fgsea's seeds, and for enrichment scores, leading edges and figures | [`gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py) |
-| Scores of gene sets in every sample (GSVA's `gsva` with `gsvaParam`, `ssgseaParam`, `zscoreParam` and `plageParam`), and their heatmap | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`, {api}`plot_pathway_scores` | GSVA 2.0.0 | exact; PLAGE up to sign | [`pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py) |
-| Activities of regulators and pathways (decoupler's `mt.ulm`, `mt.mlm` and `mt.waggr`) from samples, a cube's cell type or contrasts, the networks CollecTRI and PROGENy downloaded with their checksums and licences, and the bar plot of the top sources (`pl.barplot`) | {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate`, {api}`collectri`, {api}`progeny`, {api}`plot_top_activities` | decoupler 2.2.0, progeny 1.28.0 | exact; exact given decoupler's permutations for normalized scores | [`activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py) |
-| Adjustment for multiple testing (R's `p.adjust`) | {api}`adjust_p_values` | R 4.4.3 | exact | [`multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py) |
-| limma-voom and limma-trend on every cell type of a cube | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact; close with robust moderation | [`differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py) |
-| Options for pseudobulk data: quality weights, weights from cell numbers, a minimum count, adjustment across cell types | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact; close with quality weights | [`differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py) |
-| dream on every cell type of a cube | {api}`dream_voom`, {api}`voom_with_dream_weights`, {api}`moderated_fixed_effect_tests` | variancePartition 1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3 | close | [`dream.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/dream.py) |
-| Variance partitioning (`fitExtractVarPartModel`), on every cell type of a cube, and its figures (`plotVarPart`, `plotPercentBars`) | {api}`variance_fractions`, {api}`variance_partitioning`, {api}`plot_variance_fractions`, {api}`plot_feature_variance_fractions` | variancePartition 1.36.2, lme4 1.1-38, ggplot2 4.0.3 | close; exact at given estimates and for linear models | [`variance_partitioning.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/variance_partitioning.py) |
-| limma-voom on published single-cell data (Kang et al. 2018) | {api}`from_cells`, {api}`limma_voom` | limma 3.62.1, edgeR 4.4.0 | exact | [`interferon_stimulated_pbmcs.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/interferon_stimulated_pbmcs.py) |
-| Cell-type composition (speckle's propeller) | {api}`propeller` | speckle 1.6.0, limma 3.62.1 | exact; close for the robust prior | [`composition_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/composition_tests.py) |
-| Meta-analysis by fixed and random effects (metafor's `rma.uni`) | {api}`meta_analysis`, {api}`fit_meta_analysis` | metafor 5.0.1 | exact; close for REML and Paule-Mandel | [`meta_analysis.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/meta_analysis.py) |
-| Combined p-values of several studies (Fisher, Stouffer with weights, Tippett, Wilkinson, Pearson, Mudholkar and George, Simes), one-sided or two-sided with the directions of the effects; partial conjunctions, an effect in at least u studies | {api}`combine_p_values`, {api}`partial_conjunction` | scipy 1.18.1, metap 1.1, mpmath 1.3.0 | exact | [`combined_p_values_and_replication.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/combined_p_values_and_replication.py) |
-| The proportion of true nulls, pi0, and pi1 (qvalue's `pi0est`, its smoother and bootstrap) | {api}`proportion_of_true_nulls` | qvalue 2.38.0 | exact | [`combined_p_values_and_replication.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/combined_p_values_and_replication.py) |
-| Replication across the studies of a collection: combined and partial conjunction p-values, sign agreement, heterogeneity, pi1 between studies, genes found in k studies, and its figures | {api}`replication`, {api}`plot_study_agreement`, {api}`plot_replicated_genes` | metap 1.1, qvalue 2.38.0, metafor 5.0.1 | exact; close for REML | [`combined_p_values_and_replication.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/combined_p_values_and_replication.py) |
-| SumRank (Nakatsuka et al. 2025): every gene's best ranks summed across studies, with Irwin-Hall p-values and permutations that re-run each study's analysis; prediction of cases in held-out studies by genes chosen from the others, by SumRank and by meta-analysis; their figures | {api}`sumrank`, {api}`fit_sumrank`, {api}`held_out_prediction`, {api}`plot_sumrank`, {api}`plot_held_out_auc` | the authors' code (commit 46392f66), R 4.4.3, mpmath 1.3.0, scikit-learn 1.9.1 | exact; calibrated for p-values from random permutations | [`sumrank.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sumrank.py) |
-| Principal components of samples (DESeq2's `plotPCA`), of a matrix or a cell type of a cube, and their figure | {api}`principal_components`, {api}`plot_pca` | DESeq2 1.46.0, edgeR 4.4.0 | exact, up to the sign of each component | [`pca_and_dot_plots.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pca_and_dot_plots.py) |
-| Dot plots of the fraction of cells that express each gene and their mean expression, per cell type, from a cube (scanpy's `dotplot`) | {api}`plot_dotplot` | scanpy 1.12.4 | exact | [`pca_and_dot_plots.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pca_and_dot_plots.py) |
-| Figures of limma, edgeR, DESeq2, speckle and metafor (MDS, voom's trend, plotSA, MD and volcano plots, barcode plots, dispersions, composition, forest plots) | {api}`cytepool.plotting` | limma 3.62.1, edgeR 4.4.0, DESeq2 1.46.0, speckle 1.6.0, metafor 5.0.1 | exact for what is drawn; close for plotSA's prior | [`plotting.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/plotting.py) |
-| Tails and quantiles of distributions, special functions | {api}`cytepool.stats.distributions`, {api}`cytepool.stats.special` | mpmath 1.3.0 | exact (1e-15 to 1e-12) | [`distributions.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/distributions.py) |
-| LOWESS, natural spline bases | {api}`lowess`, {api}`natural_spline_basis` | R 4.4.3 | close for LOWESS; exact for spline bases | [`smoothing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/smoothing.py) |
-
-## Not implemented yet
-
-These methods come up in the questions that studies ask ([Overview](../index.md)) and
-have no CytePool function yet:
-
-- random slopes in mixed models;
-- composition tests with precision weights or random effects (crumblr), and Bayesian
- count models of composition;
-- ligand-receptor scoring and differential cell-cell interactions;
-- dose-response fits, benchmark doses and drug synergy;
-- factorization of samples across cell types;
-- shrinkage of effects across conditions (mashr) and meta-regression;
-- cell types aligned up the Cell Ontology;
-- edgeR's quasi-likelihood F-tests and DESeq2's tests on every cell type of a cube, and
- DESeq2's shrunken fold changes.
diff --git a/docs/source/reference/citing.md b/docs/source/reference/citing.md
index 337be26..b74e438 100644
--- a/docs/source/reference/citing.md
+++ b/docs/source/reference/citing.md
@@ -14,33 +14,43 @@ the credit for the methods belongs to their authors.
| Functions | Cite |
| --- | --- |
+| {api}`fit_linear_model`, {api}`contrasts_fit`, {api}`moderated_statistics`, {api}`squeeze_variances` | limma [@smyth2004; @ritchie2015]; for the likelihood estimator of the prior, used when the degrees of freedom differ, [@chen2025] |
+| {api}`treat` | [@mccarthy2009] |
+| {api}`adjust_p_values` | the method you chose: [@benjamini1995], [@benjamini2001], [@holm1979], [@hochberg1988] or [@hommel1988] |
+| {api}`proportion_of_true_nulls` | [@storey2002; @storey2003]; with `method="bootstrap"`, [@storey2004] |
+| {api}`tmm_factors` | [@robinson2010a] |
+| {api}`median_ratio_size_factors`, {api}`rle_factors` | [@anders2010] |
+| {api}`upper_quartile_factors` | [@bullard2010] |
+| {api}`filter_by_expression` | [@chen2016] |
+| {api}`principal_components` | DESeq2's `plotPCA` [@love2014] |
| {api}`limma_voom`, {api}`voom` | voom [@law2014]; limma [@ritchie2015]; empirical Bayes moderation [@smyth2004] |
| {api}`limma_trend` | limma-trend [@law2014]; limma [@ritchie2015; @smyth2004] |
| `robust=True` in the functions above | [@phipson2016] |
| `quality_weights=True`, {api}`sample_quality_weights`, {api}`voom_with_quality_weights` | [@ritchie2006; @liu2015] |
| `prior_weights="n_cells"` | dreamlet [@hoffman2026a] |
-| {api}`dream_voom`, {api}`voom_with_dream_weights`, {api}`moderated_fixed_effect_tests` | dream [@hoffman2021] |
-| {api}`fit_mixed_model`, {api}`fixed_effect_tests`, {api}`fixed_effect_f_test` | lme4 [@bates2015]; Satterthwaite's tests as lmerTest computes them [@kuznetsova2017]; Kenward and Roger's [@kenward1997] as pbkrtest computes them [@halekoh2014] |
-| {api}`treat` | [@mccarthy2009] |
| {api}`fit_quasi_likelihood`, {api}`quasi_likelihood_f_test` | edgeR 4 [@chen2025]; quasi-likelihood [@lund2012] |
| {api}`fit_negative_binomial_model`, {api}`common_dispersion` | [@mccarthy2012] |
| {api}`fit_deseq2`, {api}`deseq2_wald_test`, {api}`deseq2_likelihood_ratio_test` | DESeq2 [@love2014] |
-| {api}`tmm_factors` | [@robinson2010a] |
-| {api}`median_ratio_size_factors`, {api}`rle_factors` | [@anders2010] |
-| {api}`upper_quartile_factors` | [@bullard2010] |
-| {api}`filter_by_expression` | [@chen2016] |
+| {api}`dream_voom`, {api}`voom_with_dream_weights`, {api}`moderated_fixed_effect_tests` | dream [@hoffman2021] |
+| {api}`fit_mixed_model`, {api}`fixed_effect_tests`, {api}`fixed_effect_f_test` | lme4 [@bates2015]; Satterthwaite's tests as lmerTest computes them [@kuznetsova2017]; Kenward-Roger's [@kenward1997] as pbkrtest computes them [@halekoh2014] |
+| {api}`variance_partitioning`, {api}`variance_fractions` | variancePartition [@hoffman2016] |
+| {api}`propeller` | [@phipson2022] |
| {api}`camera` | [@wu2012] |
| {api}`fry` | [@giner2016] |
| {api}`roast`, {api}`mroast` | [@wu2010] |
| {api}`romer` | [@majewski2010] |
+| {api}`read_gmt` | the collection you read, as its provider asks; for MSigDB, [@subramanian2005; @liberzon2011] |
| {api}`over_representation` | limma's `kegga` and `goana` [@young2010]; the hypergeometric test [@tavazoie1999] |
| {api}`gsea` | fgsea [@korotkevich2021]; GSEA [@subramanian2005] |
-| {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate` | decoupler [@badiaimompel2022], and the network you used: CollecTRI [@muellerdott2023] or PROGENy [@schubert2018], for mouse [@holland2020b] |
| {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage` | the GSVA package [@haenzelmann2013], and the paper of the method you chose: GSVA [@haenzelmann2013], ssGSEA [@barbie2009], combined z-scores [@lee2008] or PLAGE [@tomfohr2005] |
-| {api}`read_gmt` | the collection you read, as its provider asks; for MSigDB, [@subramanian2005; @liberzon2011] |
-| {api}`propeller` | [@phipson2022] |
+| {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate` | decoupler [@badiaimompel2022], and the network you used: CollecTRI [@muellerdott2023] or PROGENy [@schubert2018], for mouse [@holland2020b] |
+| {api}`limma` | limma [@smyth2004; @ritchie2015], and the method that made the scores or activities |
| {api}`meta_analysis`, {api}`fit_meta_analysis` | metafor [@viechtbauer2010]; the estimator of the between-study variance you chose: REML [@viechtbauer2005], DerSimonian-Laird [@dersimonian1986] or Paule-Mandel [@paule1982]; Knapp-Hartung [@knapp2003] |
-| {api}`adjust_p_values` | the method you chose: [@benjamini1995], [@benjamini2001], [@holm1979], [@hochberg1988] or [@hommel1988] |
+| {api}`combine_p_values` | the method you chose: Fisher [@fisher1932], Stouffer [@stouffer1949; @liptak1958], Tippett [@tippett1931], Wilkinson [@wilkinson1951], Pearson [@pearson1933], Mudholkar and George [@george1983] or Simes [@simes1986] |
+| {api}`partial_conjunction` | [@benjamini2008] |
+| {api}`replication` | partial conjunctions [@benjamini2008]; pi1 [@storey2003] |
+| {api}`sumrank`, {api}`fit_sumrank` | SumRank [@nakatsuka2025] |
+| {api}`held_out_prediction` | [@nakatsuka2025]; the scores of the samples, UCell's [@andreatta2021] |
Each module's documentation lists, under References, every work it implements; the
[Bibliography](bibliography.md) collects them. For the data of the documentation, cite
diff --git a/docs/source/reference/cube_files.md b/docs/source/reference/cube_files.md
index 1705cb3..383145d 100644
--- a/docs/source/reference/cube_files.md
+++ b/docs/source/reference/cube_files.md
@@ -1,29 +1,34 @@
---
-description: The cube file format (a Zarr v3 group), the encoding of cube fingerprints, and the keys of provenance records, including what a tool that exports cubes should record.
+description: The specification of cube files (a Zarr v3 group), of the encoding of cube fingerprints, of provenance records and of the record an AnnData or MuData export keeps, for programs that read or write cubes.
---
# Cube files and provenance records
A cube file stores a cube so that any Zarr v3 reader can open it, and so that a reader
can tell whether it holds what was written. Its fingerprint names the cube's values, and
-its provenance record says how the cube was made. This page describes all three, for
-programs that read or write cubes; [Export a cube from another
-tool](../guides/export_cubes.ipynb) shows the steps in code, and [Save, share and trace
-cubes](../guides/save_and_trace_cubes.ipynb) what a reader can check with them.
+its provenance record says how the cube was made. This page specifies all three, and the
+record that an AnnData or MuData export keeps, for programs that read or write cubes.
+[Export a cube from another tool](../guides/export_cubes.ipynb) shows the steps in code,
+and [Save, share and trace cubes](../guides/save_and_trace_cubes.ipynb) what a reader
+can check with them.
+(cube-file-format)=
## The file
{api}`write_cube` writes a directory, by convention `.cube.zarr`, holding a Zarr
-v3 group [@miles2025]; {api}`read_cube` opens it. Format `cytepool.cube`, version 1:
+v3 group as the Zarr core specification defines it [@miles2025], which any Zarr v3
+reader can open; {api}`read_cube` opens it. Below, a "Zarr group" is a node of the
+store, and a "group" one of the cube's groups, usually cell types. Format
+`cytepool.cube`, version 1:
```text
.cube.zarr/
- zarr.json the root group, with the attributes below
- samples/ one 1-D array per sample column
- groups/ one 1-D array per group column
- features/ one 1-D array per feature column
+ zarr.json the root Zarr group; its attributes are listed below
+ samples/ a Zarr group with one 1-D array per sample column
+ groups/ a Zarr group with one 1-D array per group column
+ features/ a Zarr group with one 1-D array per feature column
n_cells integers (n_samples, n_groups); absent from bulk cubes
- layers/
+ layers/ a Zarr group with one array per layer
counts (n_groups, n_samples, n_features)
... detected, log_sum, log_sum_sq and linear_sum, when present
```
@@ -31,48 +36,166 @@ v3 group [@miles2025]; {api}`read_cube` opens it. Format `cytepool.cube`, versio
| Root attribute | Value |
| --- | --- |
| `format` | `"cytepool.cube"` |
-| `format_version` | `1`; a reader rejects other versions |
-| `axes` | `["groups", "samples", "features"]`, the axes of every layer |
-| `layers` | the layer names, in the order counts, detected, log_sum, log_sum_sq, linear_sum |
-| `has_n_cells` | whether `n_cells` is present |
-| `normalization` | how the normalized layers were computed, or null |
-| `provenance` | the provenance record (below) |
-| `digests` | the digests of the parts and the fingerprint (below) |
+| `format_version` | `1`. A reader rejects other versions; any change to the layout increments the version |
+| `axes` | `["groups", "samples", "features"]`, the axes of every layer, in order |
+| `layers` | the layer names, in the order `counts`, `detected`, `log_sum`, `log_sum_sq`, `linear_sum` |
+| `has_n_cells` | whether the `n_cells` array is present: false for bulk cubes, where cell numbers are unknown |
+| `normalization` | how the normalized layers were computed, as a mapping, or null |
+| `provenance` | the provenance record ([below](#provenance-keys)) |
+| `digests` | the digests of the cube's tables, cell numbers, normalization and of each group of each layer, and the fingerprint they make ([below](#digest-record)). {api}`write_cube` computes them while it writes the layers. A file written before CytePool stored digests has none; it is read all the same, and its fingerprint is computed from its values when it is asked for |
Other attributes are ignored, so a publisher may add, say, a title.
-**Tables.** Each column is a 1-D array with `dimension_names` set to its axis. Text is
-stored as int32 codes with the attribute `levels` (the distinct values in order of first
-appearance; -1 marks a missing value); datetimes and timedeltas as int64 counts of their
-unit; booleans and numbers as their own type. The group's attributes `columns` and
-`arrays` give the column names and the array of each.
-
-**Layers.** Chunks hold one group, every sample and up to 4,096 features, so one group,
-or a few features of it, is read without the others. Every array uses the `bytes` codec
-(little endian) followed by gzip (the default; @deutsch1996b), zstd (Python 3.14 or
-newer; @collet2021) or no compression [@zarr2023; @maitinshepard2025]. Chunks of zeros
-are not stored.
-
-**Reading and writing.** Reading is strict: the format, every attribute, every array's
-metadata and every value is checked, and the tables, cell numbers and normalization are
-checked against their digests when the file is opened, and each group of a layer
-whenever a read covers all of it. {api}`Cube.verify_fingerprint` checks everything.
-Writing is atomic: the store is written to a hidden directory beside the target and
-renamed into place when it is complete.
+(cube-file-tables)=
+### Tables
+Each table's Zarr group holds one 1-D array per column, with `dimension_names` set to
+the table's axis (`["samples"]`, `["groups"]` or `["features"]`), in chunks of up to 2²⁰
+values. Its attributes give the columns in table order: `columns` lists the column
+names and `arrays` the name of each column's array. An array takes its column's name
+when that name is portable: ASCII letters, digits, `_`, `.` and `-`, at most 64
+characters, starting with a letter, a digit or `_` but not with `__`, not ending with
+`.`, without a Windows device name before its first `.` (`con`, `prn`, `aux`, `nul`,
+`com0` to `com9`, `lpt0` to `lpt9`, in any case), not `zarr.json` in any case, not of
+the form `column_` in any case, and not equal to another column's name when case is
+ignored. Otherwise the array is named `column_`, counting from 0. Columns are
+stored as:
+
+- text: int32 codes with the attribute `levels`, the distinct values in order of first
+ appearance; code `i` stands for `levels[i]` and `-1` for a missing value;
+- datetimes and timedeltas: int64 counts of their unit, with the attribute
+ `numpy_dtype` (for example `"datetime64[s]"`); NaT is -2⁶³, as in numpy;
+- booleans and numbers: their own data type.
+
+The sample table has a column `sample_id` of unique, non-empty text ids; the group
+table a column `group_id` of such ids, and may have the text columns `cell_ontology_id`
+and `parent`; the feature table a column `feature_id` of such ids, and may have a text
+column `symbol` and a boolean column `measured`.
+
+(cube-file-layers)=
+### Layers and cell numbers
+
+Layers have `dimension_names` `["groups", "samples", "features"]` and chunks of one
+group, every sample and up to 4,096 features, so that reading one group reads only its
+own chunks, and reading some features of it only the chunks that hold them. A chunk
+holds fewer samples only where it would otherwise exceed 256 MiB (2²⁸ bytes): it then
+holds as many samples as fit, 2²⁸ bytes divided by the bytes of its features, so 8,192
+samples of 4,096 float64 values. `counts` is of an integer type or float64 (as for
+counts that are not whole numbers); `detected` is of an integer type; `log_sum`,
+`log_sum_sq` and `linear_sum` are float64.
+
+`n_cells` is int32, or int64 if a value does not fit, with `dimension_names`
+`["samples", "groups"]`, in chunks of every group and up to 2²⁰ values.
+
+Every array has the default chunk key encoding with `/`, a fill value of zero (`false`
+for bool) and the `bytes` codec, little endian (without an `endian` for types of one
+byte), followed by `gzip` (the default, at level 4 unless `compression_level` gives
+another), `zstd` (at level 3 unless given another, with checksums; Python 3.14 or newer)
+or no compression: the `bytes` and `gzip` codecs of the Zarr specifications [@zarr2023]
+and the `zstd` codec of its extensions [@maitinshepard2025], which store gzip streams
+[@deutsch1996b] and Zstandard frames [@collet2021]. Chunks that hold only zeros are not
+stored, as Zarr allows, and read as zeros. The part of Zarr v3 that CytePool reads and
+writes is listed in the API reference of {api}`cytepool.io.zarr`; zarr-python reads
+every cube file.
+
+(cube-file-reading-and-writing)=
+### Reading and writing
+
+Reading is strict: the format name and version, every attribute above, every node's
+metadata, dimension names, data types and text codes are checked, and so are the values
+of every layer group when the cube reads it. The tables, cell numbers and normalization
+are checked against their stored digests when the file is opened, and each group of a
+layer whenever a read covers all of it, in any order, so a part changed after the file
+was written is reported when it is read whole, not read as if it were the cube the file
+held. A read of some samples or features of a group is not checked against the digest;
+{api}`Cube.verify_fingerprint` checks every group, also those. Digests describe values, not their encoding: a copy with other chunks, byte
+order or compression keeps them. A store replaced while {api}`read_cube` reads it raises
+an error instead of mixing the tables of one cube with the layers of another, and so
+does a lazy layer whose file was replaced after it was opened.
+
+Writing is atomic: {api}`write_cube` writes the store into a new hidden directory next
+to the target, `..partial-` (the target's name and 8 hexadecimal
+digits, such as `.pbmc.cube.zarr.partial-3f9a0c21`), writes the root `zarr.json` last,
+and renames the directory to the target only when everything is written. A write that
+fails or is interrupted removes the partial directory and leaves the target as it was.
+With `overwrite=True`, which replaces only a cube file or an empty directory, the old
+store is renamed aside to `..replaced-`, the new one renamed into
+place and the old one deleted; if the new store cannot be moved into place, the old one
+is put back. A process killed outright can leave either hidden directory behind
+(between the two renames, the old store survives only under its hidden name). A partial
+directory has no root `zarr.json` until every array is written, so it does not open as
+a Zarr group; a process killed after that and before the rename leaves a complete store
+under the hidden name. Files are not flushed to disk with fsync, so the guarantee covers
+failures of the writing process, not of the operating system.
+
+(fingerprint-encoding)=
## Fingerprints
{api}`Cube.fingerprint` identifies the values an analysis reads: the sample, group and
-feature tables, the cell numbers, the normalization and every layer. It leaves out the
-provenance, so two cubes with the same values have the same fingerprint however they
-were made, and a result can name the cube it was computed from.
+feature tables (ids included), the cells pooled into each entry (`n_cells`), the
+normalization that defines the normalized layers, and every layer. It leaves out the
+provenance, which records how the cube was made, so two cubes with the same values have
+the same fingerprint however they were made, and a result can name the cube it was
+computed from by its fingerprint. {api}`Cube.digests` returns the digests it is built
+from.
-It is built from digests, each the SHA-256 [@nist2015] of a byte string, written as
-`sha256:` and 64 hexadecimal digits: one per table, one for the cell numbers, one for
-the normalization, and one for each group of each layer, so that one group can be
-checked without reading the others. Numbers are little endian; integer layers are hashed
-as int64 and float layers as float64, with -0.0 written as 0.0. The fingerprint is the
-digest of a text manifest that lists them:
+The fingerprint is built from the digests of the cube's parts: each table, the cell
+numbers, the normalization, and each group of each layer, so that one group can be
+checked without reading the others. Encoding version 1 ({api}`FINGERPRINT_VERSION`) is
+as follows. The encoding was checked against an independent implementation written
+from this description ([Validation](#validation-cube-fingerprints)).
+
+**Digests.** A digest is the SHA-256 hash [@nist2015] of a byte string, written as
+`sha256:` and 64 lowercase hexadecimal digits. Numbers in byte strings are little
+endian: `u64` and `i64` are unsigned and signed 8-byte integers. Text is UTF-8
+[@yergeau2003], written as its length in bytes (`u64`) followed by its bytes; a lone
+surrogate, which a Python string can hold but a cube file cannot, is written in its
+3-byte form, as Python's `surrogatepass` error handler writes it. Strings are taken as
+they are, without Unicode normalization.
+
+**Tables** (samples, groups and features): the number of rows and of columns (`u64`
+each), then for each column, in table order, its name (text), its type (text) and its
+values:
+
+- text, type `text`: the length in bytes of each value (`i64`, -1 for a missing value),
+ then the bytes of the values present, one after another;
+- `bool`: one byte per value, 0 or 1;
+- `int8` to `int64` and `uint8` to `uint64`: the values in that type;
+- `float16`, `float32` and `float64`: the values in that type, the binary formats of
+ IEEE 754 [@ieee2019], with -0.0 written as 0.0 and every NaN as the quiet NaN
+ (`0x7E00`, `0x7FC00000` or `0x7FF8000000000000`);
+- `complex64` and `complex128`: the real and imaginary parts of each value, as floats
+ of half the size written as above; a value with a NaN part is missing, and both its
+ parts are written as the quiet NaN;
+- datetimes and timedeltas, of type `datetime64[unit]` or `timedelta64[unit]` (numpy's
+ name, such as `datetime64[ms]`): the values as `i64`, NaT being -2⁶³.
+
+The type is numpy's name for the column's dtype. These are the column types cube files
+store, and the only ones a cube's tables accept. Two tables have the same digest exactly
+when {api}`Table.equals` finds them equal: the same names in the same order, the same
+types, and equal values, with missing values equal.
+
+**Cell numbers:** the number of samples and of groups (`u64` each), then `n_cells` as
+`i64`, sample by sample.
+
+**Normalization:** the mapping as a JSON value [@bray2017], each value written as a tag
+byte and a payload: `n` for null; `f` and `t` for false and true; `i` and the decimal
+digits of the number as text (with a leading `-` when negative) for a number with a
+whole value, such as 10000 or 10000.0; `d` and its 8 bytes as a float64 for any other
+number; `s` and the text for a string; `a`, the number of items (`u64`) and the items
+for an array; `o`, the number of members (`u64`) and, in the order of the UTF-8 bytes
+of their keys, each key (text) and value for an object. A number is thus taken by its
+value, however a JSON writer spells it.
+
+**Layers:** one digest for each group of each layer, of the group's samples x features
+block: the number of samples and of features (`u64` each), then the values sample by
+sample, as `i64` for an integer layer and as float64, with -0.0 written as 0.0, for a
+float layer. An integer layer holds the same numbers whatever the width of its dtype
+(a cube accepts integers from 0 to 2⁶³ - 1, and merging groups or pooling samples makes
+them int64),
+so a uint32 group and an int64 group with the same values have the same digest.
+
+**Manifest:** UTF-8 text with one line per part, each ending with a line feed (`\n`):
```text
cytepool cube fingerprint 1
@@ -85,36 +208,160 @@ normalization
layer
```
-with a `layer` line for every group of every layer. The `1` of the first line is the
-version of the encoding ({api}`FINGERPRINT_VERSION`). {api}`Cube.digests` returns the
-digests in the form the file stores them, and the module documentation of
-`cytepool.cube.fingerprints` gives the encoding of every part byte by byte, so that
-another program can compute the fingerprint. The encoding was checked against an
-independent implementation ([Validation](../validation.md#cube-fingerprints)).
+with a `layer` line for each group of each layer: layers in the order counts, detected,
+log_sum, log_sum_sq, linear_sum, and groups in the cube's order, counted from 0. The
+fingerprint is the digest of the manifest.
+
+(digest-record)=
+### The digest record
+{api}`Cube.digests` returns, and a cube file stores as its root attribute `digests`, a
+JSON object with everything the manifest lists (the order of its members does not
+matter):
+
+```text
+{"version": 1, "shape": [, , ],
+ "samples": , "groups": , "features": ,
+ "n_cells": , "normalization": ,
+ "layers": {: {"type": "int64" or "float64",
+ "groups": []}},
+ "fingerprint": }
+```
+
+(provenance-keys)=
## Provenance records
-{api}`Cube.provenance` is a JSON object with three keys that CytePool writes;
-{api}`Cube.history` and {api}`describe_provenance` print it in plain terms.
+{api}`Cube.provenance` is a JSON object whose keys below are written by the functions and
+methods that build and derive cubes. Any other key that a caller gives
+(`Cube(provenance=...)`) is kept as it is. {api}`Cube.history` and
+{api}`describe_provenance` give a record in plain terms.
-| Key | What it records |
-| --- | --- |
-| `source` | Where the data came from, as the caller gives it to {api}`from_cells`, {api}`from_bulk`, {api}`from_anndata`, {api}`from_mudata` or `Cube(provenance={"source": ...})`. Stored as given and never interpreted; its references are reported as unresolved, since following them needs the tool that wrote them. |
-| `construction` | How the cube was built: the function, the CytePool version and its settings, such as the layers computed, the target sum of the normalization and where the sample, group and feature ids came from. A cube that {api}`CubeCollection.stack` built records each study's cube by its fingerprint, with that cube's provenance and its groups under their aligned ids. |
-| `operations` | Every operation that derived the cube, oldest first: `select`, `regroup` or `collapse_samples`, with what it kept, merged or pooled, and `input`, the fingerprint of the cube it was applied to. |
+**`source`:** the caller's record of where the data came from, given to
+{api}`from_cells`, {api}`from_bulk`, {api}`from_anndata` or {api}`from_mudata` as
+`source`: for example a dataset identity, references to the exporter's artifacts (such
+as frozen cell memberships, which stay in the exporter's files) and the exporter's
+settings. CytePool stores it as JSON, as it was given, does not interpret it, and
+reports its references as unresolved: following them needs the tool that wrote them.
+Null when no record was given.
+
+**`construction`:** how the cube was built: `function` (`"from_cells"`, `"from_bulk"`,
+`"from_anndata"`, `"from_mudata"` or `"CubeCollection.stack"`), `cytepool_version`, and
+the function's settings. {api}`from_cells` records:
+
+- `n_cells`: the number of cells pooled (the cells of each sample and group are the
+ cube's `n_cells`);
+- `layers`: the layers computed;
+- `target_sum`: the total that normalization scales every cell to;
+- `library_sizes`: `"cell_totals"` (the total count of each cell) or `"given"` (the
+ `library_sizes` argument), as in the normalization;
+- `samples` and `groups`: `{"order": ..., "labels": ...}`, where `order` is
+ `"sample_table"` or `"group_table"` (the rows of the sample or group table given) or
+ `"sorted_labels"` (the distinct labels, text sorted by code point and integers
+ numerically), and `labels` is `"text"` or `"integers"` (integer labels became their
+ decimal text);
+- `features`: where the feature ids came from: `"table"`, `"ids"` (a sequence),
+ `"data_frame_columns"` or `"positions"` (`feature_0`, `feature_1`, ...).
+
+{api}`from_bulk` records `samples` and `features`, where their ids came from:
+`"table"`, `"ids"`, `"data_frame_index"` (samples) or `"data_frame_columns"`
+(features), or `"positions"`.
+
+{api}`from_anndata` and {api}`from_mudata`, reading data that CytePool did not export,
+record:
+
+- `layout`: the layout that CytePool recognized in the data, which chose the sources
+ that `layers` did not name: `"scanpy.get.aggregate"`, `"decoupler.pp.pseudobulk"` or
+ `"liana.multi.adata_to_views"`, or `"given"` for data of no layout it recognizes;
+- `rows`: the number of profiles read, and for {api}`from_mudata` `modalities`, the
+ modality names;
+- `layers`: each cube layer with its source, `"X"` or a layer name;
+- `n_cells`: where the cell numbers came from: an `obs` column, `"table"` (a table of
+ samples x groups), `"uns['psbulk_stats']"` (LIANA's statistics) or null (none);
+- `samples` and `groups`, as {api}`from_cells` records them;
+- `obs_columns`: the metadata read into the tables, null when the tables were given.
+ {api}`from_anndata` gives the `obs` columns placed in the sample table and in the
+ group table, and those left out, as `{"samples": [...], "groups": [...],
+ "left_out": [...]}`, and records the label columns as `sample_column` and
+ `group_column`; {api}`from_mudata` lists the columns of the MuData's `obs` placed in
+ the sample table.
-Because each operation names its input by fingerprint, a chain can be checked link by
-link: apply each operation to the cube with its input fingerprint, and compare the
-result's fingerprint with the next record's input. The results of an analysis, such as
-{api}`limma_voom`'s, keep the fingerprint of the cube they were computed from, the
-digests it is built from and its provenance record.
+{api}`CubeCollection.stack`, which stacks the cubes of several studies, records:
+
+- `studies`: for each study, by its name and in the order stacked, the cube that the
+ collection was given, by its `fingerprint`, with that cube's own `provenance`, and
+ `groups`: each group stacked, by its aligned id, with the ids of the study's groups
+ that it sums;
+- `align_groups_by`: the group column whose values aligned the groups;
+- `group_names`, when the collection was given them: each aligned id with the name that
+ became its group's id;
+- `within_study`: the sample columns whose values were written `study::value`, as the
+ sample ids are;
+- `layers`: the layers stacked.
+
+**`operations`:** the operations that derived the cube, oldest first. Each is an object
+with `operation`, the method, and `input`, the fingerprint of the cube it was applied
+to, so that a chain of operations can be checked link by link: apply each operation to
+the cube with its input fingerprint, and compare the result's fingerprint with the next
+record's input, or with the cube's own fingerprint after the last.
+
+- `select`: `samples`, `groups` and `features`, the ids kept, in the order of the
+ result, or null for an axis that was given as None.
+- `regroup`: `membership`, the id of each merged or renamed group with the ids of the
+ groups it sums, in the input's order. The groups not listed are kept as they are.
+- `collapse_samples`: `by`, the sample column, a list of them, or null when every
+ sample was pooled into one, and `membership`, the id of each new sample with the ids
+ of the samples it sums, in the input's order.
+- `with_columns`: `samples`, `groups` and `features`, each with the names of the
+ columns `added` and those `replaced` on that axis, or null for an axis that was given
+ as None. The values are those of the cube's tables, which its fingerprint covers.
+- `from_anndata` and `from_mudata`: the import of an AnnData or MuData that CytePool
+ exported, whose provenance the cube continues. `input` is the fingerprint that the
+ export stored, which the cube's own fingerprint equals unless the data were changed
+ in between, and `source` the caller's record, or null.
+
+`input` is recorded when it is known without reading lazy layer values. That is so for
+a cube in memory: the first operation applied to it hashes its layers, once. It is so
+for a cube read from a cube file, which stores its digests, and for a lazy cube derived
+from one when every digest it needs is known: kept unchanged from the file, or kept
+from whole reads of its groups, as {api}`Cube.fingerprint` makes them. It is null for a
+lazy cube derived from another whose groups were not read whole, such as a cube file
+regrouped and then collapsed: call {api}`Cube.fingerprint` on the intermediate cube
+before deriving from it to record its fingerprint, or check the operations after the
+last recorded fingerprint together. It is always null for a cube whose lazy layers are
+other array-likes (a memmap or an HDF5 dataset), which can change: their fingerprint is
+computed anew on every call and never kept.
+
+The results of an analysis, such as {api}`limma_voom`'s, keep the fingerprint of the
+cube they were computed from, the digests it is built from and its provenance record.
+
+(anndata-record)=
+## The record of an AnnData or MuData export
+
+{api}`to_anndata` and {api}`to_mudata` keep a record in `uns["cytepool"]` (of the
+AnnData, or of the MuData), from which {api}`from_anndata` and {api}`from_mudata` give
+back the cube, with the same fingerprint unless the data were changed in between. Only
+text and integers are stored, which every anndata writer keeps:
+
+| Key | Value |
+| --- | --- |
+| `format` | `"cytepool.cube.anndata"` or `"cytepool.cube.mudata"` |
+| `format_version` | `1` |
+| `fingerprint` | the cube's fingerprint |
+| `provenance` | the provenance record, as JSON text |
+| `normalization` | the normalization, as JSON text |
+| `layers` | JSON text: each layer's name with its numpy type, such as `{"counts": "uint32"}` |
+| `n_cells` | JSON text: whether the cube has cell numbers |
+| `samples`, `groups` | JSON text: the whole sample or group table, so that samples and groups without a row are kept, as `{"columns": [{"name": ..., "type": ..., "values": [...]}]}`; the type is `text` or numpy's name, datetimes and timedeltas are int64 counts of their unit, NaN and infinities are the strings `"NaN"`, `"Infinity"` and `"-Infinity"`, and complex values pairs of numbers |
+| `features` | JSON text: for each column of the feature table, its `name`, the `var` column that holds it (`column`, null for `feature_id`, the index of `var`) and its `type` |
+(what-an-exporter-records)=
## What an exporter records
A tool that pools cells into a cube, such as CyteArc or another single-cell analysis
platform exporting its pseudobulk profiles, builds a {api}`Cube` from its sums, with
-lazy layers if they are large, and passes its own record as `source`. CytePool keeps
-whatever the record holds; these fields make a cube traceable to the cells it came from:
+lazy layers if they are large, and passes its own record as the `source` of the
+provenance, `Cube(..., provenance={"source": record})`. CytePool keeps whatever the
+record holds; these fields make a cube traceable to the cells it came from:
| Field | Example |
| --- | --- |
diff --git a/docs/source/reference/datasets.md b/docs/source/reference/datasets.md
index c259d1b..b77611c 100644
--- a/docs/source/reference/datasets.md
+++ b/docs/source/reference/datasets.md
@@ -36,13 +36,15 @@ tests. A notebook page names its datasets in its metadata; `make -C docs execute
prepares them, links them into the directory the page runs in, and records the
fingerprint of each file it was given, which the build checks against the registry.
+(networks)=
## Networks
The activity functions ({api}`ulm`, {api}`mlm` and {api}`weighted_aggregate`) read
networks that {api}`collectri` and {api}`progeny` download on first use into
`cytepool/networks` under `XDG_CACHE_HOME` or `~/.cache` (or the directory given as
`cache_dir`), each checked against its size and SHA-256 digest; no file of them is in
-the repository.
+the repository. [Activities from networks](../statistics/activities.md#regulator-networks)
+explains how they are used.
| Network | What it holds | Source | Licence | Cite |
| --- | --- | --- | --- | --- |
diff --git a/docs/source/reference/glossary.md b/docs/source/reference/glossary.md
index c6f5611..9a05e51 100644
--- a/docs/source/reference/glossary.md
+++ b/docs/source/reference/glossary.md
@@ -1,149 +1,280 @@
---
-description: The terms the CytePool documentation uses, each in one or two sentences, with the page that explains it.
+description: The terms the CytePool documentation uses, each in one sentence, with the section that explains it.
---
# Glossary
**Activity.** How strongly a transcription factor or a pathway acts in a sample or a
-contrast, read from its target genes: high where the genes it raises are high and
-those it lowers are low, as a network such as CollecTRI or PROGENy weights them.
-[Gene-set tests](../statistics/gene_set_tests.md#activities-of-regulators-and-pathways)
+contrast, read from the signed weights of a network on its targets: high where the genes
+it raises are high and those it lowers are low. [Networks of regulators and
+pathways](../statistics/activities.md#regulator-networks)
-**Adjusted p-value.** A p-value corrected for the number of tests, here usually by the
-method of Benjamini and Hochberg, which controls the false discovery rate. [Multiple
-testing](../statistics/multiple_testing.md)
+**Adjusted p-value.** A p-value corrected for the number of tests in its family, by
+default by the method of Benjamini and Hochberg, which controls the false discovery
+rate. [Error rates and their methods](../statistics/multiple_testing.md#error-rates)
-**B-statistic.** The log-odds that a gene's coefficient or contrast is not zero.
-[Empirical Bayes moderation](../statistics/empirical_bayes.md)
+**Aliased column.** A column of a design matrix that is a combination of the columns
+before it, so that its coefficient cannot be estimated apart from theirs. [Designs that
+cannot answer the question](../statistics/linear_models.md#estimable-contrasts)
+
+**B-statistic.** The log-odds that a gene's coefficient or contrast is not zero. [The
+B-statistic](../statistics/empirical_bayes.md#b-statistic)
+
+**Between-study variance (τ²).** The variance of the studies' true effects around their
+mean in a random-effects meta-analysis, zero in the fixed-effect model. [The
+between-study variance](../statistics/meta_analysis.md#between-study-variance)
**Competitive and self-contained tests.** A competitive gene-set test asks whether a
-set's genes change more than the other genes; a self-contained test asks whether they
-change at all. [Gene-set tests](../statistics/gene_set_tests.md)
+set's genes change more than the other genes, and a self-contained test whether they
+change at all. [Competitive or
+self-contained](../statistics/gene_set_tests.md#competitive-and-self-contained)
**Contrast.** A combination of a design's coefficients that answers one question, such
-as stimulated minus control. It is *estimable* when the samples determine it. [Designs,
-linear models and contrasts](../statistics/linear_models.md)
+as stimulated minus control. [Contrasts](../statistics/linear_models.md#contrasts)
+
+**Contribution weight.** A weight that roast gives a gene of a set, whose sign says in
+which direction the gene is expected to change and whose size how much it counts. [roast
+and mroast](../statistics/gene_set_tests.md#roast)
+
+**Cook's distance.** How much one sample's count moves a gene's fit, by which DESeq2
+removes the p-value of a gene that one count drives.
+[DESeq2](../statistics/count_models.md#deseq2)
**Counts per million (CPM).** Counts divided by the effective library size, times one
-million; log-CPM is their log2, after a small count is added. [Count
-models](../statistics/count_models.md)
+million. [Counts per million and
+log-CPM](../statistics/normalization_and_filtering.md#log-cpm)
-**Cube.** CytePool's container of pseudobulk profiles: for every sample, group and
-feature, sums over cells, with tables that describe the samples, groups and features.
-[The cube and its additive layers](../statistics/cubes.md)
+**Cox-Reid adjusted profile likelihood.** The likelihood of a gene's dispersion with the
+coefficients fitted at each value, adjusted for most of the bias that fitting them
+gives, which edgeR and DESeq2 maximize to estimate dispersions. [The Cox-Reid adjusted
+profile likelihood](../statistics/count_models.md#cox-reid)
-**Design.** The sample table with the model to fit: an R formula for the fixed effects,
-random effects in lme4's notation, and named contrasts. [Designs, linear models and
-contrasts](../statistics/linear_models.md)
+**Cube.** CytePool's container of pseudobulk profiles: for every group, sample and
+feature, sums over cells, with the tables, fingerprint and provenance record that trace a
+result to its data. [What a cube records](../statistics/cubes.md#what-a-cube-records)
+
+**Design.** The sample table with the model to fit: an R formula of the fixed effects,
+random effects in lme4's notation, and named contrasts. [The
+design](../statistics/linear_models.md#designs-and-formulas)
**Digest.** The SHA-256 hash of one part of a cube, such as a table or one group of a
-layer, from which the fingerprint is built. [Cube files and provenance
-records](cube_files.md)
+layer, from which the fingerprint is built. [Fingerprints](cube_files.md#fingerprint-encoding)
**Dispersion.** The parameter of a negative binomial distribution that sets how much the
-variance exceeds the mean; its square root is the biological coefficient of variation.
-[Count models](../statistics/count_models.md)
+variance exceeds the mean, whose square root is the biological coefficient of variation.
+[The negative binomial model](../statistics/count_models.md#negative-binomial-model)
+
+**Dispersion outlier.** A gene whose own dispersion estimate lies so far above DESeq2's
+trend that DESeq2 keeps it rather than shrink it towards the trend.
+[DESeq2](../statistics/count_models.md#deseq2)
+
+**Effective library size.** A library size times its normalization factor, by which
+counts per million and the offsets of count models scale the counts. [Normalization
+factors and size
+factors](../statistics/normalization_and_filtering.md#normalization-and-size-factors)
+
+**Empirical Bayes moderation.** Shrinking each gene's residual variance towards a value
+estimated from all the genes, which steadies the tests of experiments with few samples.
+[The model](../statistics/empirical_bayes.md#empirical-bayes-model)
-**Empirical Bayes moderation.** Shrinking each gene's variance towards a value estimated
-from all genes, which steadies tests with few samples. The prior degrees of freedom say
-how much the shared value counts. [Empirical Bayes
-moderation](../statistics/empirical_bayes.md)
+**Estimable contrast.** A contrast that the samples determine, a combination of the rows
+of the design matrix, as every contrast of a design of full rank is. [Designs that cannot
+answer the question](../statistics/linear_models.md#estimable-contrasts)
-**False discovery rate.** The expected share of false calls among all calls. [Multiple
-testing](../statistics/multiple_testing.md)
+**False discovery rate.** The expected share of false calls among all calls. [Error rates
+and their methods](../statistics/multiple_testing.md#error-rates)
+
+**Family-wise error rate.** The chance of at least one false call among the tests of a
+family. [Error rates and their methods](../statistics/multiple_testing.md#error-rates)
**Feature.** A measured variable, usually a gene: a column of a samples x features
matrix.
-**Fingerprint.** A digest of a cube's values, its tables, cell numbers and layers
-included, that names the cube however it was made. [Cube files and provenance
-records](cube_files.md)
+**Fingerprint.** A SHA-256 digest of what an analysis reads of a cube, its tables, cell
+numbers, normalization and layers, which names the cube however it was made. [What a
+cube records](../statistics/cubes.md#what-a-cube-records)
-**Gene set.** A named list of genes, such as a pathway or a signature, from a collection
-such as MSigDB, often read from a GMT file. [Gene-set
-tests](../statistics/gene_set_tests.md#gene-set-files)
+**Gene set.** A named list of genes that share a function or a response, such as a
+pathway or a signature, often read from a GMT file of a collection such as MSigDB.
+[Gene-set collections](../statistics/gene_set_tests.md#gene-set-collections)
**Gene set enrichment analysis (GSEA).** A test of whether the genes of a set gather at
-the top or the bottom of a ranking of all genes by a statistic, from the largest
-deviation of a walk down the ranking, its enrichment score. [Gene-set
-tests](../statistics/gene_set_tests.md#gene-set-enrichment-analysis)
+the top or the bottom of a ranking of all genes by a statistic, scored by the largest
+deviation of a walk down the ranking. [Gene set enrichment
+analysis](../statistics/over_representation_and_gsea.md#gsea)
+
+**Group.** The first axis of a cube, usually a cell type, whose cells are pooled per
+sample, so that one group's layers form a samples x features matrix; a bulk cube has one
+group. [The layers](../statistics/cubes.md#cube-layers)
+
+**Heterogeneity.** How much the studies of a meta-analysis disagree beyond their sampling
+error, measured by Cochran's Q, I² and the between-study variance. [Tests and
+heterogeneity](../statistics/meta_analysis.md#heterogeneity)
+
+**I².** The share of the variation between the studies of a meta-analysis that comes
+from heterogeneity rather than from sampling error, reported as a proportion. [Tests and
+heterogeneity](../statistics/meta_analysis.md#heterogeneity)
+
+**Independent filtering.** DESeq2's removal of the genes of low mean count before it
+adjusts the p-values, at the threshold that gives close to the most calls.
+[DESeq2](../statistics/count_models.md#deseq2)
+
+**Inter-gene correlation.** The correlation between the genes of a set from sample to
+sample, for which camera inflates the variance of the set's mean statistic.
+[camera](../statistics/gene_set_tests.md#camera)
+
+**Kenward-Roger's method.** An adjustment of the covariance of a mixed model's estimates,
+and of the degrees of freedom of their tests, for the uncertainty of the variance
+components with few samples. [Kenward-Roger's
+method](../statistics/mixed_models.md#kenward-roger)
-**Group.** The second axis of a cube, usually a cell type: the cells of one sample are
-pooled per group. A bulk cube has one group.
+**Layer.** One statistic of a cube summed over cells: the counts, the cells that detect
+each feature, or the sums of normalized and log-normalized values. [The
+layers](../statistics/cubes.md#cube-layers)
-**Pathway score.** One number per sample for a gene set, such as a GSVA or ssGSEA score,
-that summarizes the expression of the set's genes in that sample, so that pathways can
-be compared across samples and tested like genes. [Gene-set
-tests](../statistics/gene_set_tests.md#pathway-scores-per-sample)
+**Leading edge.** The genes of a set that drive its GSEA score, from the top of the
+ranking to the walk's extreme for a positive score, or from the extreme to the bottom for
+a negative one. [Gene set enrichment
+analysis](../statistics/over_representation_and_gsea.md#gsea)
-**Heterogeneity.** How much the studies of a meta-analysis disagree beyond their
-sampling error, measured by the between-study variance tau², Cochran's Q and I².
-[Meta-analysis](../statistics/meta_analysis.md)
+**Leverage.** A sample's diagonal element of the hat matrix of a design, which says how
+much the fit at that sample rests on the sample itself, so that a leverage of 1 means a
+sample the model fits exactly. [Design checks](../statistics/linear_models.md#design-checks)
-**Kenward-Roger's method.** An adjustment of the standard errors of a mixed model's
-estimates, and of the degrees of freedom of their tests, for the uncertainty of its
-variance components in small samples. [Mixed models, REML and dream](../statistics/mixed_models.md)
+**Library size.** The total count of a cell, or of a sample or profile summed over its
+genes. [Library sizes and
+composition](../statistics/normalization_and_filtering.md#library-sizes)
-**Layer.** One statistic of a cube, summed over cells: counts, detected cells, or sums
-of normalized and log-normalized values. [The cube and its additive
-layers](../statistics/cubes.md)
+**Log-CPM.** The log2 of counts per million after a small count is added to every count,
+so that zero counts have finite logs. [Counts per million and
+log-CPM](../statistics/normalization_and_filtering.md#log-cpm)
-**Library size.** The total count of a sample, or of a profile; times a normalization
-factor, the effective library size.
+**LOWESS span.** The share of the genes that each local fit of a LOWESS trend uses,
+which voom chooses from the number of genes. [The
+span](../statistics/limma_voom.md#voom-span)
+
+**Moderated t-statistic.** A coefficient or contrast divided by a standard error that
+uses the gene's moderated variance, tested on the gene's residual degrees of freedom plus
+the prior's. [The model](../statistics/empirical_bayes.md#empirical-bayes-model)
+
+**Multidimensional scaling (MDS) plot.** limma's plot of the samples, placed so that their
+distances approximate the leading log2 fold changes between them. [Samples at a
+glance](../statistics/normalization_and_filtering.md#samples-at-a-glance)
+
+**Network.** A map from each source, a transcription factor or a pathway, to its target
+genes with signed weights, such as CollecTRI's regulons or PROGENy's footprints.
+[Networks of regulators and pathways](../statistics/activities.md#regulator-networks)
**Normalization factor and size factor.** Corrections for sequencing depth and
-composition: edgeR multiplies library sizes by normalization factors (such as TMM), and
-DESeq2 replaces them by size factors. [Count models](../statistics/count_models.md)
+composition: edgeR multiplies library sizes by normalization factors, such as TMM's, and
+DESeq2 replaces them by size factors. [Normalization factors and size
+factors](../statistics/normalization_and_filtering.md#normalization-and-size-factors)
+
+**Normalized enrichment score.** A GSEA score divided by the mean of the scores of random
+sets of its sign, so that sets of different sizes compare. [p-values from random
+sets](../statistics/over_representation_and_gsea.md#gsea-p-values)
**Over-representation analysis.** A test of whether the genes called significant hold
-more of a set's genes than chance would give, by the hypergeometric distribution against
-a universe of genes. [Gene-set
-tests](../statistics/gene_set_tests.md#over-representation-analysis)
+more of a set's genes than chance would give, by the hypergeometric distribution over a
+universe of genes. [Over-representation
+analysis](../statistics/over_representation_and_gsea.md#over-representation)
+
+**Partial conjunction.** The hypothesis that fewer than u of the studies that tested a
+gene have an effect, whose rejection claims an effect in at least u studies. [An effect
+in at least u studies](../statistics/replication.md#partial-conjunction)
+
+**Pathway score.** One number per sample for a gene set, such as a GSVA score, that
+summarizes the expression of the set's genes in that sample, so that pathways can be
+compared across samples and tested like genes. [GSVA](../statistics/pathway_scores.md#gsva)
+
+**Precision weight.** A weight for each observation, the inverse of the variance that a
+mean-variance trend predicts for it, such as voom's. [voom](../statistics/limma_voom.md#voom)
-**Precision weight.** A weight for each observation, the inverse of its predicted
-variance, such as voom's. [Precision weights](../statistics/precision_weights.md)
+**Principal components.** The directions along which the samples' values vary most, from
+the singular value decomposition of the centred values of their most variable genes, on
+which DESeq2's PCA plot places the samples. [Samples at a
+glance](../statistics/normalization_and_filtering.md#samples-at-a-glance)
-**Provenance record.** The record of where a cube's data came from (its source), how it
-was built and every operation since, each naming its input by fingerprint. [Cube files
-and provenance records](cube_files.md)
+**Prior degrees of freedom.** How much the value that all genes share counts in empirical
+Bayes moderation, as a number of degrees of freedom added to each gene's own. [The
+model](../statistics/empirical_bayes.md#empirical-bayes-model)
-**Pseudobulk profile.** The sums over the cells of one sample and group, which treats
-the sample, not the cell, as the unit of replication. [Choosing a
-method](../statistics/choosing_a_method.md)
+**Proportion of true nulls (π₀).** The share of a family's tests without an effect,
+estimated from the p-values above a threshold, since those of tests without an effect are
+uniform. [The proportion of true
+nulls](../statistics/multiple_testing.md#proportion-of-true-nulls)
+
+**Proportion with an effect (π₁).** One minus π₀, which, estimated from one study's
+p-values of the genes that another study finds, gives the share of those genes that
+change in the first study, found there or not. [How well one study replicates
+another](../statistics/replication.md#pi1-between-studies)
+
+**Provenance record.** The record of a cube's source, of how it was built and of every
+operation since, each operation with the fingerprint of its input where that is known.
+[What a cube records](../statistics/cubes.md#what-a-cube-records)
+
+**Pseudobulk profile.** The sums over the cells of one sample and group, which make the
+sample, not the cell, the unit of replication. [Why pseudobulk](../why_pseudobulk.ipynb)
**Quasi-dispersion.** In edgeR's quasi-likelihood model, a gene's own multiplier of the
-negative binomial variance. [Count models](../statistics/count_models.md)
+negative binomial variance, which empirical Bayes moderates. [edgeR's quasi-likelihood
+F-tests](../statistics/count_models.md#quasi-likelihood)
**Random intercept.** A shift shared by the samples of one level of a grouping factor,
-such as a donor, drawn from a normal distribution whose variance is estimated. [Mixed
-models, REML and dream](../statistics/mixed_models.md)
+such as a donor, drawn from a normal distribution whose variance is estimated. [The
+model](../statistics/mixed_models.md#mixed-model)
+
+**Reference level.** The first level of a factor, from which the coefficients of its
+other levels are differences in a design with an intercept. [The
+design](../statistics/linear_models.md#designs-and-formulas)
-**Reference level.** The first level of a factor, against which the coefficients of its
-other levels are differences. [Designs, linear models and
-contrasts](../statistics/linear_models.md)
+**REML.** Restricted maximum likelihood, which estimates variance components from the
+part of the data that the fixed effects do not fit, without the downward bias that
+estimating the fixed effects gives maximum likelihood estimates.
+[REML](../statistics/mixed_models.md#mixed-model-reml)
-**REML.** Restricted maximum likelihood, which estimates variance components without the
-bias that estimating the fixed effects gives maximum likelihood. [Mixed models, REML and
-dream](../statistics/mixed_models.md)
+**Residual degrees of freedom.** What a fit leaves to estimate the variance: the number of
+samples it uses minus the number of coefficients, or, in a mixed model, an approximation
+between $n - p$ and $n$ minus the rank of the fixed and random effects together.
+[REML](../statistics/mixed_models.md#mixed-model-reml)
-**Residual degrees of freedom.** The number of samples a fit uses minus the number of
-coefficients it estimates: what is left to estimate the variance.
+**Robust moderation.** Empirical Bayes moderation whose prior is estimated so that genes
+with outlying variances count less and keep more of their own variance. [Estimating the
+prior](../statistics/empirical_bayes.md#estimating-the-prior)
**Rotation test.** A test that compares a statistic with its values under random
-rotations of a linear model's residual space, which leave the data's distribution
-unchanged under the null hypothesis. [Gene-set tests](../statistics/gene_set_tests.md)
+rotations of a linear model's residual space, which leave the distribution of the data,
+the correlation between genes included, unchanged under the null hypothesis. [The effects
+of each gene](../statistics/gene_set_tests.md#gene-effects)
-**Sample.** The unit of replication: a donor, or a donor under one condition. Its cells
-are pooled into one profile per group.
+**Sample.** The unit of replication, such as a donor or a donor under one condition,
+whose cells are pooled into one profile per group.
**Sample quality weight.** A weight for each sample, from how closely its values follow
-the model across genes. [Precision weights](../statistics/precision_weights.md)
+the model across all genes. [Sample quality
+weights](../statistics/limma_voom.md#sample-quality-weights)
-**Satterthwaite's degrees of freedom.** An approximation to the degrees of freedom of a
-t-test in a mixed model. [Mixed models, REML and dream](../statistics/mixed_models.md)
+**Satterthwaite's degrees of freedom.** An approximation of the degrees of freedom of a
+mixed model's t-test, from how precisely the estimated variance components determine its
+standard error. [Satterthwaite's degrees of
+freedom](../statistics/mixed_models.md#satterthwaite)
**Source record.** The caller's record of where a cube's data came from, kept in its
-provenance as given. [Cube files and provenance records](cube_files.md)
+provenance as it was given. [Provenance records](cube_files.md#provenance-keys)
+
+**SumRank.** A test of replication that ranks the genes within each study and sums every
+gene's ranks across the studies, so that a gene must rank high in study after study.
+[SumRank](../statistics/replication.md#sum-of-ranks)
**TREAT.** A test that a fold change exceeds a threshold, rather than that it is not
-zero. [Empirical Bayes moderation](../statistics/empirical_bayes.md)
+zero. [Testing against a fold-change
+threshold](../statistics/empirical_bayes.md#treat-test)
+
+**Universe.** The genes that an over-representation analysis could have selected, by
+default the genes tested. [The
+universe](../statistics/over_representation_and_gsea.md#over-representation-universe)
+
+**Variance fraction.** The share of a gene's variance that one variable of the design
+explains, from a mixed model fitted by maximum likelihood, which variancePartition's help
+page reports gives unbiased fractions where REML can show substantial bias. [Variance
+partitioning](../statistics/mixed_models.md#variance-fractions)
diff --git a/docs/source/reference/literature.md b/docs/source/reference/literature.md
deleted file mode 100644
index 8c6310d..0000000
--- a/docs/source/reference/literature.md
+++ /dev/null
@@ -1,126 +0,0 @@
----
-description: A short reading list on sample-level statistics for single-cell and bulk data, grouped by the questions studies ask, each paper with a line on what it shows.
----
-
-# Literature
-
-A reading list grouped by the questions that studies of many samples ask
-([Overview](../index.md)). Each line says what the paper shows, in its own terms, and
-numbers are those the papers report. The methods behind CytePool's functions are cited
-on the pages of [How the statistics work](../statistics/choosing_a_method.md); every
-work is in the [Bibliography](bibliography.md).
-
-## Cells or samples
-
-- @squair2021 compared differential expression methods on 18 datasets with matched bulk
- RNA-seq. Methods that ignore the variation between biological replicates were biased
- and prone to false discoveries, finding hundreds of differentially expressed genes
- where there were no biological differences.
-- @zimmerman2021 showed that cells of one individual are not independent, and that
- ignoring it inflates the type 1 error. They proposed mixed models with a random effect
- for the individual, and found pseudobulk methods conservative in comparison.
-- @murphy2022 reran that benchmark with a measure that balances false positives and
- false negatives, and found that pseudobulk methods performed best.
-- @junttila2022 compared 18 methods for differential states in multi-subject data:
- pseudobulk methods performed generally best, and methods that treat cells as
- independent gave many false positives.
-- @crowell2020 surveyed cell-level mixed models and pseudobulk methods for differential
- state analysis across samples and conditions, with a simulation of multi-sample
- single-cell data (muscat).
-- @lun2017 showed that ignoring plate effects loses control of the type I error, and
- that summing the counts of the cells of each plate restores it, without losing power
- in their simulations.
-- @hafner2025 found that methods built for single cells offered no advantage over
- pseudobulk DESeq2 on individual datasets in nested designs, and suggested dream as a
- compromise at atlas scale.
-- @nguyen2023 benchmarked 46 workflows for data from several batches: batch-corrected
- data rarely improved the analysis of sparse data, while modelling the batch as a
- covariate helped where batch effects were large.
-- @heumos2023 summarized independent benchmarks into best-practice workflows for
- single-cell analysis.
-
-## Which genes change, and do they replicate
-
-- @kang2018 pooled cells of 8 lupus patients, split into interferon-beta stimulated and
- untreated cultures, and assigned cells to donors by their genotypes; the documentation
- uses these data ([Datasets](datasets.md)).
-- @perez2022 profiled more than 1.2 million blood cells of 162 lupus cases and 99
- controls, and found interferon-stimulated genes raised in monocytes and fewer naive
- CD4 T cells.
-- @murphy2023 re-analysed the first single-nucleus study of Alzheimer's disease: testing
- donors rather than cells gave 26 differentially expressed genes at a false discovery
- rate of 0.05, against 14,274, 549 times fewer.
-- @nakatsuka2025 found that over 85% of the Alzheimer's disease genes called in one
- dataset were not reproduced in any of 16 others, and that a meta-analysis of ranks
- across datasets found genes that predicted disease better.
-- @garg2021 re-analysed COVID-19 single-cell studies in a standard way and confirmed 8
- of 20 published results across datasets.
-
-## Which cell types expand or contract
-
-- @phipson2022 introduced propeller, which tests transformed cell-type proportions with
- linear models and empirical Bayes moderation across replicates.
-- @buttner2021 introduced scCODA, a Bayesian model of compositional single-cell data.
-- @mangiola2023 introduced sccomp, which models composition and its variability
- together, and found that the variability of proportions depends on their mean in every
- technology examined.
-- @hoffman2026b introduced crumblr, precision-weighted mixed models of cell-type
- composition that test several levels of the cell lineage at once.
-- @dann2022 introduced Milo, which tests differential abundance in neighbourhoods of a
- k-nearest neighbour graph instead of clusters.
-- @yi2024 benchmarked six differential abundance methods for condition-specific cell
- states, and gave recommendations by dataset.
-- @stephenson2021 profiled over 780,000 blood cells of 130 COVID-19 patients and related
- the expansion of cell populations to disease severity.
-
-## Which pathways and regulators respond
-
-- @wu2012 showed that gene set tests that permute genes are very sensitive to the
- correlation between genes, and introduced camera, which allows for it.
-- @wu2010 introduced ROAST, which rotates the residuals of a linear model instead of
- permuting samples, so that it applies to almost any design.
-- @subramanian2005 introduced gene set enrichment analysis.
-- @timmons2015 and @wijesooriya2022 showed that over-representation analyses depend on
- the universe of genes they test against: a universe of every gene of the genome,
- rather than of the genes measured, makes sets look significant that are not.
-- @holland2020 found that tools for pathway and transcription-factor activity built for
- bulk data, with curated gene sets, work on single-cell data, and that their results
- depend more on the gene sets than on the statistic.
-- @badiaimompel2022 collected activity methods in one framework, decoupleR, and found
- simple linear models among the best at predicting perturbed regulators.
-
-## What drives variation between donors
-
-- @hoffman2016 introduced variancePartition, which uses linear mixed models to attribute
- the variation of each gene to disease, sex, tissue, genetics or technical factors.
-- @hoffman2021 introduced dream, mixed models for repeated measures designs, and showed
- that other methods can give reproducible false positives driven by the genetic
- regulation of expression.
-- @hoffman2026a introduced dreamlet, which runs precision-weighted mixed models on the
- pseudobulk profiles of every cell type, and applied it to 1.4 million nuclei from 150
- Alzheimer's disease cases and 149 controls.
-- @yazar2022 profiled 1,267,758 blood cells of 982 donors and mapped 26,597 independent
- cis-expression quantitative trait loci across 14 cell types.
-
-## Which interactions change
-
-- @dimitrov2022 compared 16 resources and 7 methods for cell-cell communication, and
- found that the choice of both strongly changes the predicted interactions.
-- @dimitrov2024 introduced LIANA+, a framework for cell-cell communication in
- single-cell and spatial data, from one or several conditions.
-
-## How cells respond to drugs
-
-- @srivatsan2020 introduced sci-Plex and profiled about 650,000 single cells across
- about 5,000 samples of three cancer cell lines exposed to 188 compounds.
-
-## Combining studies
-
-- @czi2025 described CZ CELLxGENE Discover, a curated corpus of over 93 million unique
- cells with standardized metadata.
-- @sikkema2023 integrated 49 datasets into the Human Lung Cell Atlas, over 2.4 million
- cells from 486 individuals.
-- @urbut2019 introduced mash, which estimates many effects across many conditions while
- allowing for correlations between them.
-- @viechtbauer2010 described metafor, the R package for meta-analysis that CytePool's
- meta-analysis follows.
diff --git a/docs/source/statistics/activities.md b/docs/source/statistics/activities.md
new file mode 100644
index 0000000..df7c713
--- /dev/null
+++ b/docs/source/statistics/activities.md
@@ -0,0 +1,203 @@
+---
+description: How active transcription factors and pathways are in each sample or contrast, read from the signed targets of a network such as CollecTRI or PROGENy.
+---
+
+# Activities from networks
+
+[Pathway scores per sample](pathway_scores.md) take the genes of a set without signs. A
+transcription factor, though, raises some of its targets and lowers others, and a
+signalling pathway leaves a footprint of genes that respond to it, some up and some down.
+A network gives each such source signed weights on its targets, and its activity is read
+from them: high where the genes it raises are high and those it lowers are low.
+{api}`ulm` scores every source of a network in every sample or contrast by default, as
+decoupler computes it [@badiaimompel2022], and {api}`mlm` and {api}`weighted_aggregate`
+compute two more of decoupler's methods. [Pathway and regulator
+activity](../guides/pathway_activity.ipynb) scores the regulators and pathways of
+published data.
+
+(regulator-networks)=
+## Networks of regulators and pathways
+
+A {api}`Network` gives each source, a transcription factor or a pathway, its target
+genes, each with a weight: positive where the source raises the gene and negative where
+it lowers it. Two networks are built in:
+
+- {api}`collectri`, CollecTRI's signed regulons of human transcription factors, gathered
+ from twelve resources of curated literature and text mining [@muellerdott2023]: a
+ weight of 1 for a target that a factor activates and -1 for one that it represses.
+- {api}`progeny`, PROGENy's footprints of 14 signalling pathways: the z-scores with which
+ genes responded to the perturbation of each pathway in hundreds of human experiments
+ [@schubert2018], transferred to mouse [@holland2020b], from the R package progeny.
+
+Both download their files on first use from where they were published, check them
+against pinned SHA-256 digests and keep them in a cache, so that a machine without a
+connection can use a file copied there. Each resource of CollecTRI keeps its own terms,
+which OmniPath records [@tuerei2021]. The default level, `licence="commercial"`, keeps the
+interactions that a resource listed for commercial use supports, and
+`licence="academic"` adds those that only a resource listed for academic use supports
+([Datasets](../reference/datasets.md#networks) gives the terms and the counts). Any
+network given as a table of `source`, `target` and `weight`, or as gene sets, whose genes
+weigh 1, works the same way, and {api}`Network.select` keeps some of its sources.
+
+(activity-profiles)=
+## Profiles and targets
+
+A profile is one vector of values over genes: a sample's expression, or a contrast's
+statistics. The activity functions score
+
+- a samples x genes matrix, one profile per sample;
+- a cell type of a cube, one profile per sample, read as the pathway scores read it
+ ([The values scored](#pathway-score-values));
+- a differential-expression result, one profile per cell type and contrast, by default
+ of its moderated t-statistics, the statistics that decoupler's guides score; or one
+ profile from a table of results, or from gene ids mapped to statistics.
+
+Each profile uses the genes that have a value in it, NaN marking a missing one, so that a
+profile of a result keeps the genes its cell type tested. In sparse data, genes that are
+zero in every profile were not detected, and they are left out, as in decoupler. A
+source is scored in a profile when at least `min_targets` of its targets, matched by
+their ids, are among the profile's genes. The result, an {api}`Activities`, holds the
+scores of the profiles by sources, with their estimates (slopes, coefficients, or
+weighted means or sums), their p-values and the p-values adjusted by Benjamini and
+Hochberg's method over the sources of each profile [@benjamini1995];
+{api}`Activities.table` lists them one per row. {api}`plot_activities` draws the scores
+as a heatmap, and {api}`plot_top_activities` draws a bar for each of the sources that
+score highest in absolute value in one profile, as decoupler's `pl.barplot` does.
+
+(ulm)=
+## ULM
+
+The univariate linear model fits, for each source, a line through the values $y_g$ of the
+profile's $n$ genes against the source's weights $x_g$, which are zero at the genes
+outside its targets,
+
+$$
+y_g = \beta_0 + \beta_1 x_g + \varepsilon_g,
+$$
+
+and scores the source by the slope's t-statistic,
+$t = \hat\beta_1 / \mathrm{se}(\hat\beta_1) = r \sqrt{(n - 2) / (1 - r^2)}$ for the
+correlation $r$ between $y$ and $x$, tested on $n - 2$ degrees of freedom. Each source is
+tested against all the other genes, those of other sources included.
+
+(mlm)=
+## MLM
+
+The multivariate linear model fits all the sources of a profile at once, with an
+intercept,
+
+$$
+y_g = \beta_0 + \sum_s \beta_s x_{gs} + \varepsilon_g,
+$$
+
+so that each score, the t-statistic of $\beta_s$ with $n - m - 1$ residual degrees of
+freedom for $m$ sources, measures a source beyond what the others explain: where two
+regulators share targets, ULM credits both with their response and MLM splits it between
+them. MLM needs at least two more genes than sources, and sources whose weights are not
+linear combinations of the weights of others; it refuses those that it cannot tell
+apart, by name.
+
+(weighted-aggregate)=
+## The weighted aggregate
+
+The weighted aggregate scores a source by the weighted mean of its targets' values,
+$\sum_g w_g y_g / \sum_g |w_g|$; without the denominator, it is their weighted sum,
+PROGENy's original score [@schubert2018]. With random permutations of the genes, the
+score becomes $(\mathrm{ES} - \text{mean}) / \text{sd}$ over the permutations' scores,
+and its two-sided p-value is twice the smaller one-sided one, $(1 + b)/(1 + n)$ for the
+$b$ of $n$ permutations that score at least, or at most, as high [@phipson2010].
+
+(activities-or-scores)=
+## Activities or scores
+
+Activities use the signs and sizes of a network's weights, so that a regulator's
+repressed targets count against it, where [pathway scores](pathway_scores.md) take the
+genes of a set without signs; {api}`Network.to_gene_sets` gives each source's targets as
+a set, without their signs, for the pathway scores and the gene-set tests. Scored on the
+statistics of a contrast, activities say which regulators drive a response, in each cell
+type. Scored on the samples, they can be compared, correlated with other measurements
+and tested with a design, as pathway scores are: {api}`limma` takes the activities of
+samples with the cube's design ([Testing scores with a design](#testing-scores)). The
+activities of contrasts carry their own p-values, from each source's test across genes,
+and {api}`limma` refuses them.
+
+(activities-defaults)=
+## Defaults and choices
+
+- **The method.** {api}`ulm` by default, decoupler's default for regulators and pathways
+ alike; simple linear models were among the best at predicting perturbed regulators in
+ decoupler's benchmark [@badiaimompel2022]. {api}`mlm` separates sources whose targets
+ overlap ([MLM](#mlm)). {api}`weighted_aggregate` gives the weighted mean or sum of the
+ targets, with permutation p-values.
+- **A contrast or the samples.** Score a result's statistics, the moderated t-statistic
+ by default (`statistic_column="t"`), to see which regulators drive a response; score a
+ cube's cell type to compare its samples or to test them with a design.
+- **The networks.** `collectri()` keeps the interactions for commercial use, and
+ `collectri(licence="academic")` all of them. `progeny()` keeps the genes of each
+ pathway whose p-value is below 0.05, as decoupler does (`max_p_value`), and
+ `progeny(top=100)` the 100 with the smallest p-values, as progeny's `getModel` does by
+ default; `organism="mouse"` gives the mouse model.
+- **Targets.** `min_targets=5`, decoupler's `tmin`.
+- **Permutations.** The weighted aggregate draws 1,000 permutations from `rng`, as many as
+ decoupler; `n_permutations=0` gives the weighted means or sums alone, and
+ `statistic="sum"` the sums.
+
+(activities-limits)=
+## Limits and differences
+
+- **MLM's memory.** For each set of profiles with the same genes, MLM holds a matrix of
+ genes by sources and its decomposition, so its memory grows with the number of genes
+ times the number of sources ({api}`mlm` gives the size).
+- **PROGENy's gene names.** PROGENy names genes by the symbols of progeny 1.28.0, from
+ Bioconductor 3.20, so data named by current symbols miss the genes renamed since;
+ decoupler takes PROGENy from OmniPath, which gives some of them their current
+ symbols.
+- **No VIPER.** decoupler's VIPER is not offered ([Not implemented
+ yet](#not-implemented-yet) says why).
+- **MLM's adjusted p-values.** They are Benjamini and Hochberg's, as for the other
+ methods; decoupler leaves MLM's p-values unadjusted and reports them as adjusted.
+- **Precision.** p-values are computed in double precision, from the upper tail.
+ decoupler stores its adjusted p-values as single-precision numbers, and takes MLM's
+ p-values as one minus the distribution function, so that small ones lose their digits
+ and the smallest become 0.
+- **The weighted aggregate's p-values.** CytePool doubles the smaller one-sided
+ permutation p-value. decoupler takes the share of permutations whose absolute score
+ exceeds the observed one, a two-sided p-value already, and folds it again as if it
+ were one-sided, so that a score near zero, which most permutations exceed, gets a
+ p-value near $2/n$ for $n$ permutations. When every permutation scores alike, the
+ normalized score is NaN, where decoupler's is infinite, or 0 for a score of 0.
+- **ULM's t-statistic.** It is exact; decoupler adds 2.2e-16 to $1 - r$ and $1 + r$,
+ which changes the score only where $|r|$ is within about $10^{-6}$ of 1.
+- **Missing values and empty profiles.** Each profile uses the genes observed in it, and
+ a profile whose values are all zero has no scores; decoupler refuses missing values and
+ leaves such a profile out. Where MLM cannot tell sources apart, CytePool names them,
+ where decoupler gives them NaN t-statistics.
+- **CollecTRI's file.** The 185 interactions without references are kept, since the file
+ holds them and the paper counts them; decoupler's loader drops them.
+
+(activities-checks)=
+## Checks and further reading
+
+The activities, their bar plot and the networks are compared with decoupler 2.2.0 and
+progeny 1.28.0: exact (1e-10), the normalized scores given decoupler's permutations, and
+within 1.2e-7 for ULM's adjusted p-values, which decoupler keeps in single precision. The
+networks hold every interaction of decoupler's CollecTRI, and the 185 that its loader
+drops; CollecTRI's licence levels follow OmniPath's records, and PROGENy's weights are
+progeny's ([Validation](#validation-activity)). The tests of activities with a design are
+those of pathway scores ([Checks and further reading](#pathway-scores-checks)).
+
+Further reading:
+
+- @holland2020 found that tools for pathway and transcription-factor activity built for
+ bulk data, with curated gene sets, work on single-cell data, and that their results
+ depend more on the gene sets than on the statistic.
+- @badiaimompel2022 collected activity methods in one framework, decoupleR, and
+ benchmarked them on perturbation experiments.
+- @muellerdott2023 assembled CollecTRI, signed regulons of human transcription factors
+ that expand the coverage of earlier collections.
+- @schubert2018 introduced PROGENy, which infers the activity of signalling pathways
+ from the genes that respond to their perturbation rather than from the pathways'
+ members.
+
+[Aligning studies](aligning_studies.md) begins the last part of the book, on several
+studies.
diff --git a/docs/source/statistics/aligning_studies.md b/docs/source/statistics/aligning_studies.md
new file mode 100644
index 0000000..3aeb9d9
--- /dev/null
+++ b/docs/source/statistics/aligning_studies.md
@@ -0,0 +1,156 @@
+---
+description: How the cubes of several studies are aligned by sample, gene and cell type before their results are compared, combined or modelled together.
+---
+
+# Aligning studies
+
+[Activities from networks](activities.md) completed the analysis of one study: its genes,
+cell types, pathways and regulators. A finding counts for more when other studies of the
+same question reproduce it, and the chapters of this part compare and combine studies.
+Studies can be compared only once their samples, genes and cell types are aligned, and
+{api}`CubeCollection` aligns them, keeping each study's cube as it was given. [Align and
+combine studies](../guides/combine_studies.ipynb) aligns six simulated cohorts and analyses
+each.
+
+(why-findings-fail-to-replicate)=
+## Why findings fail to replicate
+
+Results of one study often do not hold in others: of the genes called differentially
+expressed in Alzheimer's disease in one dataset, over 85% were not reproduced in any of 16
+others [@nakatsuka2025]. Some of these are changes that the other studies were too small
+to find; others belong to one study, to its donors, its protocol or a confounder of its own.
+Telling the two apart takes several studies analysed alike.
+
+Aligned by hand, studies go wrong in two ways: donor ids repeat across studies, so that
+donor `D1` of one study is taken for donor `D1` of another, and genes that a study did not
+measure are read as zeros. Each study also names its cell types in its own words.
+
+(aligning-samples)=
+## Samples and donors
+
+A sample id means something only inside its study, so {api}`CubeCollection` names every
+sample `study::sample_id`, and does the same for the columns named in `within_study`, such
+as donor ids: donor `D1` of two studies is then two people, not one, in any design built
+on the collection. The collection's sample table ({api}`CubeCollection.samples`) joins the
+studies' sample tables, with missing values where a study lacks a column; the class's
+documentation says how the types of a column are combined, and a column of text in one
+study and of numbers in another is refused.
+
+(aligning-genes)=
+## Genes
+
+Genes are matched by `feature_id` exactly as written, so every study must use ids of one
+kind, such as Ensembl gene ids, without versions: `ENSG00000141510.17` and
+`ENSG00000141510` differ. Some tools give every study the same genes and fill the genes
+that a study lacks with zeros, so a cube can mark such genes False in a feature column
+`measured`: a study measured the genes that its cube has and does not mark so. The
+collection keeps every gene that some study measured, and {api}`CubeCollection.measured`
+records which study measured which, so that an unmeasured gene is missing for that study
+rather than zero.
+
+A missing gene changes nothing for the other genes. A gene without counts changes no
+library size, expression filter or normalization factor, so each study's results on the
+genes it measured are those of its cube as given.
+
+(aligning-cell-types)=
+## Cell types
+
+Cell types are aligned by their Cell Ontology ids [@diehl2016], the column
+`cell_ontology_id` of the group tables, when every study's cube has it, and by
+`group_id` otherwise; `align_groups_by` names another column. `group_mapping` gives some
+groups of a study an aligned id of their own, or None to leave a group out, and
+`group_names` names the aligned groups for the tables and figures that follow, such as
+CD14 monocytes for CL:0001054. Two groups of one study that receive the same aligned id are
+summed into one, exactly, since every layer of a cube is a sum ([Exact
+regrouping](cubes.md#exact-regrouping)): naive and memory CD4 T cells of one study,
+mapped to CL:0000624, become the CD4 T cells of the others.
+{api}`CubeCollection.group_members` shows which of a study's groups went into each aligned
+group.
+
+(one-analysis-per-study)=
+## One analysis per study
+
+Each study is analysed on its own. {api}`CubeCollection.aligned_cube` returns a study's
+cube in the collection's terms, with its cell types renamed and merged as aligned and only
+the genes it measured, but with the study's own samples and sample ids, so that a design
+built from the study's sample table fits it. {api}`analyse_each_study` builds that design
+for every study, with one formula, its levels and contrasts, and runs {api}`limma_voom`,
+{api}`limma_trend` or {api}`dream_voom` on each aligned cube. Each study keeps its own
+design, normalization, expression filter and variances.
+
+The {api}`StudyResults` it returns keeps the results with their collection.
+{api}`StudyResults.table` lines up the results of one cell type and contrast gene by gene,
+with a row for every study and gene, and a status that gives the reason when a study lacks
+a result for a gene: it did not measure the gene (`not_measured`), measured it and left it
+out, as its expression filter does (`not_tested`), or did not analyse the cell type or the
+contrast (`not_analysed`). These are different findings, and a missing result is never
+read as a zero. {api}`StudyResults.matrix` puts one column of every study, such as the
+p-values, into a studies x genes matrix. The [meta-analysis](meta_analysis.md) and the
+[replication](replication.md) of the next chapters take these results.
+
+(stacked-studies)=
+## A joint model of stacked studies
+
+{api}`CubeCollection.stack` builds one cube of all the studies' samples for a joint model,
+whose sample table names each sample's study. A cube holds no missing values, so the
+stacked cube keeps the genes that every study measured and the cell types that every study
+has, and refuses the others rather than fill them with zeros. Each study keeps its own
+counts and library sizes, and a normalized layer is stacked only from studies normalized
+alike. The design `~ study + condition` gives every gene a baseline in each study and one
+difference between the conditions, shared by all; with several samples per donor,
+`within_study` keeps the donors of different studies apart.
+
+A joint model suits studies made alike, with one protocol and platform, that are too small
+to estimate their variances alone. It estimates one difference for every study, so a change
+in one study enters the estimate for all, and it tests only the genes that every study
+measured.
+
+(aligning-studies-defaults)=
+## Defaults and choices
+
+- **Cell types.** Aligned by `cell_ontology_id` when every study has it, by `group_id`
+ otherwise, or by the column `align_groups_by` names; `group_mapping` and `group_names`
+ map and name them.
+- **Ids of donors and other columns.** `within_study` names the sample columns, such as
+ donor ids, that are local to each study.
+- **The analysis of each study.** {api}`limma_voom` by default in {api}`analyse_each_study`;
+ its other options, such as `groups` or `expression_filter`, apply to every study.
+- **Combine results or stack studies.** A meta-analysis of each study's results, the
+ default, needs only the results, keeps the genes that some studies did not measure, and
+ measures how far the studies disagree ([Meta-analysis](meta_analysis.md)); a joint model of
+ the stacked studies suits studies made alike, as above.
+
+(aligning-studies-limits)=
+## Limits and differences
+
+- **Equal ids only.** Cell types are aligned by equal values of a column. Rolling them up
+ to a common ancestor of the Cell Ontology needs the ontology as a resource and is not
+ implemented: give such merges in `group_mapping` ([Not implemented
+ yet](#not-implemented-yet)).
+- **Gene ids as given.** Ids are not converted between kinds or versions.
+- **A stack in memory.** {api}`CubeCollection.stack` returns a cube in memory, of the genes
+ and cell types common to every study; of a lazy study cube, only those are read. The
+ collection itself reads no layer.
+
+(aligning-studies-checks)=
+## Checks and further reading
+
+The alignment of three simulated studies, with repeated donor ids, merged cell types,
+genes filled with zeros and marked as not measured, is compared with anndata 0.13.4's
+`concat` and pandas 3.1.0, and limma-voom on each aligned study and on the stack with limma
+3.62.1 and edgeR 4.4.0 run on the studies' own counts: exact, with identical samples, genes
+and layers, and limma's tables within 3.8e-13 ([Validation](#validation-cube-collection)).
+A control shows why donor ids are written with their study: taken for one person, donor
+`D1` of two studies changes the fit of every gene.
+
+Further reading:
+
+- @diehl2016 described the Cell Ontology, whose ids name cell types alike across
+ studies.
+- @czi2025 described CZ CELLxGENE Discover, a curated corpus of over 93 million unique
+ cells with standardized metadata.
+- @sikkema2023 integrated 49 datasets into the Human Lung Cell Atlas, over 2.4 million
+ cells from 486 individuals.
+
+[Meta-analysis](meta_analysis.md), the next chapter, combines the results of the
+studies.
diff --git a/docs/source/statistics/choosing_a_method.md b/docs/source/statistics/choosing_a_method.md
index 2b1eb15..915a8e9 100644
--- a/docs/source/statistics/choosing_a_method.md
+++ b/docs/source/statistics/choosing_a_method.md
@@ -1,94 +1,237 @@
---
-description: Which CytePool function answers which question, for which design, and why the sample, not the cell, is the unit of every test.
+description: Which CytePool function answers each question about samples, the default and when to choose another, and how the chapters that explain the methods follow from one another.
---
# Choosing a method
-CytePool's tests compare samples, not cells: each takes one profile per sample, so that
-a difference is measured against the variation between donors. Cells of one donor share
-its genetics and environment, and a test that treats them as independent replicates
-finds differences that do not hold across donors [@squair2021; @zimmerman2021;
-@murphy2022], as [Why pseudobulk](../why_pseudobulk.ipynb) shows. Pool the cells of
-each sample and cell type into a cube first ({api}`from_cells`); the methods below then
-run on its pseudobulk profiles.
-
-## Genes that change in each cell type
-
-| The design | Function | What it fits |
-| --- | --- | --- |
-| Conditions, with covariates or a pairing as fixed effects, such as `~ donor + condition` | {api}`limma_voom` | Log-CPM values with voom's precision weights, a weighted linear model, empirical Bayes moderation [@law2014; @ritchie2015] |
-| The same, with library sizes within about threefold of each other | {api}`limma_trend` | Log-CPM values, a linear model, a prior variance that follows the expression level |
-| Several samples of each donor, and a variable that differs between donors, such as disease or sex | {api}`dream_voom` | voom's weights from mixed models, a random intercept per donor, Kenward-Roger's or Satterthwaite's t-tests as dream chooses them, empirical Bayes [@hoffman2021] |
-
-Each runs on every group (cell type) of a cube, one at a time, and returns a
-{api}`DifferentialExpressionResult` with a table for every cell type and contrast
-({api}`DifferentialExpressionResult.table`), a summary, and which samples each fit used
-and why the others were left out. The [limma User's
-Guide](https://bioconductor.org/packages/release/bioc/vignettes/limma/inst/doc/usersguide.pdf)
-(sections 15.4 and 15.5) says that limma-trend usually works well when the largest
-library size is not more than about three times the smallest, and that voom is more
-powerful when library sizes are quite variable. Pseudobulk library sizes grow with the
-number of cells pooled, which can differ severalfold between samples, and
-{api}`limma_voom` is the default.
-
-A fixed effect per donor (`~ donor + condition`) compares the samples of each donor with
-each other, which removes the differences between donors from the noise. It cannot test
-a variable that is constant within each donor, such as sex: that variable is confounded
-with the donor effects, and {api}`Design.check` says so. A random intercept per donor
-({api}`dream_voom`) keeps the correlation between the samples of a donor and still
-estimates donor-level effects. [Mixed models, REML and dream](mixed_models.md) explains
-the model, and [Genes that change in each cell type](../guides/genes_per_cell_type.ipynb) and
-[Repeated measures and donors](../guides/repeated_measures.ipynb) show both designs on data.
-
-## One matrix of counts
-
-Bulk RNA-seq, or one cell type taken out of a cube, is a samples x genes matrix of
-counts. Three families of methods test it:
-
-- **limma**: {api}`voom`, {api}`fit_linear_model`, {api}`contrasts_fit`, then
- {api}`moderated_statistics` or {api}`treat` for a fold-change threshold. A bulk
- experiment can also become a cube with one group ({api}`from_bulk`), on which
- {api}`limma_voom` runs the same steps.
-- **edgeR's quasi-likelihood F-tests**: {api}`fit_quasi_likelihood` and
- {api}`quasi_likelihood_f_test`, negative binomial models with a moderated
- quasi-dispersion for every gene [@chen2025].
-- **DESeq2**: {api}`fit_deseq2`, with {api}`deseq2_wald_test` for one coefficient or
- contrast and {api}`deseq2_likelihood_ratio_test` for several coefficients at once,
- such as every level of a factor [@love2014].
-
-All three moderate the variability of each gene by borrowing from the others, and they
-agree on genes with clear effects. They differ in the scale they model (log counts per
-million, or counts) and in how they shrink: limma moderates residual variances towards a
-prior, edgeR moderates quasi-dispersions, DESeq2 shrinks dispersions towards a trend in
-the mean count. [Count models](count_models.md) gives the equations. In this version,
-edgeR's and DESeq2's methods run on one matrix at a time, not on every group of a cube.
-
-## Other questions
-
-| Question | Functions | Explained in |
-| --- | --- | --- |
-| Do the genes of a pathway respond together? | {api}`camera`, {api}`fry`, {api}`roast`, {api}`mroast`, {api}`romer` | [Gene-set tests](gene_set_tests.md) |
-| Do the genes of a pathway gather at the top or the bottom of a ranking of all genes? | {api}`gsea`, with {api}`plot_enrichment` and {api}`plot_gsea_table` | [Gene-set tests](gene_set_tests.md) |
-| Are pathways over-represented among the genes that change? | {api}`over_representation`, with sets from {api}`read_gmt` | [Gene-set tests](gene_set_tests.md) |
-| Which transcription factors and pathways are active, in each sample or contrast? | {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate`, with networks from {api}`collectri` and {api}`progeny`, and {api}`plot_activities` and {api}`plot_top_activities` | [Gene-set tests](gene_set_tests.md) |
-| How active is a pathway in each sample, and does that differ between conditions? | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`, with {api}`plot_pathway_scores`; their scores tested by {api}`limma` | [Gene-set tests](gene_set_tests.md) |
-| Does the share of a cell type change? | {api}`propeller` | [Composition tests](composition.md) |
-| Do studies agree, and what is their common effect? | {api}`meta_analysis`, {api}`fit_meta_analysis` | [Meta-analysis](meta_analysis.md) |
-| How many of the calls are false? | {api}`adjust_p_values` | [Multiple testing](multiple_testing.md) |
-
-## Choices that matter in every method
-
-- **Samples with few cells.** A profile pooled from a handful of cells is noisy.
- `min_cells` (10 by default) leaves such samples out of a cell type's fit, and
- `min_counts` sets the least total count a sample needs; the scverse best practices
- keep samples with at least 10 cells and 1,000 counts [@heumos2023].
- `prior_weights="n_cells"` instead keeps every sample and weights it by its number of
- cells [@hoffman2026a].
-- **Genes with low counts.** Genes are filtered by expression with the design, by
- edgeR's rule ({api}`filter_by_expression`; @chen2016), before voom's trend is fitted.
-- **Outlying samples.** `quality_weights=True` estimates a weight for every sample from
- how well it follows the model, instead of removing it [@liu2015], and `robust=True`
- protects the prior from genes with outlying variances [@phipson2016].
-- **Many cell types.** p-values are adjusted within each cell type and contrast;
- `adjust_across_groups=True` adds an adjustment over every cell type ([Multiple
- testing](multiple_testing.md)).
+A study that compares conditions, donors or treatments asks a few questions of its
+samples: which genes change, which cell types expand, which pathways respond, and whether
+a finding holds in other studies. This page lists each question with the function that
+answers it by default, the alternatives and when to choose them, the chapter that
+explains the method and the guide that runs it on data. Every test compares samples, not
+cells, for the reasons [Why pseudobulk](../why_pseudobulk.ipynb) gives, so the cells of
+each sample and cell type are first pooled into a cube ({api}`from_cells`).
+
+(how-the-chapters-follow)=
+## How the chapters follow each other
+
+The chapters after this page explain the methods in an order where each builds on the
+ones before:
+
+- **Foundations** cover what every method shares: [Pseudobulk profiles and
+ cubes](cubes.md), the data every test reads; [Designs, linear models and
+ contrasts](linear_models.md), the model fitted to every gene; [Empirical Bayes
+ moderation](empirical_bayes.md), which shrinks each gene's variance towards a value
+ estimated from all the genes; and [Multiple testing](multiple_testing.md), which turns
+ thousands of p-values into a list of calls.
+- **Differential expression** applies them to counts: [Normalization and
+ filtering](normalization_and_filtering.md) prepares the counts, [limma-voom and
+ limma-trend](limma_voom.md) tests every cell type, [edgeR and DESeq2](count_models.md)
+ model the counts themselves, and [Mixed models and dream](mixed_models.md) allow for
+ several samples of each donor.
+- [Composition tests](composition.md) ask whether the shares of the cell types change.
+- **Gene sets and pathways** turn the results for genes into results for pathways and
+ regulators: [Gene-set tests](gene_set_tests.md), [Over-representation and
+ GSEA](over_representation_and_gsea.md), [Pathway scores per
+ sample](pathway_scores.md) and [Activities from networks](activities.md).
+- **Several studies** compare and combine the results of studies: [Aligning
+ studies](aligning_studies.md), [Meta-analysis](meta_analysis.md) and
+ [Replication](replication.md).
+
+## Which genes change in each cell type?
+
+{api}`limma_voom` tests the contrasts of a design in every cell type of a cube, with
+voom's precision weights [@law2014]. Choose instead:
+
+- {api}`limma_trend` when library sizes lie within about threefold of each other;
+- {api}`dream_voom` when donors give several samples each ([below](#several-samples-of-each-donor)).
+
+[limma-voom and limma-trend](limma_voom.md) explains both, and [Genes that change in
+each cell type](../guides/genes_per_cell_type.ipynb) runs them on published data.
+
+## Which genes change in bulk RNA-seq?
+
+{api}`limma_voom` on a cube with one group, which {api}`from_bulk` makes from a matrix of
+counts: the analysis of pseudobulk profiles, applied to bulk samples. Choose instead:
+
+- edgeR's quasi-likelihood F-tests ({api}`fit_quasi_likelihood` and
+ {api}`quasi_likelihood_f_test`) or DESeq2 ({api}`fit_deseq2` and
+ {api}`deseq2_wald_test`), which model the counts themselves with a negative binomial
+ distribution [@chen2025; @love2014];
+- to test several coefficients together, all the levels of a factor or all the time
+ points, the F-tests of {api}`quasi_likelihood_f_test` or DESeq2's likelihood ratio test
+ ({api}`deseq2_likelihood_ratio_test`);
+- the steps of limma on a matrix of counts, {api}`voom`, {api}`fit_linear_model`,
+ {api}`contrasts_fit` and {api}`moderated_statistics`, or {api}`treat` to test against
+ a fold-change threshold [@mccarthy2009].
+
+edgeR's and DESeq2's methods run on one matrix of counts, such as one cell type taken
+out of a cube. [edgeR and DESeq2](count_models.md) explains them, and [Bulk
+RNA-seq](../guides/bulk_rna_seq.ipynb) runs the three methods on a simulated
+experiment.
+
+(several-samples-of-each-donor)=
+## Which genes change, with several samples of each donor?
+
+{api}`dream_voom` gives each donor a random intercept, so that it tests both changes
+within donors and variables of the donors, such as disease or sex [@hoffman2021]. Choose
+instead:
+
+- {api}`limma_voom` with a fixed effect for each donor (`~ donor + condition`) when
+ every question is within donors, such as stimulated against control in each donor;
+- {api}`limma_voom` on the profiles pooled by donor ({api}`Cube.collapse_samples`), when
+ every donor was sampled alike, to test a variable of the donors with few donors in
+ each group, where dream's tests between donors made too many false calls with three
+ donors per group.
+
+[Mixed models and dream](mixed_models.md) explains the models, and [Repeated measures
+and donors](../guides/repeated_measures.ipynb) compares them on data.
+
+## What drives the variation between samples?
+
+{api}`variance_partitioning` gives, in every cell type, how much of each gene's
+variance each variable of the design explains [@hoffman2016]. To look at the samples
+first, {api}`principal_components` places them on the principal components of the genes
+that vary most. [Mixed models and dream](mixed_models.md) and [Normalization
+and filtering](normalization_and_filtering.md) explain them, and [Repeated measures and
+donors](../guides/repeated_measures.ipynb) and [Normalization, filtering and sample
+quality](../guides/normalization_and_quality.ipynb) show them on data.
+
+## Which cell types expand or contract?
+
+{api}`propeller` tests whether the share of each cell type differs between conditions,
+from the cell numbers of a cube [@phipson2022]. [Composition tests](composition.md)
+explains it, and [Cell-type composition](../guides/composition.ipynb) runs it on a
+simulated study.
+
+## Do the genes of a pathway respond to a contrast?
+
+{api}`camera` tests whether the genes of each set of a collection change more than the
+other genes, and allows for the correlation between them [@wu2012]. Choose instead:
+
+- {api}`roast`, {api}`mroast` or {api}`fry` for sets chosen in advance, to ask whether
+ their genes change at all: roast for one set, mroast for several with the same
+ rotations, and fry, which approximates their test of the mean without rotations, for
+ any number of sets;
+- {api}`romer`, which compares the ranks of a set's genes with those of the others, on a
+ result of {api}`limma_trend`;
+- {api}`over_representation` for a list of genes, such as those that another method
+ called significant.
+
+[Gene-set tests](gene_set_tests.md) and [Over-representation and
+GSEA](over_representation_and_gsea.md) explain them, and [Gene sets and
+pathways](../guides/gene_sets.ipynb) runs camera, fry, roast and over-representation
+analysis on published data.
+
+## Do the genes of a pathway gather at the top or bottom of a ranking?
+
+{api}`gsea`, as fgsea computes it, tests whether a set's genes gather at either end of
+the genes ordered by a statistic [@subramanian2005; @korotkevich2021]. It
+needs only a statistic for every gene, from any method. [Over-representation and
+GSEA](over_representation_and_gsea.md) explains it, and [Gene sets and
+pathways](../guides/gene_sets.ipynb) runs it.
+
+## How active is a pathway in each sample?
+
+{api}`gsva` scores every gene set in every sample [@haenzelmann2013]. Choose instead
+{api}`ssgsea`, which also compares a set's genes with the other genes of each sample, or
+{api}`combined_z_scores` and {api}`plage`, which follow each set across the samples.
+{api}`limma` tests the scores with the cube's design. [Pathway scores per
+sample](pathway_scores.md) explains them, and [Pathway and regulator
+activity](../guides/pathway_activity.ipynb) runs GSVA and tests its scores.
+
+## Which regulators and pathways are active?
+
+{api}`ulm` scores every transcription factor or pathway of a network, in every sample or
+contrast, from the signs and sizes of its weights on its targets, as decoupler computes
+it [@badiaimompel2022]; {api}`collectri` and {api}`progeny` give the networks. Choose
+instead {api}`mlm` to separate sources whose targets overlap, or
+{api}`weighted_aggregate` for the weighted mean or sum of the targets, with
+permutations. [Activities from networks](activities.md) explains them, and [Pathway and
+regulator activity](../guides/pathway_activity.ipynb) runs ULM.
+
+## What do several studies say together?
+
+{api}`meta_analysis` combines, for every gene, the estimates of several studies into one,
+by a random-effects model, and measures how far the studies disagree [@viechtbauer2010].
+Choose instead:
+
+- {api}`combine_p_values` where the studies give p-values rather than comparable
+ effects;
+- a joint model of the stacked studies ({api}`CubeCollection.stack`) for studies made
+ alike, with one protocol and platform, that are too small to estimate their variances
+ alone.
+
+[Aligning studies](aligning_studies.md) and [Meta-analysis](meta_analysis.md) explain
+them, and [Replication](replication.md#combined-p-values) the combined p-values. [Align
+and combine studies](../guides/combine_studies.ipynb) runs the meta-analysis
+and the joint model on six simulated cohorts, and [Replication across
+studies](../guides/replication_across_studies.ipynb) combines their p-values.
+
+## Which changes replicate across studies?
+
+{api}`sumrank` ranks every gene within each study and sums its ranks across the studies,
+by default those of every study, for genes up and down [@nakatsuka2025]. Choose instead:
+
+- {api}`replication` for the genes found in each number of studies, a test of a change
+ in at least u studies ({api}`partial_conjunction`), and the agreement between pairs of
+ studies;
+- {api}`held_out_prediction` to ask whether the genes chosen from some studies predict
+ the cases of another.
+
+[Replication](replication.md) explains them, and [Replication across
+studies](../guides/replication_across_studies.ipynb) runs them on six simulated cohorts.
+
+(not-implemented-yet)=
+## Not implemented yet
+
+These methods have no CytePool function yet:
+
+- **Genes that change.** edgeR's quasi-likelihood F-tests and DESeq2's tests on every
+ cell type of a cube; edgeR's likelihood ratio tests and gene-wise dispersions
+ (`glmLRT`, `estimateDisp`) and its `legacy = TRUE` methods; DESeq2's replacement of
+ outlier counts, shrunken fold changes (`lfcShrink`), local and glmGamPoi dispersion
+ trends, observation weights, normalization factors that differ between genes,
+ fold-change thresholds (`lfcThreshold`) and t-distributed Wald tests (`useT`);
+ limma's `duplicateCorrelation`, voom with a block correlation, voom's normalization
+ of log-CPM values between samples (`normalize.method`), and edgeR's `voomLmFit`.
+- **Several samples of each donor.** Random slopes in mixed models, and with them the
+ partition of the variance within subsets of the samples; sample quality weights for
+ dream; generalized linear mixed models; bootstrap intervals of the variance
+ fractions (variancePartition's `varPartConfInf`).
+- **Composition.** Precision-weighted mixed models of composition, as crumblr fits them
+ [@hoffman2026b], and Bayesian count models such as scCODA [@buttner2021] and sccomp
+ [@mangiola2023].
+- **Gene sets.** Gene-set tests of the mixed models of {api}`dream_voom`, and with a
+ block correlation (romer's `block`); limma's gene contribution weights in fry
+ (`gene.weights`); the adjustment of over-representation for gene length or abundance
+ (the `trend` of limma's `kegga` and `goana`); exact z-scores and given priors in the rotation tests
+ (`approx.zscore = FALSE`, `var.prior`, `df.prior`). decoupler's VIPER is not offered,
+ because the licence of its reference implementation allows only non-profit academic
+ use.
+- **Several studies.** Meta-regression on study-level covariates and multivariate
+ meta-analysis of correlated effects; shrinkage of effects across conditions or
+ studies, as mash does [@urbut2019]; the r-values of a primary study and its follow-up
+ [@heller2014]; metap's methods of Edgington, Lancaster, the inverse t, the mean z and
+ vote counting; weights for the studies of SumRank, and its permutations of designs
+ with random effects; the proportion of studies that SumRank sums, chosen by
+ cross-validation of held-out prediction as the paper chose it (a loop over the
+ proportions does it today); cell types aligned up the Cell Ontology.
+- **Questions not answered yet.**
+ - Grouping patients by their profiles, such as by a factorization of samples across
+ cell types, and predicting a continuous outcome from them, such as the severity of
+ a disease, where held-out prediction tells cases from controls.
+ - Which cell-cell interactions change between conditions, by ligand-receptor scoring
+ and tests of differential interactions. @dimitrov2022 compared 16 resources and 7
+ methods for cell-cell communication, and found that the choice of both strongly
+ changes the predicted interactions; @dimitrov2024 introduced LIANA+, a framework
+ for cell-cell communication in single-cell and spatial data, from one or several
+ conditions.
+ - How cells respond to drugs and doses: dose-response fits, benchmark doses and drug
+ synergy. @srivatsan2020 introduced sci-Plex and profiled about 650,000 single cells
+ across about 5,000 samples of three cancer cell lines exposed to 188 compounds.
+ - Effects that depend on genotype are left to tools that map expression quantitative
+ trait loci. @yazar2022 profiled 1,267,758 blood cells of 982 donors and mapped
+ 26,597 independent cis-expression quantitative trait loci across 14 cell types.
+
+The chapters begin with [Pseudobulk profiles and cubes](cubes.md).
diff --git a/docs/source/statistics/composition.md b/docs/source/statistics/composition.md
index cc15543..731297c 100644
--- a/docs/source/statistics/composition.md
+++ b/docs/source/statistics/composition.md
@@ -1,54 +1,153 @@
---
-description: How propeller tests changes in cell-type proportions from the cell numbers of a cube, with transformed proportions, a linear model and empirical Bayes moderation across cell types.
+description: Whether a condition changes the share of each cell type in the samples, tested by propeller on the cell numbers of a cube.
---
# Composition tests
-A condition can change which cells are present as well as what they express. A cube
-records how many cells of each type every sample holds ({api}`Cube.n_cells`), and
-{api}`propeller` tests whether the share of each cell type differs between conditions,
-as speckle's `propeller` does [@phipson2022]. Proportions estimated from single-cell
-data are variable, so the test compares samples, the biological replicates, not cells.
-[Cell-type composition](../guides/composition.ipynb) runs it on a simulated study.
+[Mixed models and dream](mixed_models.md) closed the chapters on which genes change in
+each cell type. A condition can also change which cells a sample holds, when a cell type
+expands or contracts. A cube records how many cells of each type every sample holds
+({api}`Cube.n_cells`), and {api}`propeller` tests whether the share of each cell type
+changes with the condition, by speckle's method [@phipson2022]. Proportions
+estimated from single-cell data are variable, so the test compares samples, the
+biological replicates, as the tests of genes do. [Cell-type
+composition](../guides/composition.ipynb) runs it on a simulated study whose answer is
+known.
+(propeller-steps)=
## The steps
1. **Proportions.** Sample $j$, with $x_{ij}$ cells of type $i$ and $N_j$ cells in all,
has the proportion $p_{ij} = x_{ij} / N_j$ of type $i$.
2. **Transform.** The logit, $\log(q_{ij} / (1 - q_{ij}))$, of the proportions after
half a cell is added to every count,
- $q_{ij} = (x_{ij} + 0.5) / \sum_i (x_{ij} + 0.5)$, so that a type absent from a
+ $q_{ij} = (x_{ij} + 0.5) / \sum_i (x_{ij} + 0.5)$, so that a cell type absent from a
sample has a finite value; or the arcsine square root, $\arcsin\sqrt{p_{ij}}$.
3. **Linear model.** The transformed proportions of every cell type are fitted with the
- design ([Designs, linear models and contrasts](linear_models.md)).
+ design, matched to the samples by `sample_id` ([Designs, linear models and
+ contrasts](linear_models.md)).
4. **Moderation.** The residual variances of the cell types are moderated by empirical
- Bayes, robustly by default, as speckle does ([Empirical Bayes
- moderation](empirical_bayes.md); @phipson2016).
-5. **Tests.** Each contrast is tested by its moderated t-statistic; `f_tests` names a
- factor, or several, whose levels are tested together by a moderated F-statistic.
- p-values are adjusted by the method of Benjamini and Hochberg over the cell types.
+ Bayes, robustly by default ([below](#composition-moderation)).
+5. **Tests.** Each contrast is tested by its moderated t-statistic, as speckle's
+ `propeller.ttest` does. `f_tests` names a factor, or several, each tested on its own:
+ a moderated F-statistic asks whether all its levels share one mean, as
+ `propeller.anova` does. The p-values of each test are adjusted by the method of
+ Benjamini and Hochberg over the cell types.
+
+The data are a cube that records its cell numbers, or a table of the cell numbers of
+every sample and cell type, such as a cross-tabulation of the labels of the cells.
+
+(composition-moderation)=
+## Moderation across cell types
+
+A study has a few cell types to borrow strength from, where a test of genes has
+thousands, so step 4 rests on few variances. propeller estimates their prior robustly
+by default, as speckle calls limma's `eBayes(robust = TRUE)`: with 10 cell types or
+more, a cell type whose transformed proportions vary much more than the others' gets
+smaller prior degrees of freedom of its own, and does not lower those of the others
+[@phipson2016]. Fewer cell types are too few for the robust estimate, which is then the
+ordinary one, and two cell types are not moderated at all, as in limma ([Estimating
+the prior](#estimating-the-prior)). With `trend=True`, the prior variance is a trend in
+each cell type's mean transformed proportion.
+
+(composition-result)=
+## The result
The result, a {api}`CompositionResult`, holds a table for every contrast and F-test
-({api}`CompositionResult.table`), with speckle's descriptive columns: the baseline
-proportion of every cell type, the mean proportion in each level of the factor a
-contrast compares, and their ratio. Its summary says which cell types and samples were
-left out, and why.
+({api}`CompositionResult.table`), with speckle's descriptive columns: each cell type's
+baseline proportion over the samples used and, for a contrast between the levels of a
+factor, the cell type's mean proportion at each level the contrast weighs and their
+ratio, such as $m_\text{stim} / m_\text{ctrl}$ for stimulated against control. A
+contrast compares levels when its weights, summing to zero, fall on the levels of one
+factor of the formula ({api}`CompositionResult` gives the rule): `conditionstim` in
+`~ donor + condition` compares stimulated with control, and
+`(groupA + groupB)/2 - groupC` in `~ 0 + group` three groups, but `groupA + groupB`
+compares none, and neither does the slope of a covariate, so their tables have no such
+columns. The summary says which cell types were left out, and why, the samples table
+which samples were, and the proportions table gives each proportion with its
+transformed value.
-## Choices
+(composition-defaults)=
+## Defaults and choices
-- **The denominator.** Proportions are taken over the cell types of the data. To test
- the composition within one lineage, such as the T cell subtypes, select its cell types
- first ({api}`Cube.select`); then a shift in another lineage does not move them.
+- **The denominator.** Proportions are taken over the cell types of the data, so a
+ change in one cell type moves the shares of all the others. To test the composition
+ within one lineage, such as the T cell subtypes, select its cell types first
+ ({api}`Cube.select`); then a shift in another lineage does not move them.
- **The transform.** The logit, speckle's default, or the arcsine square root
(`transform="asin"`).
-- **Samples and cell types without cells.** A sample without cells has no proportions
- and is left out; a cell type without cells in any sample used is left out, as it was
- not observed.
+- **Robust moderation.** On by default, as in speckle; it changes the prior only with
+ 10 cell types or more ([above](#composition-moderation)), and `robust=False` turns it
+ off.
+- **The design.** Pair the samples of a donor when each donor gives one in every
+ condition, as in `~ donor + condition`. speckle's test of two groups is
+ `~ 0 + group` with the contrast of the first level against the second, its test of
+ more groups `f_tests="group"`, and its tests with a covariate those of
+ `~ 0 + group + age`.
+- **Samples and cell types without cells.** A sample without cells has no proportions,
+ and a cell type that has no cells in the samples used was not observed; both are left
+ out.
+
+(composition-limits)=
+## Limits and differences
+
+- **Fixed effects only.** A design with random effects is refused, and there is no
+ `duplicateCorrelation`: a donor sampled in every condition enters the formula as a
+ fixed effect. Precision-weighted mixed models of composition (crumblr; @hoffman2026b)
+ and Bayesian count models, such as scCODA [@buttner2021] and sccomp [@mangiola2023],
+ are not implemented ([Not implemented yet](choosing_a_method.md#not-implemented-yet)).
+- **Cell numbers.** propeller reads whole numbers of cells; it does not take
+ proportions with assumed totals, which speckle's `convertDataToList` accepts.
+- **F-tests of all the levels.** An F-test compares all the levels of its factor; to
+ compare some of them, test their contrasts. The t-tests of contrasts take limma's
+ degrees of freedom, which cannot exceed the total residual degrees of freedom of the
+ cell types, and the F-tests leave theirs uncapped, as limma does ([Moderated
+ F-statistics](empirical_bayes.md#moderated-f-statistics)).
+- **Unused levels.** Given cell labels as a factor with a level that no cell has,
+ speckle keeps that cell type and tests it, where CytePool leaves it out; with the logit
+ transform, the half cell added to it shifts the transformed proportions of all the
+ other cell types. A level of the sample labels without cells is kept too: with the logit
+ transform each cell type of that sample gets the proportion one over the number of
+ cell types, a value that enters the fit as data, and with the arcsine transform limma
+ leaves the sample out, as CytePool does with either.
+- **Descriptive columns from the design's coding.** speckle regresses the proportions on
+ the columns of the design that a contrast weighs and reports the coefficients as mean
+ proportions. They are means only where those columns indicate groups of samples: for
+ `(Intercept) + groupB` in `~ group + age` they are the mean of the samples outside B
+ and a difference of means, and for `groupA + groupB`, which compares nothing, the
+ ratio is a product of two means. CytePool reads the levels that a contrast compares
+ from the design's coding ([The result](#composition-result)), so that any
+ parametrization of one comparison gives one table, and it tests the contrasts of a
+ single column that speckle refuses, such as the slope of a covariate.
+- **Data that the design fits exactly.** When the samples of each condition are copies
+ of one another, say, no variance can be estimated, and CytePool raises an error where
+ speckle returns statistics of the rounding errors, or NaN.
+
+(composition-checks)=
+## Checks and further reading
+
+{api}`propeller` is compared with the three functions of speckle 1.6.0 (`propeller`,
+`propeller.ttest`, `propeller.anova`) and limma 3.62.1, run as a separate program on the
+same cell numbers: exact (1e-10), and close (1e-6) for the robust prior of 10 cell types
+or more, which limma solves only to a tolerance
+([Validation](#validation-composition-tests)).
+
+Further reading:
-## Limits
+- @phipson2022 introduced propeller, which tests transformed cell-type proportions with
+ linear models and empirical Bayes moderation across replicates.
+- @buttner2021 introduced scCODA, a Bayesian model of compositional single-cell data.
+- @mangiola2023 introduced sccomp, which models composition and its variability
+ together, and found that the variability of proportions depends on their mean in every
+ technology examined.
+- @hoffman2026b introduced crumblr, precision-weighted mixed models of cell-type
+ composition that test several levels of the cell lineage at once.
+- @dann2022 introduced Milo, which tests differential abundance in neighbourhoods of a
+ k-nearest neighbour graph instead of clusters.
+- @yi2024 benchmarked six differential abundance methods for condition-specific cell
+ states, and gave recommendations by dataset.
+- @stephenson2021 profiled over 780,000 blood cells of 130 COVID-19 patients and related
+ the expansion of cell populations to disease severity.
-propeller fits fixed effects only: a design with random effects is refused, so a donor
-sampled in every condition enters the formula as a fixed effect, as in
-`~ donor + condition`. Precision-weighted mixed models of composition (crumblr;
-@hoffman2026b) and Bayesian count models, such as scCODA [@buttner2021] and sccomp
-[@mangiola2023], are not implemented.
+[Gene-set tests](gene_set_tests.md) begins the part that turns the results for genes
+into results for pathways and regulators.
diff --git a/docs/source/statistics/count_models.md b/docs/source/statistics/count_models.md
index 4883cbd..eac540c 100644
--- a/docs/source/statistics/count_models.md
+++ b/docs/source/statistics/count_models.md
@@ -1,27 +1,40 @@
---
-description: The negative binomial models behind edgeR's quasi-likelihood F-tests and DESeq2's Wald and likelihood ratio tests, as CytePool computes them, with the normalization they rest on.
+description: How edgeR's quasi-likelihood F-tests and DESeq2's Wald and likelihood ratio tests find the genes that change in one matrix of counts, from negative binomial models of every gene.
---
-# Count models
+# edgeR and DESeq2
-limma models log counts; edgeR and DESeq2 model the counts themselves, with a negative
-binomial distribution whose variance grows with the mean. CytePool implements edgeR 4's
-quasi-likelihood pipeline and DESeq2's analysis, each checked against the R package on
-the same data.
+[limma-voom and limma-trend](limma_voom.md) model log-CPM values. edgeR and DESeq2 model
+the counts themselves, with a negative binomial distribution whose variance grows with the
+mean, and they are the alternatives for one matrix of counts such as bulk RNA-seq, where
+{api}`limma_voom` on a cube of one group ({api}`from_bulk`) stays the default.
+{api}`fit_quasi_likelihood` and {api}`quasi_likelihood_f_test` run edgeR's quasi-likelihood
+F-tests [@chen2025], and {api}`fit_deseq2` with {api}`deseq2_wald_test` or
+{api}`deseq2_likelihood_ratio_test` runs DESeq2 [@love2014]. [Bulk
+RNA-seq](../guides/bulk_rna_seq.ipynb) runs both, with limma-voom, on a simulated experiment
+whose true changes are known.
-## Normalization
+(one-matrix-of-counts)=
+## One matrix of counts
-Counts depend on how deeply each sample was sequenced and on its composition: a few very
-highly expressed genes in one sample take reads from all others. Two corrections are
-standard. edgeR multiplies each library size by a normalization factor, from the trimmed
-mean of M values ({api}`tmm_factors`; @robinson2010a), the upper quartile
-({api}`upper_quartile_factors`; @bullard2010) or the relative log expression
-({api}`rle_factors`; @anders2010). DESeq2 replaces library sizes by size factors, the
-median of each sample's ratios to the geometric mean of every gene
-({api}`median_ratio_size_factors`; @anders2010). {api}`cpm` and {api}`log2_cpm` give
-counts per million, and {api}`filter_by_expression` keeps genes with enough counts in
-enough samples for the design [@chen2016].
+Bulk RNA-seq, or one cell type taken out of a cube ({api}`Cube.layer`), is a samples x genes
+matrix of counts. limma, edgeR and DESeq2 all test it, and they agree on genes with clear
+effects. All three moderate the variability of each gene by borrowing from the others; they
+differ in the scale they model, log-CPM values or counts, and in how they shrink: limma
+moderates residual variances towards a prior ([Empirical Bayes
+moderation](empirical_bayes.md)), edgeR moderates quasi-dispersions, and DESeq2 shrinks
+dispersions towards a trend in the mean count.
+The choice rests mostly on the design and on what comes next. edgeR's User's Guide (section
+1.4) recommends its quasi-likelihood F-tests for bulk RNA-seq, because they account for the
+uncertainty of the dispersion estimates, which gives stricter control of the error rate.
+In DESeq2, the likelihood ratio test asks about several coefficients together, all the
+levels of a factor or all the time points, which its Wald test takes one at a time;
+edgeR's F-tests and limma's moderated F-tests also test several at once. limma's tables
+carry moderated standard errors, which a [meta-analysis](meta_analysis.md) combines across
+studies.
+
+(negative-binomial-model)=
## The negative binomial model
For the count $y_{gi}$ of gene $g$ in sample $i$,
@@ -32,9 +45,11 @@ y_{gi} \sim \operatorname{NB}(\mu_{gi}, \phi_{gi}), \qquad
\log \mu_{gi} = x_i' \beta_g + o_{gi},
$$
-with $\phi$ the dispersion and the offset $o_{gi}$ usually the log of the effective
-library size [@mccarthy2012]. $\sqrt{\phi}$ is the biological coefficient of variation.
-The coefficients minimize the deviance
+with $\phi$ the dispersion and the offset $o_{gi}$ usually the log of the effective library
+size ([Normalization factors and size factors](#normalization-and-size-factors);
+@mccarthy2012). $\sqrt{\phi}$ is the biological coefficient of variation: edgeR's User's Guide
+gives typical values of 0.4 for human samples, 0.1 for genetically identical model organisms
+and 0.01 for technical replicates. The coefficients minimize the deviance
$$
D_g = \sum_i w_{gi}\, d(y_{gi}, \mu_{gi}, \phi_{gi}), \qquad
@@ -42,71 +57,225 @@ d(y, \mu, \phi) = 2\left[y \log\frac{y}{\mu} - \left(y + \frac{1}{\phi}\right)
\log\frac{1 + \phi y}{1 + \phi \mu}\right],
$$
-which {api}`fit_negative_binomial_model` finds for every gene, as edgeR's `glmFit` does
-({api}`NegativeBinomialFit`), and which {api}`negative_binomial_deviance` and
-{api}`negative_binomial_unit_deviance` compute exactly [@dunn2018].
+with prior weights $w_{gi}$. {api}`fit_negative_binomial_model` finds the coefficients of
+every gene at dispersions that are given, as edgeR's `glmFit` does
+({api}`NegativeBinomialFit`), and {api}`negative_binomial_unit_deviance` and
+{api}`negative_binomial_deviance` compute the unit deviances and their sums exactly
+[@dunn2018]. A one-way layout, a design with as many distinct rows as columns such as one
+mean per group, is fitted one group at a time. Newton's method fits the other designs,
+started as edgeR starts it, from the fit of a single mean to all the samples, with steps
+damped where they would raise the deviance [@levenberg1944; @marquardt1963]. The
+log-likelihood is concave in the coefficients, so its maximum is unique when it exists.
+
+It does not exist where the fitted values of some samples can fall towards zero while the
+others stay put, as when every count of a group is zero: the fitted values of those samples
+tend to zero, and their coefficients grow without bound. The fits stop with such
+coefficients large but finite; in a one-way layout, a group without counts gets a log mean
+of $-10^8$, as in edgeR. edgeR's log fold changes, and those of these fits, come from a
+second fit after an average count of 0.125, edgeR's default, is added to every count,
+which shrinks them towards zero and keeps them finite [@phipson2013]; the tests do not use
+them.
+
+(cox-reid)=
+### The Cox-Reid adjusted profile likelihood
+edgeR and DESeq2 both estimate dispersions by maximizing, over $\phi$, the adjusted profile
+log-likelihood of a gene,
+
+$$
+\ell_g(\phi, \hat\beta_g(\phi)) - \tfrac12 \log\det(X' W_g X), \qquad
+W_g = \operatorname{diag}\!\left(\frac{\mu_{gi}}{1 + \phi \mu_{gi}}\right),
+$$
+
+where $\hat\beta_g(\phi)$ maximizes the likelihood at that $\phi$; DESeq2 holds it at a
+first fit instead [@cox1987; @mccarthy2012]. The adjustment, which a REML criterion
+parallels, removes most of the downward bias that estimating the coefficients gives the
+maximum likelihood estimate of the dispersion [@robinson2008]. {api}`common_dispersion`
+maximizes the sum over the genes, edgeR's common dispersion; DESeq2 maximizes each gene's
+own.
+
+(quasi-likelihood)=
## edgeR's quasi-likelihood F-tests
-The quasi-likelihood model gives each gene a second, gene-wise dispersion $\sigma_g^2$
-on top of a negative binomial dispersion $\phi$ shared by all genes [@lund2012]:
+The quasi-likelihood model gives each gene a second, gene-wise dispersion $\sigma_g^2$, the
+quasi-dispersion, on top of a negative binomial dispersion $\phi$ shared by all genes
+[@lund2012]:
$$
\operatorname{var}(y_{gi}) = \sigma_g^2 (\mu_{gi} + \phi \mu_{gi}^2).
$$
-{api}`fit_quasi_likelihood` follows edgeR 4's defaults [@chen2025]:
-
-1. $\phi$ is the Cox-Reid common dispersion of the most highly expressed genes
- ({api}`common_dispersion`): the maximum over $\phi$ of the summed adjusted profile
- log-likelihoods $\ell_g(\phi) - \tfrac{1}{2}\log\det(X' W_g X)$ [@cox1987;
- @mccarthy2012].
-2. Each gene's quasi-dispersion is its deviance over its residual degrees of freedom,
- both adjusted for small counts: each unit deviance is approximated by a scaled
- chi-squared variable with the exact mean and variance of the unit deviance
- ({api}`negative_binomial_unit_deviance_moments`, which gives
- {api}`UnitDevianceMoments`), where edgeR approximates them.
-3. The quasi-dispersions are moderated by empirical Bayes towards a trend in the average
- log CPM ({api}`average_log2_cpm`), with limma's estimator for unequal degrees of
- freedom ([Empirical Bayes moderation](empirical_bayes.md)).
+$\phi$ carries the variance common to the genes, and $\sigma_g^2$ the variation between
+genes, which empirical Bayes moderates as limma moderates residual variances.
+{api}`fit_quasi_likelihood` takes the steps of edgeR 4.4's `glmQLFit` with its defaults
+(`legacy = FALSE`; @chen2025):
+
+1. **Abundance.** Each gene's average log-CPM ({api}`average_log2_cpm`, edgeR's
+ `aveLogCPM`, with an average count of 2 added) measures its expression: it orders the
+ genes for step 2, and the trends of steps 4 and 5 follow it.
+2. **The dispersion.** Unless it is given, $\phi$ is the Cox-Reid common dispersion
+ ([above](#cox-reid)) of the most highly expressed genes, a share that is smaller the
+ more genes and residual degrees of freedom there are.
+3. **Quasi-dispersions.** Each gene's fit uses the working dispersion $\phi / A$, $A$
+ coming from step 4. edgeR 4 treats a count's unit deviance as $s\,\chi^2_k$, a
+ chi-squared variable on $k$ degrees of freedom scaled by $s$, with the unit deviance's
+ mean $E$ and variance $V$: $s = V / (2E)$ and $k = 2E^2 / V$
+ ({api}`negative_binomial_unit_deviance_moments`, which gives them in a
+ {api}`UnitDevianceMoments`). The unit deviance over $s$ then carries $k (1 - h)$
+ degrees of freedom, $h$ being the count's leverage. Summed over the samples, these give
+ each gene an adjusted deviance and fractional degrees of freedom, whose ratio is its
+ quasi-dispersion. The adjustment matters for small counts, where a unit deviance is far
+ from chi-squared on one degree of freedom.
+4. **The average quasi-dispersion** $A$, which "scales the NB dispersion", as edgeR's help
+ page says. It starts at 1 and is computed again twice, from fits at the current
+ $\phi / A$: the 0.9 quantile, over the genes, of the trend that smooths the fourth roots
+ of their quasi-dispersions against the abundance, raised back to the fourth power, or 1
+ if that is larger.
+5. **Moderation.** The quasi-dispersions are moderated by empirical Bayes towards a trend
+ in the abundance, by the estimator that limma uses where degrees of freedom differ
+ ([Estimating the prior](#estimating-the-prior)), robustly with `robust=True`
+ [@phipson2016].
The fit is a {api}`QuasiLikelihoodFit`. {api}`quasi_likelihood_f_test` fits the model
-without the tested coefficients or contrasts, and divides the likelihood ratio statistic
-by its degrees of freedom and by the posterior quasi-dispersion; its
-{api}`QuasiLikelihoodTest` has edgeR's columns: log fold change, log CPM, F, p-value and
-FDR. The [edgeR User's
-Guide](https://bioconductor.org/packages/release/bioc/vignettes/edgeR/inst/doc/edgeRUsersGuide.pdf)
-(section 1.4) recommends these F-tests for bulk RNA-seq because they account for the
-uncertainty of the dispersion estimates, which gives stricter error rate control.
+without the tested coefficients, or with the tested contrasts at zero, at the same working
+dispersion, and divides the likelihood ratio statistic by its degrees of freedom $r$ and by
+the posterior quasi-dispersion. The F-statistic has $r$ and $d_0 + d_g$ degrees of freedom,
+the prior's and the gene's adjusted ones, capped at the total of the genes' residual
+degrees of freedom. Its {api}`QuasiLikelihoodTest` has the columns of edgeR's `topTags`:
+log fold change, log-CPM, F, p-value and the false discovery rate of Benjamini and
+Hochberg.
+(deseq2)=
## DESeq2
-DESeq2 models the same distribution on the log2 scale with size factors $s_i$,
-$\log_2 \mu_{gi} = \log_2 s_i + x_i' \beta_g$, and shrinks the dispersions in four steps
-[@love2014], which {api}`deseq2_dispersions` reproduces ({api}`DESeq2Dispersions`):
-
-1. gene-wise estimates, the maxima of the Cox-Reid adjusted likelihood;
-2. a trend in the mean normalized count $m$, $\alpha_{tr}(m) = a_0 + a_1 / m$, fitted by
- a gamma regression;
-3. a normal prior on the log dispersions around the trend, with its variance from the
- spread of the gene-wise estimates;
-4. maximum a posteriori estimates, keeping the gene-wise estimate of genes far above the
- trend (dispersion outliers).
-
-{api}`fit_deseq2` fits every gene at those dispersions ({api}`DESeq2Fit`).
-{api}`deseq2_wald_test` tests a coefficient or contrast $c$ by
-$c'\hat\beta / \sqrt{c'\Sigma c}$ against the normal distribution;
+DESeq2 models the same distribution on the log2 scale, with the size factors $s_i$ in place
+of the library sizes, $\log_2 \mu_{gi} = \log_2 s_i + x_i' \beta_g$, and one dispersion
+$\alpha_g$ per gene [@love2014]. {api}`deseq2_dispersions` estimates the dispersions as
+`estimateDispersions()` does ({api}`DESeq2Dispersions`):
+
+1. **Gene-wise estimates**: the maxima of the Cox-Reid adjusted likelihood
+ ([above](#cox-reid)), conditional on means fitted at a rough dispersion.
+2. **A trend** $\alpha_{tr}(m) = a_0 + a_1 / m$ in each gene's mean normalized count $m$,
+ fitted by a gamma regression to the gene-wise estimates.
+3. **A prior**: the log dispersions are normal around the log trend, with a variance
+ estimated from the spread of the gene-wise estimates about it, less the part that
+ sampling explains.
+4. **Maximum a posteriori estimates**, which shrink each gene towards the trend; a gene
+ whose gene-wise estimate lies far above the trend, a dispersion outlier, keeps it.
+
+{api}`fit_deseq2` then fits each gene at its dispersion, as `DESeq()` does
+({api}`DESeq2Fit`). {api}`deseq2_wald_test` refers $c'\hat\beta / \sqrt{c' \Sigma c}$, for
+a coefficient or contrast $c$, to the normal distribution;
{api}`deseq2_likelihood_ratio_test` tests a reduced design by the difference of the
-deviances against a chi-squared distribution, which tests several coefficients at once,
-such as every level of a factor or every time point. Both report DESeq2's `results()`
-columns ({api}`DESeq2Results`), filter genes with low mean counts before adjusting the
-p-values (independent filtering; @bourgon2010) and remove the p-values of genes with an
-influential outlier count (Cook's distance; @cook1977).
-
-## What differs from the R packages
-
-CytePool's fits converge to the maximum where edgeR's and DESeq2's stop at a tolerance,
-and its deviances and unit deviance moments are exact where edgeR approximates them; the
-differences are reported, with their sizes, on the [Validation](../validation.md) page.
-DESeq2's replacement of outlier counts, shrunken fold changes (`lfcShrink`) and edgeR's
-`legacy = TRUE` methods are not implemented.
+deviances against a chi-squared distribution, which tests several coefficients at once.
+Both report the columns of DESeq2's `results()` ({api}`DESeq2Results`). Before the p-values
+are adjusted, the genes of low mean count are filtered out, at the lowest threshold whose
+number of genes below `alpha` comes close to the most that any threshold gives
+(independent filtering; @bourgon2010), and a gene whose count in one sample is so
+influential that its Cook's distance exceeds the 0.99 quantile of an F distribution loses
+its p-value [@cook1977].
+
+(count-models-defaults)=
+## Defaults and choices
+
+- **Which method.** {api}`limma_voom` on a cube of one group by default; edgeR's
+ quasi-likelihood F-tests for the stricter error control that edgeR's User's Guide
+ describes; DESeq2's Wald tests, or its likelihood ratio test for several coefficients
+ at once ([One matrix of counts](#one-matrix-of-counts)).
+- **The genes and the factors.** {api}`fit_quasi_likelihood` takes the counts it is
+ given, so filter them by expression and pass TMM factors, as edgeR's workflow does
+ ([Normalization and filtering](normalization_and_filtering.md)); {api}`fit_deseq2`
+ takes every gene, with DESeq2's size factors by default, and filters on the mean count
+ when it adjusts the p-values.
+- **The dispersion of the quasi-likelihood fit.** Estimated from the data by default. One
+ that is given should come from a model of all the genes, common or trended, and not be
+ estimated gene by gene, as edgeR's help page advises, since the variation between genes
+ is for the quasi-dispersions to carry.
+- **The prior of the quasi-dispersions.** A trend in the abundance by default
+ (`trend=True`), or a constant (`False`), or a trend in another covariate; `robust=True`
+ protects it from genes with outlying quasi-dispersions, as edgeR's User's Guide
+ recommends.
+- **DESeq2's trend.** `trend="parametric"`, DESeq2's default, or `"mean"`, the mean of the
+ gene-wise estimates.
+- **DESeq2's tests.** `alpha` (0.1, as in `results()`) sets the false discovery rate that
+ the independent filter is chosen for, `independent_filtering=False` keeps every gene,
+ and `cooks_cutoff` changes or turns off the removal of p-values by Cook's distance.
+
+(count-models-limits)=
+## Limits and differences
+
+- **One matrix at a time.** The fits run on one matrix of counts, such as one cell type
+ taken out of a cube ({api}`Cube.layer`); their tests of every cell type of a cube, and
+ the methods of edgeR and DESeq2 that [Not implemented yet](#not-implemented-yet)
+ lists, are not implemented.
+- **Dispersions as given.** {api}`fit_negative_binomial_model` takes the dispersions as
+ given, and {api}`common_dispersion` estimates one common to every gene.
+- **Exact deviances and moments.** The deviances are exact. edgeR adds 1e-8 to every
+ count and mean, and approximates the unit deviance at small dispersions and at large
+ dispersions times means; it also approximates the mean and variance of the unit
+ deviance, which are exact here, so that adjusted deviances, quasi-dispersions and
+ F-statistics differ slightly from edgeR's.
+- **Fits that reach the maximum.** The fits converge to the maximum of the likelihood,
+ by steps that use its exact curvature, the observed information, where edgeR's
+ documentation describes Fisher scoring, which uses the expected information and has
+ the same maximum. edgeR's one-way fits agree with them; its fits of other designs stop
+ short of the maximum, the more so where dispersions and weights differ between
+ observations, and leave fitted values that tend to zero between about 1e-8 and 1e-7,
+ far above CytePool's, so that the log fold changes of such designs differ slightly. In
+ a design with an intercept, a gene without counts gets coefficients that make its log
+ mean $-10^8$, as edgeR's one-way fit does, where edgeR's other fits return missing
+ values.
+- **Infinite inputs.** Infinite dispersions or offsets, on which edgeR's fits run without
+ end, are refused.
+- **The robust prior.** Genes without residual degrees of freedom, such as genes without
+ counts, are left out of the test for outlying quasi-dispersions, where limma counts them
+ as zero variances ([Limits of empirical Bayes moderation](#empirical-bayes-limits)).
+- **Average log-CPM with offsets per observation.** edgeR's `aveLogCPM` then gives the
+ first gene a value that ignores its offsets, and the value enters the ranking, the
+ trends and that gene's log-CPM; {api}`average_log2_cpm` uses every gene's offsets.
+- **DESeq2's estimates.** CytePool finds the maximum of each dispersion's objective, where
+ DESeq2's line searches stop once an iteration gains less than 1e-6, and can stall where
+ they start. DESeq2's fits stop once the deviance changes by under 1e-8 of its value,
+ short of the fixed point for genes of large dispersion, while every fit here
+ converges. With three or fewer residual degrees of freedom, the prior's variance is the
+ maximum likelihood estimate of the model of the log residuals, the limit of DESeq2's
+ criterion as its densities of simulated residuals become exact: DESeq2 chooses it from
+ a grid of 1,000 values on [0, 8] by matching such densities, so that its choice moves
+ with the simulated sample.
+- **Where DESeq2's trend fails.** When the parametric trend has a coefficient that is not
+ positive, or does not converge in 11 fits, DESeq2 substitutes a local regression, which
+ is not implemented here; CytePool falls back to the mean trend, with a warning, as
+ PyDESeq2 does.
+- **Outlier counts are not replaced.** The results are those of `DESeq(...,
+ minReplicatesForReplace = Inf)`, which equal DESeq2's defaults where no row of the design
+ occurs seven times or more.
+
+(count-models-checks)=
+## Checks and further reading
+
+Each function is compared with edgeR 4.4.0 or DESeq2 1.46.0, run as separate programs on
+simulated counts with genes and groups without counts:
+
+- Negative binomial fits and deviances: close (1e-6) against edgeR's `glmFit`, and against
+ the maximum where edgeR stops short; unit deviances against 150-digit arithmetic
+ ([Validation](#validation-negative-binomial-fits)).
+- Quasi-likelihood F-tests: close (1e-6) where both programs compute the same quantity,
+ and within 2e-2 where edgeR approximates the moments of the unit deviance or its fits stop
+ short ([Validation](#validation-quasi-likelihood)).
+- DESeq2's dispersions, fits and tests, each stage given DESeq2's own inputs: close (1e-6;
+ 1e-5 for what is computed from DESeq2's fits), with PyDESeq2 0.5.4 for comparison
+ ([Validation](#validation-deseq2-tests)).
+
+Further reading:
+
+- @mccarthy2012 introduced generalized linear models of counts with the Cox-Reid
+ dispersion for multifactor RNA-seq experiments.
+- @lund2012 introduced quasi-likelihood tests with shrunken dispersions for RNA-seq.
+- @chen2025 described edgeR 4, whose quasi-likelihood method adjusts deviances for small
+ counts.
+- @love2014 introduced DESeq2's shrinkage of dispersions and fold changes.
+- @bourgon2010 showed that filtering genes on a statistic independent of the test raises
+ the number of discoveries.
+
+[Mixed models and dream](mixed_models.md), the next chapter, allows for samples of the
+same donor.
diff --git a/docs/source/statistics/cubes.md b/docs/source/statistics/cubes.md
index f853daa..20f51aa 100644
--- a/docs/source/statistics/cubes.md
+++ b/docs/source/statistics/cubes.md
@@ -1,22 +1,26 @@
---
-description: What a CytePool cube holds, why every layer is a sum over cells, how merging cell types and pooling samples stay exact, the principal components of its samples and the dot plot of its cells, and how the cubes of several studies are aligned.
+description: What a cube of pseudobulk profiles holds, why each of its layers is a sum over cells, and what it records so that a result can be traced to its data.
---
-# The cube and its additive layers
+# Pseudobulk profiles and cubes
-A cube holds one pseudobulk profile for every sample and group (usually a cell type):
-the sums, over the cells of that sample and group, of each feature's counts and of a few
-statistics of its normalized values. {api}`from_cells` builds it from a cells x features
-count matrix and the sample and group label of every cell; {api}`from_bulk` wraps a
-samples x features matrix as a cube with one group. [Pool cells into pseudobulk
-profiles](../guides/pool_cells.ipynb) shows these steps on data.
+[Choosing a method](choosing_a_method.md) lists the questions a study asks of its
+samples. Every method that answers them starts from a cube, which holds, for every
+sample, group (usually a cell type) and feature (usually a gene), sums over the sample's
+cells of that group ([Why pseudobulk](../why_pseudobulk.ipynb) says why the tests
+compare samples). {api}`from_cells` builds a cube from the counts of the cells and the
+sample and group of each cell, and {api}`from_bulk` wraps a samples x features matrix of
+bulk counts as a cube with one group. [Pool cells
+into pseudobulk profiles](../guides/pool_cells.ipynb) builds one from a simulated study.
+(cube-layers)=
## The layers
For sample $j$, group $g$ and feature $f$, let $C_{jg}$ be the cells of that sample and
-group, $y_{cf}$ the count of cell $c$, $L_c$ its library size (its total count, or a
-given value) and $T$ the target sum (10,000 by default). Each cell's normalized value is
-$x_{cf} = y_{cf} T / L_c$, and the layers are
+group, $y_{cf}$ the count of cell $c$, and $L_c$ the cell's library size: its total
+count, or a value given as `library_sizes`. Each cell's counts are scaled to a target
+sum $T$, 10,000 by default, so that its normalized values are $x_{cf} = y_{cf} T / L_c$,
+and the layers, with the number of cells of each profile, are
$$
\begin{aligned}
@@ -29,124 +33,204 @@ n_{jg} &= |C_{jg}|.
\end{aligned}
$$
-Layers are stored as groups x samples x features, so that one group is a samples x
-features matrix, the shape a per-group model fits. The counts layer is the one every
-cube needs; the others are optional. Bulk cubes have counts only and no cell numbers.
-
-{api}`Cube.layer` returns a layer, for one group or all. From the sums come the
-quantities of every entry: {api}`Cube.mean` (the mean normalized value, on the log or
-the linear scale), {api}`Cube.variance` (the sample variance of the log values,
-$(\text{log\_sum\_sq} - \text{log\_sum}^2 / n) / (n - 1)$),
-{api}`Cube.fraction_detected` (detected over $n$) and {api}`Cube.library_sizes` (the
-counts summed over features).
+{api}`Cube.n_cells` gives the numbers of cells. Layers are stored as groups x samples x
+features, so that one group is a samples x features matrix, the shape that the model of
+one cell type is fitted to, and {api}`Cube.layer` returns a layer for one group or for
+every group. The counts are the one layer that every cube needs, and the other layers
+describe the cells. A cube from {api}`from_bulk` has counts only, and no cell numbers.
+(exact-regrouping)=
## Exact regrouping
Every layer is a sum over cells, so the profile of a union of cells is the sum of the
-profiles of its parts. Merging two cell types into one, or pooling the samples of a
-donor, needs no cells:
+profiles of its parts:
$$
\text{layer}(C_{jg} \cup C_{jh}) = \text{layer}(C_{jg}) + \text{layer}(C_{jh}).
$$
+Merging two cell types into one, or pooling the samples of a donor, therefore needs no
+cells, and gives the profiles that pooling the cells would have given.
{api}`Cube.regroup` merges groups, {api}`Cube.collapse_samples` pools the samples that
-share a value of a sample column, and {api}`Cube.select` keeps some samples, groups and
-features. Integer layers are summed exactly, and a sum that would exceed the largest
-64-bit integer raises an error instead of wrapping around. Means and variances, which
-are not sums, cannot be merged this way; that is why the cube keeps sums and derives
-them.
-
-## Samples and cells at a glance
-
-Two figures show a cube's data before any test. {api}`principal_components` places the
-samples of one cell type on the principal components of their most variable genes
-[@pearson1901], as DESeq2's `plotPCA` does [@love2014]. The samples are prepared as
-for an MDS plot: those with fewer than 10 cells are left out, genes are kept by
-{api}`filter_by_expression`, and the values are log-CPM with TMM factors and an average
-count of 2 added. The `top` genes of largest variance over the samples (500 by
-default), each centred on its mean, form a samples x genes matrix $X = U S V^\top$;
-the samples' coordinates are $U S$, each component's share of the variance is its
-squared singular value over their sum, and the result, a {api}`PrincipalComponents`,
-keeps the loadings $V$ of the genes. The sign of a component is arbitrary; the
-coordinate of largest absolute value is made positive. `plot_pca` draws two
-components, labelled with their shares as `plotPCA` labels them.
-
-The dot plot shows, for chosen genes and cell types, the fraction of the cells that
-express each gene and their mean expression, as scanpy draws it [@wolf2018]. A cube
-holds what it needs without the cells. Because every layer is a sum, the fraction and
-the mean over the cells of all the samples pooled,
+share the values of sample columns, such as the tissues of one donor,
+{api}`Cube.with_columns` adds a column to pool by, and {api}`Cube.select` keeps some
+samples, groups and features. A mean or a variance cannot be merged this way, which is
+why a cube keeps sums and derives the rest from them. Integer layers are summed exactly,
+in 64-bit integers.
+
+(what-the-sums-give)=
+## What the sums give
+
+The sums give the statistics that describe the cells of each profile, with $n$ its
+number of cells: {api}`Cube.mean`, the mean normalized value on the log or the linear
+scale, $\text{log\_sum} / n$ or $\text{linear\_sum} / n$; {api}`Cube.variance`, the
+sample variance of the log values,
+$(\text{log\_sum\_sq} - \text{log\_sum}^2 / n) / (n - 1)$;
+{api}`Cube.fraction_detected`, the share of the cells with a positive count,
+$\text{detected} / n$; and {api}`Cube.library_sizes`, the counts of each profile summed
+over the features.
+
+(dot-plot)=
+### The dot plot
+
+A dot plot shows, for chosen genes and cell types, the fraction of the cells that
+express each gene and their mean expression, as scanpy draws it [@wolf2018]. Because
+every layer is a sum, the fraction and the mean over the cells of all the samples
+pooled,
$$
f_{gf} = \frac{\sum_j \text{detected}_{gjf}}{\sum_j n_{jg}}, \qquad
m_{gf} = \frac{\sum_j \text{log\_sum}_{gjf}}{\sum_j n_{jg}},
$$
-are those computed from the cells themselves, and so is the mean over the cells that
-express the gene, $\sum_j \text{log\_sum}_{gjf} / \sum_j \text{detected}_{gjf}$,
-since $\log(1 + 0) = 0$ for the others. {api}`Cube.fraction_detected` and {api}`Cube.mean`
-give the same quantities per sample. `plot_dotplot` draws them, pooled over every sample
-or some, with scanpy's scaling of the means by gene or by cell type and its groups of
-genes.
-
-## What a cube carries
-
-- **Tables.** Samples, groups and features each have a table ({api}`Table`) with a
- unique text id (`sample_id`, `group_id`, `feature_id`) and any other columns, such as
- donor and condition. Designs are built from the sample table and matched to the cube
- by `sample_id`, never by position.
-- **A fingerprint.** {api}`Cube.fingerprint` is a SHA-256 digest of the tables, cell
- numbers, normalization and every layer [@nist2015]. Two cubes with the same values
- have the same fingerprint however they were made, so a result can name the cube it
- came from. Its byte-level encoding is documented, so another program can compute it
- ([Cube files and provenance records](../reference/cube_files.md)).
-- **A provenance record.** {api}`Cube.provenance` says where the data came from (a
- record the caller gives as `source`), how the cube was built, and every operation
- since, each with the fingerprint of the cube it was applied to. {api}`Cube.history`
- prints it in plain terms.
-
+are those computed from the cells themselves, and so is the mean over only the cells
+that express it, $\sum_j \text{log\_sum}_{gjf} / \sum_j \text{detected}_{gjf}$, since
+$\log(1 + 0) = 0$ for the others. A cube therefore draws the dot plot without its cells:
+{api}`plot_dotplot` pools every sample or some, and scales the means by gene or by cell
+type and draws groups of genes, as scanpy does.
+
+(what-a-cube-records)=
+## What a cube records
+
+Besides its layers, a cube keeps what is needed to trace a result to its data.
+
+- **Tables.** Samples, groups and features each have a table ({api}`Table`), with a
+ unique text id (`sample_id`, `group_id` and `feature_id`) and any other columns: each
+ sample's donor and condition, each group's Cell Ontology id, by which the cell types
+ of several studies are aligned ([Aligning studies](aligning_studies.md)), or each
+ gene's symbol. A design takes its variables from the sample table, and is matched to
+ the cube's samples by `sample_id` ([The design](linear_models.md#designs-and-formulas)).
+- **A fingerprint.** {api}`Cube.fingerprint` is a SHA-256 digest [@nist2015] of
+ everything an analysis reads (the tables, the cell numbers, the normalization of the
+ normalized layers, and every layer) but not of the provenance: cubes that hold the
+ same values share a fingerprint, whatever their history, and a result, such as those
+ of {api}`limma_voom` and {api}`propeller`, names by it the cube it came from. The
+ fingerprint is built from one digest for each table, for the cell numbers, for the
+ normalization and for each group of each layer ({api}`Cube.digests`). A cube file
+ stores the digests, so that one group can be checked when it is read, without reading
+ the others, where a single digest of the whole cube would need every layer read to
+ check any part of it. The encoding is specified byte by byte, so that another program
+ can compute a fingerprint ([Fingerprints](../reference/cube_files.md#fingerprint-encoding)).
+- **A provenance record.** {api}`Cube.provenance` says how the values were made, under
+ three keys: `source`, where the data came from, as the caller recorded it;
+ `construction`, the function that built the cube and its settings; and `operations`,
+ one entry per later operation, oldest first, each naming its input by fingerprint
+ when that is known without reading lazy layers ([Provenance
+ records](../reference/cube_files.md#provenance-keys) says when). A chain of operations
+ can then be checked link by link, back to the cube it started from.
+ {api}`Cube.history` gives the record in plain terms, as text to print.
+
+(profiles-from-other-tools)=
+## Profiles pooled by other tools
+
+scanpy's `get.aggregate` and decoupler's `pp.pseudobulk` pool cells into an AnnData
+with one row per sample and cell type, and LIANA into a MuData with one modality per
+cell type [@virshup2024; @bredikhin2022]. {api}`from_anndata` and {api}`from_mudata`
+read these layouts into a cube, and {api}`to_anndata` and {api}`to_mudata` write a cube
+in them, with a record that lets an import restore the same cube, down to its
+fingerprint. Since a cube's layers are sums, an import takes only what is a sum or gives
+one exactly: summed counts, the number of cells that detect each feature, and sums of
+normalized values. Means, variances and medians are refused: a variance or a median
+gives no sum, and a mean times the number of cells gives the sum exactly only where the
+sum is a whole number, while the tools that give means give sums too. [Work with
+AnnData and MuData](../guides/anndata_and_mudata.ipynb) converts cubes both ways.
+
+(cubes-larger-than-memory)=
## Cubes larger than memory
-{api}`write_cube` saves a cube as a Zarr v3 store [@miles2025], in chunks of one group,
-every sample and up to 4,096 features, and {api}`read_cube` opens it without reading the
-layers: an analysis then reads one group at a time, and only the chunks that hold the
-features it needs. The differential-expression functions keep one group in memory at a
-time. A layer can also be any array-like with `shape`, `dtype` and indexing, such as a
-numpy memmap or an HDF5 or Zarr array, read as needed; {api}`Cube.to_memory` reads every
-lazy layer at once.
-
-{api}`to_anndata` and {api}`to_mudata` convert a cube to the pseudobulk layouts of
-scverse, and {api}`from_anndata` and {api}`from_mudata` read them back.
-
-## Several studies
-
-Results of one study often do not replicate in the next [@nakatsuka2025], and studies
-are compared or combined only once their samples, genes and cell types are aligned.
-Aligned by hand, they go wrong in two ways: donor ids repeat across studies, and genes
-that a study did not measure are read as zeros. {api}`CubeCollection` aligns the cubes
-of several studies and keeps each as it was given:
-
-- **Samples** are named `study::sample_id`, and so are the values of the columns of ids
- that `within_study` names, such as donors: donor `D1` of one study is not donor `D1`
- of another.
-- **Genes** are aligned by `feature_id`, such as Ensembl ids, and
- {api}`CubeCollection.measured` says which study measured which: a gene that a study's
- cube lacks, or marks as not measured in its feature column `measured`, is missing in
- that study, not zero.
-- **Cell types** are aligned by their Cell Ontology ids [@diehl2016] when every cube
- has them, by group id otherwise, or by a mapping for each study. Groups of one study
- that share an id are merged, exactly, since every layer is a sum.
-
-Each study is then analysed on its own: {api}`CubeCollection.aligned_cube` gives its
-cube with the aligned cell types and the genes it measured, and with its own samples and
-library sizes, and an analysis such as {api}`limma_voom` with the study's own design
-gives results keyed by the aligned cell types. {api}`analyse_each_study` runs one on
-every study, and the {api}`StudyResults` it returns lines them up gene by gene
-({api}`StudyResults.table`) for {api}`meta_analysis`, which combines them
-([Meta-analysis](meta_analysis.md)). For a joint model with the study as a covariate,
-{api}`CubeCollection.stack` puts the studies into one cube with the genes that every
-study measured and the cell types that every study has: a cube holds no missing values,
-so it refuses the others rather than fill them with zeros. The collection's provenance
-names each study's cube by its fingerprint, with the alignment, and so does the
-provenance of a stacked cube. [Combine studies](../guides/combine_studies.ipynb) aligns
-six cohorts this way.
+{api}`write_cube` saves a cube as a Zarr v3 store [@miles2025], in chunks of one group
+and up to 4,096 features, and {api}`read_cube` opens it without reading its layers: a
+group is read when an analysis needs it, and of that group only the chunks of the
+features it selects. The analyses of every cell type hold one cell type in memory at a
+time, so they run on cubes larger than memory, also on a cube whose layers are other
+array-likes read as needed, such as a numpy memmap or an HDF5 dataset ({api}`Cube`).
+[Cube files and provenance records](../reference/cube_files.md) specifies the file.
+
+(cubes-defaults)=
+## Defaults and choices
+
+- **Layers.** {api}`from_cells` computes all five. The tests need only the counts and
+ the cell numbers, and the dot plot also `detected` and `log_sum`; a cube of counts
+ alone takes an eighth of the memory of all five layers (`layers=["counts"]`).
+- **Normalization of the cells.** Each cell is scaled to `target_sum`, 10,000, by its
+ total count, and its log values are $\log(1 + x)$, as scanpy's `normalize_total` and
+ `log1p` compute them. `library_sizes` gives other
+ totals, such as those over every gene when the counts hold only some genes. Only the
+ normalized layers depend on it; the counts, and the tests, do not.
+- **Samples and groups.** By default the distinct labels, sorted. A sample table
+ (`samples=`) or group table (`groups=`) sets their order, carries the variables of
+ the design, and gives a sample without cells a profile of zeros.
+- **Rare cell types.** A cell type with few cells in most samples gives noisy
+ profiles; {api}`Cube.regroup` merges it into its lineage, exactly, as [Pool cells into
+ pseudobulk profiles](../guides/pool_cells.ipynb) does.
+- **Files.** {api}`write_cube` compresses with gzip at level 4 by default;
+ `compression="zstd"` uses zstd, on Python 3.14 and newer, and `compression=None`
+ stores the values as they are. {api}`Cube.to_memory` reads every lazy layer at once,
+ for analyses that read the same groups many times.
+
+(cubes-limits)=
+## Limits and differences
+
+- **Memory.** {api}`from_cells` needs the counts of the cells in memory, and returns
+ dense layers: 32 bytes per sample, group and feature with all five layers.
+- **Whole numbers.** Integer layers and cell numbers hold whole numbers up to
+ $2^{63} - 1$, the largest signed 64-bit integer, and a sum beyond it raises an error
+ instead of wrapping around. Normalization works in float64, which is exact for counts
+ up to $2^{53}$.
+- **Variances from sums.** {api}`Cube.variance` computes the variance from raw moments,
+ which lose digits when the values of a profile are nearly constant and far from zero.
+ Log-normalized expression is far from that case ([Validation](#validation-cube)
+ gives the bound).
+- **Profiles without cells.** The fraction detected of a profile without cells is NaN,
+ zero cells out of zero, where decoupler reports 0.
+- **Scattered reads.** A read decompresses every chunk that holds a selected feature.
+ A few neighbouring genes are therefore quick to read, but genes scattered over a file
+ decompress nearly every chunk of each group, and a few samples of every gene every
+ chunk ([Timings](#validation-timings)).
+- **What a read checks.** Reading a whole group of a cube file checks it against the
+ group's digest; reading some samples or features of a group does not, and
+ {api}`Cube.verify_fingerprint` checks those too. A cube in memory is hashed once, by
+ the first operation applied to it ({api}`Cube.fingerprint` gives the cost).
+- **Other array-likes.** A lazy layer of another array-like is read with its
+ orthogonal indexing when it has one, as zarr-python's arrays do, and otherwise a whole
+ group per read, and the cube copies what it returns. Such an array can change,
+ so its fingerprint is computed anew on every call, and the operations applied to it
+ record no input fingerprint.
+- **Profiles filtered before an import.** Profiles that another tool removed after
+ pooling, such as by a filter of samples with few cells, are missing from merged
+ groups and pooled samples: import the profiles first, and leave such samples out in
+ the analysis.
+
+(cubes-checks)=
+## Checks and further reading
+
+Each part of a cube is compared with an independent implementation, run as a separate
+program on the same data:
+
+- The layers, merged groups and pooled samples, and the means, variances and fractions
+ they give: scanpy 1.12.4, exact (identical counts, 1e-10 otherwise)
+ ([Validation](#validation-cube)).
+- {api}`from_cells` and {api}`from_bulk`: scanpy 1.12.4 and decoupler 2.2.0, exact
+ ([Validation](#validation-pool-cells)).
+- Cube files, and reads of selected features: zarr-python 3.4.1, identical, and 1e-12
+ for merged groups and pooled samples that a read gives
+ ([Validation](#validation-cube-files) and [Validation](#validation-cube-reads)).
+- Fingerprints: a second implementation written from the specification, which reads
+ cube files with zarr-python 3.4.1, identical digests
+ ([Validation](#validation-cube-fingerprints)).
+- Conversions to and from AnnData and MuData: scanpy 1.12.4, decoupler 2.2.0, LIANA
+ 1.10.0, anndata 0.13.4 and mudata 0.4.1, exact
+ ([Validation](#validation-anndata-conversion)).
+- The dot plot: scanpy 1.12.4, exact ([Validation](#validation-pca-and-dot-plots)).
+
+Further reading:
+
+- @wolf2018 introduced scanpy, the toolkit for single-cell analysis whose aggregation
+ of cells and dot plot the cube is compared with.
+- @virshup2024 described anndata, the annotated data matrices in which scverse tools
+ keep cells and their pseudobulk profiles.
+- @miles2025 specify version 3 of Zarr, the format of cube files, which any Zarr v3
+ reader can open.
+
+[Designs, linear models and contrasts](linear_models.md) turns the questions asked of
+these profiles into one model for every gene.
diff --git a/docs/source/statistics/empirical_bayes.md b/docs/source/statistics/empirical_bayes.md
index 2921d8a..0195c37 100644
--- a/docs/source/statistics/empirical_bayes.md
+++ b/docs/source/statistics/empirical_bayes.md
@@ -1,19 +1,23 @@
---
-description: Empirical Bayes moderation of gene-wise variances as limma computes it, its prior estimators, moderated t and F statistics, the B-statistic, and TREAT's test against a fold-change threshold.
+description: Why each gene's residual variance is shrunk towards a value shared by all genes, how that prior is estimated, and the moderated t, F and B statistics and TREAT that follow.
---
-# Empirical Bayes moderation and TREAT
+# Empirical Bayes moderation
-With a few samples, each gene's residual variance $s_g^2$ is a poor estimate: some genes
-look precise by chance and give large t-statistics. Empirical Bayes moderation borrows
-strength from all genes by shrinking each variance towards a common value [@smyth2004].
-Every test of a linear model in CytePool moderates this way: {api}`moderated_statistics`
-and {api}`treat` on a fit, and the differential-expression, gene-set and composition
-functions inside.
+[Designs, linear models and contrasts](linear_models.md) gave every gene an estimate of
+each contrast and a residual variance $s_g^2$. With a few samples, $s_g^2$ is a poor
+estimate: some genes look precise by chance and get large t-statistics. Empirical Bayes
+moderation borrows strength from all the genes, by shrinking each variance towards a
+value estimated from them [@smyth2004]. {api}`moderated_statistics` moderates a fit as
+limma's `eBayes` does, and so do {api}`limma_voom`, {api}`limma_trend`,
+{api}`dream_voom` and {api}`propeller` in their tests. [Normalization, filtering and
+sample quality](../guides/normalization_and_quality.ipynb) shows the moderated variances
+of one cell type.
+(empirical-bayes-model)=
## The model
-The residual variance of gene $g$ on $d_g$ degrees of freedom, and the true variance
+The residual variance of gene $g$ on $d_g$ degrees of freedom, and its true variance
$\sigma_g^2$, follow
$$
@@ -22,51 +26,83 @@ s_g^2 \mid \sigma_g^2 \sim \frac{\sigma_g^2\, \chi^2_{d_g}}{d_g}, \qquad
$$
so that $s_g^2$ follows $s_0^2$ times an $F(d_g, d_0)$ distribution. The prior
-$(s_0^2, d_0)$ is estimated from all genes, and each variance is replaced by its
+$(s_0^2, d_0)$ is estimated from all the genes, and each gene's variance becomes its
posterior value
$$
\tilde s_g^2 = \frac{d_0 s_0^2 + d_g s_g^2}{d_0 + d_g}.
$$
-The moderated t-statistic $t_g = \hat\beta_g / (u_g \tilde s_g)$, with $u_g$ the
-unscaled standard error of the coefficient or contrast, follows a t distribution on
-$d_0 + d_g$ degrees of freedom when the coefficient is zero; its two-sided p-value comes
-from the tail of that distribution ({api}`t_sf`), on fractional degrees of freedom too.
-Large $d_0$ means the genes have similar variances and the prior counts for much;
-$d_0 = 0$ is the ordinary t-test. {api}`moderated_statistics` returns them in a
-{api}`ModeratedFit`, and {api}`squeeze_variances` applies the same moderation to any set
-of variances ({api}`SqueezedVariances`).
-
+The moderated t-statistic $t_g = \hat\beta_g / (u_g \tilde s_g)$, for a coefficient or
+contrast $\hat\beta_g$ with unscaled standard error $u_g$, follows a t distribution on
+$d_0 + d_g$ degrees of freedom when the coefficient is zero. These are capped, as in
+limma, at the total of all the genes' residual degrees of freedom, and the two-sided
+p-value comes from the tail of that distribution ({api}`t_sf`), on fractional
+degrees of freedom too. A large $d_0$ means that the genes have similar variances and
+the prior counts for much; $d_0 = 0$ gives the ordinary t-test, and an infinite $d_0$
+gives every gene the prior variance. {api}`moderated_statistics` returns the statistics
+in a {api}`ModeratedFit`, whose {api}`ModeratedFit.table` has the columns of limma's
+`topTable`, and {api}`squeeze_variances` moderates any set of variances
+({api}`SqueezedVariances`).
+
+(estimating-the-prior)=
## Estimating the prior
-- **Method of moments** ({api}`fit_f_distribution`, limma's `fitFDist`): matches the
- mean and variance of $\log s_g^2$, corrected for its sampling distribution, to those
+The prior is estimated from the variances of all the genes, by one of two estimators
+(`prior_estimator`):
+
+- **The method of moments** ({api}`fit_f_distribution`, limma's `fitFDist`) matches the
+ mean and variance of $\log s_g^2$, corrected for their sampling distribution, to those
of the scaled F distribution [@smyth2004, section 6.2], and returns a
- {api}`FDistributionFit`. With a covariate, as in limma-trend, $s_0^2$ follows a
- natural cubic spline ({api}`natural_spline_basis`) in the mean log-expression of the
- genes [@phipson2016, section 10.1].
-- **Robust** ({api}`fit_f_distribution_robust`, `robust=True`): Winsorizes the log
- variances so that hypervariable genes do not lower $d_0$, and gives outlying genes a
- smaller prior df of their own [@phipson2016] ({api}`RobustFDistributionFit`).
+ {api}`FDistributionFit`. When the variances vary no more than sampling explains,
+ $d_0$ is infinite.
- **Weighted moments and profile likelihood** ({api}`fit_f_distribution_likelihood`,
- limma's `fitFDistUnequalDF1`): for degrees of freedom that differ between genes or are
- fractional, as with missing values, zero weights, mixed models or the quasi-likelihood
- of edgeR 4 [@chen2025] ({api}`LikelihoodFDistributionFit`).
-
-By default the likelihood estimator is used when the positive degrees of freedom differ
-between genes and the method of moments otherwise, as in limma since version 3.61.8
-(`legacy = NULL`).
-
-## Moderated F and the B-statistic
-
-The moderated F-statistic tests that all coefficients of a gene are zero:
-$F = t' R^{+} t / r$, with $R$ the correlation matrix of the coefficients and $r$ its
-rank. CytePool uses each gene's own correlation matrix when the fit has one (missing
-values or observation weights), which is the correct F-statistic [@smyth2004, section
-7]; limma uses the correlation of the unweighted design for every gene.
-
-The B-statistic is the log-odds that a coefficient is non-zero [@loennstedt2002], in the
+ limma's `fitFDistUnequalDF1`) estimate $s_0^2$ from a weighted mean of the log
+ variances, in which a variance counts less the fewer degrees of freedom it has, and
+ $d_0$ by maximizing the likelihood of the variances [@chen2025]
+ ({api}`LikelihoodFDistributionFit`). They suit degrees of freedom that differ between
+ genes, or are small or fractional, as with missing values, zero weights, mixed models
+ or edgeR's quasi-likelihood.
+
+By default, as in limma since version 3.61.8 (`legacy = NULL`), the likelihood
+estimator is used when the genes that have residual degrees of freedom do not all have
+the same number of them, and the method of moments otherwise; genes without residual
+degrees of freedom do not count.
+
+With a covariate, the prior variance follows a trend. limma-trend gives it each gene's
+mean log-expression (`trend=True`): $s_0^2$ then becomes a natural cubic spline in it for
+the method of moments ({api}`natural_spline_basis`; @phipson2016, section 10.1), a
+LOWESS trend for its robust form below, and a weighted LOWESS trend for the likelihood
+estimator.
+
+Genes with outlying variances, such as hypervariable genes, widen the spread of the
+variances, and so lower $d_0$ for every gene. With `robust=True` they do not
+[@phipson2016]: the prior is estimated so that they count less, and they get smaller
+prior degrees of freedom of their own. With the method of moments, the log variances are
+Winsorized at their 5% and 90% quantiles ({api}`fit_f_distribution_robust`, which
+returns a {api}`RobustFDistributionFit`); fewer than 10 usable variances are too few to
+Winsorize at these quantiles, and the robust estimate is then the ordinary one, as in
+limma. With the likelihood estimator, which robust moderation uses when the degrees of
+freedom differ, nothing is Winsorized: the outlying genes get smaller prior weights, and
+the prior is fitted again, as limma does.
+
+(moderated-f-statistics)=
+## Moderated F-statistics
+
+The moderated F-statistic tests that all the coefficients of a gene's fit, or all its
+contrasts, are zero: $F = t' R^{+} t / r$, with $t$ the moderated t-statistics, $R$
+their correlation matrix and $r$ its rank, on $r$ and $d_0 + d_g$ degrees of freedom.
+When the fit holds a correlation matrix for each gene, as it does with missing values or
+observation weights, CytePool uses each gene's own, which gives the correct F-statistic
+[@smyth2004, section 7]; limma uses the correlation of the design without weights for
+every gene, which `f_statistic="limma"` reproduces. The two agree when one factorization
+serves every gene. Unlike those of the t-tests, the denominator degrees of freedom of the
+F-tests are not capped, as in limma.
+
+(b-statistic)=
+## The B-statistic
+
+The B-statistic is the log-odds that a coefficient is not zero [@loennstedt2002], in the
form of @smyth2004:
$$
@@ -74,26 +110,100 @@ B = \log\frac{p}{1 - p} + \frac{1}{2}\log r
+ \frac{1 + d}{2}\log\frac{t^2 + d}{t^2 r + d},
$$
-with $p$ the assumed proportion of non-zero coefficients (0.01 by default),
-$d = d_0 + d_g$, $r = v / (v + v_0)$, $v$ the squared unscaled standard error and $v_0$
-the prior variance of non-zero coefficients, estimated from the largest t-statistics.
-When every gene has the same degrees of freedom and unscaled standard error, as in a fit
-without weights or missing values, B orders the genes as $|t|$ does.
+where $p$ is the assumed proportion of coefficients that are not zero (0.01 by default),
+$d$ the degrees of freedom of the moderated t-statistic, $r = v / (v + v_0)$, $v$ the
+squared unscaled standard error, and $v_0$, estimated from the largest t-statistics, the
+prior variance of the coefficients that are not zero. When every gene has the same
+degrees of freedom and unscaled standard error, as in a fit without weights or missing
+values and with one prior for all genes, B orders the genes as $|t|$ does.
+(treat-test)=
## Testing against a fold-change threshold
-A small p-value says a fold change is not zero, not that it is large. TREAT tests
-$H_0: |\beta| \le \tau$ against $|\beta| > \tau$ [@mccarthy2009]. With the moderated
-t-statistic $t$, its standard error $\mathrm{se}$ and $\delta = \tau / \mathrm{se}$, the
-p-value is the worst case over the null interval,
+A small p-value says that a fold change is not zero, not that it is large. TREAT tests
+$H_0: |\beta| \le \tau$ against $|\beta| > \tau$ [@mccarthy2009]. Its p-value is the
+largest that any $\beta$ of the null interval allows, which the ends $\pm\tau$ give: for
+a moderated t-statistic $t$ with standard error $\mathrm{se}$, and
+$\delta = \tau / \mathrm{se}$,
$$
p = P(T > |t| - \delta) + P(T > |t| + \delta),
$$
-with $T$ on $d_0 + d_g$ degrees of freedom. {api}`treat` returns a {api}`TreatFit` and
-takes the threshold as a log2 fold change (`lfc`) or a fold change (`fc`, 1.2 by
-default, as in limma). Filtering significant genes by their estimated fold change does
-not test the threshold; TREAT does, and @mccarthy2009 found that it improves on the
-false discovery rate of existing methods when the size of the change is taken into
-account.
+where $T$ has the moderated t-statistic's degrees of freedom. {api}`treat` returns a
+{api}`TreatFit`, whose t-statistics are shrunk towards zero by the threshold,
+$\operatorname{sign}(t) \max(|t| - \delta, 0)$. Filtering significant genes by their
+estimated fold change does not test the threshold; TREAT does, and @mccarthy2009 found
+that it improves on the false discovery rate of existing methods when the size of the
+change is taken into account. Thresholds are typically fold changes of 1.1, 1.2 or 1.5,
+and since TREAT asks for evidence that a change exceeds the threshold, an estimated fold
+change must be clearly above it to be significant.
+
+(empirical-bayes-defaults)=
+## Defaults and choices
+
+- **The estimator.** limma's default rule (`prior_estimator=None`), above;
+ `"moments"` or `"likelihood"` fixes one, as limma's `legacy = TRUE` or `FALSE` does.
+- **A trend.** None by default. `trend=True` lets the prior variance follow the mean
+ log-expression, as limma-trend needs; voom's precision weights model the trend
+ already, so limma-voom moderates without one ([limma-trend](limma_voom.md#limma-trend)).
+- **Robust moderation.** Off by default, as in limma's `eBayes`; `robust=True` when a
+ plot of the residual variances against expression shows outlying genes, as limma's
+ `plotSA` does ({api}`plot_residual_variances`). {api}`propeller` moderates robustly by
+ default, as speckle does.
+- **F-statistics.** Exact by default; `f_statistic="limma"` gives limma's
+ ([Moderated F-statistics](#moderated-f-statistics)).
+- **The B-statistic.** `proportion=0.01`, and `stdev_coef_lim=(0.1, 4)` to bound the
+ standard deviation of a coefficient that is not zero, as in limma.
+- **The threshold of TREAT.** A fold change of 1.2 by default (`fc`), as in limma, or a
+ log2 fold change (`lfc`); the threshold applies to the absolute log2 fold change, so a
+ fold change and its inverse give the same test.
+
+(empirical-bayes-limits)=
+## Limits and differences
+
+- **Robust moderation with unusable variances.** The robust likelihood estimator leaves
+ the genes without a usable variance, such as those without residual degrees of
+ freedom, out of its test for outliers. limma 3.62.1 counts a missing variance there
+ as a zero one, which becomes an outlier and changes which other genes count as
+ outliers. CytePool's estimator also keeps the prior weights in its second fit, where
+ limma's drops them. Without unusable variances or prior weights, the two agree
+ ([Validation](#validation-empirical-bayes) gives the sizes).
+- **Too few variances.** {api}`fit_f_distribution_likelihood` needs at least two usable
+ variances above zero, and raises an error otherwise, where limma returns NA. Where
+ exactly two are above zero, both fit a constant prior, but limma returns NaN when other
+ variances are not usable, which CytePool leaves out. With one or two genes nothing is
+ moderated, and each gene keeps its own variance, as limma does since version 3.61.12.
+- **Infinite covariates.** An infinite covariate is placed one unit beyond the largest or
+ smallest finite one, as limma's `fitFDist` places it, where its `fitFDistUnequalDF1`
+ returns NaN.
+- **TREAT's t-statistic** is NaN where the coefficient is NaN, or where a contrast of
+ zeros meets a threshold of zero; limma reports 0 for both.
+- **Many degrees of freedom.** With residual degrees of freedom of ten million and
+ more, limma's likelihood loses precision, and its prior degrees of freedom move away
+ from those of the likelihood computed to 60 digits, which CytePool's agree with
+ ([Validation](#validation-empirical-bayes)).
+
+(empirical-bayes-checks)=
+## Checks and further reading
+
+Every estimator and statistic is compared with limma 3.62.1, run as a separate program
+on the same fits: exact (1e-10), and close (1e-6) for the robust prior, which limma
+solves only to a tolerance, and for the likelihood prior, whose search both programs
+stop at a tolerance ([Validation](#validation-empirical-bayes)).
+
+Further reading:
+
+- @smyth2004 introduced the hierarchical model of gene-wise variances and the moderated
+ t-statistic, for microarray experiments with few arrays.
+- @loennstedt2002 introduced the B-statistic, the log-odds of differential expression,
+ for replicated microarray data.
+- @phipson2016 estimated the prior robustly, which protects against hypervariable genes
+ and improves the power to detect differential expression.
+- @mccarthy2009 introduced TREAT, which tests whether a fold change exceeds a
+ threshold.
+- @chen2025 described edgeR 4, whose estimate of the prior of variances on unequal or
+ fractional degrees of freedom {api}`fit_f_distribution_likelihood` follows.
+
+[Multiple testing](multiple_testing.md) turns the p-values of thousands of genes into a
+list of calls with a known error rate.
diff --git a/docs/source/statistics/gene_set_tests.md b/docs/source/statistics/gene_set_tests.md
index 0d666c8..2098989 100644
--- a/docs/source/statistics/gene_set_tests.md
+++ b/docs/source/statistics/gene_set_tests.md
@@ -1,368 +1,311 @@
---
-description: Competitive and self-contained gene-set tests on a linear model, as limma's camera, fry, roast, mroast and romer compute them; gene set enrichment analysis of ranked genes, as fgsea computes it; over-representation analysis of the genes called significant, as limma's kegga computes it; scores of gene sets in every sample by GSVA, ssGSEA, combined z-scores and PLAGE, as the GSVA package computes them; activities of transcription factors and pathways from the networks CollecTRI and PROGENy, as decoupler computes them; gene sets read from GMT files; and how to choose between the tests.
+description: Whether the genes of a pathway respond to a contrast, tested on the linear model of every gene by limma's camera, fry, roast, mroast and romer.
---
# Gene-set tests
-A gene-set test asks a question of a pathway or a signature as a whole, for one contrast
-of the linear model fitted to every gene. Two questions differ [@goeman2007]:
-
-- **Self-contained**: are the genes of the set differentially expressed at all?
- {api}`fry`, {api}`roast` and {api}`mroast`.
-- **Competitive**: are they more differentially expressed than the other genes?
- {api}`camera` and {api}`romer`; {api}`gsea`, which asks it of a ranking of all genes;
- and {api}`over_representation`, which asks it of the genes called significant.
-
-A self-contained test finds a set significant when its genes respond, however many other
-genes respond too: in a response that touches thousands of genes, many sets are. A
-competitive test asks whether a set stands out from the other genes, and so suits
-ranking many sets, such as a pathway collection. The five tests of limma work on the
-linear model, so they allow any design and contrast and do not permute samples; all but
-romer take voom's precision weights. Each tests a cell type of a result of
-{api}`limma_voom` or {api}`limma_trend`, whose fit it computes again
-({api}`DifferentialExpressionResult.fit`), or values with a design and a contrast;
-romer, which takes sample weights only, tests a result of limma-trend. Gene set
-enrichment analysis needs only a statistic of every gene, and over-representation
-analysis only the genes that an analysis called significant.
-[Gene sets and pathways](../guides/gene_sets.ipynb) and
-[Pathway and regulator activity](../guides/pathway_activity.ipynb) run these methods on
-published data.
-
+The chapters so far asked which genes change ([limma-voom and limma-trend](limma_voom.md))
+and which cell types expand or contract ([Composition tests](composition.md)). This part
+asks the question of a pathway or a signature as a whole: do the genes of a set respond
+to a contrast? {api}`camera` answers it by default for a collection of sets: it asks
+whether the genes of each set change more than the other genes, and allows for the
+correlation between them [@wu2012]. {api}`fry`, {api}`roast` and {api}`mroast` ask whether
+the genes of a set change at all, and {api}`romer` compares the ranks of a set's genes
+with those of the others. All five test one contrast of the linear model that
+{api}`limma_voom` or {api}`limma_trend` fitted to a cell type. [Gene sets and
+pathways](../guides/gene_sets.ipynb) runs camera, fry and roast on published data.
+
+(gene-set-collections)=
+## Gene-set collections
+
+A gene set names genes that share a function or a response, such as a pathway, the
+targets of a transcription factor or a signature. Collections such as MSigDB
+[@liberzon2011; @liberzon2015], Reactome and WikiPathways distribute them as GMT files,
+and so do the Gene Ontology's GMT exports, such as MSigDB's: one set per line, a name,
+a description and the genes, in fields separated by tabs [@subramanian2005; @gsea2026].
+{api}`read_gmt` reads such a file into a {api}`GeneSets`, a mapping from set names to
+gene ids that every function of this part takes as its gene sets, and {api}`write_gmt`
+writes one back. CytePool bundles no collection, since each provider states its own
+terms. A collection records the name, version and licence that you give when you read
+it, with the name and SHA-256 digest of the file, so that a result can say which sets it
+used.
+
+Sets name genes by symbol, by Entrez id or by Ensembl id, and an analysis must name them
+the same way: the tests read a result's ids from a column of its features (`id_column`,
+such as `"symbol"`). {api}`GeneSets.restrict_to` matches a collection to the genes that
+an analysis measured, as limma's `ids2indices` does, and its error when nothing matches
+is the sign that the two name genes differently. {api}`GeneSets.filter_by_size` then
+keeps sets by their size among the genes measured, and {api}`GeneSets.select` by name.
+The GSEA program skips sets of under 15 or over 500 genes by default [@gsea2026]; the
+gene-set tests, GSEA and the pathway scores of CytePool test sets of every size unless
+told otherwise, as fgsea and GSVA do.
+
+(competitive-and-self-contained)=
+## Competitive or self-contained
+
+Two questions can be asked of a set's genes [@goeman2007]. A *self-contained* test asks
+whether they respond to the contrast at all: {api}`fry`, {api}`roast` and {api}`mroast`
+ask it. A *competitive* test asks whether they respond more than the other genes:
+{api}`camera` and {api}`romer` ask it, as do the over-representation analysis and the
+gene set enrichment analysis of the [next chapter](over_representation_and_gsea.md). In
+a response that touches thousands of genes, the genes of many sets respond, and a
+self-contained test finds each of those sets significant; a competitive test asks which
+sets respond more than the other genes, and so ranks a collection. A self-contained test
+suits a set chosen in advance, such as an interferon signature in an interferon
+stimulation.
+
+(correlated-genes)=
+## Genes that respond together
+
+The genes of a set are seldom independent: co-regulated genes rise and fall together
+from sample to sample. A test that takes them as independent draws, such as one that
+compares a set with random sets of genes, counts one shared fluctuation many times, and
+its p-values are too small for sets whose genes are correlated [@goeman2007; @wu2012].
+The tests of this chapter allow for it in two ways. camera inflates the variance of a
+set's mean statistic by a factor that grows with the correlation between its genes. fry,
+roast, mroast and romer compare a set's statistic with its values under rotations of the
+residual space, which keep the correlation between genes as it is.
+
+(gene-effects)=
## The effects of each gene
-The five tests on the linear model start from the same decomposition [@wu2010, section 4.1]. The design is
-reparametrized so that the contrast is its last coefficient, and the QR decomposition of
-that design turns each gene's data into a contrast effect $u_0$ and $d$ residual effects
-$u_1, \dots, u_d$, independent with variance $\sigma^2$ under the model. The contrast
-effect over the residual standard deviation is the gene's t-statistic.
-
+The five tests start from the same decomposition [@wu2010, section 4.1]. The design, of n
+samples and p coefficients, is reparametrized to make the contrast its last
+coefficient, and the QR decomposition of that design turns the values of a gene into a
+contrast effect $u_0$ and $d = n - p$ residual effects $u_1, \dots, u_d$. Under the
+model they are independent, each with the gene's variance $\sigma^2$, and $u_0$ over the
+residual standard deviation is the gene's t-statistic. With precision weights, each
+gene's values and design are first multiplied by the square roots of its weights.
+
+Under the null hypothesis $u_0$ has mean zero too, so the $d + 1$ effects of a gene are
+independent with equal variances, and a rotation of them leaves their distribution
+unchanged. One rotation applied to the effects of every gene then leaves the joint
+distribution of the data unchanged, the correlation between genes included [@wu2010;
+@langsrud2005]. Rotations take the place of permutations of the samples, which a small
+experiment has too few of and a design with several factors or covariates does not
+allow: they need only residual degrees of freedom.
+
+A result of {api}`limma_voom` or {api}`limma_trend` is tested on the fit of one cell type,
+which {api}`DifferentialExpressionResult.fit` computes again: its log-CPM values and
+precision weights, the design rows of the samples fitted, the contrast's weights on the
+columns kept, and the moderation the result used. Values with a design and a contrast
+are tested the same way.
+
+(camera)=
## camera
-Genes are correlated, so a set's genes are not independent evidence. camera turns each
-gene's moderated t-statistic into a z-score and compares the mean z-score of the set's
-$m$ genes with that of the other genes by a two-sample t-test, with the variance of the
-set's mean inflated by
+camera turns each gene's moderated t-statistic into a z-score and compares the mean
+z-score of the set's $m$ genes with that of the other genes by a two-sample t-test, with
+the variance of the set's mean inflated by
$$
\mathrm{VIF} = 1 + (m - 1)\rho,
$$
-for an inter-gene correlation $\rho$ [@wu2012]. The default $\rho = 0.01$ is limma's;
-`inter_gene_correlation=None` estimates it for each set from its residual effects.
-`use_ranks=True` gives the rank version, a correlation-adjusted Wilcoxon-Mann-Whitney
-test.
+where $\rho$ is the correlation between the set's genes [@wu2012]. The default
+$\rho = 0.01$ is limma's. `inter_gene_correlation=None` estimates $\rho$ for each set
+from the residual effects of its genes, each divided by the gene's residual standard
+deviation: the sum of squares of their mean over the set, times $m/d$, estimates the
+variance inflation factor. `use_ranks=True` gives the rank version, a
+Wilcoxon-Mann-Whitney test of the set's z-scores against the others, with the variance
+of the rank sum adjusted for the correlation [@barry2008; @wu2012]. The p-value is
+two-sided, and the direction says whether the set's genes went up or down.
+(fry)=
## fry
-fry averages each gene's standardized effects over the set into a vector $b$ of $d + 1$
-values. Under the null hypothesis a random rotation of the residual space leaves the
-data's distribution unchanged, so
+fry asks whether a set's genes respond in one direction, for the directional p-value, or
+each in its own direction, for the mixed p-value [@giner2016]. It divides each gene's
+$d + 1$ effects by an estimate of the gene's standard deviation and averages them over
+the set's genes into a vector $b$. Under the null hypothesis a random rotation turns $b$
+uniformly over its sphere, so
$$
T = \frac{b_0}{\sqrt{\sum_{k=1}^{d} b_k^2 / d}}
$$
-follows a t distribution on $d$ degrees of freedom: the p-value that roast with the mean
-statistic would give with infinitely many rotations [@giner2016]. fry is fast enough for
-thousands of sets and gives a directional and a mixed p-value for each.
-
+follows a t distribution on $d$ degrees of freedom. When the prior degrees of freedom are
+very large, as when every gene has the same residual variance, this is the p-value that
+roast with the mean statistic gives with infinitely many rotations; otherwise fry
+approximates it. The mixed statistic sums the squared standardized contrast effects of
+the set's genes, and its p-value under rotation is approximated by the beta distribution
+whose mean and variance match its own; for a set of one gene it equals the gene's
+two-sided t-test p-value.
+
+By default (`standardize="posterior_sd"`) each gene is divided by a posterior standard
+deviation: the mean square of its $d + 1$ effects with the largest left out, so that a
+gene's own strong response does not inflate it, moderated by the prior of [empirical
+Bayes moderation](empirical_bayes.md). `"residual_sd"` divides by the root of that mean
+square, unmoderated, and `"none"` leaves the effects as they are, which suits genes of
+equal variance. fry draws no rotations, so it is fast enough for thousands of
+sets, and its p-values are not limited by a number of rotations.
+
+(roast)=
## roast and mroast
-roast draws random rotations of the residual space and compares a set statistic with its
-values under them [@wu2010; @langsrud2005]. The statistic summarizes the genes'
-z-scores: their mean (the default), which detects sets in which most genes change in the
-same direction, or a floored mean, the mean of the top half or the mean square, which
-detect sets in which fewer genes change, the mean square the fewest. With $b$ of $B$
-rotations at least as extreme as the observed statistic, the p-value is
-
-$$
-p = \frac{b + 1}{B + 1}
-$$
-
-[@phipson2010], so it is never zero; each rotation also gives its mirror image for the
-up and down alternatives. roast reports the proportion of the set's genes that are
-active in each direction. {api}`mroast` tests many sets with the same rotations and
-adjusts their p-values. Rotations come from a seeded numpy generator, so a seed gives
-the same p-values every time; they agree with limma's within Monte Carlo error.
-
-## romer
-
-romer ranks every gene's moderated t-statistic among all genes and compares a set's mean
-rank with its values when every gene's rotated statistics are ranked again
-[@majewski2010]. Like gene set enrichment analysis [@subramanian2005] it pits the sets
-against each other, but by rotating the residual space instead of permuting samples, so
-it works for any contrast with residual degrees of freedom. Before rotating, each gene's
-contrast effect is shrunk by its posterior probability of being differentially
-expressed, so that strongly responding genes do not inflate the rotated statistics.
-
-## Gene-set files
-
-Gene sets come from collections such as MSigDB [@liberzon2011; @liberzon2015],
-Reactome, WikiPathways and the Gene Ontology, which distribute them as GMT files: one
-set per line, with its name, a description and its genes, separated by tabs
-[@subramanian2005; @gsea2026]. {api}`read_gmt` reads such a file into a
-{api}`GeneSets`, a mapping of set name to gene ids, which every function on this page
-takes as its gene sets. CytePool bundles no collection: each provider states its own
-terms. The collection records the source, version and licence that you give when you
-read it, with the name of the file and the SHA-256 digest of its bytes, so that a
-result can say which sets it used; {api}`write_gmt` writes a collection back.
-
-Sets name genes by symbol, by Entrez id or by Ensembl id, and an analysis must name
-them the same way. {api}`GeneSets.restrict_to` keeps the genes of each set that an
-analysis measured, drops the sets left without genes, as limma's `ids2indices` does,
-and refuses ids that match no gene at all. {api}`GeneSets.filter_by_size` then keeps
-the sets of a range of sizes, counted within the analysis: GSEA leaves out sets of
-fewer than 15 or more than 500 genes by default, since two or three genes can make a
-small set significant [@gsea2026]. {api}`GeneSets.select` takes sets by name.
-
-## Over-representation analysis
-
-Over-representation analysis asks whether the genes called significant hold more of a
-set's genes than chance would give [@tavazoie1999]. Of a universe of $N$ genes, $n$
-are selected; a set has $K$ genes in the universe, and $k$ of them are selected. Were
-the $n$ genes drawn at random from the universe, $k$ would follow the hypergeometric
-distribution, and the p-value is the chance of an overlap at least as large:
+roast draws random rotations of the residual space and sets the observed set statistic
+against the statistics of the rotated data [@wu2010; @langsrud2005]. Each gene's
+moderated t-statistic becomes a z-score, and the statistic summarizes the z-scores of the
+set's genes, for the alternatives up, down and mixed:
-$$
-P(X \ge k) = \sum_{j=k}^{\min(K, n)}
-\frac{\binom{K}{j} \binom{N - K}{n - j}}{\binom{N}{n}},
-$$
-
-the one-sided p-value of Fisher's exact test of the table of genes in and out of the
-set, selected or not [@fisher1922; @rivals2007]. The expected overlap is $K n / N$.
-{api}`over_representation` computes it as limma's `kegga` and `goana` do [@young2010],
-for the genes that go up, those that go down and those that change either way in a
-group and contrast of a differential-expression result or in a table of results, or
-for a list of genes; the adjusted p-values are Benjamini and Hochberg's over the sets
-of each list. {api}`plot_over_representation` draws the top sets as dots for each
-list, as clusterProfiler compares lists of genes [@yu2012].
-
-The universe is the genes that could have been selected, by default the genes tested.
-The genes of the genome, or of all the sets, include genes that were never expressed
-or tested, and make the p-values too small [@timmons2015; @wijesooriya2022]. Genes of
-the universe that are in no set stay in it, as in `kegga`.
-
-The test takes genes, not samples, as the units drawn, independently of each other.
-The genes of a set are often correlated, as co-regulated genes are, so its p-values
-are too small for sets that respond together [@goeman2007], the problem that camera
-solves for a linear model [@wu2012]. The result also depends on the cutoff
-that selects the genes, and ignores how much each one changes. In RNA-seq data, long
-and highly expressed genes are called significant more often, which favours sets of
-such genes [@young2010]; the adjustment that `kegga` and `goana` offer for this is not
-implemented.
-
-## Gene set enrichment analysis
-
-Gene set enrichment analysis (GSEA) ranks all genes by a statistic, such as the moderated
-t-statistic of a contrast, and asks whether the genes of a set gather at the top or at
-the bottom of the ranking [@subramanian2005]. A walk down the ranking of $N$ genes rises
-at each of the $m$ genes of the set by its share of the set's weight and falls at each
-other gene by $1/(N - m)$; after the $i$-th gene of the set, at rank $p_i$, it stands at
-
-$$
-\mathrm{RS}_i = \frac{\sum_{j \le i} |r_j|^q}{\sum_{j = 1}^{m} |r_j|^q}
-- \frac{p_i - i}{N - m},
-$$
+- `"mean"`, the default, their mean, which makes a set significant when most of its genes
+ change in the same direction;
+- `"floormean"`, the mean with values on the wrong side of zero, or for the mixed
+ alternative small values, raised to a floor, and `"mean50"`, the mean of the larger
+ half, which detect sets in which as few as a quarter of the genes change;
+- `"msq"`, the mean square, which detects smaller shares still when their changes are
+ large, but which a few genes can drive.
-with $r_j$ the statistics of the set's genes and $q$ the exponent: 1 for the weighted
-score of GSEA, 0 for the unweighted one of @mootha2003. The enrichment score is the
-walk's largest deviation from zero, positive when the set's genes gather at the top and
-negative when they gather at the bottom, and the leading edge is the set's genes that
-the walk passes before its extreme, which drive the score. {api}`gsea` computes them as
-fgsea does [@korotkevich2021], from a group and contrast of a differential-expression
-result, a table of results or a mapping of gene id to statistic. Genes with equal
-statistics keep the order in which they are given, as in fgsea, so that the score of a
-set whose genes tie with others depends on that order; a warning says how many tie.
-
-The scores of random sets of the same size give the null distribution, so that the test
-is competitive, like camera's, and it permutes genes, not samples. Like
-over-representation analysis it treats genes as independent: the genes of a set that
-respond together make its p-values too small [@goeman2007; @wu2012]. Each score is
-divided by the mean of the random scores of its sign, so that sets of different sizes
-compare, and the p-value is the share of the random scores of its sign that reach it.
-With $n$ random sets it is at least $1/(n + 1)$; fgsea's multilevel method, the default,
-reaches far smaller ones [@korotkevich2021]. It draws random sets of the size of the set,
-keeps at each level the half with the higher scores and moves them by a Markov chain that
-keeps their scores above the level's median, so that each level about halves the
-probability, until the median reaches the observed score; `log2_error` gives the expected
-standard deviation of $\log_2 p$.
-
-The random sets come from seeds that `rng` draws, through the Mersenne Twister
-[@matsumoto1998] seeded as C++ seeds it, as fgsea draws them: given fgsea's seeds,
-{api}`gsea` gives fgsea's results exactly, and a seed gives the same results every time.
-{api}`plot_enrichment` draws the walk of one set, as fgsea's `plotEnrichment` does, and
-{api}`plot_gsea_table` the top sets with their genes on the ranking, as its
-`plotGseaTable` does.
-
-## Pathway scores per sample
-
-The tests above ask one question of a contrast. A pathway score instead turns the
-expression of a set's genes in each sample into one number, so that pathways can be
-compared across samples, drawn as a heatmap, correlated with other measurements, and
-tested with the same designs and contrasts as genes [@haenzelmann2013]. {api}`gsva`,
-{api}`ssgsea`, {api}`combined_z_scores` and {api}`plage` compute the four methods of the
-GSVA package, from a samples x genes matrix or a cell type of a cube, whose log-CPM
-after TMM normalization (or counts, for GSVA's Poisson kernel) they score over the genes
-that `filterByExpr` keeps. They return a {api}`PathwayScores`, profiles x sets, with the
-table of the profiles (the samples scored) and the genes each set used;
-{api}`PathwayScores.table` lists the scores one per row.
-
-**GSVA** first puts each gene on a common scale: the probability of a value at most as
-high under the gene's distribution across the $n$ samples, estimated by a kernel,
+If $b$ of the $B$ rotated statistics are at least as extreme as the observed one, the
+p-value is
$$
-F_{ij} = \frac{1}{n} \sum_{k=1}^{n} \Phi\!\left(\frac{x_{ij} - x_{ik}}{h_i}\right),
+p = \frac{b + 1}{B + 1}
$$
-with the bandwidth $h_i$ a quarter of the gene's standard deviation, for continuous
-values such as log-CPM, or the Poisson probability $P(Y \le x_{ij})$ with mean
-$x_{ik} + 1/2$ for counts. Each sample ranks its $p$ genes by $F_{ij}$, and each gene
-weighs $|p/2 - r_{ij}|$, so that the genes at both ends of the ranking weigh most. A walk
-down the ranking rises at the genes of a set and falls at the others, as in gene set
-enrichment analysis, and the score is the walk's largest deviation above zero minus its
-largest deviation below: high for a set whose genes are higher than usual in the sample,
-low for one whose genes are lower, and between $-1$ and $1$. GSVA estimates $\Phi$ from a
-table of its values at steps of 0.001, and {api}`gsva` does the same, so that genes are
-ranked as in GSVA.
-
-**ssGSEA** ranks the genes of each sample by their values and sums the same walk over
-all genes rather than taking its extreme, with the genes of the set weighted by their
-ranks to the power $\alpha = 1/4$ [@barbie2009]. By default every score is divided by the
-range of all the scores, over sets and samples, so that a score depends on the other
-sets and samples scored with it. **Combined z-scores** standardize each gene across
-samples and add the $k$ z-scores of a set, divided by $\sqrt{k}$ [@lee2008]. **PLAGE**
-takes the standardized values of a set's genes as a genes x samples matrix and scores
-the samples by its first right singular vector, the direction along which the genes vary
-most together [@tomfohr2005]; its sign is arbitrary, and CytePool chooses the one that
-makes the scores rise with the set's mean standardized expression.
-
-ssGSEA ranks the values themselves, so that a set of highly expressed genes scores high
-in every sample. GSVA first sets each gene against its own distribution across the
-samples, so that a set scores high where its genes are higher than usual, compared with
-the other genes of the sample. Combined z-scores and PLAGE also set each gene against
-itself across samples, but do not compare it with other genes. GSVA's guide notes that
-the scores of larger sets vary less; to test them, fit the design to the scores and let
-the prior variance follow the set sizes, as its guide does with limma-trend:
-`limma(scores, design, trend=scores.n_genes)`, with the design built for the cube.
-{api}`plot_pathway_scores` draws the scores as a heatmap, with the samples grouped by a
-variable.
-
-The scores agree with GSVA's to rounding, except where CytePool differs on purpose:
-PLAGE's sign, GSVA's walk in samples with missing values, and inputs on which GSVA
-stops with an error. A gene's rank is decided by comparing its kernel estimate with
-those of the other genes, and genes with equal estimates in exact arithmetic, such as
-genes with the same low counts in different samples, can be ordered by the last bit of
-R's probabilities: their ranks, and the scores of their sets, can then differ slightly
-from GSVA's.
-
-## Activities of regulators and pathways
-
-A transcription factor raises some of its target genes and lowers others, and a
-signalling pathway leaves a footprint of genes that respond to it. A network gives each
-such source signed weights on its targets: 1 for a target that a transcription factor
-activates and -1 for one it represses in CollecTRI's regulons [@muellerdott2023], and in
-PROGENy the z-scores with which genes responded to the perturbation of each of 14
-pathways in hundreds of experiments [@schubert2018; @holland2020b]. A source is active
-in a profile, the expression of a sample or the statistics of a contrast, when its
-targets are high where their weights are positive and low where they are negative.
-{api}`ulm`, {api}`mlm` and {api}`weighted_aggregate` score every source in every profile
-as decoupler does [@badiaimompel2022], from a samples x genes matrix, a cell type of a
-cube (its log-CPM, as for the pathway scores) or a differential-expression result, one
-profile per group and contrast, by default of its moderated t-statistics. They return an
-{api}`Activities`, profiles x sources, which {api}`limma` tests with the design built
-for the cube, as it tests pathway scores; {api}`Activities.table` lists the scores one
-per row.
-
-**ULM**, the univariate linear model, fits for each source a line through the values
-of the profile's $n$ genes against the source's weights $x_g$, zero for the genes that
-are not its targets,
-
-$$
-y_g = \beta_0 + \beta_1 x_g + \varepsilon_g,
-$$
+[@phipson2010], so it is never zero. The two-sided p-value asks whether the set's genes
+went one way, up or down, and the mixed p-value whether they changed in either direction
+gene by gene. Each rotation also gives its mirror image for the up and down
+alternatives, so their p-values count $2B$ rotations: p-values reach down to
+$1/(B + 1)$ for the two-sided and mixed alternatives and $1/(2B + 1)$ for up or down,
+0.0005 and 0.00025 with the default 1,999 rotations. roast also reports the proportion
+of the set's genes that are active in each direction, with z-scores beyond $\sqrt 2$ in
+size [@wu2010]. Contribution weights of opposite signs mark genes expected to change in
+opposite directions, as in a signature of genes up and genes down, and larger weights
+count more (`gene_weights`, or a mapping of gene to weight as the set).
+
+{api}`mroast` tests many sets with the same rotations and adjusts their p-values for the
+number of sets by Benjamini and Hochberg's method, which it applies to mid-p-values
+[@routledge1994], since they suit false discovery rates, as limma does by default. The
+rotations come from a numpy generator: a seed (`rng`) gives the same p-values every
+time.
+
+(romer)=
+## romer
-and the score is the t-statistic of the slope,
-$t = \hat\beta_1 / \mathrm{se}(\hat\beta_1) = r \sqrt{(n - 2) / (1 - r^2)}$, with $r$
-the correlation of $y$ and $x$, tested on $n - 2$ degrees of freedom. Each source is
-tested against all the other genes, those of other sources included. **MLM**, the
-multivariate linear model, fits all the sources at once, with an intercept,
-$y_g = \beta_0 + \sum_s \beta_s x_{gs} + \varepsilon_g$, so that each score, the
-t-statistic of $\beta_s$ on $n - m - 1$ degrees of freedom for $m$ sources, measures a
-source beyond what the others explain: where two regulators share targets, ULM
-credits both with their response and MLM splits it between them. MLM needs more genes
-than sources, and sources whose weights are not linear combinations of the weights of
-others; it refuses those it cannot tell apart, by name. The **weighted aggregate** is
-the weighted mean of the values of a source's targets, $\sum_g w_g y_g / \sum_g |w_g|$,
-or their weighted sum, which is PROGENy's original score [@schubert2018]. With random
-permutations of the genes (1,000 by default, as decoupler draws them), the score is
-normalized by those of the permutations, $(\mathrm{ES} - \text{mean}) / \text{sd}$, and
-its p-value doubles the smaller of the two one-sided permutation p-values
-[@phipson2010].
-
-Targets are matched to the genes' ids exactly, and genes whose values are zero in
-every profile are left out, as decoupler leaves them out. A source with fewer than
-`min_targets` targets among a profile's genes (5, decoupler's `tmin`) has no score
-there; NaN marks a missing value, so that each profile of a differential-expression
-result uses the genes its group tested. The adjusted p-values are Benjamini and
-Hochberg's over the sources of each profile [@benjamini1995]. {api}`plot_activities`
-draws the activities as a heatmap, and {api}`plot_top_activities` the sources with the
-largest scores of one profile as bars, as decoupler's `pl.barplot` draws them.
-
-The networks are a {api}`Network`: each source with its targets and weights, and the
-network's name, version, licence and citation. {api}`collectri` and {api}`progeny`
-download their files on first use from where they were published, check them against
-pinned SHA-256 digests, and keep them in a cache, so that a machine without a
-connection can use a file copied there. CollecTRI gathers twelve resources, each with
-its own terms, which OmniPath records [@tuerei2021]: `licence="commercial"`, the
-default, keeps the 39,896 interactions that a resource listed for commercial use
-supports, leaving out the 3,279 that GEREDB alone supports, which OmniPath lists for
-academic use; `licence="academic"` keeps all 43,175 (the
-[Datasets](../reference/datasets.md#networks) page gives the terms). PROGENy's weights
-are those of the R package progeny, under the Apache License 2.0, for human and mouse;
-{api}`progeny` keeps the genes with a p-value below 0.05, as decoupler does, or the
-`top` genes of each pathway, as progeny's `getModel` does, 100 by default there. Its
-genes are named by the symbols of its release; decoupler reads PROGENy through
-OmniPath, which renames some of them to their current symbols. Any network given as a
-table of `source`, `target` and `weight`, or gene sets with weights of 1, works the
-same way, and {api}`Network.select` keeps some of its sources.
-
-The scores, estimates and p-values agree with decoupler's to rounding, and PROGENy's
-weights with progeny's. CytePool differs on purpose in three places: it adjusts MLM's
-p-values, which decoupler reports unadjusted as its adjusted ones; its permutation
-p-values double the smaller tail, where decoupler's send a score near zero to $2/n$ for
-$n$ permutations; and it gives a profile whose values are all zero NaN scores, where
-decoupler leaves it out. decoupler's VIPER is not offered,
-because the licence of its reference implementation allows only non-profit academic
-use.
-
-## Choices
-
-- **Competitive or self-contained.** Rank a collection with camera; ask about one set
- chosen in advance, such as an interferon signature in an interferon stimulation, with
- fry or roast.
-- **Correlation.** camera's small default correlation ranks co-regulated sets higher; an
- estimated correlation controls the error rate strictly for each set.
-- **Rotations.** roast's smallest p-value is $1/(B+1)$; use enough rotations for the
- number of sets adjusted for.
-- **Weights.** Pass a voom result to use its precision weights; romer takes sample
- weights only.
-- **Over-representation or camera.** Over-representation analysis needs only the
- genes called significant, so it takes the results of any method and lists from
- elsewhere. camera uses every gene's statistic and allows for the correlation between
- genes, so with a linear model at hand it is the sounder competitive test.
-- **GSEA or camera.** GSEA needs only a statistic of every gene, from any method, and
- uses the whole ranking rather than a cutoff; like over-representation analysis it
- takes genes as independent, which camera does not. Set `min_size` and `max_size`, as
- GSEA's 15 and 500, to leave out sets too small to rely on and too large to mean much.
-- **Scores or tests.** Pathway scores serve analyses beyond one contrast: heatmaps,
- correlations and models with any design. For one contrast, camera, fry and GSEA test
- the genes' own statistics, where scores first reduce each set to one number per
- sample. GSVA and ssGSEA compare a set's genes with the other genes of each sample;
- combined z-scores and PLAGE follow each set across samples.
-- **Activities or scores.** Activities use the signs and sizes of a network's weights,
- so that a regulator's repressed targets count against it; pathway scores treat every
- gene of a set alike. ULM is decoupler's default for regulators and pathways alike;
- MLM separates sources whose targets overlap, provided the network gives it fewer
- sources than genes and no two sources it cannot tell apart. For a contrast, score its
- statistics; to compare samples, or to test activities with a design, score the
- samples.
+romer is a competitive test by ranks [@majewski2010]. It ranks every gene's moderated
+t-statistic among all genes, ranking $t$ for the up alternative, $-t$ for down and
+$|t|$ for mixed, and compares a set's mean rank with the mean ranks that the rotations
+give, each rotation ranking every gene again; the mean, the floor mean or the mean of the
+larger half summarizes the ranks, as for roast. Like gene set enrichment analysis it
+compares a set's genes with the other genes, but its null distribution comes from
+rotations of the residual space, not from random sets of genes. Before the rotations,
+romer shrinks each gene's contrast effect according to the gene's posterior probability
+of differential expression, so that the genes that respond strongly do not inflate the
+rotated statistics. Its p-values are $(b + 1)/(B + 1)$, at least 0.0001 with the default
+9,999 rotations, and are not adjusted for the number of sets, as in limma.
+
+(gene-set-tests-choices)=
+## Defaults and choices
+
+- **Competitive or self-contained.** {api}`camera` ranks a collection by default; for one
+ set chosen in advance, {api}`fry` or {api}`roast` asks whether it responds at all
+ ([Competitive or self-contained](#competitive-and-self-contained)).
+- **The correlation.** camera's small default correlation, 0.01, ranks highly
+ co-regulated sets, which are often the interpretable ones, higher; a correlation
+ estimated for each set (`inter_gene_correlation=None`) controls the error rate
+ strictly for every set, as limma's documentation says of the two. Negative
+ correlations count as zero unless `allow_negative_correlation=True`.
+- **fry or roast.** fry approximates the directional p-value of mroast's mean statistic
+ without rotations, so it suits many sets. Its mixed p-value comes from an
+ approximation, and roast's from the rotations themselves.
+- **The set statistic.** `set_statistic="mean"`, limma's default, asks that most genes
+ change together; `"floormean"`, `"mean50"` and `"msq"` find sets in which fewer genes
+ change.
+- **Rotations.** roast and mroast draw 1,999 rotations by default and romer 9,999, as
+ limma does. Use enough of them for the number of sets that the p-values are adjusted
+ for: limma's documentation recommends at least 9,999 for publication.
+- **Weights.** A result of limma-voom is tested with its precision weights. romer takes
+ sample weights only, as limma's does, so it tests a result of {api}`limma_trend`.
+- **The moderation.** camera, fry, roast and mroast moderate the variances as the result
+ did: with the trend of limma-trend, and robustly when the result was ([Estimating the
+ prior](#estimating-the-prior)); `trend` and `robust` change it. romer always moderates
+ them without a trend and not robustly, as limma's romer, which offers neither.
+- **Gene ids.** `id_column` names the column of a result's features that holds the ids
+ the sets use, `"feature_id"` by default.
+
+(gene-set-tests-limits)=
+## Limits and differences
+
+- **Fixed effects only.** The tests fit a linear model of fixed effects: a design with
+ random effects is refused, there is no block correlation, and a result of
+ {api}`dream_voom` is refused, as its fits are mixed models. Test a result of
+ {api}`limma_voom` with the donor as a fixed effect instead. Every value must be
+ observed, with a positive weight, so that every gene has the same residual degrees of
+ freedom.
+- **Not implemented.** fry's gene contribution weights (limma's `gene.weights`), and exact
+ z-scores and given priors in the rotation tests (`approx.zscore = FALSE`, `var.prior`,
+ `df.prior`) ([Not implemented yet](#not-implemented-yet)).
+- **Rotations.** CytePool draws its rotations from numpy, and limma from R, so their
+ p-values agree within Monte Carlo error; given limma's rotations, CytePool returns
+ limma's p-values. mroast tests every set with the same rotations, where limma draws new
+ ones for each set; each p-value is a rotation p-value either way.
+- **fry's mixed p-value of small sets.** For a set of 2 to $d$ genes, limma approximates
+ the rotation p-value with the set's own dimensions only, which can be far from it;
+ CytePool uses all $d + 1$ dimensions, as for larger sets. For larger sets the two
+ agree, and the approximation that both use can be much too small far in the tail.
+- **fry's posterior standard deviations.** limma's expected value of a gene's mean square
+ without its largest effect is slightly above the exact value that CytePool uses, which
+ moves fry's p-values by a small fraction of their value.
+- **Equal statistics in roast.** roast and mroast count a rotated statistic that equals
+ the observed one as at least as extreme, as Wu et al. and Phipson and Smyth define the
+ p-value, and as limma's romer counts it; limma's roast counts only larger ones. The two
+ differ only where a floor-mean or mean-square statistic is the smallest it can be: a
+ set with no gene on one side of zero has a one-sided p-value of 1 here and about 1/2
+ in limma, and a set whose z-scores all lie within $\pm 0.674$ has a mixed floor-mean
+ p-value of 1 here.
+- **Contribution weights.** A gene whose weight is zero is dropped from the set, and the
+ mean square and the mixed floor mean weigh each gene by the size of its weight, as the
+ paper does. limma keeps a gene of weight zero in the set; its mean-square p-values
+ change, beyond 1,000 rotations, when every weight is multiplied by the same number, and
+ with weights of both signs its up, down and two-sided ones depart from the paper's; and
+ its mixed floor mean takes a negative weight as evidence against the set.
+- **Sets without meaning.** A set without genes, or that holds every gene in camera, has
+ no p-value and no direction, and the adjustment of the others leaves it out; limma
+ reports a direction for it, and in some cases a p-value. A gene that a set lists twice
+ counts once, where limma counts it twice, and camera's rank test reports 1 where its
+ continuity correction puts both tails above 1/2 and limma reports a p-value above 1.
+- **Values that overflow.** romer's shrinkage stays finite for genes with very large
+ effects, where limma's becomes NaN beyond a log-odds of 709.8 (an absolute t above
+ about 38 with an infinite prior df) and changes the p-values of every set, and the
+ z-scores of the rotation tests stay finite for t-statistics whose square overflows.
+- **Bases of the residual space.** With observation weights each gene's residual effects
+ have a basis of their own, on which camera's estimated correlations and fry's
+ standardized statistics depend. Where a pivot of the decomposition is zero but for
+ rounding, limma leaves rounding noise in a few percent of genes, a different few in
+ camera and in fry, so that its basis there, and with it its results, can change with
+ the last bits of the data; CytePool's does not.
+- **Robust camera.** camera takes `robust`, which limma's camera does not offer.
+
+(gene-set-tests-checks)=
+## Checks and further reading
+
+Each test is compared with limma 3.62.1, run as a separate program on the same data:
+
+- camera and fry: exact (1e-10); camera with robust moderation close (1e-6); fry's
+ default standardization, with or without a trend or robust moderation, within 1e-3, as
+ it differs on purpose; fry's mixed p-values within 0.1 of a simulation of the rotation
+ test ([Validation](#validation-gene-set-tests)).
+- roast, mroast and romer: exact (1e-12) when given the rotations that limma drew,
+ mroast against limma run on each set from the same seed, and calibrated, within Monte
+ Carlo error, when each program draws its own
+ ([Validation](#validation-rotation-gene-set-tests)).
+- GMT files read and written, and sets matched to an analysis as limma's `ids2indices`
+ matches them: identical, against R 4.4.3, decoupler 2.2.0's `read_gmt` and limma
+ 3.62.1 ([Validation](#validation-gene-set-files-and-ora)).
+
+Further reading:
+
+- @goeman2007 set out the competitive and self-contained questions that a gene-set test
+ can ask, and the problems of tests that take genes as independent.
+- @wu2012 showed that gene set tests that permute genes are very sensitive to the
+ correlation between genes, and introduced camera, which allows for it.
+- @wu2010 introduced ROAST, which rotates the residuals of a linear model instead of
+ permuting samples, so that it applies to almost any design.
+- @langsrud2005 introduced rotation tests for multivariate linear models.
+- @giner2016 presented fry, which approximates roast's test of the mean without
+ rotations.
+- @majewski2010 described romer, the rotation test of the mean ranks of a set's genes.
+
+[Over-representation and GSEA](over_representation_and_gsea.md), the next chapter,
+needs no linear model.
diff --git a/docs/source/statistics/limma_voom.md b/docs/source/statistics/limma_voom.md
new file mode 100644
index 0000000..6ff6a90
--- /dev/null
+++ b/docs/source/statistics/limma_voom.md
@@ -0,0 +1,272 @@
+---
+description: Which genes change between conditions in each cell type, tested by limma-voom or limma-trend on the pseudobulk profiles of every cell type of a cube.
+---
+
+# limma-voom and limma-trend
+
+This chapter answers the question that [Normalization and
+filtering](normalization_and_filtering.md) prepared the counts for: which genes change
+between conditions in each cell type?
+{api}`limma_voom` answers it by default. It runs limma-voom [@law2014; @ritchie2015] on
+every cell type of a cube, one cell type at a time, and {api}`limma_trend` runs
+limma-trend the same way. [Genes that change in each cell
+type](../guides/genes_per_cell_type.ipynb) shows both on published data.
+
+(limma-voom-steps)=
+## What runs in each cell type
+
+Each cell type is fitted on its own, with its own samples, genes, normalization factors,
+mean-variance trend and prior, in the steps of the RNA-seq workflows that the limma and
+edgeR User's Guides describe:
+
+1. **Samples.** The samples with too few cells or counts in the cell type are left out
+ ([Samples with few cells or counts](#samples-with-few-cells)). The cell type's design
+ matrix keeps the rows of the full design matrix that belong to the remaining samples,
+ so every column keeps its meaning.
+2. **Genes.** edgeR's `filterByExpr`, given that design matrix, keeps the genes with
+ enough counts, and the library sizes become the totals over the genes kept
+ ([Filtering by expression](#filtering-by-expression)).
+3. **Normalization.** TMM factors scale the library sizes ([Normalization factors and
+ size factors](#normalization-and-size-factors)).
+4. **voom.** voom turns the counts into log-CPM values with a precision weight for each
+ value ([below](#voom)), and a weighted linear model is fitted to every gene ([Designs,
+ linear models and contrasts](linear_models.md)).
+5. **Tests.** Each contrast is estimated from the fit ([Contrasts](#contrasts)), each
+ gene's residual variance is moderated by empirical Bayes ([Empirical Bayes
+ moderation](empirical_bayes.md)), and the p-values of each contrast are adjusted by
+ Benjamini and Hochberg's method over the genes of the cell type ([Multiple
+ testing](#adjusting-across-cell-types)).
+
+In R these are `filterByExpr` and `calcNormFactors` of edgeR, then `voom`, `lmFit`,
+`contrasts.fit`, `eBayes` and `topTable` of limma, and the results agree with theirs
+([Checks and further reading](#limma-voom-checks)).
+
+With fewer samples, the design matrix of a cell type can lose rank, for instance when all
+the samples of one donor were left out. The cell type is then fitted on the columns kept
+by R's `qr()`, and each contrast is tested where its samples can estimate it; the others
+are reported ([Designs that cannot answer the question](#estimable-contrasts)). A cell
+type that cannot be fitted at all, such as one left without residual degrees of freedom,
+gets the reason in the result's summary ({api}`DifferentialExpressionResult`), and the
+other cell types are fitted as usual.
+
+(voom)=
+## voom
+
+Log counts are noisier at low counts than at high ones, and a linear model that weighs
+every observation alike gives the noisy ones too much say. voom estimates the
+mean-variance trend of the log counts and gives every observation a precision weight
+from it, so that the weighted linear model and the empirical Bayes moderation of limma
+apply to counts [@law2014].
+
+For counts $r_{gi}$ of gene $g$ in sample $i$ and effective library sizes $R_i$ (library
+sizes times normalization factors), voom works with log-CPM values, log2 counts per
+million:
+
+$$
+y_{gi} = \log_2 \frac{r_{gi} + 0.5}{R_i + 1}\, 10^6 .
+$$
+
+The design is fitted to every gene by least squares, which gives fitted values
+$\hat\mu_{gi}$ and residual standard deviations $s_g$. The square roots $s_g^{1/2}$ are
+smoothed by LOWESS ({api}`lowess`; @cleveland1979) against each gene's average log
+count, $x_g = \bar y_g + \overline{\log_2(R_i + 1)} - \log_2 10^6$, and each observation
+gets the weight
+
+$$
+w_{gi} = \operatorname{lo}(\lambda_{gi})^{-4}, \qquad
+\lambda_{gi} = \log_2\!\left(2^{\hat\mu_{gi}} (R_i + 1) / 10^6\right),
+$$
+
+the inverse of the variance that the trend predicts at its fitted log count. {api}`voom`
+returns the log-CPM values, the weights and the trend in a {api}`VoomResult`.
+
+(voom-span)=
+### The span
+
+Each local fit of the LOWESS uses a share of the genes, the span. By default the span is
+chosen from the number of genes $n$ by limma's rule ({api}`choose_lowess_span`): 1 up to
+50 genes, and
+
+$$
+\text{span} = 0.3 + 0.7\,(50 / n)^{1/3}
+$$
+
+beyond, so 0.65 at 400 genes and 0.395 at 20,000: the part of the span above 0.3 falls
+in proportion to $n^{-1/3}$, as the width of a histogram's bins does in the rules of
+@scott1979 and @freedman1981. limma's `voom` chooses the span this way by default since
+limma 3.66; its default before was 0.5, which `span=0.5` reproduces. In a cube, each cell
+type's span follows the number of its genes that pass the filter, and the cell type's
+record keeps it: in the data of @kang2018 the spans run from 0.44 in the CD4 T cells
+(6,295 genes) to 0.59 in the megakaryocytes (704 genes).
+
+(limma-trend)=
+## limma-trend
+
+limma-trend uses no precision weights. Instead, the empirical Bayes prior for the
+variances follows the mean-variance trend. {api}`limma_trend` takes the steps above, but
+the counts become log-CPM values with a prior count of 3, as the limma User's Guide
+computes them for limma-trend; every gene is fitted by unweighted least squares, or with
+sample weights when they are asked for; and the prior variance follows the genes' average
+log-CPM (`eBayes(trend = TRUE)`; [Estimating the prior](#estimating-the-prior)). The
+standard errors of contrasts are then exact, as limma's are, since every gene has the
+same weights.
+
+voom applies the trend to each observation, with its sample's library size; limma-trend
+applies it to each gene, and assumes that library sizes are not very different. limma's
+documentation says that voom should be more powerful when library sizes are very
+different, and that otherwise the two methods should give similar results (voom's help
+page; @law2014). The limma User's Guide (sections 15.4 and 15.5) says that limma-trend
+usually works well when the largest library size is not more than about three times the
+smallest. Pseudobulk library sizes grow with the number of cells pooled, which can differ
+severalfold between samples, so {api}`limma_voom` is the default.
+
+(prior-weights-from-cells)=
+## Prior weights from the number of cells
+
+A pseudobulk profile sums the counts of its cells, so its log-CPM values estimate the mean
+expression of those cells, whose variance falls as one over their number: the precision of
+a profile is proportional to its number of cells, as the dreamlet paper argues
+[@hoffman2026a]. `prior_weights="n_cells"` makes the number of cells of each sample in
+the cell type its prior weight, as dreamlet does. In {api}`limma_voom` they weight the
+fits from which voom estimates its trend, and multiply its precision weights; in
+{api}`limma_trend` they are sample weights of the linear model. Only their ratios within
+a cell type count, so weights ten times larger change nothing. The cube must record its
+cell numbers, as one from {api}`from_cells` does.
+
+(sample-quality-weights)=
+## Sample quality weights
+
+Some samples follow the model less closely than others: pooled from few cells, sequenced
+less deeply, or taken from an outlying donor. Removing them loses power, and keeping them
+adds noise. Sample quality weights down-weight them instead [@ritchie2006; @liu2015]. For
+pseudobulk data, a limma author recommends sample weights, estimated by edgeR's
+`voomLmFit`, in preference to a covariate of quality such as RNA integrity [@smyth2025].
+For gene $g$ and sample $j$, with prior weights $w_{gj}$, the model gives the mean and the
+variance
+
+$$
+E(y_{gj}) = x_j' b_g, \qquad \operatorname{var}(y_{gj}) = \sigma_g^2\, e^{u_j} / w_{gj},
+$$
+
+where the log variance factors $u = Z\gamma$ are common to all genes and sum to zero. The
+sample quality weights are $e^{-u_j}$, whose product is one. {api}`sample_quality_weights`
+estimates $\gamma$ by restricted maximum likelihood (REML; @smyth2002) or gene by gene
+[@ritchie2006], as limma's `arrayWeights` does, with a prior worth 10 genes at equal
+weights (`prior_n`) that keeps the weights near one when the evidence is weak.
+
+{api}`voom_with_quality_weights` combines them with voom as limma's
+`voomWithQualityWeights` does: voom; sample quality weights from its log-CPM values and
+weights; voom again, with those as prior weights; sample quality weights again, from the
+new trend's weights; and the new trend's weights times the new sample quality weights.
+`quality_weights=True` runs it in every cell type of {api}`limma_voom`, gene by gene, as
+`voomWithQualityWeights` does by default. {api}`limma_trend` estimates the weights from
+its log-CPM values by REML, which `arrayWeights` chooses for data without prior weights or
+missing values, and fits them as sample weights. The result's sample table keeps each
+sample's weight. Where the weights cannot be estimated, for example when fewer than two
+residual degrees of freedom remain, each is one, and a warning names the cell type.
+
+In a design that pairs two samples per donor, such as `~ donor + condition`, the two
+samples of a donor have equal and opposite residuals (up to voom's precision weights), so an
+outlying sample pulls its partner's weight down with it: limma-trend gives both samples
+the same weight, and limma-voom gives them similar ones.
+
+(limma-voom-defaults)=
+## Defaults and choices
+
+The defaults follow limma's workflow for RNA-seq, with two additions: samples with fewer
+than 10 cells in a cell type are left out, and contrasts get exact standard errors
+([Contrasts](#contrasts)).
+
+- **voom or trend.** {api}`limma_voom` by default; {api}`limma_trend` when library sizes
+ lie within about threefold of each other ([limma-trend](#limma-trend)).
+- **Samples, genes and normalization.** `min_cells`, `min_counts`, `expression_filter`
+ and `normalization` set the samples, genes and library sizes of each cell type's fit,
+ and `prior_count` the prior count of limma-trend, as [Normalization and
+ filtering](#normalization-defaults) gives them.
+- **The span**, of {api}`limma_voom`: chosen in each cell type by default ([The
+ span](#voom-span)); `span=0.5` reproduces limma's voom before limma 3.66.
+- **Sample weights.** `quality_weights=True` estimates a weight for each sample from the
+ data, and a mapping changes the settings of the estimate, such as
+ `{"method": "reml"}`; `prior_weights="n_cells"` takes the weights from the numbers of
+ cells. The two are alternatives: limma's `voomWithQualityWeights` takes no prior
+ weights, and edgeR's `voomLmFit` refuses prior weights together with estimated sample
+ weights, as {api}`limma_voom` and {api}`limma_trend` do.
+- **Hypervariable genes.** `robust=True` estimates the prior robustly, so that genes with
+ outlying variances do not distort it [@phipson2016] ([Estimating the
+ prior](#estimating-the-prior)).
+- **Contrasts of several coefficients.** Their standard errors are exact by default;
+ in {api}`limma_voom`, `contrast_standard_errors="limma"` reproduces limma's
+ approximation ([Contrasts](#contrasts)). In limma-trend the two agree.
+- **Many cell types.** p-values are adjusted within each cell type and contrast;
+ `adjust_across_groups=True` adds a column adjusted over every cell type ([Multiple
+ testing](#adjusting-across-cell-types)).
+
+(limma-voom-limits)=
+## Limits and differences
+
+- **Fixed effects only.** A design with random effects is refused. Several samples of a
+ donor are paired in the design (`~ donor + condition`), or the donor gets a random
+ effect with {api}`dream_voom` ([Mixed models and dream](mixed_models.md)). limma's
+ `duplicateCorrelation` and voom's block correlation are not implemented.
+- **No voomLmFit.** limma's documentation now recommends edgeR's `voomLmFit` over `voom`,
+ in particular to estimate a correlation within blocks or for sparse counts with many
+ zeros (voom's help page); `voomLmFit` is not implemented.
+- **Normalization.** Normalization factors are TMM factors or none; voom's
+ microarray-style normalization of log-CPM values (`normalize.method`) is not offered.
+- **Groups of samples.** {api}`limma_voom` and {api}`limma_trend` give each sample a
+ weight of its own. limma's variance models for groups of samples (`var.group`,
+ `var.design`) are offered by {api}`sample_quality_weights` and
+ {api}`voom_with_quality_weights`, on one matrix.
+- **Rank-deficient cell types.** Every contrast that a cell type's samples can estimate is
+ tested, where limma's `contrasts.fit` refuses some and changes the meaning of others
+ ([Designs that cannot answer the question](#estimable-contrasts)).
+- **Values without meaning are refused.** {api}`voom` raises an error where limma's voom
+ returns values without meaning: for a sample whose library size is zero, or where the
+ trend is not positive at a fitted value.
+- **Designs of full rank.** {api}`voom` needs a design of full column rank, where limma's
+ drops the aliased columns; {api}`limma_voom` fits each cell type on the columns it
+ keeps.
+- **Prior weights in voom's weights.** With prior weights, {api}`voom` returns the product
+ of the trend's weights and the prior weights, the weights that a fit needs; limma's
+ `voom` returns only the trend's weights, which its help page says modify the prior
+ weights.
+- **Where REML stops.** {api}`sample_quality_weights` stops REML where limma stops, which
+ can be short of the maximum of the likelihood, so that its weights are limma's; a
+ smaller `tolerance` converges further. The gene-by-gene algorithm depends on the order
+ of the genes, as limma's does.
+
+(limma-voom-checks)=
+## Checks and further reading
+
+Each step is compared with limma 3.62.1 and edgeR 4.4.0, run as separate programs on the
+same data:
+
+- voom and its span: exact (1e-10), and close (1e-6) on rarefied counts
+ ([Validation](#validation-voom)).
+- Sample quality weights, alone and with voom: close (1e-6), the tier of iterative
+ estimates ([Validation](#validation-sample-quality-weights)).
+- {api}`limma_voom` and {api}`limma_trend` on simulated cubes, with rank-deficient cell
+ types: exact, and close with robust moderation
+ ([Validation](#validation-differential-expression)); with sample quality weights, prior
+ weights, `min_counts` and adjustment across cell types: exact, and close with sample
+ quality weights ([Validation](#validation-differential-expression-options)).
+- {api}`limma_voom` on the published data of @kang2018, from the cells to the tables of 8
+ cell types: exact ([Validation](#validation-interferon-stimulated-pbmcs)).
+
+Further reading:
+
+- @law2014 introduced voom, whose precision weights let limma's linear models and
+ empirical Bayes moderation analyse RNA-seq counts.
+- @ritchie2015 described limma and its workflows for RNA-seq and microarray data.
+- @ritchie2006 introduced quality weights for the arrays of a microarray experiment, and
+ @liu2015 combined them with voom's weights for RNA-seq, which improved power.
+- @kang2018 pooled cells of 8 lupus patients, split into interferon-beta stimulated and
+ untreated cultures, and assigned cells to donors by their genotypes; the documentation
+ uses these data ([Datasets](../reference/datasets.md)).
+- @perez2022 profiled more than 1.2 million blood cells of 162 lupus cases and 99
+ controls, and found interferon-stimulated genes raised in monocytes and fewer naive
+ CD4 T cells.
+
+The next chapters model what limma-voom does not: the counts themselves ([edgeR and
+DESeq2](count_models.md)), and several samples of each donor ([Mixed models and
+dream](mixed_models.md)).
diff --git a/docs/source/statistics/linear_models.md b/docs/source/statistics/linear_models.md
index 12dc58d..e692690 100644
--- a/docs/source/statistics/linear_models.md
+++ b/docs/source/statistics/linear_models.md
@@ -1,32 +1,51 @@
---
-description: How CytePool turns a sample table and an R formula into a design matrix, fits one linear model to every gene, and tests named contrasts, with the checks that catch designs that cannot answer the question.
+description: Which comparison a design asks of the samples, how one linear model is fitted to every gene, and which contrasts of its coefficients the samples can estimate.
---
# Designs, linear models and contrasts
+[Pseudobulk profiles and cubes](cubes.md) gave every sample a profile in each cell type.
+Which genes change is then a question about those samples: which of them are compared,
+and what else differs between them. A {api}`Design` states it: the sample table, the
+fixed effects as an R formula, and named contrasts; {api}`fit_linear_model` fits it to
+every gene at once, as limma's `lmFit` does, and {api}`contrasts_fit` turns each gene's
+fit into the contrasts. {api}`limma_voom` takes these steps in every cell type, and
+[Genes that change in each cell type](../guides/genes_per_cell_type.ipynb) builds a
+paired design on published data.
+
+(designs-and-formulas)=
## The design
-A {api}`Design` joins the sample table, the fixed effects as an R formula, optional
-random effects in lme4's notation and named contrasts. The formula follows the notation
-of @wilkinson1973 as R reads it [@chambers1992]: `~ condition + batch` adds terms, `a:b`
-is an interaction, `a * b` is `a + b + a:b`, and `~ 0 + group` has no intercept.
-{api}`model_matrix` codes it as R's `model.matrix` does: a numeric column is a
-covariate, a text or boolean column is a factor coded by treatment contrasts, indicators
-of every level but the first.
-
-Columns are named as R names them: the intercept `(Intercept)` ({api}`INTERCEPT`), a
-level after its factor (`conditionstim`), interactions joined by `:`, and names that are
-not syntactic in R between backticks ({api}`quote_name`). {api}`parse_formula` expands a
-formula into its terms ({api}`FormulaTerms`), {api}`parse_random_effects` reads random
-effects ({api}`RandomEffect`), and {api}`model_matrix` returns the matrix with its
-column names ({api}`ModelMatrix`).
-
-The first level of a factor is the reference, and every coefficient of that factor is a
-difference from it. Without `levels`, the levels of a text column are sorted by code
-point, which is R's order only in the C locale; pass
-`levels={"condition": ["ctrl", "stim"]}` whenever the reference matters, which is almost
-always.
+A design starts from the sample table: one row per sample, with a `sample_id` and the
+variables of the question, such as the condition and the donor. It is matched to the
+samples of a cube by `sample_id`, never by position, so it may name only some of the
+cube's samples, and in any order.
+
+The fixed effects are an R formula, in the notation of @wilkinson1973 as R reads it
+[@chambers1992]: `~ condition + batch` adds terms, `a:b` is an interaction, `a * b`
+expands to `a + b + a:b`, `a / b` nests `b` within `a`, `(a + b + c)^2` holds the terms
+up to two-way interactions, and `~ 0 + group` has no intercept. {api}`model_matrix`
+builds the same columns as R's `model.matrix`: a numeric column is a covariate, and a
+text or boolean column is a factor, coded by treatment contrasts (an indicator for each
+level but the first), or by an indicator for every level where R codes it so, as in
+`~ 0 + group`. Columns are named as R names them: the intercept `(Intercept)`
+({api}`INTERCEPT`), a level after its factor (`conditionstim`), interactions joined by
+`:`, and names that are not syntactic in R between backticks ({api}`quote_name`).
+{api}`parse_formula` expands a formula into R's terms ({api}`FormulaTerms`) and
+{api}`model_matrix` returns the matrix with its column names ({api}`ModelMatrix`); their
+entries in the API reference give the grammar and the coding in full.
+
+In a design with an intercept, the first level of a factor is its reference, and every
+coefficient of the factor is a difference from it. Without `levels`, the levels of a
+pandas categorical follow its categories, and those of a text column are sorted by code
+point; pass `levels={"condition": ["ctrl", "stim"]}` whenever the reference matters,
+which is almost always. Random effects,
+written in lme4's notation as in `random="(1 | donor)"` [@bates2015] and read by
+{api}`parse_random_effects` into {api}`RandomEffect` terms, are for mixed models
+([Mixed models and dream](mixed_models.md)); the models of this chapter have fixed
+effects.
+(one-model-per-gene)=
## One model for every gene
Every gene $g$ of a samples x genes matrix of log-expression values is fitted with the
@@ -36,46 +55,180 @@ $$
E(y_g) = X a_g, \qquad \operatorname{var}(y_g) = \sigma_g^2\, W_g^{-1/2} R\, W_g^{-1/2},
$$
-where $W_g$ holds the gene's precision weights (from voom, or sample weights) and $R$ is
-the identity, or the correlation matrix of a known correlation between the samples of a
-block [@smyth2005]. {api}`fit_linear_model` returns a {api}`LinearModelFit` with, for
-every gene, the coefficients $\hat a_g$, the residual variance $s_g^2$ on $d_g$ residual
-degrees of freedom, and the unscaled standard errors $u_{gk}$, so that the standard
-error of coefficient $k$ is $u_{gk} s_g$. A gene with missing values, or with weights of
-its own, is fitted to its observed samples by a QR decomposition of its own
-[@golub2013]; missing values and zero weights lower its $d_g$.
+where $W_g$ holds the gene's weights, such as voom's precision weights or sample weights,
+and $R$ is the identity, or the correlation matrix of a known correlation between the
+samples of a block, such as those of one donor [@smyth2005]. {api}`fit_linear_model`
+returns a {api}`LinearModelFit` with, for every gene, the coefficients $\hat a_g$, the
+residual variance $s_g^2$ on $d_g$ residual degrees of freedom, and the unscaled standard
+errors $u_{gk}$, so that the standard error of coefficient $k$ is $u_{gk} s_g$. When no
+value is missing and the weights, if any, are positive sample weights, one factorization
+of the design serves every gene. A gene with missing values, or with weights of its own,
+is fitted to its observed samples by a QR decomposition of its own [@golub2013]: missing
+values and zero weights lower its $d_g$, and a coefficient that its remaining samples
+cannot estimate is NaN.
+(contrasts)=
## Contrasts
-A contrast is a combination $c$ of the coefficients, such as the difference between two
-groups. {api}`make_contrasts` reads named expressions such as `"groupB - groupA"` or
-`"(groupB + groupC)/2 - groupA"` written in the design's column names, as limma's
-`makeContrasts` does [@ritchie2015], into {api}`Contrasts`, a mapping of contrast names
-to weights; {api}`Contrasts.select` takes some of them by name, and
-{api}`Design.with_contrasts` returns the design with other contrasts.
-{api}`contrasts_fit` re-expresses each gene's fit: the estimate is $c' \hat a_g$ and its
-unscaled variance $c' V_g c$, with $V_g$ the gene's unscaled covariance matrix.
-Contrasts are matched to the coefficients by name, never by position.
-
-With weights that differ between genes, limma's `contrasts.fit` approximates the
-standard errors of contrasts of correlated coefficients from the covariance of the
-unweighted design. CytePool computes them exactly by default; `method="limma"`
-reproduces the approximation.
+A contrast is a combination $c$ of the coefficients that answers one question, such as
+the difference between two groups. {api}`make_contrasts` reads named expressions, as in
+`"groupB - groupA"` and `"(groupB + groupC)/2 - groupA"`, written in the design's column
+names, as limma's `makeContrasts` does [@ritchie2015], into {api}`Contrasts`, a mapping
+of contrast names to weights; {api}`Contrasts.select` takes some of them by name, and
+{api}`Design.with_contrasts` gives the design other contrasts. {api}`contrasts_fit`
+re-expresses each gene's fit: the estimate is $c' \hat a_g$, with the unscaled variance
+$c' V_g c$, where $V_g$ is the gene's unscaled covariance matrix. Weights that name their
+coefficients, such as a {api}`Contrasts` or a pandas Series, are matched to them by name;
+a plain vector or matrix is read in the order of the coefficients.
+
+When one factorization serves every gene, $V_g$ is the same for every gene. Otherwise
+each gene has its own, and limma's `contrasts.fit` approximates the standard error of a
+contrast from the gene's own standard errors and the correlations of the coefficients
+in the design without weights. The approximation is exact for contrasts of uncorrelated
+coefficients, such as the group means of a one-way design, and otherwise depends on how
+the design is written. In `~ condition + age` with three conditions, stimulated against
+control is one coefficient when control is the reference level and the difference of
+two coefficients when another level is, and its approximate standard error changes with
+the choice. CytePool
+computes the standard errors exactly by default, from each gene's own $V_g$, so that
+every parametrization of a design gives the same tests; `method="limma"` reproduces
+limma's approximation, which can be far from the exact value
+([Validation](#validation-linear-models) compares them).
+
+(paired-design)=
+## A paired design
+When every donor gives a sample in each condition, the design `~ donor + condition`
+gives each donor a coefficient of its own, so that the conditions are compared within
+donors and the differences between donors do not count as noise; `conditionstim` then
+estimates the change that the stimulation makes within a donor. A variable that is
+constant within each donor, such as sex, age or disease, cannot be tested in this
+design: its column is a combination of the donor columns, and {api}`Design.check`
+reports it as aliased and confounded with the donor. A random effect for the donor
+tests such a variable ([Mixed models and dream](mixed_models.md)), and so does a
+comparison of the profiles pooled by donor ({api}`Cube.collapse_samples`), when every
+donor was sampled alike.
+
+(estimable-contrasts)=
## Designs that cannot answer the question
-A contrast is estimable when it is a combination of the rows of $X$, that is, when it is
-orthogonal to the null space of $X$ [@searle1971]. In a design of full rank every
-contrast is. Rank deficiency arises when a column is a combination of others, decided as
-R's `qr()` decides it: for example when two factors group the samples alike, or, within
-one cell type, when every sample of a donor was left out. {api}`limma_voom` then fits
-the column space of the group's design and tests only the contrasts its samples can
-estimate, reporting the others.
-
-{api}`Design.check` reports such problems before anything is fitted, as a
-{api}`DesignReport` of {api}`DesignIssue` entries ({api}`DesignReport.kinds` lists their
-kinds), each with what to do: aliased columns, no residual degrees of freedom,
-confounded factors, empty levels, levels with one sample, samples fitted exactly
-(leverage 1, the diagonal of the hat matrix; @hoaglin1978), numeric codes that look like
-batches, labels that look like missing values, contrasts that cannot be estimated, and
-random effects whose variances cannot be estimated.
+A contrast is estimable when the samples determine it: when it is a combination of the
+rows of $X$, or equivalently orthogonal to the null space of $X$ [@searle1971]. In a
+design of full rank every contrast is. A design loses rank when a column is a
+combination of others, decided as R's `qr()` decides it, which calls a column aliased
+when its part orthogonal to the columns before it is at most $10^{-7}$ of its length.
+That happens when two factors group the samples alike, when a variable is constant
+within the levels of another, or, in one cell type, when every sample of a donor was
+left out.
+
+When a cell type's design loses rank, {api}`limma_voom` fits it on the columns kept by
+R's `qr()`, which are the ones limma's `lmFit` keeps, and tests each contrast that the
+cell type's samples can estimate. The estimate and its exact standard error are then
+those of any full-rank parametrization of the same model, even for a contrast that weighs
+a column the fit left out. A contrast that the samples cannot estimate is not computed,
+and the result names the relations it violates. limma does otherwise on both counts. Its
+`contrasts.fit` stops with an error for a contrast that weighs a column it left out, and
+so for every contrast passed with it, which in limma calls for a full-rank
+parametrization; and it computes a contrast that is not estimable from the columns it
+kept, whose meaning has then changed. With every sample of the reference donor D1 left
+out of `~ donor + condition`, limma reports for `donorD2`, donor D2 against D1, the
+difference between donor D2 and the last donor
+([Validation](#validation-differential-expression)). A cell type left without residual
+degrees of freedom, or without a contrast it can estimate, is not fitted, and the result
+says why ([What runs in each cell type](limma_voom.md#limma-voom-steps)).
+
+(design-checks)=
+## Design checks
+
+Most problems of a design can be found before anything is fitted. {api}`Design.check`
+returns a {api}`DesignReport` of {api}`DesignIssue` entries, whose messages say what to
+change, and {api}`DesignReport.kinds` lists their kinds. An error means that something
+the analysis needs cannot be estimated, as when columns are aliased, factors are
+confounded, a level is empty, no residual degrees of freedom are left, or the variance of
+a random effect cannot be estimated. A warning marks a design that fits but may not mean
+what was intended: a level with one sample; a sample fitted exactly, or nearly so, by the
+model, with a leverage (its diagonal element of the hat matrix [@hoaglin1978]) of 1 or
+above 0.99; a numeric column that looks like codes of batches or ids of donors, which
+would enter the model as one slope; or labels that look like missing values, or that
+differ in nothing but case or spaces and so split one group in two.
+[The designs page](/api/designs.html) of the API reference lists every check.
+
+(linear-models-defaults)=
+## Defaults and choices
+
+- **The reference level.** The first level of each factor: in the order that `levels`
+ gives, or else that of a pandas categorical's categories, or else by code point
+ ([The design](#designs-and-formulas)).
+- **Group means or differences.** `~ 0 + group` estimates the mean of each group, and
+ its contrasts compare them, as `groupB - groupA` does; `~ group` estimates the
+ reference group and the differences from it. With exact standard errors, both give
+ the same tests of the same comparison ([Contrasts](#contrasts)).
+- **Pairing or a random effect.** The paired design suits questions within donors; a
+ random effect suits variables of the donors, and donors with unequal numbers of
+ samples ([Mixed models and dream](mixed_models.md)).
+- **Weights.** Sample weights, one per sample, or observation weights, one per sample
+ and gene, such as voom's; a weight of zero leaves an observation out of the fit.
+- **A block correlation.** `block` and `correlation` fit a known correlation between
+ the samples of each block; with a correlation of at least zero, that is a random
+ intercept whose correlation is given rather than estimated.
+- **Contrast standard errors.** Exact by default; `method="limma"` reproduces limma's
+ approximation ([Contrasts](#contrasts)).
+
+(linear-models-limits)=
+## Limits and differences
+
+- **Formulas that R reads otherwise.** Three forms whose R reading is seldom what was
+ meant are refused: a `-` that removes nothing, as in `~ a - b`; `^` of a single term,
+ as in `~ x^2`, which R reads as `~ x`; and a fractional power, which R truncates.
+ Function calls such as `log(age)`, `I(age^2)` or `factor(w)` are refused too: add a
+ column of the computed values to the sample table instead.
+- **Level order.** Text levels are sorted by code point in every locale. R sorts them
+ by the collation of its session's locale, which gives the same order only with
+ `LC_COLLATE=C`.
+- **Contrasts that are not linear.** {api}`make_contrasts` accepts linear combinations
+ of columns only. limma's `makeContrasts` evaluates any expression, and turns
+ `groupB - 1`, products, ratios and powers into weights without meaning.
+- **Contrasts by name.** limma's `contrasts.fit` reads the rows of the weights in their
+ order, and merely warns when their names do not match the coefficients'
+ ([Contrasts](#contrasts)).
+- **Coefficients the data do not determine.** A coefficient that a gene's remaining
+ samples cannot estimate is NaN ([One model for every gene](#one-model-per-gene)).
+ limma instead drops whichever collinear column comes later and reports the rest, which
+ may include coefficients that the data do not determine.
+- **Rank and random effects.** {api}`fit_linear_model` needs a design of full column
+ rank, and refuses random effects, which it would drop, except one random intercept that
+ `block` and `correlation` give; the message says what fits the design. limma's
+ `duplicateCorrelation`, which estimates the correlation of the blocks, is not
+ implemented.
+- **Memory.** A fit with observation weights or missing values keeps a covariance
+ matrix for every gene ({api}`fit_linear_model` gives its size).
+- **Ordered factors.** An ordered pandas categorical is coded as an unordered factor,
+ by treatment contrasts, where R would use polynomial contrasts.
+
+(linear-models-checks)=
+## Checks and further reading
+
+Designs and fits are compared with R and limma, run as separate programs on the same
+data:
+
+- Model matrices, R's terms, the rank and aliased columns of designs, leverages and the
+ estimability of contrasts: R 4.4.3, exact (identical, and 1e-10; 1e-8 absolute for the
+ leverages of badly scaled designs); named contrasts: limma 3.62.1; random-effect
+ terms: lme4 2.0.6 ([Validation](#validation-design)).
+- {api}`fit_linear_model` and {api}`contrasts_fit`: `lmFit` and `contrasts.fit` of
+ limma 3.62.1, exact (1e-10) ([Validation](#validation-linear-models)).
+
+Further reading:
+
+- @wilkinson1973 introduced the symbolic description of factorial models from which
+ the formulas of S and R grew, and @chambers1992 described the formulas and the coding
+ of factors of S, which R follows.
+- @smyth2004 set out limma's approach to experiments with few samples: one linear model
+ fitted to every gene, whose contrasts are tested by moderated statistics.
+- @searle1971 gave the theory of linear models of less than full rank, and of the
+ functions of their coefficients that the data can estimate.
+- @hoaglin1978 described the hat matrix, whose diagonal gives the leverage of each
+ sample.
+
+[Empirical Bayes moderation](empirical_bayes.md) makes the tests of these models hold
+with few samples.
diff --git a/docs/source/statistics/meta_analysis.md b/docs/source/statistics/meta_analysis.md
index 9d43838..d36b6ab 100644
--- a/docs/source/statistics/meta_analysis.md
+++ b/docs/source/statistics/meta_analysis.md
@@ -1,279 +1,150 @@
---
-description: Fixed-effect and random-effects meta-analysis of every gene across studies, as metafor computes it, with the between-study variance, Cochran's Q, I-squared and the Knapp-Hartung adjustment; combined p-values, partial conjunction tests of replication, Storey's pi0 and pi1, and how well studies replicate each other; SumRank, which asks that a gene rank high in most studies; and the prediction of cases in held-out studies.
+description: What several studies say together about each gene, by a fixed-effect or random-effects meta-analysis of their estimates, with the between-study variance and the heterogeneity of the studies.
---
# Meta-analysis
-A single study often finds differences that others do not reproduce: of the genes called
-differentially expressed in Alzheimer's disease in one dataset, over 85% were not
-reproduced in any of 16 others [@nakatsuka2025]. A meta-analysis combines the estimates
-of several studies into one, weighs each by its precision, and measures how much the
-studies disagree. {api}`fit_meta_analysis` does it for every gene at once, as metafor's
-`rma.uni` does for one effect without moderators [@viechtbauer2010], and
-{api}`meta_analysis` does it for the results of {api}`limma_voom` or {api}`limma_trend`
-on several cubes, kept together as a {api}`StudyResults` and matched by cell type,
-contrast and gene id. {api}`fit_meta_analysis` returns a {api}`MetaAnalysisFit`, and
-{api}`meta_analysis` a {api}`MetaAnalysisResult`, with a table for every cell type and
-contrast ({api}`MetaAnalysisResult.table`). Where the studies give p-values rather than
-comparable effects, or to ask in how many studies a gene changes, their p-values are
-combined, and {api}`replication` says how well the studies replicate each other (see
-[](#combined-p-values) and below). The guides
-[Combine studies](../guides/combine_studies.ipynb) and
-[Replication across studies](../guides/replication_across_studies.ipynb) run them on six
-simulated cohorts whose true changes are known.
-
+[Aligning studies](aligning_studies.md) analysed each study on its own and lined up the
+results gene by gene. A meta-analysis combines, for every gene, the studies' estimates of
+one effect into one, weighs each study by its precision, and measures how far the studies
+disagree. {api}`meta_analysis` does it for the results of {api}`limma_voom` or
+{api}`limma_trend` on several studies, a {api}`StudyResults`, as metafor's `rma.uni` does
+for one effect without moderators [@viechtbauer2010], and {api}`fit_meta_analysis` does it
+for arrays of effects and standard errors, into a {api}`MetaAnalysisFit`. [Align and combine
+studies](../guides/combine_studies.ipynb) runs it on six simulated cohorts whose true
+changes are known.
+
+(meta-analysis-model)=
## The model
Study $i$ estimates a gene's effect by $y_i$, with a standard error $s_i$ whose square,
-$v_i$, is taken as known. In the random-effects model the true effects of the studies
-vary around a mean $\mu$ with the between-study variance $\tau^2$ [@dersimonian1986]:
+$v_i$, the analysis takes as known. The random-effects model lets the true effects of the
+studies vary around a mean $\mu$, with the between-study variance $\tau^2$
+[@dersimonian1986]:
$$
y_i = \mu + u_i + e_i, \qquad u_i \sim N(0, \tau^2), \qquad e_i \sim N(0, v_i).
$$
-With the weights $w_i = 1 / (v_i + \tau^2)$, $\hat\mu = \sum w_i y_i / \sum w_i$ with
-standard error $1 / \sqrt{\sum w_i}$. The fixed-effect model, one true effect for every
-study, is the case $\tau^2 = 0$: the inverse-variance weighted mean.
+Each study weighs $w_i = 1 / (v_i + \tau^2)$ in the estimate
+$\hat\mu = \sum_i w_i y_i / \sum_i w_i$, whose standard error is $1 / \sqrt{\sum_i w_i}$.
+The fixed-effect model, one true effect for every study, is the case $\tau^2 = 0$: the
+inverse-variance weighted mean (metafor's equal-effects model). {api}`meta_analysis` takes
+each study's log2 fold changes and standard errors, matched by cell type, contrast and gene
+id, and returns a {api}`MetaAnalysisResult` with a table for every cell type and contrast
+({api}`MetaAnalysisResult.table`). {api}`plot_forest` draws one gene's estimate in every
+study with the pooled estimate, as metafor's `forest` does [@lewis2001].
+(between-study-variance)=
## The between-study variance
+`method` chooses the estimator of $\tau^2$, over the $k$ studies that tested a gene:
+
- `"reml"`, metafor's default: the restricted maximum likelihood estimate
- [@viechtbauer2005]. Where the restricted likelihood has two maxima, as when an
- imprecise study lies far from precise ones that agree, CytePool takes the larger one;
- metafor's Fisher scoring can stop at the smaller one.
+ [@viechtbauer2005]. Where the restricted likelihood has two maxima, as when an imprecise
+ study lies far from precise ones that agree, the estimate is the maximum with the higher
+ likelihood.
- `"dl"`: DerSimonian and Laird's method of moments,
- $\max(0, (Q - (k - 1)) / (\sum w_i - \sum w_i^2 / \sum w_i))$ with $w_i = 1 / v_i$.
-- `"pm"`: Paule and Mandel's estimate [@paule1982], at which the generalized Q statistic
- equals its expectation $k - 1$.
-- `"fixed"`: $\tau^2 = 0$.
-
+ $\max(0, (Q - (k - 1)) / (\sum_i w_i - \sum_i w_i^2 / \sum_i w_i))$, for $w_i = 1 / v_i$
+ and Cochran's $Q$ below.
+- `"pm"`: Paule and Mandel's estimate [@paule1982], the $\tau^2$ that makes the
+ generalized $Q$ statistic, $\sum_i w_i (y_i - \hat\mu)^2$, equal to its expectation,
+ $k - 1$, or zero where none does.
+- `"fixed"`: $\tau^2 = 0$, the fixed-effect model.
+
+(heterogeneity)=
## Tests and heterogeneity
The default z-test refers $\hat\mu / \mathrm{SE}$ to the normal distribution.
-`test="knapp_hartung"` multiplies the standard error by $\sqrt{q}$, with
-$q = \sum w_i (y_i - \hat\mu)^2 / (k - 1)$, and uses a t distribution on $k - 1$ degrees
+`test="knapp_hartung"` multiplies the standard error by $\sqrt{q}$, where
+$q = \sum_i w_i (y_i - \hat\mu)^2 / (k - 1)$, and uses a t distribution on $k - 1$ degrees
of freedom [@hartung1999; @sidik2002; @knapp2003], which metafor's documentation
-recommends for few studies.
+recommends for few studies. Where the studies agree more closely than their standard
+errors imply, $q < 1$, and the adjusted standard error is the smaller.
Cochran's $Q = \sum (y_i - \hat\mu_{\text{fixed}})^2 / v_i$ tests whether the studies
agree [@cochran1954]. $I^2$ is the share of the variation that comes from heterogeneity
rather than sampling error, and $H^2$ the ratio of the total to the sampling variance
-[@higgins2002]; CytePool reports $I^2$ as a proportion, where metafor reports a
-percentage.
-
-## Genes that some studies did not measure
-
-NaN marks a study that did not measure or test a gene. Each gene is analysed over the
-studies that measured it; {api}`meta_analysis` analyses a gene tested by at least
-`min_studies` studies (two by default, since one study says nothing about $\tau^2$). It
-takes limma's moderated standard errors as the known sampling errors.
-
-A gene missing from a study's results was not measured there, or was measured and left
-out, as by the expression filter, which are different findings. For the studies of a
-{api}`CubeCollection`, each analysed on its aligned cube by {api}`analyse_each_study`,
-{api}`StudyResults.table` lines up the results of one cell type and contrast with a row
-for every study and gene, which says whether the study tested the gene and, where it did
-not, why: it did not measure the gene, measured it and left it out, or did not analyse
-the cell type or the contrast.
-
-(combined-p-values)=
-## Combined p-values
-
-{api}`combine_p_values` combines, for every gene, the p-values $p_1, \dots, p_k$ of the
-$k$ studies that tested it into one p-value of the hypothesis that no study has an
-effect, as scipy's `combine_pvalues` and the R package metap compute them; it returns a
-{api}`CombinedPValues`. With $p_{(1)} \le \dots \le p_{(k)}$ in order:
-
-| Method | Statistic | Its distribution without effects |
-| --- | --- | --- |
-| `"fisher"` [@fisher1932] | $X = -2 \sum \log p_i$ | $\chi^2_{2k}$, upper tail |
-| `"stouffer"` [@stouffer1949; @liptak1958] | $Z = \sum w_i z_i / \sqrt{\sum w_i^2}$, $z_i = \Phi^{-1}(1 - p_i)$ | standard normal, upper tail |
-| `"tippett"` [@tippett1931] | $p_{(1)}$ | $1 - (1 - p_{(1)})^k$ |
-| `"wilkinson"` [@wilkinson1951] | $p_{(r)}$ | beta with parameters $r$ and $k - r + 1$ |
-| `"pearson"` [@pearson1933] | $X = -2 \sum \log (1 - p_i)$ | $\chi^2_{2k}$, lower tail |
-| `"mudholkar_george"` [@george1983] | $\sum \log((1 - p_i)/p_i)$, scaled | Student's t on $5k + 4$ degrees of freedom |
-| `"simes"` [@simes1986] | $\min_j k\, p_{(j)} / j$ | the p-value itself |
-
-Fisher's and Tippett's methods respond to the strongest study; Stouffer's, the logit and
-Pearson's to evidence that the studies share. Stouffer's weights are equal by default;
-the square roots of the studies' sample sizes are the usual choice [@zaykin2011]. A
-small p-value of Wilkinson's maximum ($r = k$) says that every study has some evidence,
-not that every study has an effect: that is the partial conjunction below. A gene that
-some studies did not test is combined over the others, and `n_studies` says how many.
-
-The methods are defined for one-sided p-values, for one direction of effect. Two-sided
-p-values combined as they are test whether some study has an effect in either
-direction, and effects in opposite directions do not cancel. `directions`, the signs of
-the studies' effects, such as their log2 fold changes, turns two-sided p-values into
-one-sided ones, $p/2$ for an effect above zero and $1 - p/2$ below, as metap's
-`two2one`; each direction is combined, and the p-value is
-$\min(1, 2 \min(p_\text{up}, p_\text{down}))$ [@owen2009], which for Stouffer's method
-is the two-sided p-value of $Z$. The one-sided p-value near 0 is kept exactly, so
-p-values near 1e-300 keep their digits. p-values of 0 and 1 are combined as the limits
-they are, where metap leaves them out; one study gives its own p-value.
-
-## Replicated in at least u studies
-
-One study's effect makes the global hypothesis false, so a small combined p-value does
-not say that an effect replicates. {api}`partial_conjunction` tests the hypothesis that
-fewer than $u$ of the $n$ studies that tested a gene have an effect by combining its
-$n - u + 1$ largest p-values, $p_{(u)}, \dots, p_{(n)}$ [@benjamini2008]: by Simes's
-method, $\min_{i \ge u} (n - u + 1)\, p_{(i)} / (i - u + 1)$, or Fisher's or Stouffer's
-(unweighted) on $n - u + 1$ p-values. For $u = 1$ it is the global test, and for
-$u = n$ every method gives $p_{(n)}$. With `directions`, the effects must share their
-direction. Benjamini and Hochberg's adjustment of these p-values over the genes
-controls the false discovery rate of the claims that a gene has an effect in at least
-$u$ studies [@benjamini2008].
-
-## The proportion of true nulls and pi1
-
-Among $m$ tests, null p-values are uniform, so the p-values at least $\lambda$ hold
-about $\pi_0 m (1 - \lambda)$ null ones, and
-$\hat\pi_0(\lambda) = \#\{p \ge \lambda\} / (m (1 - \lambda))$ estimates the
-proportion of true nulls [@storey2002]. {api}`proportion_of_true_nulls` gives it as
-qvalue's `pi0est` does, with its defaults and options, and returns a
-{api}`TrueNullProportion`: at one $\lambda$; smoothed over
-$\lambda = 0.05, 0.10, \dots, 0.95$ by a cubic smoothing spline with 3 degrees of
-freedom, read at the largest $\lambda$ [@storey2003]; or at the $\lambda$ that
-minimizes a bootstrap estimate of its mean squared error [@storey2004]. The details that
-qvalue's documentation leaves open were found by running it: it counts the p-values at
-least $\lambda$, where the paper counts those above; its smoother is R's
-`smooth.spline`, whose search for the smoothing parameter CytePool follows step by step;
-and its bootstrap measures the bias from the 10% quantile of the
-$\hat\pi_0(\lambda)$. $\pi_1 = 1 - \pi_0$ of one study's p-values among the genes
-another study finds is the share of them with an effect in it, which counts effects too
-small for the second study to find on its own [@storey2003].
-
-## Replication across the studies of a collection
-
-{api}`replication` reads one cell type and contrast of the studies' results, a
-{api}`StudyResults`, lined up by {api}`StudyResults.table`, and returns a
-{api}`ReplicationResult` with four tables:
-
-- `features`: for every gene, the studies that tested it and find it (adjusted p-value
- below `alpha`), up and down; the combined p-value, by Stouffer's method weighted by
- the square roots of the studies' samples and signed by their log2 fold changes; the
- partial conjunction p-value of an effect in at least `at_least` studies in one
- direction (Simes's by default), both adjusted over the genes; the combined direction
- as a sign and the share of the studies whose effect has it; and Cochran's $Q$, $I^2$
- and $\tau^2$ of its log2 fold changes, from {api}`fit_meta_analysis`;
-- `study_pairs`: for every ordered pair of studies, $\pi_1$ and the sign agreement of
- the genes the first finds in the second, and how many the second finds too. $\pi_0$
- is estimated at $\lambda = 0.5$ by default: on the few dozen p-values of the genes
- one study finds, qvalue's smoother, which reads the largest $\lambda$, often gives
- $\pi_1$ of 0 or 1 (`null_proportion={}` asks for it);
-- `found_in`: the genes that at least $k$ studies find, and in one direction;
-- `studies`: each study's samples, weight, and the genes it tested and finds.
-
-Most genes that one study finds are found by no other [@nakatsuka2025], and these
-tables say how far that holds. {api}`plot_study_agreement` draws $\pi_1$ or the sign
-agreement of every pair of studies, and {api}`plot_replicated_genes` the genes that
-each number of studies finds.
-
-## SumRank: genes that rank high in most studies
-
-A meta-analysis weighs each study by its precision, so a gene with a very small p-value
-in one or two studies can pass without any sign of change in the others; such genes
-predicted case status poorly in studies left out [@nakatsuka2025]. SumRank asks instead
-that a gene rank high in most studies. {api}`fit_sumrank` computes it from a signed
-statistic of every gene in every study and returns a {api}`SumRankFit`; {api}`sumrank`
-computes it from the results of one cell type and contrast of the studies of a
-collection, a {api}`StudyResults`, and returns a {api}`SumRankResult`, whose table
-({api}`SumRankResult.features`) gives each gene's statistics and p-values, up and down,
-and whose table of studies ({api}`SumRankResult.studies`) says which studies tested the
-cell type and contrast, and why the others did not.
-
-Each study ranks the genes that every study tested by its signed $-\log_{10}$
-p-value, from $r = 1$ for the gene most up to $r = G$; tied genes share their average
-rank. The normalized rank $u = (r - 1) / (G - 1)$ runs from 0 to 1, and a gene's
-statistic sums its $m$ smallest normalized ranks over the $k$ studies, those of the
-studies that rank it best, capped at their mean under the null:
-
-$$
-S_g = \min\Big(\sum_{j=1}^{m} u_{g(j)},\ \frac{m}{2}\Big), \qquad
-m = \lfloor p\,k \rfloor,
-$$
-
-with $u_{g(1)} \le \dots \le u_{g(k)}$ and the proportion $p$ of the studies
-(`proportion_of_studies`), or $m$ given itself (`n_studies_summed`). The paper sums
-every study when there are few, and chose $p$ by prediction in held-out studies when
-there are many (0.65 of 17 studies of Alzheimer's disease, 0.55 of 16 of COVID-19).
-Genes down have the statistic of the ranks $1 - u$. A gene that some study did not test,
-because it did not measure it or its expression filter left it out, is not ranked, as
-the paper ranks the genes every study measured: a rank says where a gene stands among
-the genes ranked with it.
-
-If each study ranked the genes at random, the normalized ranks would be independent
-uniform variables, and $S_g$ below the cap would follow the Irwin-Hall distribution of a
-sum of $m$ of them [@irwin1927; @hall1927], whose distribution function gives the
-Irwin-Hall p-value. With every study summed it is exact under that null; the sum of the
-best $m$ of $k$ ranks is smaller than a sum of $m$ ranks, so with $m < k$ these p-values
-are much too small: in six simulated studies without changes, summing three, 37% of them
-fell below 0.05, and {api}`sumrank` warns when it gives them. The paper takes its
-p-values from permutations instead: each study's labels are permuted and its analysis
-run again, and the statistics of every gene in every permutation make one null
-distribution, $N$ statistics in all, against which a gene of statistic $s$ has the
-p-value
-
-$$
-p = \frac{1 + \#\{\text{null statistics} \le s\}}{1 + N},
-$$
-
-never zero [@phipson2010]. Pooling over the genes gives small p-values from few
-permutations, 100 permutations of 10,000 genes reaching $10^{-6}$. With
-`n_permutations`, {api}`sumrank` permutes the variable whose levels the contrast
-compares (or `permute`) in each study's design among the samples of its fit of the cell
-type, from a seeded `numpy.random.Generator`, and re-runs the study's {api}`limma_voom`
-or {api}`limma_trend` with the design and settings its result records: 0.21 s per study
-and permutation for 40 samples and 10,000 genes with limma-voom, so that 100
-permutations of 20 such studies take about 7 minutes on a laptop. {api}`plot_sumrank`
-draws every gene's p-value, the genes in alphabetical order, as the authors of SumRank
-draw it.
-
-Where CytePool departs from the paper and its authors' code, which the validation runs:
-
-- The Irwin-Hall p-value is the lower tail of the distribution. The paper calls its
- Irwin-Hall p-values two-sided, which would double them, and its code takes the
- Irwin-Hall density at the statistic, which is not a probability; both order the genes
- as the statistic does, so the permutation p-values do not depend on it.
-- Permutation p-values count ties as at least as extreme and add one. The authors' code
- takes the share of null statistics strictly below a gene's, which is 0 for a gene
- beyond every one of them; its sums are formed in floating point, so that equal
- statistics can compare as unequal, where CytePool forms them exactly.
-- The labels are permuted among the samples of each study's fit, which keeps its
- numbers of cases and controls; the paper permutes them among all the individuals of a
- dataset and draws again until every cell type has two cases and two controls.
-
-## Prediction in held-out studies
-
-How well genes found in some studies replicate can be measured in a study that had no
-part in finding them. {api}`held_out_prediction` leaves each study of a
-{api}`StudyResults` out in turn, chooses genes up and genes down in each cell type from
-the other studies' results, by SumRank and by the random-effects meta-analysis above
-with REML and the Knapp-Hartung adjustment (the paper's comparison), scores every case
-and control of the held-out study, the two levels that the contrast compares, and
-reports the area under the ROC curve of cases against controls [@hanley1982], by study
-and method, in a {api}`HeldOutPrediction` ({api}`HeldOutPrediction.auc`). A sample's
-score is UCell's [@andreatta2021]: in each cell type, the ranks of the chosen genes
-among all the genes of the sample's profile, scaled to [0, 1] over the study's samples,
-the genes up counted for the sample and the genes down against it, summed over the cell
-types, as the authors' code sums them. The genes chosen and the scores are in
-{api}`HeldOutPrediction.features` and {api}`HeldOutPrediction.scores`, and
-{api}`plot_held_out_auc` draws the AUC of every study by method. The paper fits a
-logistic regression on the score and lets pROC choose the direction of each study's AUC
-[@robin2011], so that a score that ranks controls above cases also scores above 0.5;
-here cases must score higher, and an AUC below 0.5 says the genes predict the wrong
-direction.
-
-## Limits
-
-The studies are taken as independent, with known sampling variances. Meta-regression on
-study-level covariates, multivariate meta-analysis of correlated effects, and shrinkage
-across conditions such as mash [@urbut2019] are not implemented. Combined p-values
-assume independent studies whose p-values are uniform without effects; for discrete
-tests they are conservative. The r-values of a primary study and its follow-up
-[@heller2014] are not implemented. metap's methods of Edgington (sums of p-values),
-Lancaster (inverse chi-squared), the inverse t, the mean z and vote counting are left
-out. SumRank weighs every study alike, as the paper notes, and its permutations re-run
-designs without random effects only.
+[@higgins2002], computed from the estimate of $\tau^2$, as metafor computes them. $I^2$ is
+reported as a proportion, where metafor reports a percentage.
+
+(untested-genes)=
+## Genes that some studies did not test
+
+A gene that a study did not test has a NaN estimate there, and is meta-analysed over the
+studies that did. {api}`meta_analysis` analyses a gene that at least `min_studies`
+studies tested (two by default, since one study says nothing about $\tau^2$); with
+`min_studies=1`, a gene that one study tested keeps that study's estimate and its z-test,
+as metafor gives it for one study. {api}`StudyResults.table` says why a study did not test
+a gene ([One analysis per study](#one-analysis-per-study)).
+
+Each study's standard errors are those of its moderated t-statistics, from limma's
+empirical Bayes moderation. Like any sampling variance in a meta-analysis, they are taken
+as known, without their degrees of freedom, so that the z-test of a gene that only one
+study tested gives a smaller p-value than that study's own moderated t-test.
+
+(meta-analysis-defaults)=
+## Defaults and choices
+
+- **Fixed or random effects.** The random-effects model by default, which lets the true
+ effect vary between studies, so that a change in one study alone raises $\tau^2$ and
+ widens the interval. The fixed-effect model (`method="fixed"`) answers for the studies at
+ hand and assumes that they estimate one effect; it gives the most precise studies more
+ weight than the random-effects model does.
+- **The estimator of $\tau^2$.** REML, metafor's default; DerSimonian and Laird's, which
+ needs no iterations; or Paule and Mandel's.
+- **The test.** z-tests by default; the Knapp-Hartung adjustment with few studies, as
+ metafor's documentation recommends.
+- **Genes that some studies did not test.** `min_studies` (2) sets how many studies must
+ test a gene.
+- **Intervals.** `confidence_level` (0.95) sets the level of the intervals.
+
+(meta-analysis-limits)=
+## Limits and differences
+
+- **Independent studies with known variances.** The studies are taken as independent, and
+ their sampling variances as known. Meta-regression on study-level covariates,
+ multivariate meta-analysis of correlated effects, and shrinkage across conditions such
+ as mash [@urbut2019] are not implemented ([Not implemented
+ yet](#not-implemented-yet)).
+- **The largest maximum.** CytePool's REML estimate is the largest maximum of the
+ restricted likelihood. metafor's Fisher scoring starts from the Hedges estimate,
+ $\max(0, \sum_i (y_i - \bar y)^2 / (k - 1) - \bar v)$, and can stop at the smaller of
+ two maxima, or set $\tau^2$ to zero, with a warning that Fisher scoring "may have gotten
+ stuck at a local maximum", where the likelihood is higher at zero than at the maximum it
+ found. Where the observed information exceeds twice the expected one, Fisher scoring
+ oscillates: metafor then stops with an error, or at its default threshold somewhere in
+ the oscillation, and the remedy of its help page, `control = list(stepadj = 0.5)`,
+ converges to CytePool's estimate.
+- **Estimates to rounding.** The REML and Paule-Mandel estimates converge to rounding.
+ With its default settings metafor stops earlier, and where the restricted likelihood is
+ largest at zero although the Hedges estimate is positive, its step halving approaches
+ zero and stops near its threshold, short of zero.
+- **No search interval.** metafor's Paule-Mandel search stops with an error when the root
+ lies beyond $\max(100, 10\,\mathrm{mad}(y)^2)$, the end of its search interval; CytePool's
+ search has no upper limit.
+- **Standard errors.** A standard error must be positive and finite where it is given.
+ For a zero standard error metafor stops, or fits by REML without $Q$, $I^2$ and $H^2$;
+ for an infinite one it stops.
+
+(meta-analysis-checks)=
+## Checks and further reading
+
+{api}`fit_meta_analysis` is compared with metafor 5.0.1's `rma.uni`, feature by feature,
+in ten cases from 2 to 30 studies, and {api}`meta_analysis` with metafor and R's `p.adjust`
+on the limma-voom tables of three simulated studies: exact (1e-10) for the fixed effect
+and DerSimonian-Laird, and close (1e-6) for REML and Paule-Mandel, run in metafor to
+convergence ([Validation](#validation-meta-analysis)).
+
+Further reading:
+
+- @viechtbauer2010 described metafor, the R package for meta-analysis that CytePool's
+ meta-analysis follows.
+- @dersimonian1986 introduced the random-effects meta-analysis of clinical trials with its
+ moment estimator of the between-study variance.
+- @higgins2002 introduced $I^2$ and $H^2$ to quantify heterogeneity.
+- @viechtbauer2005 compared the bias and efficiency of the estimators of the
+ between-study variance.
+
+[Replication](replication.md), the last chapter, asks in how many studies a gene
+changes.
diff --git a/docs/source/statistics/mixed_models.md b/docs/source/statistics/mixed_models.md
index de016f7..427d4e9 100644
--- a/docs/source/statistics/mixed_models.md
+++ b/docs/source/statistics/mixed_models.md
@@ -1,21 +1,43 @@
---
-description: Linear mixed models with random intercepts fitted to every gene by REML, Satterthwaite's and Kenward-Roger's degrees of freedom, F-tests of several contrasts, dream's precision weights and moderated tests for repeated measures designs, and variance partitioning.
+description: Which genes change when donors give several samples each, tested by dream's mixed models with a random intercept per donor, and which variables drive the variation, by variance partitioning.
---
-# Mixed models, REML and dream
-
-When a study has several samples per donor (two conditions, several tissues, a time
-course), the samples of a donor are correlated. A fixed effect per donor removes that
-correlation, but it also absorbs every variable that is constant within a donor, such as
-disease status or sex, which then cannot be tested. A random intercept per donor models
-the correlation instead and keeps donor-level variables testable. dream applies this to
-gene expression [@hoffman2021], and dreamlet to the pseudobulk profiles of every cell
-type [@hoffman2026a]. [Repeated measures and donors](../guides/repeated_measures.ipynb)
-shows dream and variance partitioning on data.
-
+# Mixed models and dream
+
+[edgeR and DESeq2](count_models.md), like limma-voom, take the samples as independent. When
+donors give several samples each, under two conditions, in several tissues or along a time
+course, the samples of a donor are correlated. {api}`dream_voom` fits every gene with a
+linear mixed model that gives each donor a random intercept, as variancePartition's dream
+does [@hoffman2021], in every cell type of a cube, as dreamlet does [@hoffman2026a].
+{api}`variance_partitioning` asks the related question of how much of each gene's variance
+each variable explains. [Repeated measures and donors](../guides/repeated_measures.ipynb)
+compares dream with a fixed donor effect on data, and partitions the variance.
+
+(fixed-or-random-donor)=
+## A fixed or a random donor effect
+
+A fixed effect for each donor, the paired design `~ donor + condition`, compares each
+donor's samples with each other, but cannot test a variable that is constant within a
+donor, such as disease or sex ([A paired design](linear_models.md#paired-design)). A
+random intercept for each donor models the correlation of a donor's samples instead, and
+keeps such variables testable.
+
+When every donor gives a sample in every condition and every question is within donors,
+the paired design and {api}`limma_voom` are the simpler of the two, and the two models find
+nearly the same genes, as the guide shows. A variable of the donors needs the random
+effect, which also suits donors with unequal numbers of samples. A test between donors
+rests on the variation between them: with three donors in each group, dream's tests of a variable of the donors
+called too many genes in the guide's simulation, while its tests within donors held their
+error rate. dream_voom computes what variancePartition's dream computes
+([Checks](#mixed-models-checks)), so this is a property of the method with few donors.
+When every donor was sampled alike, compare the profiles pooled by donor
+({api}`Cube.collapse_samples`) with {api}`limma_voom` for such a question, and test the
+changes within donors with {api}`dream_voom`.
+
+(mixed-model)=
## The model
-Every gene is fitted with the same model (lme4's notation `(1 | donor)`; @bates2015):
+Every gene is fitted with the same model, in lme4's notation `(1 | donor)` [@bates2015]:
$$
y = X\beta + Z_1 b_1 + \dots + Z_K b_K + e, \qquad
@@ -23,175 +45,283 @@ b_k \sim N(0, \sigma_k^2 I), \qquad e \sim N(0, \sigma^2 W^{-1}),
$$
where $X$ holds the fixed effects, $Z_k$ assigns each sample to a level of the $k$-th
-grouping factor (one random intercept per level), and $W$ holds the gene's precision
+grouping factor, one random intercept per level, and $W$ holds the gene's precision
weights. Grouping factors may be crossed, `(1 | donor) + (1 | batch)`, or nested,
-`(1 | donor/sample)`. Random slopes are not implemented yet.
+`(1 | donor/sample)`. The correlation of two samples of one donor, with no other factor and
+equal weights, is $\sigma_1^2 / (\sigma_1^2 + \sigma^2)$, the intra-class correlation.
+(mixed-model-reml)=
## REML
-{api}`fit_mixed_model` estimates the variance components by restricted maximum
-likelihood [@patterson1971], as lme4's `lmer` does, into a {api}`MixedModelFit`. With
-$\theta_k = \sigma_k / \sigma$ and $V = W^{-1} + \sum_k \theta_k^2 Z_k Z_k'$, the fixed
-effects and $\sigma^2$ are profiled out, and the criterion minimized is lme4's profiled
-REML deviance
+{api}`fit_mixed_model` estimates the variance components by restricted maximum likelihood
+(REML; @patterson1971), as lme4's `lmer` does, into a {api}`MixedModelFit`. REML estimates
+them from the part of the data that the fixed effects do not fit, which removes the
+downward bias that estimating the fixed effects gives the maximum likelihood estimates of
+variances. Let $\theta_k = \sigma_k / \sigma$, the relative standard deviations, and let
+$V = W^{-1} + \sum_k \theta_k^2 Z_k Z_k'$. With $\beta$ and $\sigma^2$ profiled out, the
+criterion is a function of the $\theta_k$ alone, lme4's profiled REML deviance, which is
$$
\log\det V + \log\det(X' V^{-1} X) + (n - p)\left(1 + \log\frac{2\pi r^2}{n - p}\right),
$$
-with $r^2 = (y - X\hat\beta)' V^{-1} (y - X\hat\beta)$. Newton's method minimizes it
-with exact derivatives [@harville1977]; a variance component whose estimate is zero is
-held exactly at zero, the boundary that lme4 reports as a singular fit. Random effects
-are predicted by their best linear unbiased predictors [@henderson1975]. `method="ml"`
-maximizes the likelihood instead.
-
+with $r^2 = (y - X\hat\beta)' V^{-1} (y - X\hat\beta)$ for $n$ samples and $p$ coefficients.
+Newton's method minimizes it with exact derivatives [@harville1977]. A variance component
+whose estimate is zero is held exactly at zero, the boundary that lme4 reports as a
+singular fit. `method="ml"` maximizes the likelihood instead. The fitted values include
+the random effects, at their best linear unbiased predictions [@henderson1975]. Their hat
+matrix $H$ gives the approximate residual degrees of freedom
+$\operatorname{tr}((I - H)'(I - H))$, which lie between $n - p$, with every component at
+zero, and $n$ less the rank of the combined design of fixed and random effects; dream
+moderates on them ([below](#dream)).
+
+(satterthwaite)=
## Satterthwaite's degrees of freedom
The t-statistic of a contrast $c$ of the fixed effects is $c'\hat\beta / \sqrt{c' C c}$,
with $C$ the covariance matrix of the estimates. Its distribution is approximated by a t
-distribution with Satterthwaite's degrees of freedom [@satterthwaite1946;
-@giesbrecht1985],
+distribution with Satterthwaite's degrees of freedom [@satterthwaite1946; @giesbrecht1985],
$$
\nu = \frac{2 v^2}{g' A g}, \qquad v = c' C c,
$$
-where $g$ holds the derivatives of $v$ with respect to the variance parameters and $A$
-is their asymptotic covariance matrix. {api}`fixed_effect_tests` gives them in
-{api}`FixedEffectTests`, computed as lmerTest does [@kuznetsova2017], with exact
-derivatives where lmerTest takes numerical ones. With every component at zero,
-$\nu = n - p$, the degrees of freedom of the fixed-effects model.
+where $g$ is the gradient of $v$ in the variance parameters, and $A$ their asymptotic
+covariance matrix: twice the inverse Hessian of the REML deviance.
+{api}`fixed_effect_tests` gives them in {api}`FixedEffectTests`, as lmerTest does
+[@kuznetsova2017], with exact derivatives where lmerTest takes numerical ones. A component
+estimated at zero adds nothing to $g' A g$, and with every component at zero $\nu = n - p$,
+the degrees of freedom of the fixed-effects model.
-## Kenward and Roger's method
+(kenward-roger)=
+## Kenward-Roger's method
-With few samples the variance components are estimated imprecisely, and $C$, computed
-as if they were known, understates the variance of the estimates. Kenward and Roger
-[@kenward1997] adjust it: with $\Sigma = \sum_i \phi_i G_i$ the covariance of the
-observations, linear in the variance parameters $\phi$,
-$P_i = -X'\Sigma^{-1} G_i \Sigma^{-1} X$,
-$Q_{ij} = X'\Sigma^{-1} G_i \Sigma^{-1} G_j \Sigma^{-1} X$ and $W$ the inverse of
-the expected information of $\phi$ under REML,
+With few samples the variance components are estimated imprecisely, and $C$, computed as if
+they were known, understates the variance of the estimates. Kenward and Roger
+[@kenward1997] adjust it. Let $W$ be the inverse of REML's expected information for the
+variance parameters $\phi$, on which the covariance of the observations,
+$\Sigma = \sum_i \phi_i G_i$, depends linearly, and let
+$P_i = -X'\Sigma^{-1} G_i \Sigma^{-1} X$ and
+$Q_{ij} = X'\Sigma^{-1} G_i \Sigma^{-1} G_j \Sigma^{-1} X$; then
$$
C_A = C + 2\,C \Big(\sum_{ij} W_{ij} \big(Q_{ij} - P_i C P_j\big)\Big) C .
$$
-`fixed_effect_tests(fit, df_method="kenward_roger")` divides each estimate by its
-standard error from $C_A$, with Satterthwaite's formula for the degrees of freedom on
-the expected information, as pbkrtest computes them [@halekoh2014] and lmerTest's
-`ddf = "Kenward-Roger"` reports them. In balanced designs the adjustment vanishes and
-the tests are those of the analysis of variance. A variance component estimated at
-zero takes part, which lowers the degrees of freedom below Satterthwaite's.
-
-With precision weights, pbkrtest builds $\Sigma$ without lme4's prior weights while
-it takes $C$ from the weighted fit. dream divides each gene's weights by their mean,
-so its tests treat every observation as having its gene's average weight.
-`kenward_roger_weights="average"`, the default, computes these tests; `"precision"`
-uses each observation's own weight, which is Kenward and Roger's method for the
-weighted model. With voom's weights the first can make standard errors smaller than
-the unadjusted ones, and the second never does; the [Validation](../validation.md)
-page compares them.
-
+`fixed_effect_tests(fit, df_method="kenward_roger")` divides each estimate by its standard
+error from $C_A$, with Satterthwaite's formula for the degrees of freedom on the expected
+information, as pbkrtest computes them [@halekoh2014] and lmerTest's
+`ddf = "Kenward-Roger"` reports them. In balanced designs the adjustment vanishes and the
+tests are those of the analysis of variance. A variance component at zero still counts
+here, which lowers the degrees of freedom below Satterthwaite's.
+
+With precision weights, pbkrtest builds $\Sigma$ without lme4's prior weights while it
+takes $C$ from the weighted fit, and dream divides each gene's weights by their mean, so
+dream's tests treat every observation as having its gene's average weight.
+`kenward_roger_weights="average"`, the default, computes these tests; `"precision"` uses
+each observation's own weight, which is Kenward-Roger's method for the weighted model.
+With voom's precision weights, `"average"` can give standard errors below the unadjusted
+ones, which `"precision"` never does. The cost of each gene grows with the cube of the
+number of samples, as in pbkrtest, so that in large studies Kenward-Roger's tests take far
+longer than Satterthwaite's ([Timings](#validation-timings) gives measured times).
+
+(mixed-model-f-tests)=
## Several contrasts at once
-{api}`fixed_effect_f_test` tests that several contrasts are all zero, such as every
-difference among three time points, in a {api}`FixedEffectFTest`. With Satterthwaite's
-method, as lmerTest's `contestMD`, the contrasts are replaced by the eigenvectors of
-their covariance matrix, whose estimates are uncorrelated; $F$ is the mean of their
-squared t-statistics, and its denominator degrees of freedom $2E/(E-r)$, with
-$E = \sum_m \nu_m / (\nu_m - 2)$ over their degrees of freedom, match the mean of
-$F$ [@fai1996; @kuznetsova2017]. With Kenward and Roger's method, as pbkrtest's
-`KRmodcomp`, the Wald statistic with $C_A$ is scaled and referred to an $F$
-distribution whose first two moments it matches [@kenward1997]. The numerator degrees
-of freedom $r$ are the number of linearly independent contrasts.
-
+{api}`fixed_effect_f_test` tests that several contrasts are all zero, such as the
+differences between three time points, in a {api}`FixedEffectFTest`. With Satterthwaite's
+method, as in lmerTest's `contestMD`, the contrasts are first rotated onto the
+eigenvectors of their covariance matrix, so that their estimates are uncorrelated; $F$
+averages the squares of their t-statistics and is referred to an $F$ distribution with
+the same mean, whose denominator degrees of freedom are $2E/(E-r)$ for
+$E = \sum_m \nu_m / (\nu_m - 2)$ over the rotated contrasts [@fai1996; @kuznetsova2017].
+With Kenward-Roger's method, as in pbkrtest's `KRmodcomp`, the Wald statistic computed
+with $C_A$ is scaled and referred to an $F$ distribution whose first two moments it
+matches [@kenward1997]. The numerator degrees of freedom $r$ are the number of
+linearly independent contrasts.
+
+(dream)=
## dream
-dream is limma-voom with the mixed model in place of the linear model:
-
-1. {api}`voom_with_dream_weights`: voom's precision weights, from the mean-variance
- trend of the mixed model of every gene, fitted by maximum likelihood, read at the
- fitted values with the predicted random effects.
-2. {api}`fit_mixed_model` by REML with those weights, and {api}`fixed_effect_tests`
- with dream's degrees of freedom: by default Kenward and Roger's for fewer than 20
- samples, as pbkrtest computes them with every observation at the average weight,
- and Satterthwaite's otherwise.
-3. {api}`moderated_fixed_effect_tests`: empirical Bayes moderation of each gene's
- residual variance on the approximate residual degrees of freedom of the mixed model,
- $\operatorname{tr}((I - H)'(I - H))$ for its hat matrix $H$; the moderated
- t-statistic has the degrees of freedom of its test plus the prior's, and the result
- ({api}`ModeratedFixedEffectTests`) adds a z-statistic with the same p-value, so that
- tests on different degrees of freedom compare. Its moderated F-statistic,
- $t' R^{-1} t / r$ for the moderated t-statistics and the correlation $R$ of their
- estimates, tests several contrasts at once, as `topTable` does.
-
-{api}`dream_voom` runs these steps on every cell type of a cube, with
-`df_method="adaptive"` choosing the degrees of freedom in each cell type from the
-samples left in it, and `f_tests` naming sets of contrasts to test at once. As
-variancePartition does, it moderates the residual variance of an observation of each
-gene's average weight, which keeps the mean-variance trend that voom's weights remove;
-`trend=True` lets the prior follow it.
-
-dream switches from Kenward and Roger's method to Satterthwaite's at 20 samples
-(its help page says "<= 20"; with 20 it uses Satterthwaite's), and for a gene whose
-adjusted covariance matrix is ill conditioned, which depends on the units of
-continuous covariates. Its `topTable` computes the p-values of an F-test of every gene
-on the degrees of freedom of the first gene; here each gene has its own, those of $r$
-uncorrelated moderated t-statistics combined as lmerTest combines those of its
-F-tests, so that an F-test of one contrast is its t-test. The
-[Validation](../validation.md) page reports both differences and their sizes.
-
+dream is limma-voom with the mixed model in place of the linear model
+[@hoffman2021]. {api}`dream_voom` takes, in every cell type, the steps of
+[limma-voom](#limma-voom-steps) up to the normalization, then those of variancePartition's
+`voomWithDreamWeights`, `dream`, `eBayes` and `topTable`:
+
+1. **Precision weights**, {api}`voom_with_dream_weights`: voom's ([voom](#voom)), with each
+ gene's mixed model, fitted by maximum likelihood, in place of the linear model, and the
+ trend evaluated at fitted values that include the predicted random effects.
+2. **Fits and tests**: {api}`fit_mixed_model` by REML with those weights, and
+ {api}`fixed_effect_tests` with dream's degrees of freedom.
+3. **Moderation**, {api}`moderated_fixed_effect_tests`: empirical Bayes moderation of each
+ gene's residual variance on the approximate residual degrees of freedom of the mixed
+ model ([Empirical Bayes moderation](empirical_bayes.md)). The moderated t-statistic has
+ the degrees of freedom of its test plus the prior's, and the result
+ ({api}`ModeratedFixedEffectTests`) adds a z-statistic with the same p-value, which puts
+ tests on different degrees of freedom on one scale.
+
+The degrees of freedom follow dream's default, `df_method="adaptive"`: Kenward-Roger's in
+a cell type of fewer than 20 samples and Satterthwaite's from 20 on, as dream chooses
+them, although its help page gives the threshold as "<= 20". Where the adjusted covariance
+matrix of a gene is not positive definite, or its condition number exceeds 1e6, dream
+falls back to Satterthwaite's method for that gene, and so does {api}`dream_voom`; since
+the condition number changes with the units of continuous covariates, it helps to centre
+and scale them. In each cell type, the mixed model takes its random effects from the
+samples that remain, and a cell type whose samples cannot estimate them, such as one with
+one sample per donor, is reported and not fitted.
+
+As in variancePartition, the variance that dream_voom moderates is that of an observation
+with the gene's average weight. With voom's precision weights, that is roughly the variance
+that the trend predicts at the gene's expression level, so the variances moderated still
+follow the mean-variance trend that the weights had taken out; `trend=True` lets the prior
+follow it too.
+
+`f_tests` gives sets of contrasts, each tested at once by the moderated F-statistic
+$t' R^{-1} t / r$ of its moderated t-statistics and the correlation $R$ of their estimates,
+as `topTable` gives it for several coefficients. Each gene's F-test has degrees of freedom
+of its own, those of $r$ uncorrelated moderated t-statistics combined as
+[above](#mixed-model-f-tests), so that with one contrast the F-test and the t-test agree.
+
+(variance-fractions)=
## Variance partitioning
Which variables drive the differences between samples, and which must be adjusted for?
-Variance partitioning [@hoffman2016] fits every gene with one mixed model that holds
-every variable of the design, the categorical ones as random intercepts (donor,
-condition, batch) and the continuous ones as fixed effects (age), and reports the
-fraction of the gene's variance that each variable explains:
+Variance partitioning [@hoffman2016] fits every gene with one mixed model that holds every
+variable of the design, the categorical ones as random intercepts (donor, condition, batch)
+and the continuous ones as fixed effects (age), and reports the fraction of the gene's
+variance that each variable explains:
$$
\frac{\hat\sigma^2_v}{\sum_k \hat\sigma^2_k + \sum_j \hat\sigma^2_{\beta_j} +
\hat\sigma^2},
$$
-where a random effect $k$ contributes its variance component $\hat\sigma^2_k$, a
-fixed-effect term $j$ the variance of its fitted contribution over the samples,
+where a random effect $k$ contributes its variance component $\hat\sigma^2_k$; a
+fixed-effect term $j$, the variance over the samples of its part of the fitted values,
$\hat\sigma^2_{\beta_j} = \operatorname{var}(X_j \hat\beta_j)$, and the residual its
-variance $\hat\sigma^2$. The fractions of a gene sum to one; without fixed effects,
-the fraction of a random effect is the intra-class correlation of two samples that
-share its level and no other.
+variance $\hat\sigma^2$. The fractions of a gene sum to one; without fixed effects, the
+fraction of a random effect is the intra-class correlation of two samples that share its
+level and no other.
{api}`variance_fractions` computes them into a {api}`VarianceFractions`, as
variancePartition's `fitExtractVarPartModel` does:
-- The variances are estimated by maximum likelihood, variancePartition's default: REML
- estimates the variances after removing the fixed effects, which biases the fractions
- of the fixed effects. `method="reml"` fits by REML.
-- With precision weights, such as voom's, each gene's weights are divided by their
- mean, so that the residual variance is that of an observation of average weight, and
- the variances of the fitted fixed effects are weighted.
-- With random effects, the fixed-effect terms share the variance of all the fitted
- fixed effects in proportion to the variances of their own contributions (found by
- probing variancePartition). A design without random effects is fitted by least
- squares, and each of its terms, factors included, has the variance of its own
- contribution.
-- A variance component estimated at zero, a singular fit in lme4, gives a fraction of
- exactly zero.
-
-{api}`variance_partitioning` partitions the variance of every cell type of a cube
-after the steps of {api}`dream_voom` up to voom's precision weights, which come from
-the mean-variance trend of the mixed model, and records the samples each fit used in
-a {api}`VariancePartitioningResult`; {api}`VariancePartitioningResult.table` gives the
-fractions of one cell type. {api}`plot_variance_fractions` draws variancePartition's
-violins of the fractions over the genes [@hintze1998], with Silverman's bandwidth
-[@silverman1986], and {api}`plot_feature_variance_fractions` its bars for chosen genes.
-The residual fraction mixes biological variation, the precision of the measurements
-and technical variation that the design does not name, so its size alone does not say
-whether variables are missing from the design.
-
-## Not yet implemented
-
-Random slopes are planned; they would also partition the variance within subsets of
-the samples, such as the variance between donors within each tissue. A gene that the
-fixed and random effects reproduce exactly has no REML estimate; it is left unfitted
-rather than given a variance of zero.
+- The variances are estimated by maximum likelihood, variancePartition's default, which its
+ help page explains: REML can bias the fractions substantially, where maximum likelihood
+ gave unbiased ones. `method="reml"` fits by REML.
+- With precision weights, such as voom's, each gene's weights are scaled to a mean of one,
+ so that the residual variance refers to an observation of average weight, and the
+ variances of the fitted fixed effects are weighted ones.
+- With random effects, the variance of the fitted fixed effects as a whole is split among
+ their terms in proportion to the variance of each term's own contribution. Without random
+ effects, the design is fitted by least squares, and each term, factors included, keeps
+ the variance of its contribution.
+- A variance component at zero, lme4's singular fit, has a fraction of exactly zero. The
+ fractions are unaffected by rescaling a gene's weights or values, shifting its values,
+ or relabelling the levels of a factor.
+
+{api}`variance_partitioning` takes the steps of {api}`dream_voom` as far as voom's
+precision weights in every cell type of a cube, partitions the variance, and records the
+samples each fit used in a {api}`VariancePartitioningResult`;
+{api}`VariancePartitioningResult.table` gives the fractions of one cell type.
+{api}`plot_variance_fractions` draws variancePartition's violins of the fractions over the
+genes [@hintze1998], with Silverman's bandwidth [@silverman1986], and
+{api}`plot_feature_variance_fractions` its bars for chosen genes. The residual fraction
+mixes biological variation, the precision of the measurements and technical variation that
+the design does not name, so its size alone does not say whether variables are missing
+from the design.
+
+(mixed-models-defaults)=
+## Defaults and choices
+
+- **A fixed or a random donor effect.** {api}`limma_voom` with `~ donor + condition` when
+ every question is within donors; {api}`dream_voom` for a variable of the donors or
+ unequal numbers of samples; the profiles pooled by donor and {api}`limma_voom` for a
+ question between donors with few donors in each group ([above](#fixed-or-random-donor)).
+- **Degrees of freedom.** `df_method="adaptive"`, dream's default, as above;
+ `"satterthwaite"` or `"kenward_roger"` fixes one for every cell type.
+ `kenward_roger_weights="average"` computes Kenward-Roger's tests as dream does, and
+ `"precision"` with each observation's weight.
+- **The span** of the trend of {api}`dream_voom` and {api}`voom_with_dream_weights`: chosen
+ from the number of genes, as voom chooses it ([The span](#voom-span)); `span=0.5`
+ reproduces variancePartition's `voomWithDreamWeights`.
+- **The prior.** A constant prior by default, as in variancePartition; `trend=True` lets it
+ follow the expression level, and `robust=True` protects it from genes with outlying
+ variances.
+- **Weights.** `prior_weights="n_cells"` weights each sample by its number of cells, as
+ dreamlet does ([Prior weights from the number of cells](#prior-weights-from-cells)).
+- **The genes tested.** `expression_filter={"sample_groups": "condition"}` makes
+ dream_voom and limma_voom test the same genes of the same samples ([Filtering by
+ expression](#filtering-by-expression)).
+- **Variance partitioning.** `method="ml"`, variancePartition's default, or `"reml"`.
+
+(mixed-models-limits)=
+## Limits and differences
+
+- **Random intercepts only.** Random slopes, and with them the partition of the variance
+ within subsets of the samples, such as `(tissue + 0 | donor)`, are not implemented, nor
+ are generalized linear mixed models, bootstrap intervals of the fractions
+ (`varPartConfInf`) and sample quality weights for dream ([Not implemented
+ yet](#not-implemented-yet)).
+- **Complete data.** Every gene needs a value, with a positive weight, in every sample: a
+ missing value or a zero weight is an error, where lme4 leaves out a sample whose value
+ is missing and gives a zero weight an infinite REML criterion. A design whose fixed and
+ random effects, taken together, span every sample fits every gene exactly and is
+ refused, although lme4 can fit it.
+- **Genes without an optimum.** A gene whose values the fixed and random effects
+ reproduce, such as one constant within every donor, has no REML or maximum likelihood
+ estimate: the criterion keeps falling as the residual variance shrinks to zero. It is
+ left unfitted, with NaN tests and fractions, and out of dream's trend. lme4 warns, or
+ fits it with a residual variance near zero, which variancePartition keeps in the trend,
+ tests, and gives a donor fraction near one.
+- **Local optima.** Like lme4's optimizer, Newton's method finds a local minimum, which is
+ not the lowest where the criterion has several.
+- **dream's choices.** dream decides which genes fall back to Satterthwaite's method from
+ R's estimate of the condition number, and dream_voom from the exact one; they can differ
+ within about 2% of 1e6. dream's `topTable` computes the p-values of every gene's F-test
+ on the degrees of freedom of the first gene for the first coefficient listed, so that
+ they depend on the order of the genes and of the coefficients; here each gene has its
+ own. Beyond a p-value of 1e-300, the z-statistic is computed from the logarithm of the
+ tail probability, where `topTable`'s stops at 37.07. A design without random effects is
+ refused, where dream fits it with limma; dream_voom points to {api}`limma_voom`.
+- **Variance partitioning.** With random effects, every categorical variable must be one
+ of them, and the formula needs an intercept, as variancePartition requires. The
+ variables are listed in the design's order, where variancePartition sorts the random
+ effects alphabetically. With `method="reml"` and no random effects, the residual
+ variance is REML's, over $n - p$, where variancePartition divides by $n$ whatever its
+ `REML` argument says.
+
+(mixed-models-checks)=
+## Checks and further reading
+
+The fits and tests are compared with lme4, lmerTest, pbkrtest and variancePartition 1.36.2,
+run as separate programs, gene by gene:
+
+- REML fits and Satterthwaite's t-tests against lmerTest 3.2.1 on lme4 2.0.6: close
+ (1e-6; 1e-5 for Satterthwaite's degrees of freedom, which lmerTest differentiates
+ numerically) ([Validation](#validation-mixed-models)).
+- Kenward-Roger's t-tests and F-tests, and Satterthwaite's F-tests, against pbkrtest 0.5.3
+ and lmerTest 3.1-3, on lme4 1.1-38 with variancePartition: exact (1e-10) at given
+ estimates, and 1e-5 for the degrees of freedom and p-values of Satterthwaite's F-tests,
+ which lmerTest differentiates numerically; dream's choice of degrees of freedom and its
+ F-tests against variancePartition: close (1e-6)
+ ([Validation](#validation-kenward-roger-and-f-tests)).
+- dream's weights, fits and moderated tests, and dream_voom on a cube, against
+ variancePartition and limma 3.62.1: close (1e-6; 1e-5 for Satterthwaite's degrees of
+ freedom and what uses them) ([Validation](#validation-dream)).
+- Variance partitioning and its figures against variancePartition: close (1e-6), exact at
+ given estimates ([Validation](#validation-variance-partitioning)).
+
+Further reading:
+
+- @hoffman2016 introduced variancePartition, which uses linear mixed models to attribute
+ the variation of each gene to disease, sex, tissue, genetics or technical factors.
+- @hoffman2021 introduced dream, mixed models for repeated measures designs, and showed
+ that other methods can give reproducible false positives driven by the genetic
+ regulation of expression.
+- @hoffman2026a introduced dreamlet, which runs precision-weighted mixed models on the
+ pseudobulk profiles of every cell type, and applied it to 1.4 million nuclei from 150
+ Alzheimer's disease cases and 149 controls.
+
+[Composition tests](composition.md), the next chapter, turns from the genes of each cell
+type to the shares of the cell types.
diff --git a/docs/source/statistics/multiple_testing.md b/docs/source/statistics/multiple_testing.md
index dfde191..6be4396 100644
--- a/docs/source/statistics/multiple_testing.md
+++ b/docs/source/statistics/multiple_testing.md
@@ -1,21 +1,24 @@
---
-description: How CytePool adjusts p-values for testing thousands of genes, the family-wise and false discovery rate methods of R's p.adjust, and adjustment within or across cell types.
+description: How the p-values of thousands of tests become a list of calls with a known error rate, within or across cell types, and how many of the tests have an effect at all.
---
# Multiple testing
-Testing 20,000 genes at a p-value of 0.05 calls about 1,000 of them even if none
-changed. An adjustment for multiple testing turns each p-value into one that controls an
-error rate over the family of tests. {api}`adjust_p_values` computes the adjustments of
-R's `p.adjust`. Every differential-expression result adjusts its p-values by the method
-of Benjamini and Hochberg over the genes of one cell type and contrast, as limma's
-`topTable` does; {api}`deseq2_wald_test` and {api}`deseq2_likelihood_ratio_test` adjust
-over the genes that pass DESeq2's independent filter on the mean count [@bourgon2010].
+[Empirical Bayes moderation](empirical_bayes.md) gave every gene a p-value for each
+contrast. Testing 20,000 genes at a p-value of 0.05 calls about 1,000 of them even if
+none changed, so the p-values are adjusted for the number of tests, to control an error
+rate over the family of tests. {api}`adjust_p_values` computes the adjustments of R's
+`p.adjust`, by the method of Benjamini and Hochberg by default, as limma's `topTable`
+adjusts them. [Genes that change in each cell type](../guides/genes_per_cell_type.ipynb)
+adjusts within and across cell types on published data.
-## The methods
+(error-rates)=
+## Error rates and their methods
-With $n$ tests in a family and the sorted p-values $p_{(1)} \le \dots \le p_{(n)}$, the
-adjusted value of $p_{(i)}$ is, capped at 1:
+The family-wise error rate is the chance of any false call; the false discovery rate is
+the expected share of false calls among all calls. For $n$ tests in a family, with
+p-values sorted as $p_{(1)} \le \dots \le p_{(n)}$, each method adjusts $p_{(i)}$ to
+the value below, capped at 1:
| Method | Controls | Adjusted p-value |
| --- | --- | --- |
@@ -26,33 +29,121 @@ adjusted value of $p_{(i)}$ is, capped at 1:
| `"bh"` [@benjamini1995] | false discovery rate | $\min_{j \ge i} n\, p_{(j)} / j$ |
| `"by"` [@benjamini2001] | false discovery rate under any dependence | the `"bh"` value times $\sum_{k=1}^{n} 1/k$ |
-The family-wise error rate is the chance of any false call; the false discovery rate is
-the expected share of false calls among all calls. Genome-wide screens usually use the
-false discovery rate: a list of 200 genes at an adjusted p-value of 0.05 is expected to
-hold at most about 10 false ones. Bonferroni's and Holm's methods hold whatever the
-dependence between the tests; Benjamini and Yekutieli's method controls the false
-discovery rate under any dependence, at a cost in power [@benjamini2001].
+A test is called when its adjusted p-value is below the chosen rate. Genome-wide screens
+usually control the false discovery rate: calling the genes whose adjusted p-values are
+below 0.05 keeps the expected share of false calls at 5% or less, about 10 in a list of
+200. Bonferroni's and Holm's methods
+hold whatever the dependence between the tests. Benjamini and Hochberg's method controls
+the false discovery rate for independent tests and for tests with positive dependence,
+and Benjamini and Yekutieli's under any dependence, at a cost in power [@benjamini2001].
+(adjusting-across-cell-types)=
## Adjusting within or across cell types
{api}`limma_voom`, {api}`limma_trend` and {api}`dream_voom` adjust over the genes of one
-cell type and contrast, so a call is judged within its cell type. With
+cell type and contrast, so that a call is judged within its cell type. With
`adjust_across_groups=True` they add a column adjusted over every gene of every cell
-type for the same contrast, one family per contrast. In simulations, the adjustment over
-all cell types lowered both the false discovery rate and the power, and so was very
-conservative [@crowell2020]. The adjustment across cell types is not larger for every
-test: a family with more small p-values can lower an adjusted value, as [Genes that
-change in each cell type](../guides/genes_per_cell_type.ipynb#adjusting-within-or-across-cell-types)
-shows on published data.
+type for the same contrast, one family per contrast. Adjust within each cell type to
+rank its genes, and across cell types when the study's list of genes, of every cell
+type, is the result. In simulations, the adjustment over all cell types lowered both the
+false discovery rate and the power, and so was very conservative [@crowell2020]. It is
+not larger for every test: a family with more small p-values can lower an adjusted
+value, as [Genes that change in each cell
+type](../guides/genes_per_cell_type.ipynb#guide-adjusting-across-cell-types) shows
+on published data.
+(other-families)=
## Other families
-Gene-set tests adjust over the sets ({api}`camera`, {api}`fry`, {api}`mroast`);
-{api}`romer`'s p-values are not adjusted, as in limma, and {api}`adjust_p_values`
+Gene-set tests adjust over the sets ({api}`camera`, {api}`fry`, {api}`mroast`); the
+p-values of {api}`romer` are not adjusted, as in limma, and {api}`adjust_p_values`
adjusts them. A composition test adjusts over the cell types, and a meta-analysis over
-the genes of each cell type and contrast.
+the genes of each cell type and contrast. {api}`deseq2_wald_test` and
+{api}`deseq2_likelihood_ratio_test` adjust over the genes that pass DESeq2's
+independent filter on the mean count, which raises the number of calls at the same
+false discovery rate [@bourgon2010].
+
+(proportion-of-true-nulls)=
+## The proportion of true nulls
+
+Adjusted p-values say which tests to call; the proportion of the tests without an
+effect, $\pi_0$, says how many effects there are in all, called or not. The p-values of
+tests without an effect are uniform, so among $m$ tests the p-values at least $\lambda$
+hold about $\pi_0 m (1 - \lambda)$ of them, and
+
+$$
+\hat\pi_0(\lambda) = \frac{\#\{p \ge \lambda\}}{m (1 - \lambda)}
+$$
+
+estimates $\pi_0$ [@storey2002], with a bias that falls, and a variance that grows, as
+$\lambda$ rises. {api}`proportion_of_true_nulls` gives it as qvalue's `pi0est` does, with
+its defaults and options, and returns a {api}`TrueNullProportion`: at one $\lambda$;
+smoothed over $\lambda = 0.05, 0.10, \dots, 0.95$ by a cubic smoothing spline with 3
+degrees of freedom and read at the largest $\lambda$, the default [@storey2003]; or at
+the $\lambda$ whose bootstrap estimate of mean squared error is smallest
+[@storey2004]. The estimate is capped at 1. Its complement, $\pi_1 = 1 - \pi_0$, is the
+share of the tests with an effect: the p-values of one study among the genes that
+another finds give it as a rate of replication ([How well one study replicates
+another](replication.md#pi1-between-studies)).
+
+(multiple-testing-defaults)=
+## Defaults and choices
+
+- **The method.** `"bh"`, where R's `p.adjust` defaults to `"holm"`: a screen of many
+ genes asks for a list with few false calls, which the false discovery rate measures.
+ `"holm"` suits a few tests chosen in advance, when any false call matters.
+- **The family.** Each cell type and contrast by default; `adjust_across_groups=True`
+ adds a family of every cell type ([above](#adjusting-across-cell-types)).
+ {api}`adjust_p_values` adjusts all the p-values it is given as one family, or each
+ slice along `axis`, such as each contrast of a matrix of genes by contrasts.
+- **Families larger than observed.** `n` declares a family larger than the p-values
+ given, as R's `p.adjust` does: the missing p-values count as larger than every one
+ given for Bonferroni's and Holm's methods, and as 1 for the others.
+- **Missing p-values**, marked NaN, stay NaN and do not count as tests.
+- **The estimate of $\pi_0$.** The smoother over qvalue's grid of $\lambda$ by default.
+ One value of `lambdas`, such as 0.5, suits a few dozen p-values, on which the
+ smoother, which reads the largest $\lambda$, often gives an estimate of 0 or 1;
+ `method="bootstrap"`, `smooth_df` and `smooth_log` are qvalue's options.
+
+(multiple-testing-limits)=
+## Limits and differences
+
+{api}`adjust_p_values` follows R's `p.adjust`. {api}`proportion_of_true_nulls` follows
+qvalue 2.38.0, also where qvalue's documentation leaves details open, such as counting
+the p-values equal to $\lambda$, and differs from it on purpose in these:
+
+- **No p-value at the largest $\lambda$.** qvalue stops with an error, with the smoother
+ and the bootstrap alike. CytePool smooths the count of zero as it is, and gives it no
+ variance in the bootstrap; the smoother of logarithms, which a count of zero leaves
+ without a value, refuses it. This is common among the genes that one study finds,
+ whose p-values in another study are small.
+- **An estimate at most 0.** qvalue stops; CytePool gives 0.
+- **More than 49 values of $\lambda$.** R's smoother then places knots at only some
+ of them; CytePool places one at every $\lambda$. Repeated values of $\lambda$ are
+ refused.
+
+(multiple-testing-checks)=
+## Checks and further reading
+
+- {api}`adjust_p_values`, every method on every case: R 4.4.3's `p.adjust`, exact
+ (1e-10) ([Validation](#validation-multiple-testing)).
+- {api}`proportion_of_true_nulls`, with qvalue's defaults and options: qvalue 2.38.0's
+ `pi0est`, exact (1e-10) ([Validation](#validation-combined-p-values-and-replication)).
+
+Further reading:
+
+- @holm1979 introduced a sequentially rejective procedure that controls the family-wise
+ error rate and rejects at least as much as Bonferroni's.
+- @simes1986 introduced an improved Bonferroni test of a set of hypotheses, and
+ @hochberg1988 and @hommel1988 procedures that build on it.
+- @benjamini1995 introduced the false discovery rate and the procedure that controls
+ it, and @benjamini2001 showed that it holds under positive dependence and gave a
+ procedure for any dependence.
+- @storey2002 estimated the proportion of true nulls from the p-values above a
+ threshold, and @storey2003 introduced the q-value for genome-wide studies, with that
+ estimate smoothed over thresholds.
-{api}`proportion_of_true_nulls` estimates the share of the tests without an effect,
-$\pi_0$, as qvalue's `pi0est` does [@storey2002]; the
-[replication of studies](meta_analysis.md#the-proportion-of-true-nulls-and-pi1) reads
-$\pi_1 = 1 - \pi_0$ of one study's p-values among the genes another finds.
+The foundations are in place: a cube of profiles, a design fitted to every gene,
+moderated tests and their adjustment. [Normalization and
+filtering](normalization_and_filtering.md) begins the part on differential expression,
+which applies them to counts.
diff --git a/docs/source/statistics/normalization_and_filtering.md b/docs/source/statistics/normalization_and_filtering.md
new file mode 100644
index 0000000..2720441
--- /dev/null
+++ b/docs/source/statistics/normalization_and_filtering.md
@@ -0,0 +1,239 @@
+---
+description: How counts are put on a common scale across samples, and which genes and samples enter a test, with normalization factors, log-CPM, filtering by expression and the samples left out.
+---
+
+# Normalization and filtering
+
+[Multiple testing](multiple_testing.md) closed the foundations. Counts need two steps
+before any test. Samples are sequenced to different depths and hold different mixes of
+transcripts, so their counts are put on a common scale; and genes and samples with too few
+counts carry no evidence, so they are left out. In every cell type, {api}`limma_voom`
+scales the libraries by TMM factors ({api}`tmm_factors`) and keeps the genes that edgeR's
+rule passes ({api}`filter_by_expression`), and so do limma-trend and dream.
+[Normalization, filtering and sample quality](../guides/normalization_and_quality.ipynb)
+takes these steps one at a time on published data.
+
+(library-sizes)=
+## Library sizes and composition
+
+The library size of a sample is its total count, and a pseudobulk profile's grows with
+the number of cells pooled into it; {api}`Cube.library_sizes` gives it for every sample
+and group. The count of a gene grows with the library size, and dividing by it removes the
+depth of sequencing.
+
+It does not remove composition. When a few genes take a larger share of one library, as
+when a stimulation raises a set of interferon-stimulated genes, every other gene takes a
+smaller share of it, and looks lower there although its expression did not change. The
+normalization factors below correct for composition, on the assumption that most genes do
+not change between the samples. The analyses take the library sizes over the genes that
+pass the expression filter ([below](#filtering-by-expression)), as edgeR's
+`keep.lib.sizes = FALSE` does.
+
+(normalization-and-size-factors)=
+## Normalization factors and size factors
+
+edgeR keeps the library sizes $N_i$ and multiplies each by a normalization factor $f_i$,
+into the effective library size $N_i f_i$; the factors are scaled to a geometric mean of
+one. Three rules give them, as edgeR's `normLibSizes` (`calcNormFactors`) computes them:
+
+- **TMM**, the trimmed mean of M values ({api}`tmm_factors`; @robinson2010a), the default.
+ Each sample $k$ is compared with a reference sample $r$ over the genes counted in both.
+ With proportions $p_{gi} = y_{gi} / N_i$, a gene has the log ratio
+ $M_g = \log_2 (p_{gk} / p_{gr})$ and the average log proportion
+ $A_g = \tfrac12 \log_2 (p_{gk}\, p_{gr})$. The genes in the lowest and highest 30% of
+ $M$, or in the lowest and highest 5% of $A$, are trimmed, and $\log_2 f_k$ averages
+ the $M$ values left, each weighted by its inverse variance $1 / v_g$, with
+ $v_g = (N_k - y_{gk}) / (N_k y_{gk}) + (N_r - y_{gr}) / (N_r y_{gr})$. The reference
+ is the sample whose ratio of upper quartile to library size is nearest the mean of
+ these ratios.
+- **The upper quartile** ({api}`upper_quartile_factors`; @bullard2010): the 75% quantile
+ of each sample's counts, over the genes counted in some sample, divided by its library
+ size.
+- **RLE**, the relative log expression ({api}`rle_factors`; @anders2010): the median
+ ratio of a sample's counts to each gene's geometric mean over the samples, taken over
+ the genes counted in every sample and divided by the library size.
+
+DESeq2 replaces the library sizes by size factors $s_i$, by which it divides the counts
+into its normalized counts: {api}`median_ratio_size_factors` gives the median of each
+sample's ratios to the genes' geometric means, over the genes counted in every sample
+[@anders2010], and `method="poscounts"` its form for sparse counts, in which genes with
+zeros take part.
+
+TMM is the default of edgeR's `calcNormFactors` and of the limma and edgeR workflows for
+RNA-seq. The trimming leaves out the genes that change most, so that a minority of strongly
+changed genes, like those of the stimulation above, does not move the factors. TMM, RLE and
+DESeq2's size factors assume that most genes do not change between samples; the upper
+quartile assumes that one quantile of the counts does not, and corrects less where a
+response changes many genes, as the guide shows.
+
+(log-cpm)=
+## Counts per million and log-CPM
+
+Counts per million (CPM) divide each count by its effective library size, times a
+million: $10^6\, y_{gi} / (N_i f_i)$ ({api}`cpm`). Log-CPM values are their log2, after a
+small count is added so that zero counts have finite logs. edgeR adds a prior count $c$
+that is an average count, scaled by library size: sample $i$ gets
+$c_i = c\, R_i / \bar R$, with $R_i$ the effective library sizes and $\bar R$ their mean,
+added to every count and twice to its library size ({api}`log2_cpm`):
+
+$$
+\log_2 \frac{y_{gi} + c_i}{R_i + 2 c_i}\, 10^6 .
+$$
+
+The prior count sets the floor of the log values and damps the variance of small counts.
+Each method adds the count that its reference tool adds:
+
+- **voom** ({api}`voom`, and so {api}`limma_voom` and {api}`dream_voom`) adds 0.5 to every
+ count and 1 to every library size ([voom](#voom));
+- **limma-trend** ({api}`limma_trend`) adds an average count of 3
+ ([limma-trend](#limma-trend));
+- **the MDS plot and the principal components** of a cube add an average count of 2,
+ edgeR's default, which the edgeR User's Guide uses for its MDS plot
+ ([below](#samples-at-a-glance)), and so do the pathway scores and activities of a
+ cube's cell type ([The values scored](#pathway-score-values));
+- **edgeR's average log-CPM** ({api}`average_log2_cpm`), which ranks the genes and carries
+ the trends of the quasi-likelihood fit, adds an average count of 2 too
+ ([edgeR's quasi-likelihood F-tests](#quasi-likelihood)).
+
+(filtering-by-expression)=
+## Filtering by expression
+
+A gene with a few counts in a few samples cannot show a change that a test could find, and
+it costs a test in the adjustment for multiple testing. Its log-CPM values also mislead:
+they sit near the floor that the prior count sets, so their spread is small for want of
+counts, not because they are precise. Such genes bend voom's mean-variance trend down at
+its low end, where voom would then give noisy observations the largest weights; the guide
+shows the trend with and without the filter.
+
+{api}`filter_by_expression` keeps a gene by edgeR's `filterByExpr` rule [@chen2016]: its
+CPM must reach $10 / \operatorname{median}(R) \cdot 10^6$, the CPM of 10 counts at the
+median effective library size, in at least $n$ samples, and its counts summed over all
+samples must reach 15. $n$ is the size of the smallest group of samples; with a design it
+is $1 / \max_i h_i$, the smallest inverse leverage of the design with an intercept added,
+which equals the smallest group's size for a one-way design. Beyond 10 samples, $n$ is
+relaxed to $10 + 0.7\,(n - 10)$. `min_count`, `min_total_count`, `large_n` and `min_prop`
+change these settings.
+
+{api}`limma_voom`, {api}`limma_trend` and {api}`dream_voom` filter each cell type with its
+own design, as the User's Guides of edgeR and limma do. Only the fixed effects of a design
+set $n$. A paired design such as `~ donor + condition` gives every sample a high leverage,
+so a gene needs counts in fewer samples than with the condition alone, and two designs of
+the same samples then test different genes.
+`expression_filter={"sample_groups": "condition"}` groups the samples by a column of the
+design's sample table instead of by the design, so that different designs of the same
+samples test the same genes.
+
+(samples-with-few-cells)=
+## Samples with few cells or counts
+
+A profile pooled from a handful of cells is noisy: it estimates the mean expression of the
+sample's cells of that type from few of them. In each cell type, {api}`limma_voom`,
+{api}`limma_trend` and {api}`dream_voom` fit only the samples that have at least
+`min_cells` cells there (10 by default; a cube that does not record cell numbers skips
+this rule), some counts, and at least `min_counts` counts over every gene of the cube (0
+by default), so that a sample with exactly `min_cells` cells stays in, as in decoupler's
+`filter_samples`. The scverse best practices keep samples with at least 10 cells and
+1,000 counts [@heumos2023], which `min_counts=1000` adds to `min_cells`. The design
+matrix of a cell type is then the rows of the design matrix for the samples left ([What
+runs in each cell type](#limma-voom-steps)), and the result records which samples each
+fit used and why the others were left out ({api}`DifferentialExpressionResult`).
+{api}`plot_cell_numbers` shows the entries of a cube that have fewer than `min_cells`
+cells.
+
+Leaving a sample out loses its information. Two alternatives keep every sample and weight
+it instead: prior weights from its number of cells ([Prior weights from the number of
+cells](#prior-weights-from-cells)) and sample quality weights estimated from the data
+([Sample quality weights](#sample-quality-weights)).
+
+(samples-at-a-glance)=
+## Samples at a glance
+
+Two figures place the samples of one cell type before any test, so that a sample far from
+its group, or a variable that the design leaves out, shows. From a cube, both take the
+cell type's log-CPM values, with TMM factors and an average count of 2
+([above](#log-cpm)), as the edgeR User's Guide prepares them for its MDS plot: without
+the samples that have too few cells (`min_cells`, 10 by default) or no counts, and over
+the genes that pass the filter with all samples in one group.
+
+- **The MDS plot** ({api}`plot_mds`), limma's `plotMDS` [@ritchie2015], measures the
+ distance between two samples by their leading log2 fold change, which is the root mean
+ square of the 500 log2 fold changes of largest size between the two, chosen afresh for
+ every pair. Classical scaling then places the samples so that their distances on the
+ plot approximate these [@gower1966].
+- **The principal components** ({api}`principal_components`, drawn by {api}`plot_pca`)
+ are those that DESeq2's `plotPCA` computes [@pearson1901; @love2014]. The 500 genes of
+ largest variance over the samples, each centred on its mean, make up the matrix
+ $X = U S V^\top$ of samples by genes, whose singular value decomposition gives the
+ samples' coordinates $U S$ and the genes' loadings $V$; each component explains its
+ squared singular value's share of their sum. The result, a {api}`PrincipalComponents`,
+ keeps the loadings, which say which genes a component is made of.
+
+The two measure distances differently: MDS compares each pair of samples by the genes that
+differ most between those two, and PCA compares every sample by the same 500 genes.
+`gene_selection="common"` makes the MDS plot a principal component plot. The sign of a
+component, or of an MDS dimension, is arbitrary: both make the coordinate of largest
+absolute value positive (of coordinates within a relative 1e-9 of it, the first), so that
+neither the order of the genes nor the sign of the values changes the result.
+
+(normalization-defaults)=
+## Defaults and choices
+
+- **Normalization.** TMM factors by default; `normalization=None` in {api}`limma_voom`,
+ {api}`limma_trend` and {api}`dream_voom` uses library sizes alone. On a matrix of counts,
+ {api}`upper_quartile_factors`, {api}`rle_factors` and {api}`median_ratio_size_factors`
+ give the alternatives. When a response changes many genes, check that the factors make
+ sense, as the guide does.
+- **The genes tested.** `expression_filter=True` applies edgeR's rule with each cell
+ type's design; a mapping changes its settings, or names the column whose values group
+ the samples (`"sample_groups"`); `False` tests every gene.
+- **The samples.** `min_cells` (10) and `min_counts` (0) set which samples enter each
+ cell type's fit; `min_counts=1000` adds the scverse threshold of counts.
+- **The prior count**, of {api}`log2_cpm` and {api}`limma_trend`: 2 and 3 by default, as
+ above.
+- **Looking at the samples.** `top`, 500 by default as in limma and DESeq2, sets how many
+ genes each distance of {api}`plot_mds` or each component of {api}`principal_components`
+ uses.
+
+(normalization-limits)=
+## Limits and differences
+
+- **Factors that differ between genes.** Normalization factors and size factors are one
+ per sample. DESeq2's normalization factors for every gene and sample are not
+ implemented ([Not implemented yet](#not-implemented-yet)).
+- **Refusals where edgeR returns values.** A sample whose counts are all zero has no
+ library size to normalize, and {api}`tmm_factors` raises an error where edgeR's TMM
+ with a given reference sample returns a factor of 1; an upper quartile of zero raises an
+ error where edgeR returns NaN factors; and {api}`filter_by_expression` given both
+ sample groups and a design raises an error, where edgeR ignores the design.
+- **Two samples.** {api}`principal_components` returns one component for two samples,
+ since the centred values of $n$ samples have at most $n - 1$ dimensions; DESeq2's
+ `plotPCA` returns a second, of rounding noise.
+
+(normalization-checks)=
+## Checks and further reading
+
+The factors, CPM and the filter are compared with edgeR 4.4.0 and DESeq2 1.46.0 on dense,
+sparse and tied counts: exact (1e-10), and the filter keeps the same genes
+([Validation](#validation-normalization)). The functions repeat R's floating-point
+operations wherever one decides a discrete outcome, such as TMM's reference sample or a
+cut-off of the filter, so that on the validation's inputs they agree to the last bit. The
+principal components are compared with DESeq2 1.46.0's `plotPCA`
+([Validation](#validation-pca-and-dot-plots)), and the MDS plot with limma 3.62.1's
+`plotMDS` ([Validation](#validation-plotting)): exact, up to the sign of each
+component.
+
+Further reading:
+
+- @robinson2010a introduced TMM normalization and showed that composition, a few
+ highly expressed genes taking reads from the others, biases comparisons that scale by
+ library size alone.
+- @anders2010 introduced the median-of-ratios size factors with DESeq.
+- @bullard2010 evaluated normalization and testing methods for mRNA-seq and proposed
+ scaling by the upper quartile of the counts.
+- @chen2016 described edgeR's quasi-likelihood pipeline from reads to pathways, with the
+ rule for filtering genes by expression that `filterByExpr` applies.
+- @heumos2023 reviewed best practices for single-cell analysis, with thresholds of cells
+ and counts for the samples of a pseudobulk analysis.
+
+[limma-voom and limma-trend](limma_voom.md), the next chapter, tests every cell
+type.
diff --git a/docs/source/statistics/over_representation_and_gsea.md b/docs/source/statistics/over_representation_and_gsea.md
new file mode 100644
index 0000000..28fec72
--- /dev/null
+++ b/docs/source/statistics/over_representation_and_gsea.md
@@ -0,0 +1,184 @@
+---
+description: Whether a pathway's genes are over-represented among the genes that change, or gather at the top or bottom of a ranking of all genes, by over-representation analysis and GSEA.
+---
+
+# Over-representation and GSEA
+
+The tests of [Gene-set tests](gene_set_tests.md) read the linear model of every gene.
+Often there is less to go on: a list of the genes that an analysis called significant,
+or one statistic for every gene, from any method. This chapter tests gene sets from
+those. {api}`gsea` is the default along a ranking: it asks whether each set's genes sit
+near the top or the bottom when all genes are ordered by a statistic, as fgsea computes
+it [@subramanian2005; @korotkevich2021]. {api}`over_representation` asks whether a set
+has more of its genes among those called significant than chance would give, as limma's
+`kegga` computes it [@young2010]. [Gene sets and
+pathways](../guides/gene_sets.ipynb) runs both on published data.
+
+(over-representation)=
+## Over-representation analysis
+
+Say that $n$ genes of a universe of $N$ are selected, such as the genes called
+significant, and that a set has $K$ genes in the universe, $k$ of them selected. Were the
+$n$ genes drawn at random from the universe, $k$ would be hypergeometric with mean
+$K n / N$, the overlap expected by chance, and the p-value is the chance of an overlap
+at least as large [@tavazoie1999]:
+
+$$
+P(X \ge k) = \sum_{j=k}^{\min(K, n)}
+\frac{\binom{K}{j} \binom{N - K}{n - j}}{\binom{N}{n}},
+$$
+
+which is also the p-value of Fisher's exact test, one-sided, for the table of the genes in
+and out of the set, selected or not [@fisher1922; @rivals2007].
+
+{api}`over_representation` selects the genes of a cell type and contrast of a
+differential-expression result, or of any table of results, whose adjusted p-value is
+below `alpha`. Like `kegga` and `goana` on a fit of limma, it tests the genes up and the
+genes down as two lists, and adds both together as a third. A list of genes from
+elsewhere is tested as it is, against a universe that you give. The p-values of each list
+are adjusted over its sets by Benjamini and Hochberg's method [@benjamini1995], and
+{api}`plot_over_representation` draws the sets with the smallest p-values in any list,
+with a dot for each list, as clusterProfiler compares lists of genes [@yu2012].
+
+(over-representation-universe)=
+### The universe
+
+The universe holds every gene that the analysis could have selected, which by default
+means every gene with a p-value. The genes of the genome, or of all the sets, include
+genes that were never expressed, or were filtered out before the tests, and so could not
+have been selected; they make the p-values too small [@timmons2015; @wijesooriya2022].
+Genes outside a universe that you give are left out, even when selected or in a set, and
+genes of the universe that belong to no set still count, as in `kegga`.
+
+(gsea)=
+## Gene set enrichment analysis
+
+Gene set enrichment analysis (GSEA) orders all genes by a statistic, such as a contrast's
+moderated t-statistic, and asks whether a set's genes crowd towards either end of the
+ranking [@subramanian2005]. It needs no cutoff, and the statistic can come from any
+method. A walk down the ranking of $N$ genes steps up at each of the set's $m$ genes and
+down at every other gene; after the $i$-th gene of the set, at rank $p_i$, it stands at
+
+$$
+\mathrm{RS}_i = \frac{\sum_{j \le i} |r_j|^q}{\sum_{j = 1}^{m} |r_j|^q}
+- \frac{p_i - i}{N - m},
+$$
+
+with $r_j$ the statistics of the set's genes and $q$ the `exponent`: 1 for the weighted
+score of GSEA, 0 for the unweighted one of @mootha2003. The enrichment score is the
+walk's furthest point from zero: positive when the set's genes gather at the top and
+negative when they gather at the bottom. The leading edge is the genes that drive the
+score: the set's genes up to the walk's extreme, counted from the top for a positive
+score and from the bottom for a negative one.
+
+{api}`gsea` ranks the statistics of a cell type and contrast of a differential-expression
+result, of a table of results, or of gene ids mapped to statistics, and tests each set
+with at least one gene in the ranking; a set of every ranked gene is not tested.
+{api}`plot_enrichment` draws the walk of one set, as fgsea's `plotEnrichment` does, and
+{api}`plot_gsea_table` the top sets with their genes on the ranking, as its
+`plotGseaTable` does.
+
+(gsea-p-values)=
+### p-values from random sets
+
+The scores of random sets of the same size give the null distribution, so that the test
+is competitive, like camera's, and it permutes genes, not samples. Dividing a score by
+the mean random score of its sign gives the normalized enrichment score, which lets sets
+of different sizes compare, and the p-value is the share of the random scores of its sign
+that reach it: with $n$ random scores of that sign, at least $1/(n + 1)$. fgsea's
+multilevel method, the default, reaches far smaller p-values [@korotkevich2021]. It draws
+random sets of the size of the set, keeps at each level the half with the higher scores
+and moves them by a Markov chain that keeps their scores above the level's median, so
+that each level about halves the probability, until the median reaches the observed
+score; `log2_error` estimates the standard deviation of $\log_2 p$. With the multilevel
+method, a set with under 10 random scores of its sign gets no p-value, as in fgsea.
+
+The random sets come from seeds that `rng` draws, through the Mersenne Twister
+[@matsumoto1998] seeded as C++ seeds it, as fgsea draws them: a seed gives the same
+results every time, and given fgsea's seeds, {api}`gsea` gives fgsea's results exactly.
+CytePool's routines are adapted from fgsea's, under its MIT licence ([Related
+software](../reference/related_software.md)).
+
+(over-representation-and-gsea-defaults)=
+## Defaults and choices
+
+- **GSEA or over-representation.** {api}`gsea` uses the whole ranking, without a cutoff.
+ {api}`over_representation` needs only the genes called significant, so it takes the
+ results of any method and lists from elsewhere. With a linear model at hand,
+ {api}`camera` uses every gene's statistic and allows for the correlation between
+ genes, so it is the sounder competitive test ([camera](#camera)).
+- **The statistic.** gsea ranks genes by the moderated t-statistic of a result
+ (`statistic_column="t"`); any column of a table, such as the log fold change, can rank
+ them instead.
+- **The weight of each gene.** `exponent=1` weighs the genes of a set by the size of
+ their statistics, as GSEA does; `exponent=0` weighs them alike.
+- **One end of the ranking.** `score_type="std"` takes the larger deviation of the walk
+ in either direction; `"pos"` and `"neg"` look only at the top or the bottom, as for
+ statistics that are all positive, such as $-\log_{10} p$.
+- **p-values.** The multilevel method by default, after 1,000 permutations, with 101 sets
+ at each level and p-values down to `eps`, 1e-50; `method="permutation"` takes the
+ permutation p-values alone, as fgsea's `fgseaSimple` does.
+- **Set sizes.** gsea tests sets of every size, as fgsea does; `min_size=15` and
+ `max_size=500` reproduce the GSEA program's defaults ([Gene-set
+ collections](#gene-set-collections)).
+- **The cutoff.** over_representation selects genes with an adjusted p-value below
+ `alpha`, 0.05, read from `p_value_column`, which may name another adjustment, such as
+ `"adjusted_p_value_across_groups"`. Without directions, `fold_change_column=None`, the
+ selected genes are tested as one list.
+
+(over-representation-and-gsea-limits)=
+## Limits and differences
+
+- **Genes taken as independent.** Both tests take genes, not samples, as the units drawn,
+ independently of each other, so the genes of a set that respond together make its
+ p-values too small ([Genes that respond together](#correlated-genes)).
+- **A cutoff and a bias.** Over-representation analysis depends on the cutoff that
+ selected the genes and ignores how much each one changes. In RNA-seq data, long and
+ highly expressed genes are called significant more often, which favours sets of such
+ genes [@young2010]; the adjustment for it that `kegga` and `goana` offer (`trend`) is
+ not implemented.
+- **Tied statistics.** As in fgsea, tied genes are ranked in the order given, so a set's
+ score can depend on that order; a warning counts the ties.
+- **Differences from kegga.** A set with no genes in the universe gets a row without a
+ p-value; `kegga` drops it from its table. For a fit of limma, `kegga` counts genes
+ without a p-value in its universe, and adjusts the p-values of only the genes that have
+ an id, so that which genes it selects depends on which genes have an id, such as a
+ symbol. CytePool takes a gene without a p-value as not tested, and the adjusted p-values
+ of the results as they are, the genes that the analysis called significant. `kegga` and
+ `goana` report no adjusted p-values of the sets.
+- **Differences from fgsea.** CytePool adjusts the p-values of the sets that have one.
+ When no set goes to the multilevel method, fgsea adjusts them as though the sets with
+ too few random scores of their sign had been tested too, which changes the adjusted
+ p-values of the sets it reports. A set tested alone draws its random sets with fgsea's
+ own sampler, as several sets do, where fgsea uses R's `sample.int`; random samples of
+ half of the genes or more are shuffled by the Fisher-Yates algorithm
+ [@durstenfeld1964], where fgsea calls C++'s `std::shuffle`, an algorithm that each C++
+ library chooses for itself. The results agree within Monte Carlo error. Genes without
+ an id or a statistic are left out, where fgsea refuses them, and the table lists the
+ sets in the order given, where fgsea sorts them by name.
+
+(over-representation-and-gsea-checks)=
+## Checks and further reading
+
+- Over-representation analysis, with its dot plot, is compared with limma 3.62.1's
+ `kegga` and R 4.4.3's `phyper`: exact (1e-10)
+ ([Validation](#validation-gene-set-files-and-ora)).
+- Gene set enrichment analysis and its figures are compared with fgsea 1.32.2: exact
+ (1e-10) given fgsea's seeds, and calibrated, within 4 standard errors over 20 runs, when
+ each program draws its own random sets ([Validation](#validation-gsea)).
+
+Further reading:
+
+- @subramanian2005 described gene set enrichment analysis with its weighted score, the
+ one that `gsea` computes by default; @mootha2003 had introduced it with the unweighted
+ score.
+- @korotkevich2021 introduced fgsea, whose multilevel method estimates p-values far
+ smaller than permutations can.
+- @timmons2015 and @wijesooriya2022 showed that over-representation analyses depend on
+ the universe of genes they test against: a universe of every gene of the genome,
+ rather than of the genes measured, makes sets look significant that are not.
+- @young2010 showed that RNA-seq calls long genes significant more often, and adjusted
+ the test of gene categories for it.
+
+[Pathway scores per sample](pathway_scores.md), the next chapter, gives every sample a
+score of each gene set.
diff --git a/docs/source/statistics/pathway_scores.md b/docs/source/statistics/pathway_scores.md
new file mode 100644
index 0000000..118eddd
--- /dev/null
+++ b/docs/source/statistics/pathway_scores.md
@@ -0,0 +1,189 @@
+---
+description: How active a pathway is in each sample, as one score per sample from GSVA, ssGSEA, combined z-scores or PLAGE, and how to test those scores with a design.
+---
+
+# Pathway scores per sample
+
+The tests of [Gene-set tests](gene_set_tests.md) and [Over-representation and
+GSEA](over_representation_and_gsea.md) ask one question of one contrast. A pathway score
+instead turns the expression of a set's genes in each sample into one number, so that
+pathways can be compared across samples, drawn as a heatmap, correlated with other
+measurements and tested with any design [@haenzelmann2013]. By default {api}`gsva`
+computes the scores by gene set variation analysis, and {api}`ssgsea`,
+{api}`combined_z_scores` and {api}`plage` compute the other three methods of the GSVA
+package. [Pathway and regulator activity](../guides/pathway_activity.ipynb) scores
+published data with GSVA and tests the scores.
+
+(pathway-score-values)=
+## The values scored
+
+Each method scores a samples x genes matrix of expression values, or a cell type of a
+cube. A cell type gives its samples with enough cells (`min_cells`, 10 by default, as for
+{api}`limma_voom`; [Samples with few cells or counts](#samples-with-few-cells)) and its
+genes that pass edgeR's filter ([Filtering by expression](#filtering-by-expression)), as
+log-CPM values ([log-CPM](#log-cpm)) or, with `values="counts"`, as counts: the values
+that GSVA's guide scores. NaN marks a missing value.
+
+Sets with fewer than `min_size` or more than `max_size` genes among those scored are left
+out. The result, a {api}`PathwayScores`, holds the scores of the samples by sets, with the
+table of the samples and the genes that each set used, and {api}`PathwayScores.table`
+lists the scores one per row; {api}`plot_pathway_scores` draws them as a heatmap, with
+the samples grouped by a variable.
+
+(gsva)=
+## GSVA
+
+GSVA first puts each gene on a common scale. A kernel estimate of the gene's
+distribution over the $n$ samples gives each value the probability that the gene's value
+is no higher,
+
+$$
+F_{ij} = \frac{1}{n} \sum_{k=1}^{n} \Phi\!\left(\frac{x_{ij} - x_{ik}}{h_i}\right)
+$$
+
+for gene $i$ in sample $j$, with a bandwidth $h_i$ of one quarter of gene $i$'s standard
+deviation, for continuous values such as log-CPM. For counts the kernel is the Poisson
+probability $P(Y \le x_{ij})$ with mean $x_{ik} + 1/2$, and without a kernel $F_{ij}$ is
+the share of the samples whose value is at most $x_{ij}$. Each sample ranks its $p$ genes
+by $F_{ij}$ and gives the gene of rank $r_{ij}$ the weight $|p/2 - r_{ij}|$, so that the
+genes at both ends of the ranking weigh most. A walk down the ranking rises at the genes
+of a set and falls at the others, as in [gene set enrichment analysis](#gsea), and the
+score is the walk's largest deviation above zero minus its largest deviation below: high
+for a set whose genes are higher than usual in the sample, low for one whose genes are
+lower, and between $-1$ and $1$.
+
+(ssgsea)=
+## ssGSEA
+
+Single-sample GSEA ranks the genes of each sample by their values and sums the same walk
+over all genes rather than taking its extreme, with the genes of the set weighted by
+their ranks to the power $\alpha = 1/4$ [@barbie2009]. By default the scores are then
+scaled by their range over every set and sample, as @barbie2009 scaled them, so that a
+score depends on the other sets and samples scored with it.
+
+(combined-z-scores)=
+## Combined z-scores
+
+Each gene is standardized across the samples, and a set scores the sum of its $k$ genes'
+z-scores over $\sqrt{k}$: a standard normal score if its genes are independent and
+standard normal [@lee2008].
+
+(plage)=
+## PLAGE
+
+PLAGE standardizes the genes of a set and scores the samples by the first right singular
+vector of their genes x samples matrix: the samples' coordinates on the direction along
+which the set's genes vary most together [@tomfohr2005]. Across the samples, a set's
+scores have mean zero and length one. The vector could as well point the other way;
+CytePool points it where the scores rise with the set's mean standardized expression.
+
+(how-pathway-scores-differ)=
+## How the scores differ
+
+ssGSEA ranks the values themselves, so that a set of highly expressed genes scores high
+in every sample. GSVA first sets each gene against its own distribution across the
+samples, so that a set scores high where its genes are higher than usual, compared with
+the other genes of the sample. Combined z-scores and PLAGE also set each gene against
+itself across the samples, but do not compare it with other genes: combined z-scores add
+the genes of a set alike, and PLAGE follows the pattern along which they vary most
+together. GSVA and ssGSEA thus ask how a set ranks within a sample, and combined z-scores
+and PLAGE how a sample stands among the others.
+
+(testing-scores)=
+## Testing scores with a design
+
+The scores of a set differ between conditions as the expression of a gene does, so they
+are tested as genes are, with limma's linear models, contrasts, moderated t-tests and
+adjusted p-values, as GSVA's guide does [@haenzelmann2013; @smyth2004]. {api}`limma`
+takes these steps in one call, with the {api}`Design` built for the cube. It fits the
+samples that have scores, so a sample that the scores leave out, such as one with too few
+cells, is not fitted either, and a design that the remaining samples no longer determine
+is fitted as {api}`limma_voom` fits a cell type ([Designs that cannot answer the
+question](#estimable-contrasts)). GSVA's guide notes that the scores of larger sets vary
+less, and lets the prior variance of the moderation follow the set sizes:
+`limma(scores, design, trend=scores.n_genes)` does the same. The activities of the [next
+chapter](activities.md) are tested alike.
+
+(pathway-scores-defaults)=
+## Defaults and choices
+
+- **The method.** {api}`gsva` by default, which compares a set's genes with the other
+ genes of a sample after setting each gene against its own distribution; {api}`ssgsea`
+ compares the values themselves; {api}`combined_z_scores` and {api}`plage` follow each
+ set across the samples without comparing it with other genes ([How the scores
+ differ](#how-pathway-scores-differ)).
+- **The kernel.** `kernel="auto"`, GSVA's default, chooses the Poisson kernel when the
+ values are stored as integers, such as counts, the Gaussian kernel for other values,
+ and no kernel from 200 samples on (`min_samples_without_kernel`); `"gaussian"`,
+ `"poisson"` and `"none"` choose one.
+- **The score of GSVA.** `score_type="difference"`, GSVA's default, subtracts the walk's
+ largest deviation below zero from its largest above, so that a set scores high where
+ its genes are high and low where they are low; `"kuiper"` adds the two, to score a set
+ whose genes sit at both ends of the ranking, and `"max_deviation"` takes the larger of
+ them, with its sign.
+- **The weights of the ranks.** `exponent` is GSVA's tau, 1, and ssGSEA's alpha, 1/4.
+- **ssGSEA's normalization.** `normalize=True` scales the scores by their range;
+ `normalize=False` keeps each score independent of the others.
+- **The values of a cube.** log-CPM values by default, and `values="counts"` for GSVA's
+ Poisson kernel; `min_cells` and `expression_filter` choose the samples and genes ([The
+ values scored](#pathway-score-values)).
+- **Set sizes.** Every size by default, as in GSVA; `min_size` and `max_size` limit them.
+- **Tests.** {api}`limma` with the cube's design, `trend=scores.n_genes` to let the prior
+ follow the set sizes, and `robust=True` for a robust prior.
+
+(pathway-scores-limits)=
+## Limits and differences
+
+- **The cost of GSVA.** GSVA estimates each gene's distribution from every pair of its
+ samples, so its time grows with the square of the number of samples, and the Poisson
+ kernel costs more than the Gaussian ([Timings](#validation-timings)).
+- **PLAGE's sign.** CytePool's scores rise with the set's mean standardized expression.
+ GSVA keeps the sign that its singular value decomposition happens to return, which
+ varies with the linear algebra library, so the two agree up to sign. For a set of one
+ gene, GSVA scores 1 in every sample and CytePool the gene's standardized values, over
+ their length.
+- **Missing values.** {api}`gsva` works on the observed values, as GSVA does with
+ `use = "na.rm"`, but a sample's walk falls only at its observed genes outside the set
+ and so ends at zero, where GSVA's also counts the missing genes and ends above zero.
+ GSVA's default, `use = "everything"`, gives scores without meaning, and GSVA stops on a
+ sample without any value, which here gets no scores. ssGSEA and combined z-scores
+ treat missing values as GSVA does, and PLAGE refuses them, as GSVA does; each
+ function's documentation says how.
+- **Inputs on which GSVA stops.** In ssGSEA, a set of every gene gets no score, and the
+ other scores are normalized as usual. Values stored as integers, from 200 samples on,
+ are scored without a kernel by `kernel="auto"`, as GSVA's documentation says; GSVA
+ 2.0.0 stops on both. The Poisson kernel refuses values that are not counts, where GSVA
+ stops on negative values and scores fractional ones.
+- **Ranks decided by the last bit.** Genes whose kernel estimates are equal in exact
+ arithmetic, such as genes with the same low counts in different samples, are ordered
+ by the last bit of the normal and Poisson probabilities, which CytePool computes
+ accurately but not always to the same bit as R. Such genes can take each other's
+ ranks, and the scores of their sets then differ slightly from GSVA's.
+- **Exact standard errors.** CytePool's {api}`limma` computes the standard errors of
+ contrasts exactly; they differ from those of limma's `contrasts.fit` only with missing
+ scores, or weights that differ between scores ([Contrasts](#contrasts)).
+
+(pathway-scores-checks)=
+## Checks and further reading
+
+The four methods and their heatmap are compared with GSVA 2.0.0, run as a separate
+program on the same data: exact (1e-10), PLAGE up to sign
+([Validation](#validation-pathway-scores)). {api}`limma` has no section of its own:
+`tests/test_score_tests.py` checks that its results are those of
+{api}`fit_linear_model`, {api}`contrasts_fit` and {api}`moderated_statistics`, which are
+compared with limma 3.62.1 ([Validation](#validation-linear-models) and
+[Validation](#validation-empirical-bayes)).
+
+Further reading:
+
+- @haenzelmann2013 introduced GSVA, which scores gene sets in every sample of microarray
+ and RNA-seq data.
+- @barbie2009 introduced single-sample GSEA, in a screen of the genes that cancers with
+ KRAS mutations depend on.
+- @lee2008 scored a pathway's activity in each sample by the combined z-scores of its
+ genes, and chose for each pathway the genes that best tell the conditions apart.
+- @tomfohr2005 scored pathways in each sample by the singular value decomposition of
+ their genes' expression.
+
+[Activities from networks](activities.md), the next chapter, scores regulators and
+pathways rather than gene sets.
diff --git a/docs/source/statistics/precision_weights.md b/docs/source/statistics/precision_weights.md
deleted file mode 100644
index dd10518..0000000
--- a/docs/source/statistics/precision_weights.md
+++ /dev/null
@@ -1,89 +0,0 @@
----
-description: How voom turns counts into log counts per million with a precision weight for every observation, how its span is chosen, and how sample quality weights and the number of cells pooled enter the weights.
----
-
-# Precision weights
-
-Log counts are noisier at low counts than at high ones, and a linear model that treats
-every observation alike gives the noisy ones too much say. voom estimates the
-mean-variance trend of the log counts and gives every observation a precision weight
-from it, so that the weighted linear model and the empirical Bayes moderation of limma
-apply to counts [@law2014]. [Normalization, filtering and sample
-quality](../guides/normalization_and_quality.ipynb) shows voom's trend and sample
-quality weights on data.
-
-## voom
-
-For counts $r_{gi}$ of gene $g$ in sample $i$ and effective library sizes $R_i$ (library
-sizes times normalization factors), voom works with the log2 counts per million
-
-$$
-y_{gi} = \log_2 \frac{r_{gi} + 0.5}{R_i + 1}\, 10^6 .
-$$
-
-The design is fitted to every gene by least squares, which gives fitted values
-$\hat\mu_{gi}$ and residual standard deviations $s_g$. The square roots $s_g^{1/2}$ are
-smoothed by LOWESS ({api}`lowess`; @cleveland1979) against each gene's average log
-count, $x_g = \bar y_g + \overline{\log_2(R_i + 1)} - \log_2 10^6$, and each observation
-gets the weight
-
-$$
-w_{gi} = \operatorname{lo}(\lambda_{gi})^{-4}, \qquad
-\lambda_{gi} = \log_2\!\left(2^{\hat\mu_{gi}} (R_i + 1) / 10^6\right),
-$$
-
-the inverse of the variance that the trend predicts at its fitted log count. {api}`voom`
-returns the log2 counts per million, the weights and the trend in a {api}`VoomResult`; a
-weighted fit ({api}`fit_linear_model` with `weights`) and {api}`moderated_statistics`
-complete limma-voom, which {api}`limma_voom` runs on every cell type of a cube.
-
-**The span.** The LOWESS uses a share of the genes, the span, in each local fit. By
-default it is chosen from the number of genes $n$, as limma's `voom` chooses it since
-limma 3.66: 1 up to 50 genes, and
-
-$$
-\text{span} = 0.3 + 0.7\,(50 / n)^{1/3}
-$$
-
-beyond, so 0.65 at 400 genes and 0.395 at 20,000 ({api}`choose_lowess_span`). Before
-limma 3.66 the default was 0.5; pass `span=0.5` to reproduce it. In a cube, each cell
-type's span follows the number of its genes that pass the filter, and its record keeps
-the span used.
-
-## Prior weights and the number of cells
-
-voom takes prior weights for samples, observations or genes: the trend is fitted with
-them, and the weights returned are the trend's weights times the prior weights. A
-pseudobulk profile sums the counts of its cells, so its log counts per million estimate
-the mean expression of those cells, whose variance falls as one over their number: the
-precision of a profile is proportional to its number of cells, as the dreamlet paper
-argues [@hoffman2026a]. `prior_weights="n_cells"` in {api}`limma_voom` passes each
-sample's number of cells as its prior weight.
-
-## Sample quality weights
-
-Some samples follow the model less closely than others: pooled from few cells, sequenced
-less deeply, or taken from an outlying donor. Removing them loses power; keeping them
-adds noise. Sample quality weights down-weight them instead [@ritchie2006; @liu2015].
-The model, for gene $g$ and sample $j$ with prior weights $w_{gj}$, is
-
-$$
-E(y_{gj}) = x_j' b_g, \qquad \operatorname{var}(y_{gj}) = \sigma_g^2\, e^{u_j} / w_{gj},
-$$
-
-with the log variance factors $u = Z\gamma$ shared by every gene and summing to zero.
-The sample weights are $e^{-u_j}$, whose product is one. {api}`sample_quality_weights`
-estimates $\gamma$ by residual maximum likelihood [@smyth2002] or gene by gene, as
-limma's `arrayWeights` does, with a prior worth 10 genes at equal weights that keeps the
-weights near one when the evidence is weak.
-
-{api}`voom_with_quality_weights` combines them with voom as limma's
-`voomWithQualityWeights` does: voom; sample weights from its log counts per million and
-weights; voom again with those sample weights as prior weights; sample weights again;
-and the product of the new trend's weights and the sample weights.
-`quality_weights=True` in {api}`limma_voom` runs it in every cell type. The sample
-weights and the weights from the number of cells are alternatives: limma's
-`voomWithQualityWeights` takes no prior weights, and CytePool refuses the two together.
-
-In a design that pairs two samples per donor, both samples of a donor have equal and
-opposite residuals, so an outlying sample pulls its partner's weight down with it.
diff --git a/docs/source/statistics/replication.md b/docs/source/statistics/replication.md
new file mode 100644
index 0000000..59262df
--- /dev/null
+++ b/docs/source/statistics/replication.md
@@ -0,0 +1,289 @@
+---
+description: Which changes replicate across studies, by SumRank, prediction in held-out studies, combined p-values, partial conjunctions and the agreement between pairs of studies.
+---
+
+# Replication
+
+[Meta-analysis](meta_analysis.md) combined the studies' estimates into one per gene.
+Whether a change replicates is another question: that the
+gene changes in study after study, not that the studies together find it. {api}`sumrank`
+asks it by default: each study ranks the genes, and every gene's ranks are summed across
+the studies [@nakatsuka2025]. [Replication across
+studies](../guides/replication_across_studies.ipynb) runs it on six simulated cohorts, with
+the other methods of this chapter.
+
+(sum-of-ranks)=
+## SumRank
+
+A meta-analysis weighs each study by its precision, so that one or two precise studies
+can carry a gene; the random-effects model widens the interval when the studies disagree
+([Defaults and choices](meta_analysis.md#meta-analysis-defaults)). SumRank asks instead
+that a gene rank high in every study, or, with fewer ranks summed, in most, so that one
+study alone cannot carry a gene, however small its p-value there. In the paper that
+introduced it, its genes predicted case status in studies left out better than those of a
+random-effects meta-analysis [@nakatsuka2025]. {api}`sumrank` computes it for one cell
+type and contrast of the studies' results, a {api}`StudyResults`, and returns a
+{api}`SumRankResult`, whose table ({api}`SumRankResult.features`) gives each gene's
+statistics and p-values, up and down; {api}`fit_sumrank` computes it from arrays of the
+studies' signed statistics, into a {api}`SumRankFit`.
+
+Within each study, the genes that every study tested are ranked by their signed
+$-\log_{10}$ p-value, the gene most up first, $r = 1$, and the gene most down last,
+$r = G$; tied genes share their average rank. The normalized ranks
+$u = (r - 1) / (G - 1)$ run from 0 to 1. A gene's statistic adds up its best $m$ of its
+$k$ normalized ranks, one from each of the $m$ studies that rank it highest, and is capped
+at their mean under the null:
+
+$$
+S_g = \min\Big(\sum_{j=1}^{m} u_{g(j)},\ \frac{m}{2}\Big), \qquad
+m = \lfloor p\,k \rfloor,
+$$
+
+with $u_{g(1)} \le \dots \le u_{g(k)}$ and the proportion $p$ of the studies
+(`proportion_of_studies`), or $m$ given itself (`n_studies_summed`). By default every study
+is summed, $m = k$. The paper summed every study when there were few, 3 to 6, and chose $p$
+by cross-validation when there were many: 0.65 of 17 studies of Alzheimer's disease and
+0.55 of 16 of COVID-19. Genes down have the statistic of the ranks $1 - u$. A gene missing
+from some study's results, because the study did not measure it or its expression filter
+left it out, is not ranked, as in the paper, which ranked the genes that every study
+measured: a rank says where a gene stands among the genes ranked with it.
+
+If each study ranked the genes at random, the normalized ranks would be independent
+uniform variables, and $S_g$ below the cap would follow the Irwin-Hall distribution of a
+sum of $m$ of them [@irwin1927; @hall1927], whose distribution function gives the
+Irwin-Hall p-value. With every study summed this distribution holds, but for the
+discreteness of the ranks, which is small among many genes. The sum of the best $m$ of $k$
+ranks is smaller than a sum of $m$ ranks, so with $m < k$ these p-values are much too
+small, and {api}`sumrank` warns when it gives them: in six simulated studies without
+changes, 4.8% of the p-values fell at or below 0.05 with every study summed, and 37% with
+the best three of six ([Validation](#validation-sumrank)).
+
+The paper takes its p-values from permutations instead. With `n_permutations`,
+{api}`sumrank` permutes the variable whose levels the contrast compares (or `permute`) in
+each study's design, among the samples of its fit of the cell type, from a seeded
+`numpy.random.Generator`, and runs the study's {api}`limma_voom` or {api}`limma_trend`
+again, with the design and the settings that its result records. Pooled over the genes and
+the permutations, the permuted statistics make one null distribution of $N$ values,
+against which a gene of statistic $s$ has the p-value
+
+$$
+p = \frac{1 + \#\{\text{null statistics} \le s\}}{1 + N},
+$$
+
+never zero [@phipson2010]. This pooling gives small p-values from few permutations: 100
+permutations of 10,000 genes reach $10^{-6}$. Each permutation runs every study's analysis
+once, for one cell type ([Timings](#validation-timings) gives the cost). {api}`plot_sumrank`
+draws the p-values as a Manhattan plot, a point per gene in alphabetical order, as
+SumRank's authors draw them.
+
+(held-out-prediction)=
+## Prediction in held-out studies
+
+Genes that replicate should tell cases from controls in a study that had no part in
+choosing them. {api}`held_out_prediction` leaves each study of a {api}`StudyResults` out in
+turn and chooses, in each cell type, `n_features` genes up and as many down from the other
+studies' results (50 by default), by SumRank and by the random-effects meta-analysis with
+REML and the Knapp-Hartung adjustment, the paper's comparison. It scores each of the
+held-out study's cases and controls, the two levels that the contrast compares, and reports
+the area under the ROC curve of cases against controls [@hanley1982], by study and method,
+in a {api}`HeldOutPrediction` ({api}`HeldOutPrediction.auc`).
+
+A sample's score builds on UCell's rank score [@andreatta2021]: in each cell type, how high
+the sample's profile ranks the chosen genes among all its genes, rescaled to [0, 1] across
+the study's samples, with the genes up adding to the score and the genes down taking from
+it; the cell types' scores are summed, as the authors' code sums them. A sample without a
+profile in a cell type takes the mean of the others' scaled scores there. The genes chosen
+and the scores are in {api}`HeldOutPrediction.features` and
+{api}`HeldOutPrediction.scores`, and {api}`plot_held_out_auc` draws the AUC of every study
+by method. An AUC of 0.5 says that the score does not separate cases from controls, and
+below 0.5 that the genes predict the wrong direction.
+
+(combined-p-values)=
+## Combined p-values
+
+Where the studies give p-values rather than comparable effects, they can be combined. For
+every gene that $k$ studies tested, with p-values $p_1, \dots, p_k$, {api}`combine_p_values`
+tests the hypothesis that none of these studies has an effect, by the methods of scipy's
+`combine_pvalues` and the R package metap, and returns a {api}`CombinedPValues`. With
+$p_{(1)} \le \dots \le p_{(k)}$ in order:
+
+| Method | Statistic | Its distribution without effects |
+| --- | --- | --- |
+| `"fisher"` [@fisher1932] | $X = -2 \sum \log p_i$ | $\chi^2_{2k}$, upper tail |
+| `"stouffer"` [@stouffer1949; @liptak1958] | $Z = \sum w_i z_i / \sqrt{\sum w_i^2}$, $z_i = \Phi^{-1}(1 - p_i)$ | standard normal, upper tail |
+| `"tippett"` [@tippett1931] | $p_{(1)}$ | $1 - (1 - p_{(1)})^k$ |
+| `"wilkinson"` [@wilkinson1951] | $p_{(r)}$ | beta with parameters $r$ and $k - r + 1$ |
+| `"pearson"` [@pearson1933] | $X = -2 \sum \log (1 - p_i)$ | $\chi^2_{2k}$, lower tail |
+| `"mudholkar_george"` [@george1983] | $\sum \log((1 - p_i)/p_i)$, scaled | Student's t on $5k + 4$ degrees of freedom |
+| `"simes"` [@simes1986] | $\min_j k\, p_{(j)} / j$ | the p-value itself |
+
+Fisher's and Tippett's methods respond to the strongest study; Stouffer's, the logit and
+Pearson's to evidence that the studies share. Stouffer's method weighs the studies equally
+unless given `weights`, usually the square roots of their sample sizes [@zaykin2011]. A
+small p-value of Wilkinson's maximum ($r = k$) says that every study has some evidence, not
+that every study has an effect, which the partial conjunction below tests. A gene that some
+studies did not test is combined over the others, and `n_studies` says how many; one study
+gives its own p-value, except with the logit, which approximates its statistic's
+distribution even then. p-values of exactly 0 or 1 are taken as the limits that the
+methods reach.
+
+Each method expects one-sided p-values of one direction of effect, so that an effect the
+other way gives a p-value near 1. Two-sided p-values can be combined as they are, but they
+then ask whether some study has an effect either way, and opposite effects do not cancel.
+Given the signs of the studies' effects in `directions`, such as their log2 fold changes,
+{api}`combine_p_values` makes each two-sided p-value one-sided for each direction, $p/2$
+where the effect points that way and $1 - p/2$ where it points the other, as metap's
+`two2one` does; it combines each direction and doubles the smaller result, up to 1
+[@owen2009]; for Stouffer's method, that is the two-sided p-value of $Z$. Each method uses
+$p/2$ as it is, never as $1 - (1 - p/2)$, so that p-values near 1e-300 keep their digits.
+
+(partial-conjunction)=
+## An effect in at least u studies
+
+One study's effect makes the hypothesis of no effect in any study false, so a small
+combined p-value does not say that an effect replicates. {api}`partial_conjunction` tests
+the hypothesis that fewer than $u$ of the $n$ studies that tested a gene have an effect
+[@benjamini2008]. Up to $u - 1$ studies with effects could explain the $u - 1$ smallest
+p-values, so the test sets them aside and combines the others, $p_{(u)}, \dots, p_{(n)}$:
+by Simes's method, $\min_{i \ge u} (n - u + 1)\, p_{(i)} / (i - u + 1)$, the default, or by
+Fisher's or Stouffer's (unweighted). With $u = 1$ this is the test of no effect in any
+study; with $u = n$ every method returns the largest p-value, $p_{(n)}$. With
+`directions`, the $u$ effects must point the same way. Benjamini and Hochberg's adjustment
+of these p-values over the genes controls the false discovery rate of the claims that a
+gene has an effect in at least $u$ studies [@benjamini2008]. It gains this at a cost in
+power: the change must be clear in $u$ studies, each on its own.
+
+(pi1-between-studies)=
+## How well one study replicates another
+
+A gene that one study finds and another does not may still change in the other, by too
+little for it to find. Take the genes that the discovery study finds, and their p-values
+in the replication study: one minus Storey's estimate of the proportion of these p-values
+without an effect, $\pi_1 = 1 - \pi_0$ ([The proportion of true
+nulls](multiple_testing.md#proportion-of-true-nulls)), estimates the share of the genes
+that change in the replication study, whether it finds them or not [@storey2003]. It is not
+symmetric: it reads the genes of one study in the p-values of the other. The sign
+agreement, the share of those genes whose log2 fold changes point the same way in both
+studies, says whether the changes agree in direction.
+
+By default $\pi_0$ is estimated at $\lambda = 0.5$, which suits the few dozen p-values of
+one study's findings better than qvalue's smoother ([Defaults and
+choices](multiple_testing.md#multiple-testing-defaults)); `null_proportion={}` asks for the
+smoother.
+
+(replication-tables)=
+## The replication tables
+
+{api}`replication` reads the studies' results, a {api}`StudyResults`, for one cell type
+and contrast, and returns a {api}`ReplicationResult` with four tables, each of which
+answers one question:
+
+- `features`: does each gene replicate? For every gene: how many studies tested it and how
+ many find it, with an adjusted p-value below `alpha`, up and down; its combined p-value,
+ by Stouffer's method with each study weighted by the square root of its number of samples
+ and signed by its log2 fold change; the p-value of the partial conjunction test that at
+ least `at_least` studies, two by default, have an effect in one direction, by Simes's
+ method; both adjusted over the genes; the combined direction and the share of the
+ studies that agree with it; and Cochran's $Q$, $I^2$ and $\tau^2$ of its log2 fold
+ changes ([Tests and heterogeneity](#heterogeneity)).
+- `study_pairs`: how well does each study replicate each other? For every ordered pair,
+ $\pi_1$ and the sign agreement in the second study of the genes that the first finds,
+ and how many of them the second finds too.
+- `found_in`: how many genes do at least $k$ studies find, for every $k$, and in one
+ direction?
+- `studies`: each study's number of samples and weight, and how many genes it tested and
+ finds.
+
+{api}`plot_study_agreement` shows $\pi_1$ or the sign agreement for every pair of studies
+as a heatmap, and {api}`plot_replicated_genes` the number of genes found by each number of
+studies.
+
+(replication-defaults)=
+## Defaults and choices
+
+- **Which genes replicate.** {api}`sumrank` by default, which asks that a gene rank high in
+ study after study and weighs every study alike; {api}`partial_conjunction` to claim a change
+ in at least $u$ studies, with a false discovery rate over the genes; a
+ [meta-analysis](meta_analysis.md) to estimate each gene's change with its uncertainty.
+- **SumRank's ranks and p-values.** Every study summed, with Irwin-Hall p-values, by
+ default; fewer summed (`n_studies_summed` or `proportion_of_studies`) needs permutations
+ (`n_permutations`). Held-out prediction for a few proportions of studies summed shows which
+ predicts best, as the paper chose its proportions.
+- **Which combination of p-values.** Fisher's method finds genes that any study changes;
+ Stouffer's, weighted by the square roots of the studies' sizes and the default of
+ {api}`replication`, rewards evidence that the studies share. Neither says that a change
+ replicates, which {api}`partial_conjunction` tests (`at_least`, 2, and
+ `conjunction_method`, Simes's by default).
+- **$\pi_1$.** $\pi_0$ at $\lambda = 0.5$ by default; `null_proportion` chooses qvalue's
+ smoother (`{}`) or its bootstrap (`{"method": "bootstrap"}`) for studies that find many
+ genes.
+- **Held-out prediction.** `n_features` (50) sets how many genes up and down are chosen in
+ each cell type, and `groups` the cell types scored.
+
+(replication-limits)=
+## Limits and differences
+
+- **Independent studies.** Combined p-values and partial conjunctions take the studies as
+ independent, with p-values uniform without effects; for discrete tests they are
+ conservative. {api}`replication` reads limma's two-sided p-values with the signs of the
+ log2 fold changes, so it needs a contrast, not an F-test.
+- **Not implemented.** The r-values of a primary study and its follow-up [@heller2014],
+ metap's methods of Edgington, Lancaster, the inverse t, the mean z and vote counting,
+ weights for the studies of SumRank, and permutations of designs with random effects
+ ([Not implemented yet](#not-implemented-yet)). The proportion of studies that SumRank
+ sums for held-out prediction is given, not chosen: a loop over the proportions chooses
+ it, as the guide does.
+- **SumRank's p-values.** The Irwin-Hall p-value is the lower tail of the distribution. The
+ paper calls its Irwin-Hall p-values two-sided, which would double them, and its authors'
+ code takes the Irwin-Hall density at the statistic (`dirwin.hall` of the R package
+ unifed), which is not a probability; both order the genes as the statistic does, so the
+ permutation p-values do not depend on it. Permutation p-values count ties as at least as
+ extreme and add one, so that none is zero, where the authors' code takes the share of
+ null statistics strictly below a gene's, 0 for a gene below them all. The authors' code
+ also forms each gene's sum in floating point, in the gene's own order, so that equal
+ statistics can compare as unequal, where here they are exact; it stops when it sums one
+ study; and it takes $\lfloor p\,k \rfloor$ in floating point, 56 for 0.57 of 100
+ studies, where CytePool rounds $p\,k$ first and sums 57.
+- **SumRank's permutations.** Each study's labels are shuffled among the samples of its own
+ fit, which keeps its numbers of cases and controls; the paper permutes them among all the
+ individuals of a dataset, for all its cell types at once, and draws again until every
+ cell type has at least two cases and two controls.
+- **Held-out prediction.** The paper fits a logistic regression of case status on the score
+ in the other studies, and takes the AUC of its predictions in the held-out study from
+ pROC, which by default compares the medians of the cases and controls and takes the
+ direction in which the cases score higher [@robin2011]. A regression on one score is
+ monotone in it, so that AUC is the score's own in the direction pROC chooses: a score
+ that ranks controls above cases also scores above 0.5. Here the cases must score higher,
+ as genes up minus genes down should make them, and no regression is needed. The profiles
+ are summed counts, which rank a sample's genes as their mean over its cells does; the
+ paper ranked Seurat's average of the cells' normalized expression, which weighs each
+ cell's counts by its depth.
+- **Combined p-values.** metap leaves out p-values of 0, and of 1 for `sumz` and `logitp`,
+ with the warning "some studies omitted", and needs two studies. CytePool takes p-values
+ of 0 and 1 as limits, and one study gives its own p-value, except with the logit.
+
+(replication-checks)=
+## Checks and further reading
+
+- Combined p-values and partial conjunctions are compared with scipy 1.18.1, metap 1.1 and
+ 400-digit arithmetic, the proportion of true nulls with qvalue 2.38.0, and the
+ replication tables with metap, qvalue and metafor 5.0.1: exact (1e-10; close, 1e-6, for
+ REML) ([Validation](#validation-combined-p-values-and-replication)).
+- SumRank and held-out prediction are compared with the authors' code (Apache-2.0, commit
+ 46392f66), with 200-digit arithmetic for the Irwin-Hall distribution, and with scipy
+ 1.18.1 and scikit-learn 1.9.1 for the scores and AUCs: exact (1e-12), and calibrated for
+ p-values from random permutations ([Validation](#validation-sumrank)).
+
+Further reading:
+
+- @nakatsuka2025 introduced SumRank, a meta-analysis of ranks across studies, whose genes
+ predicted disease in studies left out better than those of a random-effects
+ meta-analysis.
+- @benjamini2008 introduced partial conjunction tests, which claim an effect in at least
+ $u$ of $n$ studies.
+- @phipson2010 showed that p-values from random permutations should never be zero, and
+ how to compute them.
+
+This is the last chapter. [Choosing a method](choosing_a_method.md) maps every question to
+its method, and the [Validation](../validation.md) page gives how closely each agrees with
+the tool that defined it.
diff --git a/docs/source/validation.md b/docs/source/validation.md
index 1727d95..777f479 100644
--- a/docs/source/validation.md
+++ b/docs/source/validation.md
@@ -9,12 +9,13 @@ Every area of CytePool has a script in
[`validation/`](https://github.com/NygenAnalytics/CytePool/tree/master/validation) that
compares it with an independent reference: the package that defined the method, run
as a black box, or a high-precision computation. A script generates its inputs from
-fixed random seeds, runs the reference as a separate program on the same inputs,
+fixed random seeds, or reads published data, runs the reference as a separate program
+on the same inputs,
exchanging data through files in a temporary directory (float64 values as binary, so
they compare bit for bit), runs CytePool, prints a table with one row per check, and
exits with an error when any check falls outside its tolerance.
-This page summarises the tables the scripts printed when they were run against the
+This page summarizes the tables the scripts printed when they were run against the
package in this revision of the repository: on **2026-10-26** for `plotting.py`,
`variance_partitioning.py`, `gene_set_files_and_ora.py`, `kenward_roger_and_f_tests.py`,
`gsea.py`, `cube_collection.py`, `pathway_scores.py`,
@@ -32,52 +33,67 @@ cubes and their files (`cube.py`, `pool_cells.py`, `cube_files.py`, `cube_reads.
same table again after changes to the code it checks; and on **2026-10-24** for the
others, with the
reference versions given in each section. Every script exited without a failure.
-Each row below is a family of checks: it gives the number of printed checks it
-covers and the largest difference among them. Run a script to see every check.
+In the table of each section, a row is a family of checks: it gives the number of
+printed checks it covers and the largest difference among them. Run a script to see
+every check.
## Summary
-| Script | Area | Reference | Checks | Outcomes | Run time | Peak memory |
-| --- | --- | --- | --- | --- | --- | --- |
-| [`cube.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube.py) | Pseudobulk cubes | scanpy 1.12.4 | 20 | 19 pass, 1 information | 12.5 s | 926 MiB |
-| [`pool_cells.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pool_cells.py) | Pooling cells into cubes | scanpy 1.12.4, decoupler 2.2.0 | 27 | 27 pass | 30.2 s | 1,725 MiB |
-| [`cube_files.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_files.py) | Cube files | zarr-python 3.4.1 | 7 | 7 pass | 3.1 s | 146 MiB |
-| [`cube_reads.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_reads.py) | Reads of selected features | zarr-python 3.4.1 | 5 | 5 pass | 1 min 46 s | 500 MiB |
-| [`cube_fingerprints.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_fingerprints.py) | Cube fingerprints | the documented encoding, with zarr-python 3.4.1 | 7 | 7 pass | 4.4 s | 122 MiB |
-| [`anndata_conversion.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/anndata_conversion.py) | AnnData and MuData | scanpy 1.12.4, decoupler 2.2.0, LIANA 1.10.0, anndata 0.13.4, mudata 0.4.1 | 18 | 18 pass | 1 min 5 s | 1,483 MiB |
-| [`cube_collection.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_collection.py) | Several studies | anndata 0.13.4, pandas 3.1.0, limma 3.62.1, edgeR 4.4.0 | 15 | 14 pass, 1 control | 26.7 s | 495 MiB |
-| [`normalization.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/normalization.py) | Normalization and filtering | edgeR 4.4.0, DESeq2 1.46.0 | 50 | 46 pass, 3 deliberate differences, 1 control | 72.9 s | 745 MiB |
-| [`design.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/design.py) | Designs and contrasts | R 4.4.3, limma 3.62.1, lme4 2.0.6 | 27 | 26 pass, 1 information | 4.3 s | 221 MiB |
-| [`linear_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/linear_models.py) | Linear models | limma 3.62.1 | 218 | 200 pass, 18 information | 4.5 s | 177 MiB |
-| [`mixed_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/mixed_models.py) | Mixed models | R 4.4.3, lme4 2.0.6, lmerTest 3.2.1 | 135 | 102 pass, 29 information, 4 deliberate differences | 5 min 28 s | 330 MiB |
-| [`voom.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/voom.py) | Precision weights for counts | limma 3.62.1, edgeR 4.4.0 | 118 | 111 pass, 7 deliberate differences | 15.3 s | 310 MiB |
-| [`sample_quality_weights.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sample_quality_weights.py) | Sample quality weights | limma 3.62.1, edgeR 4.4.0 | 89 | 83 pass, 6 deliberate differences | 12.1 s | 206 MiB |
-| [`negative_binomial_fits.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/negative_binomial_fits.py) | Negative binomial models | edgeR 4.4.0, mpmath 1.3.0 | 121 | 78 pass, 30 information, 13 deliberate differences | 14.2 s | 187 MiB |
-| [`quasi_likelihood.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/quasi_likelihood.py) | Quasi-likelihood F-tests | edgeR 4.4.0, limma 3.62.1, mpmath 1.3.0 | 223 | 210 pass, 13 deliberate differences | 5.1 s | 186 MiB |
-| [`deseq2_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/deseq2_tests.py) | DESeq2's dispersions and tests | DESeq2 1.46.0, PyDESeq2 0.5.4, mpmath 1.3.0 | 45 | 28 pass, 11 information, 6 deliberate differences | 2 min 16 s | 814 MiB |
-| [`empirical_bayes.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/empirical_bayes.py) | Empirical Bayes moderation | limma 3.62.1 | 150 | 146 pass, 4 deliberate differences | 4.8 s | 195 MiB |
-| [`gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_tests.py) | Gene-set tests | limma 3.62.1 | 93 | 79 pass, 8 deliberate differences, 6 information | 6.9 s | 278 MiB |
-| [`rotation_gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/rotation_gene_set_tests.py) | Rotation gene-set tests | limma 3.62.1 | 103 | 81 pass, 20 deliberate differences, 2 information | 20.9 s | 190 MiB |
-| [`gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py) | Gene-set files and over-representation | R 4.4.3, limma 3.62.1, decoupler 2.2.0 | 34 | 29 pass, 5 deliberate differences | 4.7 s | 297 MiB |
-| [`gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py) | Gene set enrichment analysis | R 4.4.3, fgsea 1.32.2 | 24 | 23 pass, 1 deliberate difference | 7 min 3 s | 323 MiB |
-| [`pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py) | Pathway scores | R 4.4.3, GSVA 2.0.0 | 24 | 19 pass, 3 deliberate differences, 2 information | 10.7 s | 949 MiB |
-| [`activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py) | Pathway and regulator activity | decoupler 2.2.0, progeny 1.28.0, scipy 1.18.1 | 89 | 82 pass, 5 deliberate differences, 2 information | 2 min 39 s | 987 MiB |
-| [`multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py) | Multiple testing | R 4.4.3 | 15 | 15 pass | 2 min 56 s | 202 MiB |
-| [`differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py) | Differential expression | limma 3.62.1, edgeR 4.4.0 | 32 | 28 pass, 4 deliberate differences | 1.8 s | 189 MiB |
-| [`differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py) | Options for pseudobulk differential expression | limma 3.62.1, edgeR 4.4.0 | 66 | 66 pass | 5.0 s | 182 MiB |
-| [`dream.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/dream.py) | dream | variancePartition 1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3 | 63 | 53 pass, 5 information, 5 deliberate differences | 30.5 s | 390 MiB |
-| [`kenward_roger_and_f_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/kenward_roger_and_f_tests.py) | Kenward-Roger's tests and F-tests | R 4.4.3, variancePartition 1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3, pbkrtest 0.5.3 | 143 | 115 pass, 16 information, 12 deliberate differences | 3 min 59 s | 390 MiB |
-| [`variance_partitioning.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/variance_partitioning.py) | Variance partitioning | variancePartition 1.36.2, lme4 1.1-38, ggplot2 4.0.3 | 31 | 22 pass, 6 information, 3 deliberate differences | 63.1 s | 394 MiB |
-| [`interferon_stimulated_pbmcs.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/interferon_stimulated_pbmcs.py) | Differential expression on published data | limma 3.62.1, edgeR 4.4.0 | 41 | 17 pass, 24 information | 15.2 s | 617 MiB |
-| [`composition_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/composition_tests.py) | Cell-type composition | speckle 1.6.0, limma 3.62.1 | 32 | 25 pass, 7 deliberate differences | 13.1 s | 860 MiB |
-| [`meta_analysis.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/meta_analysis.py) | Meta-analysis | metafor 5.0.1 | 75 | 52 pass, 20 information, 3 deliberate differences | 59.5 s | 256 MiB |
-| [`combined_p_values_and_replication.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/combined_p_values_and_replication.py) | Combined p-values and replication | scipy 1.18.1, mpmath 1.3.0, R 4.4.3, metap 1.1, qvalue 2.38.0, metafor 5.0.1 | 54 | 47 pass, 7 deliberate differences | 67.4 s | 444 MiB |
-| [`sumrank.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sumrank.py) | SumRank and held-out prediction | R 4.4.3, the authors' code (commit 46392f66), mpmath 1.3.0, scipy 1.18.1, scikit-learn 1.9.1 | 23 | 13 pass, 9 information, 1 deliberate difference | 44.9 s | 399 MiB |
-| [`plotting.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/plotting.py) | Figures | limma 3.62.1, edgeR 4.4.0, DESeq2 1.46.0, metafor 5.0.1, speckle 1.6.0 | 26 | 23 pass, 2 information, 1 deliberate difference | 25.4 s | 810 MiB |
-| [`pca_and_dot_plots.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pca_and_dot_plots.py) | Principal components and dot plots | R 4.4.3, DESeq2 1.46.0, edgeR 4.4.0, ggplot2 4.0.3, scanpy 1.12.4 | 13 | 11 pass, 1 deliberate difference, 1 information | 19.8 s | 671 MiB |
-| [`distributions.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/distributions.py) | Distributions and special functions | mpmath 1.3.0 | 51 | 51 pass | 12 min 28 s | 174 MiB |
-| [`smoothing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/smoothing.py) | LOWESS and spline bases | R 4.4.3 | 90 | 87 pass, 1 information, 2 deliberate differences | 1.5 s | 175 MiB |
-| **All** | | | **2,397** | **2,065 pass, none failed; 186 information, 144 deliberate differences, 2 controls** | | |
+Each method is listed with the functions that implement it, the reference it is checked
+against and its version, the tier with the bounds its checks use, and the section that
+gives every check. The sections are grouped as the chapters are, and each opens with the
+checks its script printed, their outcomes, its run time and its peak memory.
+
+| Method | Functions | Checked against | Tier and bound | Section |
+| --- | --- | --- | --- | --- |
+| Pseudobulk profiles pooled from cells | {api}`from_cells` | scanpy 1.12.4, decoupler 2.2.0 | exact: identical sums of whole counts, 1e-10 otherwise | [Pooling cells into cubes](#validation-pool-cells) |
+| Bulk counts as a cube | {api}`from_bulk` | decoupler 2.2.0 | exact: identical | [Pooling cells into cubes](#validation-pool-cells) |
+| Merging groups and pooling samples; means, variances and detection | {api}`Cube` | scanpy 1.12.4 | exact: identical counts, 1e-10; variances from raw moments to a bound derived for each case | [Pseudobulk cubes](#validation-cube) |
+| Cube files | {api}`write_cube`, {api}`read_cube` | zarr-python 3.4.1 | exact: identical | [Cube files](#validation-cube-files) |
+| Reads of selected features from cube files | {api}`read_cube`, {api}`Cube.select` | zarr-python 3.4.1 | exact: identical, 1e-12 for merged groups and pooled samples | [Reads of selected features](#validation-cube-reads) |
+| Fingerprints of cubes | {api}`Cube.fingerprint`, {api}`Cube.digests` | the documented encoding, with zarr-python 3.4.1 | exact: identical digests | [Cube fingerprints](#validation-cube-fingerprints) |
+| AnnData and MuData layouts of scanpy, decoupler and LIANA | {api}`to_anndata`, {api}`from_anndata`, {api}`to_mudata`, {api}`from_mudata` | scanpy 1.12.4, decoupler 2.2.0, LIANA 1.10.0, anndata 0.13.4, mudata 0.4.1 | exact: identical, 1e-10 for sums of normalized values | [Conversions to AnnData and MuData](#validation-anndata-conversion) |
+| Model matrices from R formulas, named contrasts, design checks | {api}`Design`, {api}`model_matrix`, {api}`make_contrasts`, {api}`Design.check` | R 4.4.3, limma 3.62.1, lme4 2.0.6 | exact: identical, 1e-10; 1e-8 (absolute) for the leverages of badly scaled designs | [Designs and contrasts](#validation-design) |
+| Linear models of every gene and their contrasts (limma's `lmFit`, `contrasts.fit`) | {api}`fit_linear_model`, {api}`contrasts_fit` | limma 3.62.1 | exact: 1e-10 | [Linear models](#validation-linear-models) |
+| Empirical Bayes moderation, moderated t and F, B-statistics, TREAT | {api}`moderated_statistics`, {api}`treat`, {api}`squeeze_variances`, {api}`fit_f_distribution`, {api}`fit_f_distribution_robust`, {api}`fit_f_distribution_likelihood` | limma 3.62.1 | exact: 1e-10; close: 1e-6 for the robust and likelihood priors | [Empirical Bayes moderation](#validation-empirical-bayes) |
+| Adjustment for multiple testing (R's `p.adjust`) | {api}`adjust_p_values` | R 4.4.3 | exact: 1e-10 | [Multiple testing](#validation-multiple-testing) |
+| TMM, upper-quartile and RLE factors; CPM and log-CPM; `filterByExpr` | {api}`tmm_factors`, {api}`upper_quartile_factors`, {api}`rle_factors`, {api}`cpm`, {api}`log2_cpm`, {api}`filter_by_expression` | edgeR 4.4.0 | exact: 1e-10, identical genes kept | [Normalization and filtering](#validation-normalization) |
+| DESeq2's median-of-ratios size factors | {api}`median_ratio_size_factors` | DESeq2 1.46.0 | exact: 1e-10 | [Normalization and filtering](#validation-normalization) |
+| voom's precision weights and limma's span rule | {api}`voom`, {api}`choose_lowess_span` | limma 3.62.1, edgeR 4.4.0 | exact: 1e-10; close: 1e-6 on rarefied counts | [Precision weights for counts](#validation-voom) |
+| Sample quality weights (`arrayWeights`, `voomWithQualityWeights`) | {api}`sample_quality_weights`, {api}`voom_with_quality_weights` | limma 3.62.1, edgeR 4.4.0 | close: 1e-6; exact: 1e-10 for voom with prior weights, and for the log-CPM values and the span with quality weights | [Sample quality weights and prior weights](#validation-sample-quality-weights) |
+| limma-voom and limma-trend on every cell type of a cube | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact: 1e-10; close: 1e-6 with robust moderation | [limma-voom and limma-trend on cubes](#validation-differential-expression) |
+| Options for pseudobulk data: quality weights, weights from cell numbers, a minimum count, adjustment across cell types | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact: 1e-10; close: 1e-6 with quality weights | [Options for pseudobulk differential expression](#validation-differential-expression-options) |
+| limma-voom on published single-cell data (Kang et al. 2018) | {api}`from_cells`, {api}`limma_voom` | limma 3.62.1, edgeR 4.4.0 | exact: identical counts, 1e-10 | [Differential expression on published data](#validation-interferon-stimulated-pbmcs) |
+| Negative binomial models of every gene (edgeR's `glmFit`), deviances | {api}`fit_negative_binomial_model`, {api}`negative_binomial_deviance`, {api}`negative_binomial_unit_deviance` | edgeR 4.4.0, mpmath 1.3.0 | close: 1e-6; 1e-13 for unit deviances against mpmath | [Negative binomial models](#validation-negative-binomial-fits) |
+| Quasi-likelihood F-tests (edgeR 4's `glmQLFit`, `glmQLFTest`) | {api}`fit_quasi_likelihood`, {api}`quasi_likelihood_f_test`, {api}`common_dispersion`, {api}`average_log2_cpm` | edgeR 4.4.0, limma 3.62.1, mpmath 1.3.0 | close: 1e-6 where both programs compute the same quantity; up to 2e-2 where edgeR approximates the moments of the unit deviance, which CytePool computes exactly, and where edgeR's searches and fits stop short | [Quasi-likelihood F-tests](#validation-quasi-likelihood) |
+| DESeq2's dispersions, Wald tests and likelihood ratio tests | {api}`fit_deseq2`, {api}`deseq2_dispersions`, {api}`deseq2_wald_test`, {api}`deseq2_likelihood_ratio_test` | DESeq2 1.46.0, PyDESeq2 0.5.4, mpmath 1.3.0 | close: 1e-6, each stage given DESeq2's own inputs; 1e-5 for fitted values, Cook's distances and Wald tests, as DESeq2's fits stop up to 1.5e-6 from their fixed point; 1e-9 for deviances and likelihood ratio tests; exact: 1e-10 for size factors, the trend and the variance of the prior | [DESeq2's dispersions and tests](#validation-deseq2-tests) |
+| Mixed models with random intercepts by REML, Satterthwaite's t-tests (maximum likelihood fits are checked with dream) | {api}`fit_mixed_model`, {api}`fixed_effect_tests` | lme4 2.0.6, lmerTest 3.2.1 | close: 1e-6; 1e-5 for Satterthwaite's degrees of freedom and p-values and for fits with large variance ratios; exact: 1e-10 for the REML criterion and at given estimates | [Mixed models](#validation-mixed-models) |
+| Kenward-Roger's t-tests and F-tests, Satterthwaite's F-tests (pbkrtest's `vcovAdj` and `KRmodcomp`, lmerTest's `contestMD`); dream's choice of degrees of freedom and its F-tests | {api}`fixed_effect_tests`, {api}`fixed_effect_f_test`, {api}`dream_voom`, {api}`moderated_fixed_effect_tests` | pbkrtest 0.5.3, lmerTest 3.1-3, lme4 1.1-38, variancePartition 1.36.2, limma 3.62.1 | exact: 1e-10 at given estimates; close: 1e-6 at the optimum and for dream; 1e-5 for the degrees of freedom and p-values of Satterthwaite's F-tests | [Kenward-Roger's tests and F-tests](#validation-kenward-roger-and-f-tests) |
+| dream on every cell type of a cube | {api}`dream_voom`, {api}`voom_with_dream_weights`, {api}`moderated_fixed_effect_tests` | variancePartition 1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3 | close: 1e-6; 1e-5 for Satterthwaite's degrees of freedom and the p-values and z-statistics that use them; exact: 1e-10 for the likelihoods at given estimates | [dream's weights, fits and tests](#validation-dream) |
+| Variance partitioning (`fitExtractVarPartModel`) on every cell type of a cube, and its figures (`plotVarPart`, `plotPercentBars`) | {api}`variance_fractions`, {api}`variance_partitioning`, {api}`plot_variance_fractions`, {api}`plot_feature_variance_fractions` | variancePartition 1.36.2, lme4 1.1-38, ggplot2 4.0.3 | close: 1e-6; exact: 1e-10 at given estimates, for linear models and for the figures; 1e-3 for the widths of the violins | [Variance partitioning](#validation-variance-partitioning) |
+| Cell-type composition (speckle's propeller) | {api}`propeller` | speckle 1.6.0, limma 3.62.1 | exact: 1e-10; close: 1e-6 for the robust prior | [Cell-type composition](#validation-composition-tests) |
+| Gene-set tests camera and fry | {api}`camera`, {api}`fry` | limma 3.62.1 | exact: 1e-10; close: 1e-6 with robust moderation; 1e-3 for fry's default standardization, which differs on purpose; 0.1 (absolute) for fry's mixed p-values against random rotations | [Gene-set tests](#validation-gene-set-tests) |
+| Rotation gene-set tests roast, mroast and romer | {api}`roast`, {api}`mroast`, {api}`romer` | limma 3.62.1 | calibrated: within 4 standard deviations of the Monte Carlo difference, plus 2 / (B + 1) for B rotations; exact: 1e-12 given limma's rotations; 0.1 (absolute) for fry's mixed p-values against the rotation test | [Rotation gene-set tests](#validation-rotation-gene-set-tests) |
+| Gene-set files (GMT) read and written; collections matched to an analysis (limma's `ids2indices`) and filtered by size | {api}`read_gmt`, {api}`write_gmt`, {api}`GeneSets` | R 4.4.3, limma 3.62.1, decoupler 2.2.0 | exact: identical, 1e-10 | [Gene-set files and over-representation](#validation-gene-set-files-and-ora) |
+| Over-representation analysis (limma's `kegga`) of the genes up, down or either way, and its dot plot | {api}`over_representation`, {api}`plot_over_representation` | limma 3.62.1, R 4.4.3 | exact: 1e-10 | [Gene-set files and over-representation](#validation-gene-set-files-and-ora) |
+| Gene set enrichment analysis of ranked genes (fgsea's `fgseaMultilevel` and `fgseaSimple`), with its enrichment plot and table (`plotEnrichment`, `plotGseaTable`) | {api}`gsea`, {api}`plot_enrichment`, {api}`plot_gsea_table` | fgsea 1.32.2 | exact: 1e-10 given fgsea's seeds, and for enrichment scores, leading edges and figures; calibrated: within 4 standard errors over 20 runs in which each program draws its own random sets | [Gene set enrichment analysis](#validation-gsea) |
+| Scores of gene sets in every sample (GSVA's `gsva` with `gsvaParam`, `ssgseaParam`, `zscoreParam` and `plageParam`), and their heatmap | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`, {api}`plot_pathway_scores` | GSVA 2.0.0 | exact: 1e-10, PLAGE up to sign | [Pathway scores](#validation-pathway-scores) |
+| Activities of regulators and pathways (decoupler's `mt.ulm`, `mt.mlm` and `mt.waggr`) from samples, a cube's cell type or contrasts, the networks CollecTRI and PROGENy downloaded with their checksums and licences, and the bar plot of the top sources (`pl.barplot`) | {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate`, {api}`collectri`, {api}`progeny`, {api}`plot_top_activities` | decoupler 2.2.0, progeny 1.28.0 | exact: 1e-10, normalized scores given decoupler's permutations; 1.2e-7 for ULM's adjusted p-values, which decoupler keeps in single precision | [Pathway and regulator activity](#validation-activity) |
+| Tests of pathway scores and activities with a design (limma's `lmFit`, `contrasts.fit` and `eBayes` on scores) | {api}`limma` | the linear models and moderation of this page: `tests/test_score_tests.py` checks that the results are theirs | exact: identical | none of its own; [Linear models](#validation-linear-models), [Empirical Bayes moderation](#validation-empirical-bayes) |
+| Several studies' cubes aligned by sample, gene and cell type, with what each study measured; stacked for a joint model; each study's results lined up gene by gene | {api}`CubeCollection`, {api}`StudyResults.table` | anndata 0.13.4, pandas 3.1.0, limma 3.62.1, edgeR 4.4.0 | exact: identical, 1e-10 | [Studies aligned in a collection](#validation-cube-collection) |
+| Meta-analysis by fixed and random effects (metafor's `rma.uni`) | {api}`meta_analysis`, {api}`fit_meta_analysis` | metafor 5.0.1 | exact: 1e-10; close: 1e-6 for REML and Paule-Mandel | [Meta-analysis](#validation-meta-analysis) |
+| Combined p-values of several studies (Fisher, Stouffer with weights, Tippett, Wilkinson, Pearson, Mudholkar and George, Simes), one-sided or two-sided with the directions of the effects; partial conjunctions, an effect in at least u studies | {api}`combine_p_values`, {api}`partial_conjunction` | scipy 1.18.1, metap 1.1, mpmath 1.3.0 | exact: 1e-10 | [Combined p-values and replication](#validation-combined-p-values-and-replication) |
+| The proportion of true nulls, pi0, and pi1 (qvalue's `pi0est`, its smoother and bootstrap) | {api}`proportion_of_true_nulls` | qvalue 2.38.0 | exact: 1e-10 | [Combined p-values and replication](#validation-combined-p-values-and-replication) |
+| Replication across the studies of a collection: combined and partial conjunction p-values, sign agreement, heterogeneity, pi1 between studies, genes found in k studies, and its figures | {api}`replication`, {api}`plot_study_agreement`, {api}`plot_replicated_genes` | metap 1.1, qvalue 2.38.0, metafor 5.0.1 | exact: 1e-10; close: 1e-6 for REML | [Combined p-values and replication](#validation-combined-p-values-and-replication) |
+| SumRank (Nakatsuka et al. 2025): every gene's best ranks summed across studies, with Irwin-Hall p-values and permutations that re-run each study's analysis; prediction of cases in held-out studies by genes chosen from the others, by SumRank and by meta-analysis; their figures | {api}`sumrank`, {api}`fit_sumrank`, {api}`held_out_prediction`, {api}`plot_sumrank`, {api}`plot_held_out_auc` | the authors' code (commit 46392f66), R 4.4.3, mpmath 1.3.0, scipy 1.18.1, scikit-learn 1.9.1 | exact: 1e-12; calibrated: within 4 standard errors for p-values from random permutations | [SumRank and held-out prediction](#validation-sumrank) |
+| Figures of limma, edgeR, DESeq2, speckle and metafor (MDS, voom's trend, plotSA, MD and volcano plots, barcode plots, dispersions, composition, forest plots) | {api}`cytepool.plotting` | limma 3.62.1, edgeR 4.4.0, DESeq2 1.46.0, speckle 1.6.0, metafor 5.0.1 | exact: 1e-10 for what is drawn; close: 1e-6 where an iterative fit enters, as for sample quality weights, plotSA's prior and REML | [Figures of limma, edgeR, DESeq2, speckle and metafor](#validation-plotting) |
+| Principal components of samples (DESeq2's `plotPCA`), of a matrix or a cell type of a cube, and their figure | {api}`principal_components`, {api}`plot_pca` | DESeq2 1.46.0, edgeR 4.4.0 | exact: 1e-10, up to the sign of each component | [Principal components and dot plots](#validation-pca-and-dot-plots) |
+| Dot plots of the fraction of cells that express each gene and their mean expression, per cell type, from a cube (scanpy's `dotplot`) | {api}`plot_dotplot` | scanpy 1.12.4 | exact: 1e-10 | [Principal components and dot plots](#validation-pca-and-dot-plots) |
+| Tails and quantiles of distributions, special functions | {api}`cytepool.stats.distributions`, {api}`cytepool.stats.special` | mpmath 1.3.0 | exact: 1e-15 to 1e-12 | [Distributions and special functions](#validation-distributions) |
+| LOWESS, natural spline bases | {api}`lowess`, {api}`natural_spline_basis` | R 4.4.3 | close: 1e-6 for LOWESS; exact: 1e-10 for spline bases | [LOWESS and spline bases](#validation-smoothing) |
+
+In all, the scripts printed 2,397 checks: 2,065 pass, none failed; 186 information, 144
+deliberate differences, 2 controls.
Run time is the wall time of the whole script and peak memory the largest resident
set of any of its processes, reference programs included, both measured with GNU
@@ -87,6 +103,7 @@ and the peak memory is the largest that the script measured itself: it restarts
peak of its own process before it runs `limma_voom`, which hides the earlier, higher
peak from GNU `time`.
+(reading-the-tables)=
## How to read the tables
Each check is printed with its tolerance. Most tolerances follow three tiers:
@@ -100,9 +117,13 @@ Each check is printed with its tolerance. Most tolerances follow three tiers:
Some checks are stricter, such as the special functions (1e-15 to 1e-12) and counts
or labels that must be identical (tolerance 0), and some use a bound derived for
their case: the variances from raw moments in `cube.py`, and the leverages of badly
-scaled designs (1e-8, absolute). Differences are relative unless a row says
-otherwise; scripts that compare statistics near zero scale them as their sections
-describe. Besides **pass**, a printed check can be:
+scaled designs (1e-8, absolute). Differences are relative unless a section says
+otherwise. Two scalings recur, for values that can be near zero: a statistic, such as
+a t-statistic or a log fold change, is compared as its difference divided by
+max(\|reference\|, 1), and a p-value as its difference divided by
+max(p, 1e-300) max(1, \|log p\|), which is about the error in log p relative to
+\|log p\|. The sections say which values they scale so. Besides **pass**, a printed
+check can be:
- **information**: a value reported for context, without a tolerance, such as how far
limma's approximate standard errors are from exact ones;
@@ -115,10 +136,18 @@ describe. Besides **pass**, a printed check can be:
Sections also list deliberate differences that passing checks confirm, such as
formulas that CytePool rejects by design; the summary counts as deliberate
differences only the checks that the scripts print as such. The calibrated checks are
-those of `rotation_gene_set_tests.py`, whose tests draw random rotations; the other
-methods implemented so far are deterministic.
+those of `rotation_gene_set_tests.py`, whose tests draw random rotations, of `gsea.py`
+where each program draws its own random sets, and of `sumrank.py` for p-values from
+random permutations; the other checks compare deterministic results.
+
+## Cubes and files
-## Pseudobulk cubes
+(validation-cube)=
+### Pseudobulk cubes
+
+The script printed 20 checks (19 pass and 1 information) in 12.5 s, with a peak memory
+of 926 MiB. [Pseudobulk profiles and cubes](statistics/cubes.md#exact-regrouping) explains
+what it checks.
[`validation/cube.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube.py)
builds a cube from 28,946 simulated cells x 2,000 genes (density 0.210, 8 samples x
@@ -146,7 +175,12 @@ tolerance is a worst-case bound, 64 eps (1 + r²). At r = 8e6 that bound exceeds
so the last row only shows where raw moments stop being useful. Log-normalized
expression has small r and stays in the exact tier.
-## Pooling cells into cubes
+(validation-pool-cells)=
+### Pooling cells into cubes
+
+The script printed 27 checks (27 pass) in 30.2 s, with a peak memory of 1,725 MiB.
+[Pseudobulk profiles and cubes](statistics/cubes.md#cube-layers) explains what it
+checks.
[`validation/pool_cells.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pool_cells.py)
pools 29,068 simulated cells x 2,000 genes (12,097,859 stored counts, density 0.208,
@@ -173,7 +207,12 @@ For an entry without cells, the cube reports the fraction of cells detecting a g
as NaN (zero cells out of zero), where decoupler reports 0. The script also times
`from_cells`; see [](#validation-timings).
-## Cube files
+(validation-cube-files)=
+### Cube files
+
+The script printed 7 checks (7 pass) in 3.1 s, with a peak memory of 146 MiB. [Cube
+files and provenance records](reference/cube_files.md#cube-file-format) explains what
+it checks.
[`validation/cube_files.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_files.py)
checks the Zarr v3 cube files against zarr-python, an independent implementation.
@@ -189,7 +228,12 @@ script skips those cases.
| Chunks of those arrays left unstored because they equal the fill value | zarr-python | 295 of 840 chunks read as the fill value | more than 0 | 1 pass |
| `read_cube` of copies that zarr-python made node by node, with other chunks, big-endian bytes and gzip then zstd | zarr-python | 0 values differ | 0 | 1 pass |
-## Reads of selected features
+(validation-cube-reads)=
+### Reads of selected features
+
+The script printed 5 checks (5 pass) in 1 min 46 s, with a peak memory of 500 MiB. [Cube
+files and provenance records](reference/cube_files.md#cube-file-reading-and-writing)
+explains what it checks.
[`validation/cube_reads.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_reads.py)
checks that reads of a selection take only what they need and return what zarr-python
@@ -212,12 +256,18 @@ Every chunk that holds a selected element is opened once, and no other: 40 chunk
files for 50 neighbouring genes of the 20 groups, where reading whole groups opens all
320. The script also measures each read; see [](#validation-timings).
-## Cube fingerprints
+(validation-cube-fingerprints)=
+### Cube fingerprints
+
+The script printed 7 checks (7 pass) in 4.4 s, with a peak memory of 122 MiB. [Cube
+files and provenance records](reference/cube_files.md#fingerprint-encoding) explains
+what it checks.
No other tool computes the fingerprint of a cube, so
[`validation/cube_fingerprints.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_fingerprints.py)
-checks it against a second implementation, written from the encoding that
-`cytepool.cube.fingerprints` documents: a separate program reads cube files with
+checks it against a second implementation, written from the encoding that [Cube files
+and provenance records](#fingerprint-encoding) specifies: a separate program reads cube
+files with
zarr-python and recomputes every digest and the fingerprint with numpy and `hashlib`,
without importing CytePool. Its encoder differs from CytePool's where it can: it finds
NaN with `numpy.isnan` instead of comparing bits, splits complex values with `.real`
@@ -246,7 +296,13 @@ written after the text, every float of the normalization hashed as a float64, an
reads of whole groups in another order (pooled samples) left unchecked. The first four
each made five checks fail, and the last one the check of the changed files.
-## AnnData and MuData
+(validation-anndata-conversion)=
+### Conversions to AnnData and MuData
+
+The script printed 18 checks (18 pass) in 1 min 5 s, with a peak memory of 1,483 MiB.
+[Pseudobulk profiles and cubes](statistics/cubes.md#profiles-from-other-tools) explains
+what it checks, and [Work with AnnData and MuData](guides/anndata_and_mudata.ipynb)
+shows the conversions.
[`validation/anndata_conversion.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/anndata_conversion.py)
simulates 18,914 cells x 1,500 genes (8 samples x 10 cell types, with an empty sample
@@ -288,87 +344,14 @@ a missing profile: the import refuses them until cell numbers are given. decoupl
gives its row without cells the metadata of another row (found by probing); CytePool
reads a column as sample or cell-type metadata from the rows with cells.
-## Several studies
-
-[`validation/cube_collection.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_collection.py)
-simulates the cells of three studies of one stimulation (seed 20261026) and pools each
-with `from_cells`: `lupus`, 8 samples of 4 donors, each control and stimulated, 4 cell
-types with Cell Ontology ids and 700 genes; `covid`, 10 samples of 5 donors whose
-sample and donor ids repeat those of `lupus`, 5 cell types of which two share an
-ontology id and merge, and 750 genes, 40 of them filled with zeros and marked as not
-measured; and `atlas`, 6 unpaired samples with integer donor ids, 5 cell types without
-ontology ids, aligned by a mapping that leaves one out, and 950 genes. The collection
-aligns the cell types by ontology id and writes donor ids `study::donor`. anndata and
-pandas run as a separate program: each study's cube is exported with `to_anndata`, the
-genes marked as not measured are dropped, the rows of each sample and aligned cell type
-are summed, and the studies are joined with `anndata.concat` (outer and inner joins,
-labelled by study); pandas joins the sample tables. R runs limma-voom on each study's
-own counts, summing the cell types that share an aligned id, and on the stacked counts
-of `lupus` and `covid` with `~ donor + condition`, by the steps of
-`differential_expression.py`. Reference: anndata 0.13.4, pandas 3.1.0, numpy 2.5.4,
-Python 3.13.12; R 4.4.3, limma 3.62.1, edgeR 4.4.0. CytePool ran with numpy 2.5.4 and
-numba 0.68.0.
-
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| The sample table: ids `study::sample_id`, donors `study::donor`, the union of the columns, ages that are integers in two studies and floats in one as float64, and missing values where a study lacks a column | `pandas.concat` | 0 mismatches | 0 | 1 pass |
-| The genes, as a set and in the order in which the studies first list them, and which study measured which | `anndata.concat`, outer join (NaN where a study lacks a gene), `pandas.unique` | identical; 0 mismatches | 0 | 2 pass |
-| Every layer of each study's aligned cube: merged cell types, measured genes | the outer join's values | 0 | 1e-10 | 1 pass |
-| `stack()`: genes in order, cell types and samples; every layer; the cells of each entry, and an entry without cells | `anndata.concat`, inner join, kept to the cell types of every study | 0 | 0 and 1e-10 | 3 pass |
-| `limma_voom` on each study's aligned cube, lined up by `StudyResults.table`: logFC, AveExpr, t, P.Value, adj.P.Val, B and the standard error, in 12 cell types of 3 studies | limma, study by study | 3.8e-13 | 1e-10 | 3 pass |
-| The status of every gene of every study: tested, measured and left out, or not measured | limma's tables and each study's measured genes | 0 mismatches | 0 | 3 pass |
-| `limma_voom` on the stack of `lupus` and `covid`, `~ donor + condition` with donors `study::donor` | limma on the stacked counts | 1.4e-13 | 1e-10 | 1 pass |
-| The same with the donor ids as the studies give them | limma on the stacked counts, donors `study::donor` | 1.9 (log2 fold changes) | must exceed 1e-10 | 1 control |
-
-limma ran on each study's own counts, with the genes filled with zeros, and its tables
-equal those of the aligned cubes, which leave those genes out: genes without counts
-change no library size, expression filter or normalization factor, so a gene that a
-study did not measure can be marked missing without changing any other gene's result.
-The control shows why donor ids are written with their study: taken for one person,
-donor D1 of two studies changes the fit of every gene.
-
-## Normalization and filtering
-
-[`validation/normalization.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/normalization.py)
-compares `cytepool.normalization` with edgeR and DESeq2 on eight count matrices from
-fixed seeds, of 6 to 40 samples and 200 to 2,000 features (dense, sparse, very sparse
-with most upper quartiles zero, unequal depths, many ties, large groups, and two of
-non-integer counts), on 300 small matrices with many ties for TMM's default reference
-sample and its trimming boundaries, 100 of them chosen because a rule that rounds
-differently picks another reference, and on hand-built edge cases. Reference: R 4.4.3,
-edgeR 4.4.0, DESeq2 1.46.0; numpy 2.5.4.
-
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| TMM factors: 32 option sets, and the default and unweighted reference on the 300 small matrices | edgeR `calcNormFactors` | 0; 6,738 of 6,738 values identical | 1e-10 | 3 pass |
-| Upper-quartile factors, p = 0.75 and 0.9, and on the 300 small matrices | edgeR | 0; all 1,505 values identical, and 29 cases undefined in both | 1e-10 | 3 pass |
-| RLE factors, with and without given library sizes | edgeR | 0; all 204 values identical, and 2 cases undefined in both | 1e-10 | 2 pass |
-| DESeq2 size factors, ratio and poscounts, and poscounts on the 300 small matrices | DESeq2 `estimateSizeFactors` | 0; all 1,629 values identical, and 1 case undefined in both | 1e-10 | 3 pass |
-| poscounts factors before their scaling to geometric mean one | DESeq2 `estimateSizeFactorsForMatrix` | 1e-15 (spread of the ratio) | 1e-10 | 1 pass |
-| CPM, with normalization factors and with given library sizes | edgeR `cpm` | 0; 670,800 of 670,800 identical | 1e-10 | 3 pass |
-| log2 CPM with prior counts 0.5, 2 and 5, with normalization factors and with given library sizes | edgeR `cpm(log = TRUE)` | 0 (absolute); 1,118,000 of 1,118,000 identical | 1e-10 | 5 pass |
-| `filterByExpr` by group, with other cut-offs, with designs with and without an intercept, without group or design, and with normalization factors | edgeR `filterByExpr` | 0 of 88,200 features differ | 0 | 7 pass |
-| Edge cases of TMM and the upper quartile: empty trimmed set, no shared features, one sample, identical profiles, ties at the trimming and A cut-offs, the rules for the default reference | edgeR | 0; 25 of 25 identical | 1e-10 | 11 pass |
-| Edge cases of `filterByExpr`: counts at the CPM cut-off, all-zero and nearly collinear designs, sample thresholds a rounding error apart | edgeR | 0 of 23 features differ | 0 | 7 pass |
-| Control: four rules for TMM's default reference that differ from edgeR's only in the last bits | edgeR | 0.97; factors change in 105 of the 300 small matrices | must exceed 1e-10 | 1 control |
-| A negative count | edgeR rejects it too | both raise | n/a | 1 pass |
-| Deliberate differences, listed below | edgeR | CytePool raises, edgeR returns values | n/a | 3 deliberate differences |
-
-The functions repeat R's floating-point operations wherever an operation decides a
-discrete outcome, such as which sample is TMM's reference or which genes a cut-off
-keeps, so factors and filters agree to the last bit. The control shows that the test
-matrices are sensitive to such details: rules that are equal in exact arithmetic but
-round differently change the factors of 105 matrices. A case is undefined in both
-when edgeR or DESeq2 returns NaN factors and CytePool raises an error, as for an
-upper quartile of zero. Deliberate differences:
+## Designs and linear models
-- A sample whose counts are all zero: edgeR's TMM with `refColumn` gives it factor 1;
- CytePool raises, since such a sample has no library size to normalize.
-- An upper quartile of zero: edgeR returns NaN factors; CytePool raises.
-- Both `group` and `design` given to `filterByExpr`: edgeR ignores the design;
- CytePool raises.
+(validation-design)=
+### Designs and contrasts
-## Designs and contrasts
+The script printed 27 checks (26 pass and 1 information) in 4.3 s, with a peak memory of
+221 MiB. [Designs, linear models and
+contrasts](statistics/linear_models.md#designs-and-formulas) explains what it checks.
[`validation/design.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/design.py)
compares `cytepool.design` with R, limma and lme4 on a table of 24 samples: factors
@@ -407,7 +390,12 @@ Deliberate differences:
limma gives `groupB - 1` the weights (-1, 0, -1, -1, -1), `groupA*groupB` (0, 0, 0,
0, 0), `groupA/groupB` (Inf, 0, NaN, NaN, NaN) and `groupA^2` (1, 0, 0, 0, 0).
-## Linear models
+(validation-linear-models)=
+### Linear models
+
+The script printed 218 checks (200 pass and 18 information) in 4.5 s, with a peak memory
+of 177 MiB. [Designs, linear models and
+contrasts](statistics/linear_models.md#one-model-per-gene) explains what it checks.
[`validation/linear_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/linear_models.py)
compares `fit_linear_model` with limma's `lmFit` and `contrasts_fit` with
@@ -447,116 +435,182 @@ Deliberate differences:
- Contrast standard errors are exact by default: each comes from the gene's own
covariance matrix. limma's `contrasts.fit` combines a gene's standard errors with
the correlations of the coefficients in the design without weights. That is exact
- when one factorisation of the design serves every gene (no missing values, and no
- weights or only sample weights) and for contrasts of uncorrelated coefficients,
+ when one factorization of the design serves every gene (no missing values, and no
+ weights or only positive sample weights) and for contrasts of uncorrelated coefficients,
such as group means; otherwise it is approximate, and here its values ranged from
0.56 to 2.55 times the exact ones. `method="limma"` reproduces it.
With `--benchmark`, the script times both packages on 20,000 genes x 200 samples; see
[](#validation-timings).
-(validation-mixed-models)=
-## Mixed models
+(validation-empirical-bayes)=
+### Empirical Bayes moderation
-[`validation/mixed_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/mixed_models.py)
-compares `fit_mixed_model` with lme4's `lmer` (REML) and `fixed_effect_tests` with
-lmerTest's Satterthwaite t-tests (`summary` for every coefficient, `contest1D` for a
-contrast), gene by gene, in 9 cases from fixed seeds: one random intercept (8 donors x
-3 samples, a condition within donors and an age per donor); unbalanced groups (12
-donors with 1 to 6 samples); two crossed random intercepts (10 donors x 4 samples over
-5 batches); two nested random intercepts (`(1 | donor/sample)`, 6 donors x 3 samples x
-2 replicates, with sample labels that repeat across donors); a design simulated
-without donor variance, so that most fits lie on the boundary; observation weights
-like voom's, with one and with two random intercepts; the crossed design with random
-intercepts up to 30 times the residual standard deviation; and 2,000 genes of 16
-donors x 4 samples with observation weights. The other cases have 100 genes each. The
-standard deviation of each random intercept is drawn per gene, and is zero for a fifth
-of the genes. Reference: R 4.4.3, lme4 2.0.6, lmerTest 3.2.1.
+The script printed 150 checks (146 pass and 4 deliberate differences) in 4.8 s, with a
+peak memory of 195 MiB. [Empirical Bayes
+moderation](statistics/empirical_bayes.md#empirical-bayes-model) explains what it
+checks.
+
+[`validation/empirical_bayes.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/empirical_bayes.py)
+compares `moderated_statistics`, `treat`, `squeeze_variances`, `fit_f_distribution`,
+`fit_f_distribution_robust` and `fit_f_distribution_likelihood` with limma's `eBayes`,
+`treat`, `squeezeVar`, `fitFDist`, `fitFDistRobustly` and `fitFDistUnequalDF1`, with
+limma's `legacy` argument TRUE (the method of moments, `prior_estimator="moments"`),
+FALSE (the likelihood estimator, `"likelihood"`) or NULL (limma's default,
+`prior_estimator=None`, which takes the likelihood estimator when the positive residual
+degrees of freedom differ; genes without residual degrees of freedom do not count). Linear model cases (2,000 genes, 6 to 40 samples) are fitted by `lmFit`
+in R and `fit_linear_model` here, then moderated with a constant prior, a trend,
+robustly, and with both; they include several contrasts, some linearly dependent,
+hypervariable genes, an infinite prior df, missing values, observation weights (some of
+them zero), genes without residual df, an all-zero contrast, and 1 to 25 genes, which
+with one or two genes are not moderated (some of them all zero). Variance cases call
+the estimators directly on 2 to 6,000 variances, with and without a covariate, few
+distinct or infinite covariate values, zero, missing and unusable variances, fractional
+df from 0.05, prior weights, planted outliers with equal and unequal df, variances that
+vary less or more than any prior df explains, infinite robust prior df, three to five
+trended variances, two variances above zero among zeros, and covariates on scales of
+1e-5 and 1e-9. The weighted LOWESS of the likelihood estimator's trend is also compared
+with limma's `weightedLowess`. TREAT tests a fold change of 1.2. Reference: R 4.4.3,
+limma 3.62.1.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| REML criterion at the optimum | lme4 `REMLcrit` | 3.1e-13 | 1e-10 | 9 pass |
-| Genes whose criterion is above lme4's by more than 1e-10, relative | lme4 `REMLcrit` | 0 genes | 0 | 9 pass |
-| Coefficients, relative to the larger of their absolute value and standard error, and standard errors | lme4 `fixef`, `vcov` | 2.3e-7 | 1e-6 | 8 pass |
-| Variance components, relative to the larger of themselves and the residual variance, and the residual variance | lme4 `VarCorr`, `sigma` | 1.7e-7 | 1e-6 | 8 pass |
-| The same two families where the variance ratios are large | lme4 `fixef`, `vcov`, `VarCorr`, `sigma` | 1.1e-6 and 2.3e-6 | 1e-5 | 2 pass |
-| The theta of the gene with large ratios that differs most from lme4 | the optimum with 30 digits (mpmath) | 3.4e-13 | 1e-10 | 1 pass |
-| Genes where lme4 puts a variance at zero and CytePool does not | lme4 `getME(fit, "theta")` | 0 genes | 0 | 9 pass |
-| REML criterion at CytePool's estimates | lme4 `REMLcrit` | 3.6e-13 | 1e-10 | 9 pass |
-| Coefficients, standard errors and t-statistics (relative to max(\|t\|, 1)) at CytePool's estimates | lme4 `fixef`, `vcov`; lmerTest `summary` | 1.6e-11 | 1e-10 | 9 pass |
-| Satterthwaite df at CytePool's estimates | lmerTest `summary` | 1.3e-6 | 1e-5 | 9 pass |
-| p-values at CytePool's estimates | lmerTest `summary` | 7.3e-7 | 1e-5 | 9 pass |
-| A contrast: estimate, standard error and t-statistic | lmerTest `contest1D` | 1.6e-11 | 1e-10 | 9 pass |
-| A contrast: df and p-value | lmerTest `contest1D` | 6.6e-7 | 1e-5 | 9 pass |
-| df of the genes that differ most from lmerTest, with one and with two random intercepts | the definition with 30 digits (mpmath) | 9.7e-16 and 2.2e-11 | 1e-10 | 2 pass |
-| Fits with a variance at zero; Newton iterations; lme4's largest theta where CytePool's variance is zero | CytePool; lme4 `getME` | 12% to 54% of the genes of a case; 3.4 to 6.6 on average, at most 9; 4.4e-7 | n/a | 9 information |
-| Satterthwaite df at lme4's own optimum | lmerTest `summary` | 3.5e-6 | n/a | 9 information |
-| lme4 with its default tolerances: variance components; df | lme4 `VarCorr`; lmerTest `summary` | 1.0e-3; 8.5e-4 | n/a | 8 information |
-| lmerTest's df and lme4's theta at the genes checked with 30 digits | the definition with 30 digits (mpmath) | 1.3e-6 and 4.9e-7; 1.3e-6 | n/a | 3 information |
-| Deliberate differences, listed below | lme4 `lmer` | 3 inputs lme4 fits and CytePool refuses, 1 that lme4 fits with a warning and CytePool leaves unfitted | n/a | 4 deliberate differences |
+| t, p, B, F and its p-value, priors, posterior variances, total df and `var.prior`, with constant and trend priors, `legacy = TRUE` | `eBayes`, `treat` | 6.8e-13 | 1e-10 | 16 pass |
+| The same with robust priors | `eBayes`, `treat` | 5.1e-8 | 1e-6 | 7 pass |
+| One or two genes, not moderated, some of them all zero: NaN t-statistics and an infinite `var.prior` where limma gives them, with every option set | `eBayes`, `treat` | 1.3e-14 | 1e-10, robust 1e-6 | 12 pass |
+| Missing values and observation weights, constant and trend priors, `legacy = TRUE` | `eBayes`, `treat`; the exact F against limma's F of group means | 1.8e-13 | 1e-10 | 3 pass |
+| Missing values and observation weights, robust priors, `legacy = TRUE` | as above | 3.0e-11 | 1e-6 | 3 pass |
+| Missing values, zero observation weights and genes without residual df, by default (the likelihood estimator), constant and trend priors | `eBayes`, `treat`, `legacy = NULL` | 1.4e-12 | 1e-6 | 6 pass |
+| The same with robust priors, every gene with residual df | as above | 2.1e-12 | 1e-6 | 5 pass |
+| The likelihood estimator on equal residual df | `eBayes`, `treat`, `legacy = FALSE` | 2.8e-9 | 1e-6 | 3 pass |
+| Every component of `squeezeVar` and `fitFDist` | `squeezeVar`, `fitFDist` | 3.1e-13 | 1e-10 | 24 pass |
+| Every component of `fitFDistRobustly`, with two choices of Winsorized tails | `squeezeVar`, `fitFDistRobustly` | 8.4e-8 | 1e-6 | 20 pass |
+| Every component of `fitFDistUnequalDF1` and `squeezeVar`, with a constant prior and a trend, prior weights, fractional df, zero, missing and unusable variances | `squeezeVar`, `fitFDistUnequalDF1`, `legacy = FALSE` | 2.7e-12 | 1e-6 | 18 pass |
+| The same with `robust = TRUE`, every variance usable and no prior weights | as above | 4.6e-12 | 1e-6 | 8 pass |
+| Trends through three to five variances (a weighted straight line through at most four), two variances above zero among zeros (a constant prior, not robust), and covariates on scales of 1e-5 and 1e-9; constant and robust | as above | 2.6e-13 | 1e-6 | 13 pass |
+| The weighted LOWESS of the trend: ties, windows narrower than 1e-7, windows whose x barely vary, two distant clusters, the trend's weights | `weightedLowess` | 1.1e-15 | 1e-10 | 5 pass |
+| Robust likelihood fits with unusable variances or genes without residual df, against limma's fits of the usable features alone (the controls of the deliberate difference below) | `fitFDistUnequalDF1`, `eBayes` | 1.4e-12 | 1e-6 | 3 pass |
+| Robust likelihood fits with genes without residual df, unusable variances or prior weights | `eBayes`, `squeezeVar`, `fitFDistUnequalDF1` | 0.39 (B-statistics), 0.086 (prior variance) | n/a | 4 deliberate differences |
-The script keeps two questions apart. The optimum is compared with lme4 fitted from its
-own start, with the tolerances of its default optimiser (BOBYQA from nloptr) tightened
-to `xtol_abs = xtol_rel = 1e-14` and `ftol_abs = ftol_rel = 1e-16`; with
-`xtol_abs = 1e-12` and `ftol_abs = 1e-14` alone, some of lme4's fits stopped with a
-criterion 3e-7 above the minimum. The computations at given estimates are compared
-with lme4 and lmerTest evaluated once at CytePool's estimates, which the script passes
-as lme4's starting values with a single evaluation allowed (`maxeval = 1`, and
-`restart_edge = FALSE` so that a variance of zero stays zero). There the criterion,
-the coefficients, their standard errors and the t-statistics are closed forms, while
-lmerTest computes Satterthwaite's degrees of freedom from numerical derivatives.
-p-values are compared scaled by max(p, 1e-300) max(1, \|log p\|), as for voom.
+Each row gives the largest difference over all outputs of a case. Differences are
+relative, except for t, B, F and outlier probabilities and p-values, scaled as
+[above](#reading-the-tables). NaN and infinite values must be in the
+same places, and a prior given as one value must be one value in both. The robust
+moment estimator is in the close tier because limma solves its Winsorized moment
+equation only to a tolerance. The likelihood estimator is in the close tier too: both
+programs search the prior df by the same steps of Brent's method, which stop at a
+tolerance, and agree to about 1e-12; the largest difference, 2.8e-9, is where the
+likelihood barely changes near the largest prior df searched, so that rounding can
+move the steps. Beyond these checks, with residual df of 1e7 and more limma's
+likelihood loses precision: its prior df differed from CytePool's by up to 1e-5 at 1e7
+and 5e-2 at 1e12, where CytePool's agreed with the likelihood computed to 60 digits.
+Deliberate differences:
-Two bounds are looser than the tiers. The REML deviance computed with 30 significant
-digits (mpmath, from dense matrices) shows which program is exact in each:
+- The robust likelihood estimator leaves the genes without a usable variance out of its
+ test for outliers. limma 3.62.1 counts them there, a missing variance as a zero one,
+ which becomes an outlier and lowers the adjusted tail probabilities of the other
+ genes. With 40 of 2,000 genes without residual df, limma's robust prior df were 11.5%
+ larger with a constant prior, where limma also gave 45 genes smaller prior df as
+ outliers and CytePool gives every gene the same, and 6% to 10% larger with a trend.
+ The control rows show that without a trend CytePool's fit equals limma's of the usable
+ features alone. `fit_f_distribution_likelihood` also keeps prior weights in its second
+ robust fit, which limma's drops.
+- `fit_f_distribution_likelihood` raises an error where fewer than two usable variances
+ are above zero, where limma returns NA. With exactly two above zero both fit a
+ constant prior, but limma returns NaN when some other variances are not usable,
+ which CytePool leaves out. It moves infinite covariates one unit beyond the finite
+ range, as `fit_f_distribution` and limma's `fitFDist` do, where `fitFDistUnequalDF1`
+ returns NaN.
+- The moderated F-statistic of a fit with missing values or observation weights is
+ exact by default; `f_statistic="limma"` gives limma's approximation, which the
+ `eBayes` rows compare. The exact F has no limma counterpart, so it is checked
+ against limma's F of the same data coded as group means, whose coefficients are
+ uncorrelated for every gene.
+- `treat` reports a t-statistic of NaN for a coefficient that is NaN, and for an
+ all-zero contrast tested with a zero threshold, where limma reports 0.
-- Satterthwaite's degrees of freedom and the p-values that use them, 1e-5. lmerTest
- differentiates the deviance numerically in theta, on which it depends only through
- theta^2, so at an interior optimum its curvature in theta is about 4 theta^2 times
- that in theta^2, and the numerical Hessian loses relative precision like 1 / theta^2
- where a variance is small. At the genes whose df differ most from lmerTest's, with
- one and with two random intercepts, CytePool's df agree with the definition to
- 9.7e-16 and 2.2e-11, and lmerTest's to 1.3e-6 and 4.9e-7. The second gene has large
- variance ratios, where CytePool's factorisation of the second grouping factor loses
- digits (see the module's documentation).
-- The estimates against lme4's own optimum where the variance ratios are large, 1e-5.
- There the criterion is nearly flat in theta, and lme4's optimiser, which stops on
- changes in the criterion, places theta to about 1e-6 only: at the gene that differs
- most, CytePool's theta agree with the optimum computed with 30 digits to 3.4e-13,
- and lme4's to 1.3e-6.
+(validation-multiple-testing)=
+### Multiple testing
-The information rows count the fits with a variance at zero, which `fit_mixed_model`
-reports in `boundary` and lme4 approaches without always reaching (its theta there was
-at most 4.4e-7), and the Newton iterations per fit. They also give the df at lme4's
-own optimum, which inherit its precision where a variance is small, and the fits of
-lme4 with its default tolerances, whose variance components were up to 1.0e-3 of the
-residual variance from the optimum and whose df up to 8.5e-4 from those at the
-optimum.
+The script printed 15 checks (15 pass) in 2 min 56 s, with a peak memory of 202 MiB.
+[Multiple testing](statistics/multiple_testing.md#error-rates) explains what it checks.
-Deliberate differences:
+[`validation/multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py)
+runs every method of `adjust_p_values` (Holm, Hochberg, Hommel, Bonferroni,
+Benjamini-Hochberg, Benjamini-Yekutieli and none) on each case and compares it with
+R's `p.adjust`. Each check covers all seven methods. Reference: R 4.4.3,
+`stats::p.adjust`.
-- A zero weight is refused, as is any weight that is negative or not finite. lme4
- fits the data, and its REML criterion is then infinite.
-- A missing value is refused: this version fits complete data only. lme4 drops the
- sample (its default `na.action`), here fitting 23 of 24 samples.
-- Values constant within every donor, which the fixed and random effects reproduce
- exactly, have no REML optimum: the criterion decreases without bound as the
- residual variance goes to zero. `fit_mixed_model` returns such a feature unfitted
- (NaN, not converged). lme4 warns that the model failed to converge or is nearly
- unidentifiable, and returns a residual standard deviation of 1.1e-5 times that of
- the values.
-- A design whose fixed and random effects together span every sample (6 samples, 3
- donors and 3 batches in a cycle, and a covariate) reproduces every feature
- exactly, and `fit_mixed_model` refuses it. Its criterion can nonetheless have a
- minimum, and lme4 fits it (REML criterion 14.62).
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| 2,000 uniform values; rounded values with ties, 0 and 1; 10% NaN; 10% exactly 0 or 1; n = 2,000 for 500 values with ties and NaN | `p.adjust` | 0 | 1e-10 | 5 pass |
+| All 9,330 vectors of length 1 to 5 over {0, 0.01, 0.03, 0.5, 1, NaN}, with the default n and with n = 6 | `p.adjust` | 0 | 1e-10 | 2 pass |
+| A 2,000 x 4 matrix adjusted by column, 0 to 50% NaN | `p.adjust` | 0 | 1e-10 | 1 pass |
+| 50 random families of 2 to 2,000 values, with the default n and with n = 2,500 | `p.adjust` | 1.4e-16 | 1e-10 | 2 pass |
+| 100,000 values, 10% duplicated | `p.adjust` | 0 | 1e-10 | 1 pass |
+| The small cases written as test fixtures | `p.adjust` | 0 | 1e-10 | 4 pass |
-With `--benchmark`, the script times both packages on 20,000 genes x 120 samples; see
-[](#validation-timings).
+Of the 290,224 p-values each method adjusted, all but 5 Hommel values were identical
+to R's to the last bit.
-## Precision weights for counts
+## Differential expression
-[`validation/voom.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/voom.py)
-compares `voom` with limma's `voom` on negative binomial counts from fixed seeds, 2 to
+(validation-normalization)=
+### Normalization and filtering
+
+The script printed 50 checks (46 pass, 3 deliberate differences and 1 control) in 72.9
+s, with a peak memory of 745 MiB. [Normalization and
+filtering](statistics/normalization_and_filtering.md#normalization-and-size-factors)
+explains what it checks.
+
+[`validation/normalization.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/normalization.py)
+compares `cytepool.normalization` with edgeR and DESeq2 on eight count matrices from
+fixed seeds, of 6 to 40 samples and 200 to 2,000 features (dense, sparse, very sparse
+with most upper quartiles zero, unequal depths, many ties, large groups, and two of
+non-integer counts), on 300 small matrices with many ties for TMM's default reference
+sample and its trimming boundaries, 100 of them chosen because a rule that rounds
+differently picks another reference, and on hand-built edge cases. Reference: R 4.4.3,
+edgeR 4.4.0, DESeq2 1.46.0; numpy 2.5.4.
+
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| TMM factors: 32 option sets, and the default and unweighted reference on the 300 small matrices | edgeR `calcNormFactors` | 0; 6,738 of 6,738 values identical | 1e-10 | 3 pass |
+| Upper-quartile factors, p = 0.75 and 0.9, and on the 300 small matrices | edgeR | 0; all 1,505 values identical, and 29 cases undefined in both | 1e-10 | 3 pass |
+| RLE factors, with and without given library sizes | edgeR | 0; all 204 values identical, and 2 cases undefined in both | 1e-10 | 2 pass |
+| DESeq2 size factors, ratio and poscounts, and poscounts on the 300 small matrices | DESeq2 `estimateSizeFactors` | 0; all 1,629 values identical, and 1 case undefined in both | 1e-10 | 3 pass |
+| poscounts factors before their scaling to geometric mean one | DESeq2 `estimateSizeFactorsForMatrix` | 1e-15 (spread of the ratio) | 1e-10 | 1 pass |
+| CPM, with normalization factors and with given library sizes | edgeR `cpm` | 0; 670,800 of 670,800 identical | 1e-10 | 3 pass |
+| log2 CPM with prior counts 0.5, 2 and 5, with normalization factors and with given library sizes | edgeR `cpm(log = TRUE)` | 0 (absolute); 1,118,000 of 1,118,000 identical | 1e-10 | 5 pass |
+| `filterByExpr` by group, with other cut-offs, with designs with and without an intercept, without group or design, and with normalization factors | edgeR `filterByExpr` | 0 of 88,200 features differ | 0 | 7 pass |
+| Edge cases of TMM and the upper quartile: empty trimmed set, no shared features, one sample, identical profiles, ties at the trimming and A cut-offs, the rules for the default reference | edgeR | 0; 25 of 25 identical | 1e-10 | 11 pass |
+| Edge cases of `filterByExpr`: counts at the CPM cut-off, all-zero and nearly collinear designs, sample thresholds a rounding error apart | edgeR | 0 of 23 features differ | 0 | 7 pass |
+| Control: four rules for TMM's default reference that differ from edgeR's only in the last bits | edgeR | 0.97; factors change in 105 of the 300 small matrices | must exceed 1e-10 | 1 control |
+| A negative count | edgeR rejects it too | both raise | n/a | 1 pass |
+| Deliberate differences, listed below | edgeR | CytePool raises, edgeR returns values | n/a | 3 deliberate differences |
+
+The functions repeat R's floating-point operations wherever an operation decides a
+discrete outcome, such as which sample is TMM's reference or which genes a cut-off
+keeps, so factors and filters agree to the last bit. The control shows that the test
+matrices are sensitive to such details: rules that are equal in exact arithmetic but
+round differently change the factors of 105 matrices. A case is undefined in both
+when edgeR or DESeq2 returns NaN factors and CytePool raises an error, as for an
+upper quartile of zero. Deliberate differences:
+
+- A sample whose counts are all zero: edgeR's TMM with `refColumn` gives it factor 1;
+ CytePool raises, since such a sample has no library size to normalize.
+- An upper quartile of zero: edgeR returns NaN factors; CytePool raises.
+- Both `group` and `design` given to `filterByExpr`: edgeR ignores the design;
+ CytePool raises.
+
+(validation-voom)=
+### Precision weights for counts
+
+The script printed 118 checks (111 pass and 7 deliberate differences) in 15.3 s, with a
+peak memory of 310 MiB. [limma-voom and limma-trend](statistics/limma_voom.md#voom)
+explains what it checks.
+
+[`validation/voom.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/voom.py)
+compares `voom` with limma's `voom` on negative binomial counts from fixed seeds, 2 to
20,000 genes and 4 to 40 samples, in 26 cases: two groups, four groups with a
covariate, group means, a covariate without an intercept, one residual degree of
freedom, normalization factors (edgeR's `calcNormFactors` in R, `tmm_factors` here)
@@ -587,10 +641,9 @@ and `power` from 0 to 1. Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0.
| Span rule: limma's settings for 0 to 1e9 points, and the other settings | `chooseLowessSpan` | 0 | 1e-10 | 2 pass |
| Deliberate differences, listed below | `voom`, `chooseLowessSpan` | 4 where limma returns values and CytePool raises, 1 where limma fails and CytePool returns values; 2 for the span rule | n/a | 7 deliberate differences |
-Differences are relative, except for E, the trend's x and t, which are scaled by
-max(\|reference\|, 1); the trend's y, scaled by max(\|reference\|, median of the
-reference), because near zero both are rounding noise; and p-values, scaled by
-max(p, 1e-300) max(1, \|log p\|). E was identical to limma's to the last bit in 26 of
+Differences are relative, except for E, the trend's x, t and p-values, scaled as
+[above](#reading-the-tables), and the trend's y, scaled by max(\|reference\|, median of the
+reference), because near zero these values are rounding noise. E was identical to limma's to the last bit in 26 of
26 cases, and so were the trend's x in 24 of 24, the chosen span in the 6 cases where
limma returns one (without replication it returns none, and CytePool's is NaN), and
the span rule at all 466 settings. limma's `voom` chooses the span from every gene,
@@ -621,7 +674,12 @@ tier. Deliberate differences:
documents a `power` between 0 and 1, and `min.span` is the span for many points, a
fraction in (0, 1].
-## Sample quality weights
+(validation-sample-quality-weights)=
+### Sample quality weights and prior weights
+
+The script printed 89 checks (83 pass and 6 deliberate differences) in 12.1 s, with a
+peak memory of 206 MiB. [limma-voom and
+limma-trend](statistics/limma_voom.md#sample-quality-weights) explains what it checks.
[`validation/sample_quality_weights.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sample_quality_weights.py)
compares `sample_quality_weights` with limma's `arrayWeights`, `voom` with prior
@@ -658,9 +716,9 @@ contrasts through `makeContrasts` and named contrasts. Reference: R 4.4.3, limma
| A gene whose prior weights are all zero: the weights of the other genes against limma's without that gene | `voom` | 3.0e-14 | 1e-10 | 1 pass |
| Deliberate differences, listed below | `arrayWeights`, `voom` | 3 where limma returns values and CytePool raises, 1 where limma fails and CytePool returns values, 2 where both return values that differ | n/a | 6 deliberate differences |
-Differences are relative, except for E, the trend's x and t, scaled by
-max(\|reference\|, 1); the trend's y, scaled by max(\|reference\|, its median); and
-p-values, scaled by max(p, 1e-300) max(1, \|log p\|). The sample weights are iterative
+Differences are relative, except for E, the trend's x, t and p-values, scaled as
+[above](#reading-the-tables), and the trend's y, scaled by max(\|reference\|, its median). The
+sample weights are iterative
estimates, in the close tier, as is everything computed from them. They agree to
rounding because CytePool repeats limma's iterations and stopping rule, found by
running limma as a black box: the starting point, the prior's terms in the score and
@@ -686,150 +744,421 @@ unequal, that is the likelihood estimator. Deliberate differences:
- Zero prior weights that leave a gene's coefficients inestimable: limma returns
missing weights for the gene; CytePool's are those of the trend at its fitted values
over the samples of positive weight, and zero where the prior weight is.
-## Negative binomial models
-[`validation/negative_binomial_fits.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/negative_binomial_fits.py)
-compares `fit_negative_binomial_model` with edgeR's `glmFit`,
-`negative_binomial_deviance` with `nbinomDeviance`, and
-`negative_binomial_unit_deviance` with `nbinomUnitDeviance` and with mpmath, on
-negative binomial counts simulated from fixed seeds: 2,000 genes and 6 to 16 samples in
-15 cases, with genes without counts and groups whose counts are all zero planted in
-most. Six are one-way layouts (two groups, group means with TMM factors, an intercept
-alone, one coefficient per sample, sample weights, and dispersions, offsets and weights
-per observation), which edgeR fits group by group to its last bits. Nine are other
-designs: groups and a covariate with trended dispersions, a paired design with TMM
-factors, dispersions, offsets and weights per observation, sample weights, the Poisson
-model, sparse counts of about one read per gene and sample, a covariate such as a date
-next to an intercept, means near 1e8, and a dispersion of 2e-5. edgeR's fits of these
-stop short of the maximum, so they are compared with the maximum that Newton's method
-with the observed information, written in R for the script, reaches from edgeR's fit,
-and with `mglmLevenberg(tol = 1e-14)`, a check where it converges and information where
-it does not. The unit deviance is compared at 5,373 points, counts and means from 1e-30
-to 1e30 and dispersions from 0 to 1e12, with mpmath at 150 digits. Reference: R 4.4.3,
-edgeR 4.4.0, mpmath 1.3.0.
+(validation-differential-expression)=
+### limma-voom and limma-trend on cubes
+
+The script printed 32 checks (28 pass and 4 deliberate differences) in 1.8 s, with a
+peak memory of 189 MiB. [limma-voom and
+limma-trend](statistics/limma_voom.md#limma-voom-steps) explains what it checks.
+
+[`validation/differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py)
+simulates cells with a donor structure (seed 20261025), pools them into cubes with
+`from_cells`, and runs `limma_voom` and `limma_trend` on every cell type; R runs the
+workflow of the limma and edgeR User's Guides on the same pseudobulk counts, group by
+group: it leaves out the samples with fewer than 10 cells, keeps the rows of the
+remaining samples of a design built by `model.matrix` on the full sample table, and
+runs `filterByExpr`, `calcNormFactors`, `voom` (or `cpm(log = TRUE, prior.count = 3)`
+for limma-trend), `lmFit`, `contrasts.fit` one contrast at a time, `eBayes` and
+`topTable(sort.by = "none")`. voom chooses its span from the number of genes tested
+in each group, in both programs (`adaptive.span = TRUE` in R, limma's default since
+3.66), except in the case that fixes it at 0.5 (`span = 0.5, adaptive.span = FALSE`);
+limma's voom reports the span it chose (found by probing), and each group's span is
+compared. The cases are a paired design, `~ donor + condition` (6 donors x 2
+conditions, 4 cell types, 1,000 genes), by limma-voom and limma-trend, and an
+unpaired design with a covariate, `~ condition + age` (10 donors, 3 cell types), by
+limma-voom with robust moderation and a span of 0.5, and by limma-trend without
+filtering or normalization factors. The paired design tests the stimulation and
+three donor comparisons: `D2_vs_D1`, `D2_vs_D6` and `D3_vs_D2`. Some sample x cell
+type entries have fewer than 10 cells; in the NK cells both samples of donor D1, the
+reference level, are left out, which makes the column `donorD6` a combination of the
+others, and in the DC cells four samples remain, too few for any residual degrees of
+freedom.
+Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| One-way layouts: coefficients and fitted values, shrunk coefficients | `glmFit` | 3.0e-11 | 1e-6 | 12 pass |
-| One-way layouts: deviance, and `nbinomDeviance` at edgeR's fitted values | `glmFit`, `nbinomDeviance` | 2.3e-7 | 1e-6 | 6 pass |
-| Residual df, and fits that did not converge | `glmFit` | 0 | 0 | 15 pass |
-| Other designs: coefficients and fitted values | Newton's method in R | 1.8e-7 | 1e-6 | 9 pass |
-| Other designs where edgeR converges: coefficients and fitted values, shrunk coefficients | `mglmLevenberg(tol = 1e-14)` | 3.1e-7 | 1e-6 | 8 pass |
-| Other designs: deviance at the maximum, and at edgeR's fitted values | `nbinomDeviance` | 3.2e-7 | 1e-6 | 8 pass |
-| Genes whose maximum lies at infinity: fitted values that tend to zero, below 1e-6 in both; the others and the deviance | `mglmLevenberg(tol = 1e-14)` | 3.9e-7, 8.5e-7 | 1e-6 | 15 pass |
-| Unit deviance | mpmath; `nbinomUnitDeviance` where edgeR is accurate | 7.8e-15; 3.0e-7 | 1e-13; 1e-6 | 2 pass |
-| Refused inputs: an aliased column, a zero weight, a negative dispersion | `glmFit` | both raise | n/a | 3 pass |
-| Where edgeR stops short: its distance from the maximum | `glmFit`, `mglmLevenberg` | up to 1.6 for `glmFit` (a date), 2.3e-3 for `mglmLevenberg` | n/a | 19 information |
-| `glmFit` deviances that are not those of its own fitted values | `glmFit`, `nbinomDeviance` | 5 of 2,000 genes, with dispersions and weights per observation | n/a | 9 information |
-| Seconds for 2,000 genes, group means and a paired design | `glmFit` | CytePool 0.007 and 0.059, edgeR 0.008 and 0.062 | n/a | 2 information |
-| Deliberate differences, listed below | `glmFit`, `nbinomDeviance`, `nbinomUnitDeviance` | 9 cases of genes without counts; 2 rows for a dispersion of 2e-5; 2 for edgeR's unit deviance | n/a | 13 deliberate differences |
+| Samples used, features tested, coefficients that cannot be estimated, contrasts tested (other than the deliberate differences), voom's span, residual and prior df, in the 12 fitted groups | `filterByExpr`, `voom`, `lmFit`, `contrasts.fit`, `eBayes` | 0 mismatches | 0 | 12 pass |
+| logFC, AveExpr, t, P.Value, adj.P.Val and B of every contrast both programs compute, and the moderated standard error of logFC (`sqrt(s2.post) * stdev.unscaled`) | `topTable`, `eBayes` | 6.3e-13 | 1e-10 | 9 pass |
+| The same with robust moderation | `topTable` | 1.0e-13 | 1e-6 | 3 pass |
+| `D2_vs_D6` in the NK cells, which limma refuses, against limma's values for `D2_vs_D1`, which there estimate the same difference | `topTable` | 2.8e-13 | 1e-10 | 2 pass |
+| DC cells, without residual degrees of freedom: both programs refuse every contrast | `eBayes` | both refuse | n/a | 2 pass |
+| Deliberate differences, listed below | `contrasts.fit` | 2 contrasts limma refuses and CytePool computes, 2 limma computes and CytePool reports as not estimable | n/a | 4 deliberate differences |
-Coefficients are compared relative to max(\|reference\|, 1), fitted values relative to
-the reference, and deviances relative to the larger of the reference and the number of
-samples. A gene whose maximum lies at infinity has coefficients that depend on where
-each program stops, so for such genes only the fitted values that tend to zero (below
-1e-6 in both programs), the others and the deviance are compared. edgeR's Levenberg
-fits stop up to about 3e-4 from the maximum in `glmFit` (1e-2 with dispersions and
-weights per observation, and 1.6 with a date, where fitted values that should tend to
-zero stay near 1e2), and about 2e-7 in `mglmLevenberg(tol = 1e-14)`, but 2e-3 with
-dispersions and weights per observation, 2e-6 with sparse counts, 1e-5 with a date or
-large counts, and 2e-3 with a dispersion of 2e-5. Deliberate differences:
+Differences are scaled as [above](#reading-the-tables), values as statistics and p-values as
+p-values. limma-voom ran with
+`contrast_standard_errors="limma"`, except in the robust case, whose contrasts are
+single coefficients, for which the default exact standard errors are limma's. In
+the NK cells, `D3_vs_D2` combines two of the columns the group keeps, and agrees with
+limma's approximate standard errors too. The robust case keeps the close tier because
+limma solves the robust prior only to a tolerance; here it agreed to 1e-13.
+Deliberate differences, in the NK cells of both paired cases:
-- A gene without counts, in a design with an intercept: edgeR's Levenberg fit returns
- NA coefficients; CytePool returns those of the log mean -1e8, as edgeR's one-way fit
- does, with fitted values and deviance of zero.
-- The deviance is exact. edgeR adds 1e-8 to each count and mean, which lowers the unit
- deviance of a zero count by up to about 5e-7. Below a dispersion of 1e-4 it
- approximates the unit deviance, with an error that grows with the dispersion times
- the mean, from about 1e-6 at 1e-3 to factors from about 1; with the dispersion of
- 2e-5, its deviances, and the fits of genes whose maximum lies at infinity, differ by
- up to 0.97 and 1.2e-3. From a dispersion times the mean of 1e6 it uses the limit of
- the gamma distribution, which gives zero counts 1.6 to 2.6 times their deviance.
+- `D2_vs_D6` (`donorD2 - donorD6`) is estimable without donor D1, but it weighs the
+ aliased column `donorD6`, and limma's `contrasts.fit` stops with "trying to take
+ contrast of non-estimable coefficient". CytePool computes it.
+- `D2_vs_D1` (`donorD2`, donor D2 against D1) cannot be estimated without donor D1's
+ samples. limma computes it from the kept columns, where `donorD2` means donor D2
+ against donor D6, the values of the third row of the table; CytePool reports it as
+ not estimable.
-## Quasi-likelihood F-tests
+(validation-differential-expression-options)=
+### Options for pseudobulk differential expression
-[`validation/quasi_likelihood.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/quasi_likelihood.py)
-compares `fit_quasi_likelihood` and `quasi_likelihood_f_test` with edgeR's `glmQLFit`
-and `glmQLFTest` (edgeR 4.4's default, `legacy = FALSE`), `common_dispersion` with
-`estimateGLMCommonDisp` and `dispCoxReid`, `average_log2_cpm` with `aveLogCPM`, and
-`negative_binomial_unit_deviance_moments` with sums at 30 digits in mpmath and the
-gamma limit. The counts are simulated from fixed seeds, with a dispersion for each
-gene, in 14 cases of 1,000 genes and 8 or 9 samples, with genes without counts and
-groups whose counts are all zero planted in most: two groups with TMM factors; three
-groups and a covariate with several contrasts and a joint test; a paired design;
-robust priors with and without genes that have no counts; small counts; a dispersion
-given below the true one, so that the average quasi-dispersion is well above 1; prior
-weights; trended dispersions; no trend; a trend in another covariate; offsets per
-observation; Poisson counts; and a test that both programs refuse. R runs with its
-address space capped at 8 GB. Reference: R 4.4.3, edgeR 4.4.0, limma 3.62.1, mpmath
-1.3.0.
+The script printed 66 checks (66 pass) in 5.0 s, with a peak memory of 182 MiB.
+[limma-voom and limma-trend](statistics/limma_voom.md#limma-voom-defaults) explains what
+it checks.
+
+[`validation/differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py)
+checks the options of `limma_voom` and `limma_trend` for pseudobulk data against
+limma and edgeR. It simulates cells from 6 donors, each sampled before and after a
+stimulation (seed 20261027), in 3 cell types with 1,000 genes, pools them with
+`from_cells`, and fits `~ donor + condition` with two contrasts, `stim_vs_ctrl` and
+`D3_vs_D2`. One sample is an outlier in every cell type, two are sequenced shallowly
+in one cell type each, and in the NK cells both samples of donor D1, the reference
+level, have fewer than 10 cells, which makes the column `donorD6` a combination of the
+others. Six analyses each set some of the options, and R runs the workflow of [limma-voom and limma-trend on
+cubes](#validation-differential-expression) with the same options:
+
+- sample quality weights (`quality_weights`): limma's `voomWithQualityWeights`, gene by
+ gene and by REML, for limma-voom; `arrayWeights` on the log2 CPM, by REML and gene
+ by gene, then `lmFit` with them as sample weights, for limma-trend;
+- prior weights from cell numbers (`prior_weights="n_cells"`): R counts the cells of
+ each sample and cell type from the cells' labels, not from the cube, and uses them
+ as prior weights: `voom(weights = n)`, whose weights times n enter `lmFit`, as
+ voom's help page says its weights modify prior weights; independently, edgeR's
+ `voomLmFit(prior.weights = n)`, which forms that product itself (found by
+ probing); and `lmFit(weights = n)` for limma-trend;
+- a minimum total count per sample (`min_counts=30000`, in three analyses): R leaves
+ out the samples whose column sum is below it, here the two shallow samples, besides
+ those with fewer than 10 cells;
+- p-values adjusted across cell types (`adjust_across_groups`, in four analyses):
+ `p.adjust(method = "BH")` of the `P.Value` of the three cell types stacked, one
+ contrast at a time.
+
+Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Share of genes the dispersion comes from | `glmQLFit` | 0 | 0 | 9 pass |
-| NB dispersion | `glmQLFit`; `dispCoxReid(tol = 1e-12)` on the same genes | 8.7e-6; 8.9e-7 (6.5e-6 for designs that are not one-way layouts) | 1e-4; 1e-6 (1e-5) | 18 pass |
-| Common dispersion, with prior weights and a systematic subset | `estimateGLMCommonDisp`; `dispCoxReid(tol = 1e-12)` | 8.1e-6; 4.6e-7 | 1e-4; 1e-6 | 5 pass |
-| Average log CPM in four settings | `aveLogCPM` | 3.3e-12 | 1e-6 | 1 pass |
-| Moments of the unit deviance at 41 points, and at a mean of 1e14 | mpmath at 30 digits; the gamma limit | 1.7e-14 | 1e-11 | 2 pass |
-| Average quasi-dispersion | `glmQLFit` | 2.1e-3 | 1e-2 | 14 pass |
-| Adjusted deviances and residual df | `glmQLFit` | 3.3e-3 | 5e-3 | 14 pass |
-| Prior and posterior quasi-dispersions; prior df | `glmQLFit` | 1.4e-3 where the average quasi-dispersions agree, 5.6e-3 where they differ; 9.8e-3 | 5e-3, 1e-2; 2e-2 | 26 pass |
-| The prior and posterior that `squeeze_variances` gives edgeR's own adjusted deviances | `glmQLFit`, `squeezeVar` | 4.7e-8 | 1e-6 | 13 pass |
-| Leverages, those of fitted values that tend to zero included | `glmQLFit` | 2.8e-7 for one-way layouts at the same working dispersion; 8.7e-5 | 1e-6; 1e-3 | 14 pass |
-| logCPM | `glmQLFTest` | 2.8e-12 at a given dispersion; 2.7e-7 at an estimated one | 1e-6; 1e-5 | 16 pass |
-| logFC | `glmQLFTest` | 4.7e-7 for one-way layouts at the same working dispersion; 2.0e-4 | 1e-6; 1e-3 | 16 pass |
-| Likelihood ratio statistic (F times the posterior quasi-dispersion and its df) | `glmQLFTest` | 8.5e-10 at the same dispersion; 8.9e-6 at dispersions estimated by both; 4.5e-3 where the averages differ | 1e-6; 1e-4; 1e-2 | 16 pass |
-| F; PValue and FDR; df.total | `glmQLFTest`, `topTags` | 2.6e-3; 3.9e-3 of \|log10 p\|; 9.8e-3 | 5e-3; 5e-3; 2e-2 | 45 pass |
-| A test that leaves the null model without a coefficient | `glmQLFTest` | both raise | n/a | 1 pass |
-| Deliberate differences, listed below | `glmQLFit`, `glmQLFTest`, `squeezeVar` | 6 rows of the robust prior with genes without counts; the first gene's logCPM with offsets per observation; 6 rows of edgeR's moments | n/a | 13 deliberate differences |
+| Samples used, features tested, voom's span, residual and prior df, in the 18 fits (6 analyses x 3 cell types) | `filterByExpr`, `voom`, `lmFit`, `eBayes` | 0 mismatches | 0 | 18 pass |
+| Sample quality weights | `voomWithQualityWeights`, `arrayWeights` | 2.7e-11 | 1e-6 | 12 pass |
+| Prior weights against the number of cells R counts from the cells | `table` | 0 | 1e-10 | 6 pass |
+| logFC, AveExpr, t, P.Value, adj.P.Val and B of both contrasts, with quality weights | `topTable` | 1.1e-12 | 1e-6 | 12 pass |
+| The same with prior weights | `topTable` | 1.7e-12 | 1e-10 | 6 pass |
+| The same, limma-voom with prior weights, against edgeR | `voomLmFit` | 1.8e-13 | 1e-10 | 3 pass |
+| P-values adjusted across the cell types, with quality weights | `p.adjust` | 5.1e-13 | 1e-6 | 4 pass |
+| The same with prior weights | `p.adjust` | 7.6e-13 | 1e-10 | 4 pass |
+| Prior weights together with estimated sample weights: both programs refuse | `voomLmFit` | both refuse | n/a | 1 pass |
-Three differences set the tolerances, and the script isolates each. edgeR approximates
-the mean and variance of the unit deviance: the scale s implied by its unit matrices
-is off by up to 7.6e-4, 1.2e-3 and 1.3e-2 at dispersions of 0.01, 0.2 and 1, and the
-degrees of freedom k by up to 2.0e-4, 1.3e-4 and 4.0e-3, where CytePool's sums are
-exact. The adjusted deviances, quasi-dispersions and F-statistics therefore agree to
-about 3e-3, the average quasi-dispersion to 2.1e-3, and the prior df, which maximizes
-a likelihood that is flat at large values, to 9.8e-3. The prior and posterior that
-`squeeze_variances` computes from edgeR's own adjusted deviances agree with edgeR's to
-4.7e-8, so the moments are the only cause. Where the average quasi-dispersions differ,
-so do the working dispersions of the fits, and with them the quasi-dispersions,
-leverages, log fold changes and likelihood ratios, by about 1e-4 to 6e-3. edgeR's
-search for the Cox-Reid dispersion stops within its tolerance of 1e-5, and
-`dispCoxReid(tol = 1e-12)` on the same genes agrees with CytePool to 8.9e-7. edgeR's
-Levenberg fits stop short of the maximum (see Negative binomial models above), so log
-fold changes of designs that are not one-way layouts agree to about 2e-4, and their
-fitted values that tend to zero stop near 1e-8, far above CytePool's; both report the
-leverages of the limit for these. Deliberate differences:
+Differences are relative for the weights; the statistics and p-values are scaled as
+[above](#reading-the-tables). Everything computed from sample quality weights is in the close tier, as they
+are iterative estimates; the rest is exact. limma-voom ran with
+`contrast_standard_errors="limma"`, since `D3_vs_D2` combines two coefficients. limma's
+quality weights on the rank-deficient design of the NK cells are those on its kept
+columns (found by probing), as CytePool's are. `voomLmFit` treats some genes with zero
+counts differently from `voom` and `lmFit`: it lowers their residual df, as its help
+page says, and can move their average log count in the trend (found by probing). In
+these data that happens only where `min_counts` leaves a donor with one sample, so
+`voomLmFit` is compared in the analysis of limma-voom with prior weights that does not
+set `min_counts`. limma's `voomWithQualityWeights` takes no prior weights, and edgeR's
+`voomLmFit` stops with "Can't specify prior.weights and estimate sample weights";
+CytePool refuses the two together as well.
-- The robust prior with genes that have no counts: limma counts their missing
- quasi-dispersions as zero variances in its tests for outliers, which lowers the
- outlier probabilities of the others and, here, changes which genes are outliers;
- CytePool tests only the genes with residual degrees of freedom, as its
- `fit_f_distribution_likelihood` does (see Empirical Bayes moderation below). Without
- such genes the two agree.
-- With offsets per observation, edgeR's `aveLogCPM` gives the first gene a value that
- does not depend on its offsets, minus infinity for a gene without counts. The case
- gives the first gene no counts, a dispersion and no trend, so that the value enters
- nothing else, and every other gene is compared.
-- The moments of the unit deviance, as above: CytePool's are exact.
+(validation-interferon-stimulated-pbmcs)=
+### Differential expression on published data
-## DESeq2's dispersions and tests
+The script printed 41 checks (17 pass and 24 information) in 15.2 s, with a peak memory
+of 617 MiB. [limma-voom and limma-trend](statistics/limma_voom.md#limma-voom-steps) explains
+what it checks.
-[`validation/deseq2_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/deseq2_tests.py)
-compares `deseq2_dispersions`, `fit_deseq2`, `deseq2_wald_test` and
-`deseq2_likelihood_ratio_test` with DESeq2's `estimateDispersions`, `nbinomWaldTest`,
-`nbinomLRT` and `results`, and with PyDESeq2, on negative binomial counts simulated
-from fixed seeds: 600 genes and 4 to 12 samples in 9 cases, with 2% of the genes
-without counts. The cases are two groups; four groups, tested by the likelihood ratio
-test of the factor and by a Wald test of two levels that 15 genes have no counts in;
-two groups and a covariate; a paired design of six donors; small counts; two samples
-per group, which leave two residual degrees of freedom; an outlier sample; dispersions
-that rise with the mean; and nearly Poisson counts, 69% of whose gene-wise estimates
-are at the lower bound. Counts with an outlier in most genes, at a dispersion of 0.05,
-test the rules by which Cook's distance removes p-values under four designs: two groups
+[`validation/interferon_stimulated_pbmcs.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/interferon_stimulated_pbmcs.py)
+runs the whole path from cells to differential-expression tables on published
+single-cell data, and compares it with limma on the same pseudobulk counts. The data
+are batch 2 of GEO series
+[GSE96583](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE96583) (Kang et al.
+2018, *Nature Biotechnology* 36, 89-94): peripheral blood mononuclear cells (PBMCs) of
+8 lupus patients, each split into an untreated culture and one stimulated with
+interferon-beta for 6 hours, sequenced with 10x Genomics, the cells assigned to their
+donors by genotype. The script downloads the three files it needs (77 MB) into a cache
+directory outside the repository and checks their SHA-256 checksums. It reads them as
+data, without extracting the archive, and stops if a fact it relies on fails: the
+dimensions of the two count matrices, the gene identifiers, the number of cells of
+each type, and the rule that joins each cell to the published cell table by its
+condition and barcode (every cell matches one row and every row one cell; nothing is
+joined by position).
+
+`from_cells` pools the 24,673 singlets that have a cell type into a cube of 16 samples
+(8 donors, control and stimulated) x 8 cell types x 35,635 genes, keyed by Ensembl id
+with symbols, and its counts are checked against sums of the cells that scipy computes
+independently. `limma_voom` then tests stimulated against control in every cell type,
+with the paired design `~ donor + condition` and its defaults: samples with fewer than
+10 cells (`min_cells`) are left out, then come `filterByExpr`, TMM and voom, whose
+LOWESS span is chosen from the number of genes tested by limma's `chooseLowessSpan`
+rule, as limma's voom does by default since limma 3.66. R runs the limma User's Guide
+workflow on the counts, samples and cell numbers exported from the cube: the same
+`min_cells` rule, then `filterByExpr`, `calcNormFactors`, `voom`, `lmFit`,
+`contrasts.fit`, `eBayes` and `topTable(sort.by = "none")`, all with R's defaults
+except two settings that differ between limma versions: voom gets the span that the
+cell type's record in the CytePool result gives, with `adaptive.span = FALSE`, and
+`eBayes` gets `legacy = TRUE`. R also computes `chooseLowessSpan` of the number of
+genes tested, the span its voom would choose itself, and that span must be the same.
+Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0; CytePool ran with Python 3.13.12,
+numpy 2.5.4 and numba 0.68.0, and scipy 1.18.1 read the count matrices.
+
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| Counts of the 16 samples x 8 cell types x 35,635 genes of the cube | sums of the cells, by scipy | 0 counts differ | 0 | 1 pass |
+| Samples used, genes tested, aliased columns, residual and prior df, and voom's span, in the 8 cell types | `filterByExpr`, `lmFit`, `eBayes`, `chooseLowessSpan` | 0 mismatches | 0 | 8 pass |
+| logFC, AveExpr, t, P.Value, adj.P.Val and B of every gene tested in the 8 cell types | `topTable` | 6.6e-13 | 1e-10 | 8 pass |
+| Per cell type: genes tested and called, the interferon response, the negative control (tables below) | MSigDB, none | n/a | n/a | 24 information |
+
+Differences are scaled as [above](#reading-the-tables), values as statistics and p-values as
+p-values. The dendritic cells are fitted without
+2 of the 16 samples, and the megakaryocytes without 4, among them both samples of donor
+107: that donor's column of the design is then zero, and both programs leave it out of
+the fit.
+
+The recovery of a pre-specified list of interferon-stimulated genes is measured with
+MSigDB's Hallmark
+[interferon-alpha response](https://www.gsea-msigdb.org/gsea/msigdb/human/geneset/HALLMARK_INTERFERON_ALPHA_RESPONSE.html)
+gene set (97 genes, MSigDB v2026.2.Hs; Liberzon et al. 2015, *Cell Systems* 1,
+417-425; CC BY 4.0), matched to the data by symbol; two of its genes have earlier
+symbols in the data, FAM46A for TENT5A and WARS for WARS1. Up and down are calls at a
+5% false discovery rate:
+
+| Cell type | Cells | Samples used (left out) | Genes tested | voom span | Up | Down | Hallmark genes tested | Up | Down | Median log2 fold change |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| B cells | 2,573 | 16 (0) | 2,768 | 0.484 | 498 | 399 | 76 | 73 | 2 | 2.73 |
+| CD14+ monocytes | 5,385 | 16 (0) | 5,125 | 0.450 | 1,619 | 1,549 | 92 | 89 | 3 | 3.26 |
+| CD4 T cells | 10,389 | 16 (0) | 6,295 | 0.440 | 991 | 826 | 86 | 82 | 1 | 2.41 |
+| CD8 T cells | 2,042 | 16 (0) | 1,453 | 0.528 | 233 | 147 | 55 | 54 | 0 | 2.51 |
+| Dendritic cells | 432 | 14 (2) | 2,281 | 0.496 | 454 | 285 | 76 | 68 | 0 | 3.27 |
+| FCGR3A+ monocytes | 1,599 | 16 (0) | 2,947 | 0.480 | 814 | 663 | 84 | 82 | 1 | 2.42 |
+| Megakaryocytes | 260 | 12 (4) | 704 | 0.590 | 63 | 16 | 48 | 35 | 0 | 2.48 |
+| NK cells | 1,993 | 16 (0) | 2,292 | 0.496 | 374 | 300 | 68 | 66 | 0 | 2.13 |
+
+In the six cell types with all 16 samples, 95% to 98% of the Hallmark genes tested are
+called up, with median log2 fold changes of 2.1 to 3.3, that is 4 to 10 times the
+expression of the controls; in the dendritic cells and megakaryocytes, with fewer cells
+and samples, 89% and 73% are. The only Hallmark genes called down are IL4R, in four
+cell types, CD74 in the B cells, and PROCR and IFI30 in the CD14+ monocytes.
+
+The response to interferon is so large that recovering it says little about error
+control; a negative control, which needs no reference, does. In each cell type, the
+control samples that its fit used are split into two halves in every possible way,
+each split once, and every split is fitted with `~ split`, so that a call compares
+donors, not conditions: 35 splits of 4 against 4 samples, or 10 splits of 3 against 3
+in the dendritic cells and megakaryocytes, where 2 control samples have fewer than 10
+cells. If the donors did not differ, the Benjamini-Hochberg procedure at a 5% false
+discovery rate would make any call in an analysis with a probability of at most about
+5%, so in about 12 of these 230 analyses or fewer.
+
+| Cell type | Splits | Splits with calls | Calls | Most in one split | Genes called in more than one split |
+| --- | --- | --- | --- | --- | --- |
+| B cells | 35 | 4 | 4 | 1 | none |
+| CD14+ monocytes | 35 | 1 | 6 | 6 | none |
+| CD4 T cells | 35 | 7 | 123 | 44 | OASL, LMNA, DDIT4, ANXA2R (3 splits); YIPF5, FBLN5, RGS1, TPRG1, CD27, C16orf54, SCML4, NUCB2, SUN2, PASK, FASLG, FAM46C (2) |
+| CD8 T cells | 35 | 5 | 13 | 6 | TMSB10 (2) |
+| Dendritic cells | 10 | 2 | 2 | 1 | none |
+| FCGR3A+ monocytes | 35 | 2 | 5 | 3 | none |
+| Megakaryocytes | 10 | 0 | 0 | 0 | none |
+| NK cells | 35 | 2 | 3 | 2 | none |
+
+23 of the 230 analyses made a call, 156 calls in all, and most of them follow
+differences between the donors. Of the five splits that put donors 1244, 1256 and 1488
+in one group with a fourth donor, the three in which that donor is 1016, 1039 or 107
+make 117 calls in the CD4 T cells, among them genes of regulatory and memory T cells
+(TIGIT, TNFRSF4, TNFRSF18, FASLG, LMNA, RGS1), which suggests that the CD4 T cells of
+these donors differ in composition. The split of donors 101, 107, 1244 and 1488 against
+the others calls interferon-stimulated genes (ISG15, IFI6, IFIT1, IFIT3, LY6E, IFITM3)
+in the monocytes and the CD4 T cells of the untreated cultures, consistent with lupus
+patients differing in interferon activity. Apart from those three CD4 T splits, no
+analysis makes more than 6 calls. The donors appear to be all female, so sex does not
+divide them: XIST is counted in all 16 samples, and none of the Y-chromosome genes
+RPS4Y1, DDX3Y, KDM5D, UTY and EIF1AY in any.
+
+The script times each part on the machine described under [](#validation-timings),
+with numba, OpenMP, OpenBLAS and MKL limited to 2 threads, the data downloaded already
+and numba's cache of compiled routines filled:
+
+| Part | Seconds |
+| --- | --- |
+| Reading and checking the files, and keeping the 24,673 singlets with a cell type | 1.30 |
+| `from_cells`: 24,673 cells x 35,635 genes (14.2 million stored counts) into the cube | 0.24 |
+| `limma_voom` on the 8 cell types, first call in the process | 0.32 |
+| `limma_voom` on the 8 cell types, second call | 0.26 |
+| R: the limma workflow on the 8 cell types, from the counts in memory | 1.42 |
+| The negative control: `limma_voom` on the 230 splits, one cell type each | 3.61 |
+
+With an empty numba cache, as after installing or changing the package, the first call
+of `limma_voom` compiles its routines too, and took 17 s in a run with
+`NUMBA_CACHE_DIR` set to an empty directory; later processes load them from the cache.
+The Python process held at most 617 MiB while it read the files and pooled the
+cells. Once the cell-level matrices were released it held 232 MiB, and while
+`limma_voom` ran at most 252 MiB, so the analysis of the 8 cell types added
+20 MiB to a process that held the cube; from then to the end, with the negative
+control and the export to R, it held at most 321 MiB. R peaked at 227 MiB.
+
+(validation-negative-binomial-fits)=
+### Negative binomial models
+
+The script printed 121 checks (78 pass, 30 information and 13 deliberate differences) in
+14.2 s, with a peak memory of 187 MiB. [edgeR and
+DESeq2](statistics/count_models.md#negative-binomial-model) explains what it checks.
+
+[`validation/negative_binomial_fits.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/negative_binomial_fits.py)
+compares `fit_negative_binomial_model` with edgeR's `glmFit`,
+`negative_binomial_deviance` with `nbinomDeviance`, and
+`negative_binomial_unit_deviance` with `nbinomUnitDeviance` and with mpmath, on
+negative binomial counts simulated from fixed seeds: 2,000 genes and 6 to 16 samples in
+15 cases, with genes without counts and groups whose counts are all zero planted in
+most. Six are one-way layouts (two groups, group means with TMM factors, an intercept
+alone, one coefficient per sample, sample weights, and dispersions, offsets and weights
+per observation), which edgeR fits group by group to its last bits. Nine are other
+designs: groups and a covariate with trended dispersions, a paired design with TMM
+factors, dispersions, offsets and weights per observation, sample weights, the Poisson
+model, sparse counts of about one read per gene and sample, a covariate such as a date
+next to an intercept, means near 1e8, and a dispersion of 2e-5. edgeR's fits of these
+stop short of the maximum, so they are compared with the maximum that Newton's method
+with the observed information, written in R for the script, reaches from edgeR's fit,
+and with `mglmLevenberg(tol = 1e-14)`, a check where it converges and information where
+it does not. The unit deviance is compared at 5,373 points, counts and means from 1e-30
+to 1e30 and dispersions from 0 to 1e12, with mpmath at 150 digits. Reference: R 4.4.3,
+edgeR 4.4.0, mpmath 1.3.0.
+
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| One-way layouts: coefficients and fitted values, shrunk coefficients | `glmFit` | 3.0e-11 | 1e-6 | 12 pass |
+| One-way layouts: deviance, and `nbinomDeviance` at edgeR's fitted values | `glmFit`, `nbinomDeviance` | 2.3e-7 | 1e-6 | 6 pass |
+| Residual df, and fits that did not converge | `glmFit` | 0 | 0 | 15 pass |
+| Other designs: coefficients and fitted values | Newton's method in R | 1.8e-7 | 1e-6 | 9 pass |
+| Other designs where edgeR converges: coefficients and fitted values, shrunk coefficients | `mglmLevenberg(tol = 1e-14)` | 3.1e-7 | 1e-6 | 8 pass |
+| Other designs: deviance at the maximum, and at edgeR's fitted values | `nbinomDeviance` | 3.2e-7 | 1e-6 | 8 pass |
+| Genes whose maximum lies at infinity: fitted values that tend to zero, below 1e-6 in both; the others and the deviance | `mglmLevenberg(tol = 1e-14)` | 3.9e-7, 8.5e-7 | 1e-6 | 15 pass |
+| Unit deviance | mpmath; `nbinomUnitDeviance` where edgeR is accurate | 7.8e-15; 3.0e-7 | 1e-13; 1e-6 | 2 pass |
+| Refused inputs: an aliased column, a zero weight, a negative dispersion | `glmFit` | both raise | n/a | 3 pass |
+| Where edgeR stops short: its distance from the maximum | `glmFit`, `mglmLevenberg` | up to 1.6 for `glmFit` (a date), 2.3e-3 for `mglmLevenberg` | n/a | 19 information |
+| `glmFit` deviances that are not those of its own fitted values | `glmFit`, `nbinomDeviance` | 5 of 2,000 genes, with dispersions and weights per observation | n/a | 9 information |
+| Seconds for 2,000 genes, group means and a paired design | `glmFit` | CytePool 0.007 and 0.059, edgeR 0.008 and 0.062 | n/a | 2 information |
+| Deliberate differences, listed below | `glmFit`, `nbinomDeviance`, `nbinomUnitDeviance` | 9 cases of genes without counts; 2 rows for a dispersion of 2e-5; 2 for edgeR's unit deviance | n/a | 13 deliberate differences |
+
+Coefficients are scaled as statistics are [above](#reading-the-tables), fitted values relative to
+the reference, and deviances relative to the larger of the reference and the number of
+samples. A gene whose maximum lies at infinity has coefficients that depend on where
+each program stops, so for such genes only the fitted values that tend to zero (below
+1e-6 in both programs), the others and the deviance are compared. edgeR's Levenberg
+fits stop up to about 3e-4 from the maximum in `glmFit` (1e-2 with dispersions and
+weights per observation, and 1.6 with a date, where fitted values that should tend to
+zero stay near 1e2), and about 2e-7 in `mglmLevenberg(tol = 1e-14)`, but 2e-3 with
+dispersions and weights per observation, 2e-6 with sparse counts, 1e-5 with a date or
+large counts, and 2e-3 with a dispersion of 2e-5. Deliberate differences:
+
+- A gene without counts, in a design with an intercept: edgeR's Levenberg fit returns
+ NA coefficients; CytePool returns those of the log mean -1e8, as edgeR's one-way fit
+ does, with fitted values and deviance of zero.
+- The deviance is exact. edgeR adds 1e-8 to each count and mean, which lowers the unit
+ deviance of a zero count by up to about 5e-7. Below a dispersion of 1e-4 it
+ approximates the unit deviance, with an error that grows with the dispersion times
+ the mean, from about 1e-6 at 1e-3 to factors from about 1; with the dispersion of
+ 2e-5, its deviances, and the fits of genes whose maximum lies at infinity, differ by
+ up to 0.97 and 1.2e-3. From a dispersion times the mean of 1e6 it uses the limit of
+ the gamma distribution, which gives zero counts 1.6 to 2.6 times their deviance.
+
+(validation-quasi-likelihood)=
+### Quasi-likelihood F-tests
+
+The script printed 223 checks (210 pass and 13 deliberate differences) in 5.1 s, with a
+peak memory of 186 MiB. [edgeR and DESeq2](statistics/count_models.md#quasi-likelihood)
+explains what it checks.
+
+[`validation/quasi_likelihood.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/quasi_likelihood.py)
+compares `fit_quasi_likelihood` and `quasi_likelihood_f_test` with edgeR's `glmQLFit`
+and `glmQLFTest` (edgeR 4.4's default, `legacy = FALSE`), `common_dispersion` with
+`estimateGLMCommonDisp` and `dispCoxReid`, `average_log2_cpm` with `aveLogCPM`, and
+`negative_binomial_unit_deviance_moments` with sums at 30 digits in mpmath and the
+gamma limit. The counts are simulated from fixed seeds, with a dispersion for each
+gene, in 14 cases of 1,000 genes and 8 or 9 samples, with genes without counts and
+groups whose counts are all zero planted in most: two groups with TMM factors; three
+groups and a covariate with several contrasts and a joint test; a paired design;
+robust priors with and without genes that have no counts; small counts; a dispersion
+given below the true one, so that the average quasi-dispersion is well above 1; prior
+weights; trended dispersions; no trend; a trend in another covariate; offsets per
+observation; Poisson counts; and a test that both programs refuse. R runs with its
+address space capped at 8 GB. Reference: R 4.4.3, edgeR 4.4.0, limma 3.62.1, mpmath
+1.3.0.
+
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| Share of genes the dispersion comes from | `glmQLFit` | 0 | 0 | 9 pass |
+| NB dispersion | `glmQLFit`; `dispCoxReid(tol = 1e-12)` on the same genes | 8.7e-6; 8.9e-7 (6.5e-6 for designs that are not one-way layouts) | 1e-4; 1e-6 (1e-5) | 18 pass |
+| Common dispersion, with prior weights and a systematic subset | `estimateGLMCommonDisp`; `dispCoxReid(tol = 1e-12)` | 8.1e-6; 4.6e-7 | 1e-4; 1e-6 | 5 pass |
+| Average log CPM in four settings | `aveLogCPM` | 3.3e-12 | 1e-6 | 1 pass |
+| Moments of the unit deviance at 41 points, and at a mean of 1e14 | mpmath at 30 digits; the gamma limit | 1.7e-14 | 1e-11 | 2 pass |
+| Average quasi-dispersion | `glmQLFit` | 2.1e-3 | 1e-2 | 14 pass |
+| Adjusted deviances and residual df | `glmQLFit` | 3.3e-3 | 5e-3 | 14 pass |
+| Prior and posterior quasi-dispersions; prior df | `glmQLFit` | 1.4e-3 where the average quasi-dispersions agree, 5.6e-3 where they differ; 9.8e-3 | 5e-3, 1e-2; 2e-2 | 26 pass |
+| The prior and posterior that `squeeze_variances` gives edgeR's own adjusted deviances | `glmQLFit`, `squeezeVar` | 4.7e-8 | 1e-6 | 13 pass |
+| Leverages, those of fitted values that tend to zero included | `glmQLFit` | 2.8e-7 for one-way layouts at the same working dispersion; 8.7e-5 | 1e-6; 1e-3 | 14 pass |
+| logCPM | `glmQLFTest` | 2.8e-12 at a given dispersion; 2.7e-7 at an estimated one | 1e-6; 1e-5 | 16 pass |
+| logFC | `glmQLFTest` | 4.7e-7 for one-way layouts at the same working dispersion; 2.0e-4 | 1e-6; 1e-3 | 16 pass |
+| Likelihood ratio statistic (F times the posterior quasi-dispersion and its df) | `glmQLFTest` | 8.5e-10 at the same dispersion; 8.9e-6 at dispersions estimated by both; 4.5e-3 where the averages differ | 1e-6; 1e-4; 1e-2 | 16 pass |
+| F; PValue and FDR; df.total | `glmQLFTest`, `topTags` | 2.6e-3; 3.9e-3 of \|log10 p\|; 9.8e-3 | 5e-3; 5e-3; 2e-2 | 45 pass |
+| A test that leaves the null model without a coefficient | `glmQLFTest` | both raise | n/a | 1 pass |
+| Deliberate differences, listed below | `glmQLFit`, `glmQLFTest`, `squeezeVar` | 6 rows of the robust prior with genes without counts; the first gene's logCPM with offsets per observation; 6 rows of edgeR's moments | n/a | 13 deliberate differences |
+
+Three differences set the tolerances, and the script isolates each. edgeR approximates
+the mean and variance of the unit deviance: the scale s implied by its unit matrices
+is off by up to 7.6e-4, 1.2e-3 and 1.3e-2 at dispersions of 0.01, 0.2 and 1, and the
+degrees of freedom k by up to 2.0e-4, 1.3e-4 and 4.0e-3, where CytePool's sums are
+exact. The adjusted deviances, quasi-dispersions and F-statistics therefore agree to
+about 3e-3, the average quasi-dispersion to 2.1e-3, and the prior df, which maximizes
+a likelihood that is flat at large values, to 9.8e-3. The prior and posterior that
+`squeeze_variances` computes from edgeR's own adjusted deviances agree with edgeR's to
+4.7e-8, so the moments are the only cause. Where the average quasi-dispersions differ,
+so do the working dispersions of the fits, and with them the quasi-dispersions,
+leverages, log fold changes and likelihood ratios, by about 1e-4 to 6e-3. edgeR's
+search for the Cox-Reid dispersion stops within its tolerance of 1e-5, and
+`dispCoxReid(tol = 1e-12)` on the same genes agrees with CytePool to 8.9e-7. edgeR's
+Levenberg fits stop short of the maximum (see [Negative binomial models](#validation-negative-binomial-fits)), so log
+fold changes of designs that are not one-way layouts agree to about 2e-4, and their
+fitted values that tend to zero stop between about 1e-8 and 1e-7, far above
+CytePool's; both report the
+leverages of the limit for these. Deliberate differences:
+
+- The robust prior with genes that have no counts: limma counts their missing
+ quasi-dispersions as zero variances in its tests for outliers, which lowers the
+ outlier probabilities of the others and, here, changes which genes are outliers;
+ CytePool tests only the genes with residual degrees of freedom, as its
+ `fit_f_distribution_likelihood` does (see [Empirical Bayes moderation](#validation-empirical-bayes)). Without
+ such genes the two agree.
+- With offsets per observation, edgeR's `aveLogCPM` gives the first gene a value that
+ does not depend on its offsets, minus infinity for a gene without counts. The case
+ gives the first gene no counts, a dispersion and no trend, so that the value enters
+ nothing else, and every other gene is compared.
+- The moments of the unit deviance, as above: CytePool's are exact.
+
+(validation-deseq2-tests)=
+### DESeq2's dispersions and tests
+
+The script printed 45 checks (28 pass, 11 information and 6 deliberate differences) in 2
+min 16 s, with a peak memory of 814 MiB. [edgeR and
+DESeq2](statistics/count_models.md#deseq2) explains what it checks.
+
+[`validation/deseq2_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/deseq2_tests.py)
+compares `deseq2_dispersions`, `fit_deseq2`, `deseq2_wald_test` and
+`deseq2_likelihood_ratio_test` with DESeq2's `estimateDispersions`, `nbinomWaldTest`,
+`nbinomLRT` and `results`, and with PyDESeq2, on negative binomial counts simulated
+from fixed seeds: 600 genes and 4 to 12 samples in 9 cases, with 2% of the genes
+without counts. The cases are two groups; four groups, tested by the likelihood ratio
+test of the factor and by a Wald test of two levels that 15 genes have no counts in;
+two groups and a covariate; a paired design of six donors; small counts; two samples
+per group, which leave two residual degrees of freedom; an outlier sample; dispersions
+that rise with the mean; and nearly Poisson counts, 69% of whose gene-wise estimates
+are at the lower bound. Counts with an outlier in most genes, at a dispersion of 0.05,
+test the rules by which Cook's distance removes p-values under four designs: two groups
of four, and groups of two and five as a factor, as means without an intercept and as
-a numeric covariate. DESeq2 runs with its defaults, `DESeq(dds,
-minReplicatesForReplace = Inf)`, with `fitType = "mean"` where its parametric trend
+a numeric covariate. DESeq2 runs with its defaults, except that it does not replace outlier counts:
+`DESeq(dds, minReplicatesForReplace = Inf)` (the default is 7), with `fitType = "mean"` where its parametric trend
fails (the small counts and the rising dispersions), and each stage of CytePool is
compared given DESeq2's own inputs to it: the dispersions with their maxima, found in R
by the script from a grid of the Cox-Reid adjusted log-likelihood and the root of its
@@ -864,11 +1193,11 @@ Reference: R 4.4.3, DESeq2 1.46.0, PyDESeq2 0.5.4, mpmath 1.3.0.
| The analysis from the counts: final dispersions, p-values, statistics, genes called | DESeq2 defaults | medians 1.2e-3 to 3.8e-2 (dispersions, log scale) and 3.0e-4 to 7.6e-3 (p-values); 7 of 1,297 genes called differently at 0.1 | n/a | 4 information |
| The analysis from the counts | PyDESeq2 | medians up to 5.8e-7 (gene-wise dispersions, log scale), 4.7e-5 to 0.21 (final dispersions) and 1.7e-5 to 4.2e-2 (p-values); 5 of 281 genes called differently | n/a | 4 information |
-Dispersions are compared relative to the reference, coefficients relative to
-max(\|reference\|, 1), and Cook's and hat values relative to the larger of the
-reference and 1, as only values near the cutoff matter; statistics are compared
-relative to max(\|reference\|, 1), and p-values on the log scale, relative to
-max(\|log p\|, 1), as an error e in a statistic z moves log p by about z^2 e. Below a
+Dispersions are compared relative to the reference, coefficients and statistics are
+scaled as statistics are [above](#reading-the-tables), Cook's and hat values relative
+to the larger of the reference and 1, as only values near the cutoff matter, and
+p-values are compared on the log scale, relative to max(\|log p\|, 1), as an error e
+in a statistic z moves log p by about z^2 e. Below a
gene-wise dispersion of 1e-6 the adjusted likelihood is flat in the log dispersion,
whose derivative is proportional to the dispersion, so there the check is that both
sides place the gene below 1e-6. DESeq2 stops its fits when the deviance changes by
@@ -887,7 +1216,7 @@ differences, each within a bound that the script checks:
maxima. A gene whose rough dispersion is at the lower bound keeps it, as in DESeq2,
even where the maximum lies far above.
- DESeq2's fits at its defaults stop up to 1.9e-3 from the fixed point (log2
- coefficients, relative to max(\|reference\|, 1); bound 1e-2), and its means of the
+ coefficients, scaled as statistics; bound 1e-2), and its means of the
likelihood for designs that are not one-way layouts, from such a fit at the rough
dispersions, up to 7.5e-4 (bound 1e-2). CytePool's fits converge.
- Where DESeq2's parametric trend fails, for the small counts and the rising
@@ -910,1109 +1239,986 @@ of 0.1. PyDESeq2's gene-wise dispersions agree with CytePool's to medians of up
largest for the small counts, where DESeq2's and CytePool's trend is the mean of the
gene-wise estimates.
-## Empirical Bayes moderation
-
-[`validation/empirical_bayes.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/empirical_bayes.py)
-compares `moderated_statistics`, `treat`, `squeeze_variances`, `fit_f_distribution`,
-`fit_f_distribution_robust` and `fit_f_distribution_likelihood` with limma's `eBayes`,
-`treat`, `squeezeVar`, `fitFDist`, `fitFDistRobustly` and `fitFDistUnequalDF1`, with
-limma's `legacy` argument TRUE (the method of moments, `prior_estimator="moments"`),
-FALSE (the likelihood estimator, `"likelihood"`) or NULL (limma's default,
-`prior_estimator=None`, which takes the likelihood estimator when the residual df differ
-between genes). Linear model cases (2,000 genes, 6 to 40 samples) are fitted by `lmFit`
-in R and `fit_linear_model` here, then moderated with a constant prior, a trend,
-robustly, and with both; they include several contrasts, some linearly dependent,
-hypervariable genes, an infinite prior df, missing values, observation weights (some of
-them zero), genes without residual df, an all-zero contrast, and 1 to 25 genes, which
-with one or two genes are not moderated (some of them all zero). Variance cases call
-the estimators directly on 2 to 6,000 variances, with and without a covariate, few
-distinct or infinite covariate values, zero, missing and unusable variances, fractional
-df from 0.05, prior weights, planted outliers with equal and unequal df, variances that
-vary less or more than any prior df explains, infinite robust prior df, three to five
-trended variances, two variances above zero among zeros, and covariates on scales of
-1e-5 and 1e-9. The weighted LOWESS of the likelihood estimator's trend is also compared
-with limma's `weightedLowess`. TREAT tests a fold change of 1.2. Reference: R 4.4.3,
-limma 3.62.1.
-
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| t, p, B, F and its p-value, priors, posterior variances, total df and `var.prior`, with constant and trend priors, `legacy = TRUE` | `eBayes`, `treat` | 6.8e-13 | 1e-10 | 16 pass |
-| The same with robust priors | `eBayes`, `treat` | 5.1e-8 | 1e-6 | 7 pass |
-| One or two genes, not moderated, some of them all zero: NaN t-statistics and an infinite `var.prior` where limma gives them, with every option set | `eBayes`, `treat` | 1.3e-14 | 1e-10, robust 1e-6 | 12 pass |
-| Missing values and observation weights, constant and trend priors, `legacy = TRUE` | `eBayes`, `treat`; the exact F against limma's F of group means | 1.8e-13 | 1e-10 | 3 pass |
-| Missing values and observation weights, robust priors, `legacy = TRUE` | as above | 3.0e-11 | 1e-6 | 3 pass |
-| Missing values, zero observation weights and genes without residual df, by default (the likelihood estimator), constant and trend priors | `eBayes`, `treat`, `legacy = NULL` | 1.4e-12 | 1e-6 | 6 pass |
-| The same with robust priors, every gene with residual df | as above | 2.1e-12 | 1e-6 | 5 pass |
-| The likelihood estimator on equal residual df | `eBayes`, `treat`, `legacy = FALSE` | 2.8e-9 | 1e-6 | 3 pass |
-| Every component of `squeezeVar` and `fitFDist` | `squeezeVar`, `fitFDist` | 3.1e-13 | 1e-10 | 24 pass |
-| Every component of `fitFDistRobustly`, with two choices of Winsorized tails | `squeezeVar`, `fitFDistRobustly` | 8.4e-8 | 1e-6 | 20 pass |
-| Every component of `fitFDistUnequalDF1` and `squeezeVar`, with a constant prior and a trend, prior weights, fractional df, zero, missing and unusable variances | `squeezeVar`, `fitFDistUnequalDF1`, `legacy = FALSE` | 2.7e-12 | 1e-6 | 18 pass |
-| The same with `robust = TRUE`, every variance usable and no prior weights | as above | 4.6e-12 | 1e-6 | 8 pass |
-| Trends through three to five variances (a weighted straight line through at most four), two variances above zero among zeros (a constant prior, not robust), and covariates on scales of 1e-5 and 1e-9; constant and robust | as above | 2.6e-13 | 1e-6 | 13 pass |
-| The weighted LOWESS of the trend: ties, windows narrower than 1e-7, windows whose x barely vary, two distant clusters, the trend's weights | `weightedLowess` | 1.1e-15 | 1e-10 | 5 pass |
-| Robust likelihood fits with unusable variances or genes without residual df, against limma's fits of the usable features alone (the controls of the deliberate difference below) | `fitFDistUnequalDF1`, `eBayes` | 1.4e-12 | 1e-6 | 3 pass |
-| Robust likelihood fits with genes without residual df, unusable variances or prior weights | `eBayes`, `squeezeVar`, `fitFDistUnequalDF1` | 0.39 (B-statistics), 0.086 (prior variance) | n/a | 4 deliberate differences |
-
-Each row gives the largest difference over all outputs of a case. Differences are
-relative, except for t, B, F and outlier probabilities, scaled by
-max(\|reference\|, 1), and p-values, scaled by max(p, 1e-300) max(1, \|log p\|):
-about the error in log p relative to \|log p\|. NaN and infinite values must be in the
-same places, and a prior given as one value must be one value in both. The robust
-moment estimator is in the close tier because limma solves its Winsorized moment
-equation only to a tolerance. The likelihood estimator is in the close tier too: both
-programs search the prior df by the same steps of Brent's method, which stop at a
-tolerance, and agree to about 1e-12; the largest difference, 2.8e-9, is where the
-likelihood barely changes near the largest prior df searched, so that rounding can
-move the steps. Beyond these checks, with residual df of 1e7 and more limma's
-likelihood loses precision: its prior df differed from CytePool's by up to 1e-5 at 1e7
-and 5e-2 at 1e12, where CytePool's agreed with the likelihood computed to 60 digits.
-Deliberate differences:
-
-- The robust likelihood estimator leaves the genes without a usable variance out of its
- test for outliers. limma 3.62.1 counts them there, a missing variance as a zero one,
- which becomes an outlier and lowers the adjusted tail probabilities of the other
- genes. With 40 of 2,000 genes without residual df, limma's robust prior df were 11.5%
- larger with a constant prior, where limma also gave 45 genes smaller prior df as
- outliers and CytePool gives every gene the same, and 6% to 10% larger with a trend.
- The control rows show that without a trend CytePool's fit equals limma's of the usable
- features alone. `fit_f_distribution_likelihood` also keeps prior weights in its second
- robust fit, which limma's drops.
-- `fit_f_distribution_likelihood` raises an error where fewer than two usable variances
- are above zero, where limma returns NA. With exactly two above zero both fit a
- constant prior, but limma returns NaN when some other variances are not usable,
- which CytePool leaves out. It moves infinite covariates one unit beyond the finite
- range, as `fit_f_distribution` and limma's `fitFDist` do, where `fitFDistUnequalDF1`
- returns NaN.
-- The moderated F-statistic of a fit with missing values or observation weights is
- exact by default; `f_statistic="limma"` gives limma's approximation, which the
- `eBayes` rows compare. The exact F has no limma counterpart, so it is checked
- against limma's F of the same data coded as group means, whose coefficients are
- uncorrelated for every gene.
-- `treat` reports a t-statistic of NaN for a coefficient that is NaN, and for an
- all-zero contrast tested with a zero threshold, where limma reports 0.
+(validation-mixed-models)=
+### Mixed models
-## Gene-set tests
+The script printed 135 checks (102 pass, 29 information and 4 deliberate differences) in
+5 min 28 s, with a peak memory of 330 MiB. [Mixed models and
+dream](statistics/mixed_models.md#mixed-model-reml) explains what it checks.
-[`validation/gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_tests.py)
-compares `camera` and `fry` with limma's `camera` and `fry` on log-expression of 1,500
-genes simulated from fixed seeds, with gene-specific variances from a scaled inverse
-chi-squared prior around a mean-variance trend. The gene sets change up, down or in
-both directions, or not at all, share random sample effects (inter-gene correlation)
-with or without a change, overlap others, hold one, two or five genes, or 40% of the
-genes. The cases are three groups with the contrast given as a design column, also
-with only sets of one or more than d genes (so that fry's adjusted mixed p-values can
-be compared); three groups in interleaved order with observation weights and the
-contrast given as weights (a named contrast); counts with voom weights from `voom`
-(limma gets the same weights matrix), with the contrast both ways; a paired design with
-sample weights; residual variances that are all equal, so that the prior df is
-infinite; 16 samples with sets of 2 to 30 unchanged genes and three sets with changed
-genes; a gene the design fits exactly and ten identical genes; and 8 genes, fewer than
-the residual df. Designs are `Design` objects with named contrasts and genes are named
-by id in CytePool; R gets the design matrices and the genes' positions. camera runs with
-the default inter-gene correlation of 0.01, with it estimated for each set, with
-negative correlations allowed, with ranks, with a trend and robustly; fry with each
-standardization, and with a trend and robustly. limma's camera has no robust option: its
-reference is `cameraPR` or `rankSumTestWithCorrelation` on the z-scores of the
-moderated t-statistics of `eBayes(robust = TRUE)`, limma's `zscoreT(approx = TRUE,
-method = "hill")`, with the design reparametrised by `contrastAsCoef`; with an estimated
-correlation, the parametric p-value of `cameraPR` is converted to the df of the
-estimate. Reference: R 4.4.3, limma 3.62.1.
+[`validation/mixed_models.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/mixed_models.py)
+compares `fit_mixed_model` with lme4's `lmer` (REML) and `fixed_effect_tests` with
+lmerTest's Satterthwaite t-tests (`summary` for every coefficient, `contest1D` for a
+contrast), gene by gene, in 9 cases from fixed seeds: one random intercept (8 donors x
+3 samples, a condition within donors and an age per donor); unbalanced groups (12
+donors with 1 to 6 samples); two crossed random intercepts (10 donors x 4 samples over
+5 batches); two nested random intercepts (`(1 | donor/sample)`, 6 donors x 3 samples x
+2 replicates, with sample labels that repeat across donors); a design simulated
+without donor variance, so that most fits lie on the boundary; observation weights
+like voom's, with one and with two random intercepts; the crossed design with random
+intercepts up to 30 times the residual standard deviation; and 2,000 genes of 16
+donors x 4 samples with observation weights. The other cases have 100 genes each. The
+standard deviation of each random intercept is drawn per gene, and is zero for a fifth
+of the genes. Reference: R 4.4.3, lme4 2.0.6, lmerTest 3.2.1.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| camera: numbers of genes, correlations, directions, p-values and FDR, with a given or estimated correlation, ranks and a trend | `camera` | 1.3e-14 | 1e-10 | 35 pass |
-| camera with robust moderation | `cameraPR`, `rankSumTestWithCorrelation` | 1.7e-12 | 1e-6 | 12 pass |
-| fry with `standardize` `"none"` and `"residual_sd"`: numbers of genes, directions, p-values and FDR, mixed p-values of sets of one or more than d genes, and their FDR | `fry` | 5.2e-13 | 1e-10 | 15 pass |
-| fry with `"posterior_sd"`, with and without trend or robust moderation | `fry` | 4.4e-5 | 1e-3 | 15 pass |
-| fry with `"posterior_sd"` and an infinite prior df, where both standardize as `"none"` | `fry` | 3.3e-14 | 1e-10 | 1 pass |
-| CytePool's mixed p-values of the sets of 2 to 14 genes against 200,000 random rotations of their effects | simulation | 0.025 (absolute) | 0.1 | 1 pass |
-| fry's mixed p-values of sets of 2 to d genes | `fry` | 0.88 (absolute) | n/a | 8 deliberate differences |
-| The rotation simulation: limma's mixed p-values of the sets of 2 to 14 genes; both programs' of the sets of 15 to 30 genes; both programs' of the three sets with changed genes (relative) | simulation | 0.59, 0.030, 0.99 | n/a | 3 information |
-| Samples sorted by group, observation weights and the contrast as weights: camera, fry with `"residual_sd"`, and the genes whose last pivot R's `qr()` signs by rounding noise | `camera`, `fry`, `qr` | 0.012, 0.48; 115 of 1,500 genes | n/a | 3 information |
-
-Correlations are compared as differences, p-values relative to max(p, 1e-300) max(1,
-\|log p\|) as in `empirical_bayes.py` (limma's p-values above 1 count as 1), and a
-direction or a number of genes that differs counts as an infinite difference. Robust
-moderation is in the close tier because limma solves the robust prior df only to a
-tolerance. With `"posterior_sd"`, fry moderates each gene's mean square of its d + 1
-effects without the largest, divided by its expected value under the null hypothesis,
-as a variance on 0.92 d degrees of freedom, as limma does (found by probing); limma's
-expected value is about 7.3e-5 (d + 1) / d above the exact one that CytePool computes,
-so directional p-values differ by up to 4.9e-5 and mixed p-values by up to 5.7e-4 of
-their value, which the tolerance of 1e-3 allows. The rotation check uses sets without
-a change, whose p-values a simulation resolves: the beta approximation that both
-programs use for the mixed p-value is off by up to 0.03 at such p-values for sets of
-more than d genes, where the programs agree, and in the tail it can be much too small
-for both: sets with changed genes and rotation p-values of 3.2e-3 and about 6e-5 get
-6.3e-4 and 2.9e-7.
-
-In each gene's QR decomposition, CytePool treats a pivot that is zero up to rounding
-as zero, which R's `qr()` signs as positive. With the samples sorted by group and a
-contrast between groups given as weights, every gene's last pivot is such a zero.
-limma computes most of them exactly, but its arithmetic leaves rounding noise in a few
-percent of genes, which changes their residual bases, and camera and fry do so for
-different genes: fry's are the 115 of the 1,500 whose last pivot R's `qr()` signs by
-noise. camera's correlations of sets then differ by up to 0.012, and fry's p-values
-with `"residual_sd"` by up to a factor of 2. limma's results there change with the
-last bits of the weights, CytePool's do not; in every other case the two agree.
-
-Deliberate differences:
-
-- fry's mixed p-value of a set of 2 to d genes. Its statistic, the sum of the set's
- squared standardized contrast effects, is r'(E'E)r under a random rotation r of the
- d + 1 effects, which both programs approximate by a beta distribution with its first
- two moments. CytePool uses all d + 1 dimensions, as for larger sets; limma uses only
- the set's m nonzero eigenvalues of E'E as if r had m dimensions (found by probing).
- Against 200,000 random rotations, CytePool's mixed p-values of the sets of 2 to 14
- genes are within 0.025 and limma's off by up to 0.59.
-- A p-value above 1 from camera's rank test (its continuity correction can put both
- tails above 1/2): limma reports twice the smaller tail, CytePool 1. A gene listed
- twice in a set: limma counts it twice, CytePool once. A set without genes, or holding
- every gene in camera: NaN p-values in CytePool, left out of the other sets'
- adjustment; limma reports the direction "Up", in camera's rank test the p-value 2,
- which it adjusts with the others, and in fry a mixed p-value of 0 for an empty set.
- The tests check these.
-
-## Rotation gene-set tests
-
-[`validation/rotation_gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/rotation_gene_set_tests.py)
-compares `roast`, `mroast` and `romer` with limma's functions of the same names on
-log-expression of 1,500 genes simulated from fixed seeds, with the gene variances and
-gene sets of `gene_set_tests.py` and three sets with contribution weights: a signature
-whose genes go up or down, with weights +1 and -1; a set with weights of different
-sizes; and a set with half of its weights zero. The cases are three groups with the
-contrast given as a design column; interleaved groups with observation weights and a
-named contrast given as weights; the same with a trended prior estimated robustly;
-counts with voom weights; a paired design with sample weights; equal residual
-variances (an infinite prior df), also with effects so large that limma's log-odds of
-23 genes exceed 709.8; and contribution weights for every gene, drawn from -2, -1, 0.5,
-1 and 3. The exact checks give CytePool the rotations limma drew: a generator that
-replays the standard normal draws R gives limma after `set.seed`, in the order limma
-uses them (found by probing). limma tests each set with the same seed, which is how
-CytePool's `mroast` shares its rotations between sets. The calibrated checks let each
-program draw its own rotations, and allow a difference of 4 sd(p_1 - p_2) + 2 / (B + 1)
-for B rotations each, with the sd from the binomial variance of each estimate at their
-mean. Differences of p-values are absolute: each p-value counts rotations, so a
-difference of one count is at least 1 / (2B + 1). Reference: R 4.4.3, limma 3.62.1.
+| REML criterion at the optimum | lme4 `REMLcrit` | 3.1e-13 | 1e-10 | 9 pass |
+| Genes whose criterion is above lme4's by more than 1e-10, relative | lme4 `REMLcrit` | 0 genes | 0 | 9 pass |
+| Coefficients, relative to the larger of their absolute value and standard error, and standard errors | lme4 `fixef`, `vcov` | 2.3e-7 | 1e-6 | 8 pass |
+| Variance components, relative to the larger of themselves and the residual variance, and the residual variance | lme4 `VarCorr`, `sigma` | 1.7e-7 | 1e-6 | 8 pass |
+| The same two families where the variance ratios are large | lme4 `fixef`, `vcov`, `VarCorr`, `sigma` | 1.1e-6 and 2.3e-6 | 1e-5 | 2 pass |
+| The theta of the gene with large ratios that differs most from lme4 | the optimum with 30 digits (mpmath) | 3.4e-13 | 1e-10 | 1 pass |
+| Genes where lme4 puts a variance at zero and CytePool does not | lme4 `getME(fit, "theta")` | 0 genes | 0 | 9 pass |
+| REML criterion at CytePool's estimates | lme4 `REMLcrit` | 3.6e-13 | 1e-10 | 9 pass |
+| Coefficients, standard errors and t-statistics (relative to max(\|t\|, 1)) at CytePool's estimates | lme4 `fixef`, `vcov`; lmerTest `summary` | 1.6e-11 | 1e-10 | 9 pass |
+| Satterthwaite df at CytePool's estimates | lmerTest `summary` | 1.3e-6 | 1e-5 | 9 pass |
+| p-values at CytePool's estimates | lmerTest `summary` | 7.3e-7 | 1e-5 | 9 pass |
+| A contrast: estimate, standard error and t-statistic | lmerTest `contest1D` | 1.6e-11 | 1e-10 | 9 pass |
+| A contrast: df and p-value | lmerTest `contest1D` | 6.6e-7 | 1e-5 | 9 pass |
+| df of the genes that differ most from lmerTest, with one and with two random intercepts | the definition with 30 digits (mpmath) | 9.7e-16 and 2.2e-11 | 1e-10 | 2 pass |
+| Fits with a variance at zero; Newton iterations; lme4's largest theta where CytePool's variance is zero | CytePool; lme4 `getME` | 12% to 54% of the genes of a case; 3.4 to 6.6 on average, at most 9; 4.4e-7 | n/a | 9 information |
+| Satterthwaite df at lme4's own optimum | lmerTest `summary` | 3.5e-6 | n/a | 9 information |
+| lme4 with its default tolerances: variance components; df | lme4 `VarCorr`; lmerTest `summary` | 1.0e-3; 8.5e-4 | n/a | 8 information |
+| lmerTest's df and lme4's theta at the genes checked with 30 digits | the definition with 30 digits (mpmath) | 1.3e-6 and 4.9e-7; 1.3e-6 | n/a | 3 information |
+| Deliberate differences, listed below | lme4 `lmer` | 3 inputs lme4 fits and CytePool refuses, 1 that lme4 fits with a warning and CytePool leaves unfitted | n/a | 4 deliberate differences |
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| roast given limma's rotations: the down, up, two-sided and mixed p-values, active proportions and numbers of genes of every set, for every set statistic and case | `roast` | 5.0e-16 | 1e-12 | 19 pass |
-| mroast given the same rotations for every set: two-sided p-values, and directions (mismatched sets) | `mroast` | 0; 0 sets | 1e-12; 0 | 38 pass |
-| romer given limma's rotations: up, down and mixed p-values and numbers of genes, for every set statistic, with and without shrinkage, with sample weights and with an infinite prior df | `romer` | 0 | 1e-12 | 7 pass |
-| limma's FDR and FDR.Mixed from its p-values by mroast's rule; numbers of genes and active proportions with each program's own rotations | `mroast` | 4.4e-16 | 1e-12 | 9 pass |
-| Each program's own rotations: mroast (mean, msq, mean50; B = 1,999) and romer (mean, mean50; B = 999) | `mroast`, `romer` | 0.54 of the bound | 1 | 5 pass |
-| mroast's mean statistic with 20,000 rotations against fry, its limit, with equal variances | `fry` | 0.34 of the bound | 1 | 1 pass |
-| CytePool's fry, mixed p-values of the sets of 2 to 6 genes, against the msq rotation test with 20,000 rotations | `mroast` | 0.015 (absolute) | 0.1 | 1 pass |
-| CytePool's msq p-values with every contribution weight multiplied by 10 (B = 1,999) | `roast` | 0 | 1e-12 | 1 pass |
-| limma's fry, mixed p-values of the sets of 2 to 6 genes, against the same | `mroast` | 0.45 (absolute) | n/a | 1 information |
-| Genes whose log-odds in limma exceed 709.8 (very large effects) | `romer` | 23 genes | n/a | 1 information |
-| p-values where the observed floor-mean or mean-square statistic is the smallest it can be | `roast` | 0.50 (absolute) | n/a | 6 deliberate differences |
-| floormean's mixed p-values of sets with negative weights; msq's up, down and two-sided p-values of sets with weights of both signs | `roast` | 0.65; 0.33 (absolute) | n/a | 6 deliberate differences |
-| limma's msq p-values with every weight multiplied by 10 (B = 1,999) | `roast` | 0.45 (absolute change) | n/a | 1 deliberate difference |
-| The set with weights of zero: its number of genes, and its mean50 p-values | `roast` | 8 against 16 genes; 0.039 | n/a | 6 deliberate differences |
-| romer with shrinkage where limma's posterior probabilities are NaN | `romer` | 0.15 (absolute) | n/a | 1 deliberate difference |
+The script keeps two questions apart. The optimum is compared with lme4 fitted from its
+own start, with the tolerances of its default optimizer (BOBYQA from nloptr) tightened
+to `xtol_abs = xtol_rel = 1e-14` and `ftol_abs = ftol_rel = 1e-16`; with
+`xtol_abs = 1e-12` and `ftol_abs = 1e-14` alone, some of lme4's fits stopped with a
+criterion 3e-7 above the minimum. The computations at given estimates are compared
+with lme4 and lmerTest evaluated once at CytePool's estimates, which the script passes
+as lme4's starting values with a single evaluation allowed (`maxeval = 1`, and
+`restart_edge = FALSE` so that a variance of zero stays zero). There the criterion,
+the coefficients, their standard errors and the t-statistics are closed forms, while
+lmerTest computes Satterthwaite's degrees of freedom from numerical derivatives.
+p-values are scaled as [above](#reading-the-tables).
-The z-scores of the moderated t-statistics are Bailey's (1980) transformation on
-min(d_0 + d, 10^4) degrees of freedom; romer's shrinkage of each gene's contrast effect
-computes its log-odds on d_0 + d degrees of freedom, without the limit, and with the
-proportion of true null hypotheses of limma's `propTrueNull(method = "lfdr")`. These
-conventions, the set statistics' definitions where limma differs from the paper (the
-mean-50 statistic averages the ceiling(m / 2) largest values, and romer's floor-mean
-mixed statistic floors \|t\| at 1), and the order of limma's random draws were found
-by probing, and the exact checks confirm them, with an infinite prior df too. Without
-shrinkage, romer agrees with limma exactly on the data with very large effects, which
-shows that the difference with shrinkage comes from limma's NaN posterior
-probabilities. The fry checks give the reference that `gene_set_tests.py` approximated
-by simulation: the mixed p-values of fry for sets of 2 to d genes, where CytePool uses
-all d + 1 dimensions of the rotation and limma only the set's, against the rotation
-test of the same statistic, which `mroast`'s msq statistic is when all genes have the
-same residual variance (up to the limit on the df of the z-scores).
+Two bounds are looser than the tiers. The REML deviance computed with 30 significant
+digits (mpmath, from dense matrices) shows which program is exact in each:
-Deliberate differences:
+- Satterthwaite's degrees of freedom and the p-values that use them, 1e-5. lmerTest
+ differentiates the deviance numerically in theta, on which it depends only through
+ theta^2, so at an interior optimum its curvature in theta is about 4 theta^2 times
+ that in theta^2, and the numerical Hessian loses relative precision like 1 / theta^2
+ where a variance is small. At the genes whose df differ most from lmerTest's, with
+ one and with two random intercepts, CytePool's df agree with the definition to
+ 9.7e-16 and 2.2e-11, and lmerTest's to 1.3e-6 and 4.9e-7. The second gene has large
+ variance ratios, where CytePool's factorization of the second grouping factor loses
+ digits (see the module's documentation).
+- The estimates against lme4's own optimum where the variance ratios are large, 1e-5.
+ There the criterion is nearly flat in theta, and lme4's optimizer, which stops on
+ changes in the criterion, places theta to about 1e-6 only: at the gene that differs
+ most, CytePool's theta agree with the optimum computed with 30 digits to 3.4e-13,
+ and lme4's to 1.3e-6.
-- roast and mroast count a rotated statistic equal to the observed one as at least as
- extreme, as Wu et al. (2010) and Phipson and Smyth (2010) define the p-value and as
- limma's `romer` does; limma's `roast` counts only larger ones. Where the observed
- floor-mean or mean-square statistic is the smallest it can be (a set without a gene
- on one side of zero, or, for the floor-mean mixed statistic, without a gene whose
- \|z\| exceeds 0.674), CytePool's p-value is 1 and limma's about 1/2. The script
- checks that CytePool's p-value is 1 in each such case.
-- With contribution weights a_g, the mean-square statistic weighs each gene's z^2 by
- \|a_g\|, as Wu et al. (2010) define it; limma's up, down and two-sided p-values differ
- from it for sets with weights of both signs, and with more than 1,000 rotations its
- p-values change when every weight is multiplied by the same number (here by up to
- 0.45 for a factor of 10). The floor-mean mixed statistic weighs each gene by
- \|a_g\|, where limma takes a negative weight as evidence against the set; the script
- checks that CytePool's mixed p-value is limma's with the absolute weights.
-- A gene of contribution weight zero leaves the set: it counts neither in the number
- of genes nor among the values that the mean-50 statistic averages. limma keeps it in
- both, and leaves it out of the active proportions, as CytePool does.
-- romer's posterior probability that a gene is differentially expressed is
- 1 / (1 + exp(-lods)). limma's is NaN beyond log-odds of 709.8 (\|t\| above about 38
- with an infinite prior df), which makes the gene's shrunk contrast effect NaN and
- changes the p-values of every set (found by probing).
-- A set without genes, or whose genes all have weight zero, has NaN p-values and no
- direction in CytePool and is left out of the other sets' adjustment; limma reports
- "down" and NA, or the smallest possible p-value when all weights are zero. A gene
- listed twice counts once in CytePool, twice in limma. mroast tests every set with the
- same rotations, where limma draws new ones for each set. The tests check these.
+The information rows count the fits with a variance at zero, which `fit_mixed_model`
+reports in `boundary` and lme4 approaches without always reaching (its theta there was
+at most 4.4e-7), and the Newton iterations per fit. They also give the df at lme4's
+own optimum, which inherit its precision where a variance is small, and the fits of
+lme4 with its default tolerances, whose variance components were up to 1.0e-3 of the
+residual variance from the optimum and whose df up to 8.5e-4 from those at the
+optimum.
-## Gene-set files and over-representation
+Deliberate differences:
-[`validation/gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py)
-checks the reading and writing of GMT files, the matching of gene sets to an analysis,
-and over-representation analysis. Four GMT files of 60 sets are simulated from fixed
-seeds in the styles of MSigDB (names in capitals, links as descriptions, symbols),
-Reactome (names with spaces and punctuation, lines ending in a carriage return and a
-line feed, tabs at the ends of lines), WikiPathways (names joined by %, Entrez ids, a
-byte-order mark, blank lines) and the GMT exports of the Gene Ontology (descriptions
-"na" or empty, genes listed twice, a set without genes, a name outside ASCII,
-compressed by gzip). `read_gmt` reads each file and `write_gmt` writes it back; R reads
-both files by hand (`readLines`, fields split at tabs), and decoupler's `read_gmt`
-reads the MSigDB-style file. A file of 60 sets of symbols is matched to the 2,000 genes
-of an analysis by `GeneSets.restrict_to` and by limma's `ids2indices`, the sets of 15
-to 200 genes are kept, and `camera` tests them on simulated log-expression, given the
-sets that CytePool read, against limma's `camera` given those that R read.
+- A zero weight is refused, as is any weight that is negative or not finite. lme4
+ fits the data, and its REML criterion is then infinite.
+- A missing value is refused: this version fits complete data only. lme4 drops the
+ sample (its default `na.action`), here fitting 23 of 24 samples.
+- Values constant within every donor, which the fixed and random effects reproduce
+ exactly, have no REML optimum: the criterion decreases without bound as the
+ residual variance goes to zero. `fit_mixed_model` returns such a feature unfitted
+ (NaN, not converged). lme4 warns that the model failed to converge or is nearly
+ unidentifiable, and returns a residual standard deviation of 1.1e-5 times that of
+ the values.
+- A design whose fixed and random effects together span every sample (6 samples, 3
+ donors and 3 batches in a cycle, and a covariate) reproduces every feature
+ exactly, and `fit_mixed_model` refuses it. Its criterion can nonetheless have a
+ minimum, and lme4 fits it (REML criterion 14.62).
-`over_representation` runs on simulated results of 4,000 genes, 8% up and 6% down at
-an FDR of 5%, with a block of tied p-values, and 305 sets of up to 600 genes, a fifth
-of each set outside the results: sets enriched for genes up, down or both, or for none,
-two identical sets, a set that lists genes twice, a set of every gene, a set outside
-the universe, and a set with two features of one symbol, one up and one down. Its table
-is compared with limma's `kegga` of a fit with the same p-values and log fold changes,
-which selects the genes up and down itself, and with kegga's default method given the
-genes of each direction and the universe: by feature id; by symbol, which 160 features
-share in pairs; by symbol where 131 features lack one; with a universe of 60% of the
-genes, 100 genes of the sets outside the results and 50 genes of no set; with 416 genes
-without a p-value; and for a list of 230 genes, 30 of them outside the universe and
-some listed twice. The adjusted p-values are compared with `p.adjust` of kegga's
-p-values, the order of the sets with `topKEGG`'s, the p-values also with R's `phyper`
-on a grid of 247 points that reaches 1e-300 and underflow, and the dot plot's sets,
-dots and colours with kegga's counts and adjusted p-values. Reference: R 4.4.3, limma
-3.62.1, decoupler 2.2.0.
+With `--benchmark`, the script times both packages on 20,000 genes x 120 samples; see
+[](#validation-timings).
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| GMT files in four styles: the names, descriptions and genes of every set, read, written and read again | R, by hand | 0 sets differ | 0 | 4 pass |
-| The MSigDB-style file: names and genes | decoupler `read_gmt` | 0 sets differ | 0 | 1 pass |
-| `restrict_to`: the sets kept and the positions of their genes | `ids2indices` | identical | 0 | 1 pass |
-| `filter_by_size(15, 200)`, then `camera` of the 34 sets kept | `camera` | 3.1e-15 | 1e-10 | 1 pass |
-| P(X >= k) at 247 points | `phyper` | 2.1e-12 | 1e-10 | 1 pass |
-| Genes up and down that kegga selects from a fit, by feature id and by shared symbols: numbers of genes, overlaps, p-values, adjusted p-values and the order of the sets | `kegga` of a fit, `p.adjust`, `topKEGG` | 6.2e-14 | 1e-10 | 4 pass |
-| Genes up, down and either way, by feature id, by shared or missing symbols, with a universe given and without p-values for some genes, and a list of genes: the same, and the numbers selected, the size of the universe and the expected overlaps | `kegga`, `p.adjust`, `topKEGG` | 9.7e-14 | 1e-10 | 16 pass |
-| The dot plot: the 20 sets shown and their order, the area and colour of its 60 dots | `kegga` | identical | 0 | 1 pass |
-| Features without a symbol, which kegga of a fit leaves out of its adjustment | `kegga` of a fit | 1.9 (relative) | n/a | 2 deliberate differences |
-| Genes without a p-value, which kegga of a fit keeps in the universe | `kegga` of a fit | 5.3 (relative) | n/a | 2 deliberate differences |
-| Sets without genes in the universe: rows with NaN p-values here, none in kegga's table | `kegga` | 1 to 23 sets per case | n/a | 1 deliberate difference |
+(validation-kenward-roger-and-f-tests)=
+### Kenward-Roger's tests and F-tests
-p-values, adjusted p-values and expected overlaps are compared relative to max(p,
-1e-300); numbers of genes, overlaps, names and orders must be identical. kegga selects
-the genes of a fit whose adjusted p-value is below its `FDR`, strictly, up or down by
-the sign of the coefficient; it counts a gene id on several features once, in both
-lists when one feature goes up and another down; it leaves out features without an id;
-and `topKEGG` orders sets by p-value, ties by the number of genes. These conventions
-were found by probing, and the checks by symbol confirm them. Half of the features
-without a symbol are changed genes, so that kegga's adjustment without them moves
-other genes across the cutoff and the difference shows.
+The script printed 143 checks (115 pass, 16 information and 12 deliberate differences)
+in 3 min 59 s, with a peak memory of 390 MiB. [Mixed models and
+dream](statistics/mixed_models.md#kenward-roger) explains what it checks.
-Deliberate differences:
+[`validation/kenward_roger_and_f_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/kenward_roger_and_f_tests.py)
+compares `fixed_effect_tests(df_method="kenward_roger")` with lmerTest's `summary` and
+`contest1D` (`ddf = "Kenward-Roger"`) and pbkrtest's `vcovAdj`, and
+`fixed_effect_f_test` with pbkrtest's `KRmodcomp` and lmerTest's `contestMD` (by
+Kenward-Roger and by Satterthwaite), gene by gene, in 9 cases of 24 genes from fixed
+seeds: a time course of three time points within 6 donors with an age per donor, with
+the F-tests of two contrasts and of three that span two dimensions; unbalanced donors
+with 1 to 5 samples; crossed donor and batch intercepts; nested intercepts
+(`(1 | donor/sample)`); a design simulated without donor variance, so that 15 of the 24
+fits lie on the boundary; and observation weights like voom's, with one and with two
+random intercepts, each taken both ways: every observation at the average weight,
+against lme4 fits whose weights are divided by their mean, as dream fits them, and the
+precision weights, against pbkrtest on the same model without weights, its values,
+fixed effects and random effects multiplied by the square roots of the weights, which is
+the weighted model. It then compares dream with variancePartition's `dream` with
+`ddf = "Kenward-Roger"`, `"adaptive"` and `"Satterthwaite"`, `eBayes` and `topTable`,
+on negative binomial counts of 40 genes in studies of 12, 18, 19, 20 and 24 samples (a
+time course of three time points and an age), including `topTable`'s F-test of time,
+and `dream_voom` with its defaults on two cubes: cell types left with 12 and with 24
+samples, and one of 15 samples whose age, 32 years on average and about 1 apart, makes
+the adjusted covariance matrices ill conditioned. Reference: R 4.4.3, variancePartition
+1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3, pbkrtest 0.5.3 and minqa 1.2.8.
-- A set without genes in the universe has a row with NaN p-values in CytePool, left
- out of the adjustment of the others; kegga leaves the set out of its table.
-- Genes without a p-value, such as those DESeq2's independent filtering leaves out:
- kegga's method for a fit keeps them in the universe, where CytePool, which takes them
- as not tested, leaves them out. Given the universe of every gene, CytePool gives
- kegga's values; the tests check this.
-- Features without an id, such as a symbol: kegga's method for a fit leaves them out
- of its Benjamini-Hochberg adjustment, so that the genes it selects depend on which
- features have a symbol. CytePool takes the adjusted p-values of the results, the
- genes the analysis called significant. With the p-values adjusted over the features
- with a symbol, CytePool gives kegga's values; the tests check this.
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| Adjusted covariance matrix of the coefficients, relative to each gene's largest entry | pbkrtest `vcovAdj` | 1.4e-13 | 1e-10 | 9 pass |
+| Kenward-Roger t-tests of every coefficient and a contrast: estimates (relative to the larger of their absolute value and standard error), standard errors and t-statistics (relative to max(\|t\|, 1)) | lmerTest `summary`, `contest1D` | 1.7e-12 | 1e-10 | 9 pass |
+| Their df and p-values | lmerTest `summary`, `contest1D` | 1.8e-12 | 1e-10 | 9 pass |
+| Kenward-Roger F-tests: F, numerator and denominator df, scaling and p-value, and lmerTest's F and df | pbkrtest `KRmodcomp`, lmerTest `contestMD` | 8.2e-13 | 1e-10 | 13 pass |
+| Satterthwaite F-statistics | lmerTest `contestMD` | 8.2e-13 | 1e-10 | 13 pass |
+| Satterthwaite F-tests: denominator df and p-values | lmerTest `contestMD` | 1.9e-7 | 1e-5 | 13 pass |
+| Kenward-Roger standard errors and df at lme4's own optimum | lmerTest `summary` | 1.2e-7 | 1e-6 | 9 pass |
+| dream: precision weights | `voomWithDreamWeights` | 2.1e-8 | 1e-6 | 5 pass |
+| dream with each `ddf`: standard errors and df of the tests | `dream` | 8.0e-8 | 1e-6 | 9 pass |
+| Moderated t-statistics, B-statistics and z-statistics (relative to max(\|x\|, 1)), total df and p-values | `eBayes`, `topTable` | 5.0e-8 | 1e-6 | 9 pass |
+| Moderated F-statistics of time | `topTable` | 4.1e-8 | 1e-6 | 11 pass |
+| `dream_voom`: samples used and features tested | the rules of `dream_voom` | 0 differences | 0 | 3 pass |
+| `dream_voom` with its defaults: t, B and z-statistics, df and p-values of every contrast | `dream`, `eBayes`, `topTable` | 3.3e-7 | 1e-6 | 3 pass |
+| Fits with a variance at zero; the two treatments of the weights; the default against Satterthwaite's method | CytePool | below | n/a | 16 information |
+| Deliberate differences, listed below | variancePartition | 12 inputs | n/a | 12 deliberate differences |
-## Gene set enrichment analysis
+At CytePool's estimates, which the script passes to lme4 as starting values with one
+evaluation allowed, as for [mixed models](#validation-mixed-models), pbkrtest computes
+Kenward-Roger's quantities from dense matrices without numerical derivatives, so
+they are compared to the exact tier; lmerTest's Satterthwaite F-tests differentiate the
+deviance numerically, compared to 1e-5. At lme4's own optimum, with Powell's BOBYQA and
+its trust region shrunk to 1e-10 as in the section on dream, and in dream's fits, which
+the script runs with that optimizer, the estimates agree to the close tier. P-values are
+scaled as [above](#reading-the-tables).
-[`validation/gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py)
-checks gene set enrichment analysis and its figures against fgsea. Statistics of 4,000
-genes are simulated from fixed seeds like moderated t-statistics, 6% of the genes up and
-5% down, with 60 genes tied at 1.25 and 30 at zero, with 90 gene sets of 1 to 1,000
-genes, a fifth of each outside the ranking: sets enriched for genes up, down or both, or
-for none, a set of every gene, which neither program tests, a set of one gene, half of
-the tied genes, a set that lists genes twice and two identical sets. `gsea` and fgsea's
-`fgsea` run with the same options: fgsea's defaults (its multilevel method, 1,000
-permutations, 101 sets per level, eps 1e-50); gseaParam 0 and 0.5; scoreType "neg";
-scoreType "pos" on -log10 p-values, which are all positive; sets of 15 to 500 genes,
-with eps 0, 51 sets per level and 2,000 permutations; permutations alone (fgsea's
-`nperm`, 2,500 in three chunks); statistics mostly positive, with 6 sets spread evenly
-along the ranking, a set of a few genes near its top and others spread along it, whose
-p-value is about 0.05, and 3 sets of 25 genes from its bottom, whose negative scores
-fewer than 10 random sets of their size reach, so that no set goes to the multilevel
-method; and one set alone.
+The checks confirm what probing pbkrtest, lmerTest and variancePartition as black boxes
+found, and the documentation of `cytepool.stats.mixed_models` and
+`cytepool.stats.dream` lists: pbkrtest takes the inverse of the expected information
+of REML, includes a variance component estimated at zero, which 15 fits of the
+boundary case have, and builds the covariance matrix of the observations without lme4's
+prior weights while it takes the covariance matrix of the coefficients from the
+weighted fit; lmerTest's `contest1D` and `summary` use the adjusted standard errors with
+pbkrtest's degrees of freedom; `dream` uses Kenward-Roger's method with the adjusted
+standard errors for 19 samples and Satterthwaite's for 20 under `ddf = "adaptive"`, and
+`topTable`'s F-statistic is limma's, from the moderated t-statistics and each gene's
+covariance matrix of the coefficients, the adjusted one under Kenward-Roger's
+method. In the ill-conditioned cell type `dream` tested 24 genes by Satterthwaite's
+method because R's estimate of the condition number of their adjusted covariance
+exceeded 1e6, and `dream_voom` the same 24, from the exact condition number.
-The R script records the seeds that fgsea draws where it hands them to its compiled code
-(`trace` of `fgseaSimpleImpl` and `multilevelImpl`), and `gsea` draws the same through
-`rng`; the two programs then draw the same random sets, and must agree exactly. fgsea
-draws the random sets of a set tested alone with R's `sample.int`, which cannot be
-replayed: that case is exact for the enrichment score and leading edge, and calibrated
-for the rest, with seeds of their own (R's 1 to 20 and numpy's 1 to 20): the mean
-log10 p-value and the mean normalized score of each set over 20 runs must agree within
-4 standard errors of their difference, for 36 sets and for the set alone. The figures:
-what `plot_enrichment` draws against `plotEnrichmentData`, for three sets with gseaParam
-1 and 0.5, and the bars and numbers of `plot_gsea_table` against the ticks of
-`plotEnrichmentData`, scaled as `plotGseaTable` scales them, and its numbers as it
-writes them. Reference: R 4.4.3, fgsea 1.32.2.
+The information rows give the size of the two treatments of the weights. Against the
+precision weights, every observation at the average weight gave standard errors 0.976
+times as large at the median (0.51 to 1.05) and df 0.91 times (0.26 to 3.7) with one
+random intercept, and 0.977 (0.81 to 1.02) and 0.89 (0.27 to 3.4) with two; its standard
+errors were below the unadjusted ones for 61% and 33% of the genes, the precision
+weights' for none. And they give how dream's default changes the results of small
+studies against Satterthwaite's method, with which `dream_voom` tested them before:
+with 12 samples, p-values 1.12 times as large at the median (0.97 to 1.66 for 90% of the
+genes), with standard errors 1.06 times as large at the median and smaller for 20% of
+the genes; with 18 samples, 1.00 (0.91 to 1.07), and standard errors 1.00, smaller for
+48%; in the cube's cell type with 12 samples, 0.99 (0.73 to 1.12), with as many genes
+below an adjusted p-value of 0.05 (12) as before.
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| The sets tested, their sizes, leading edges and enrichment scores, in all 9 cases | `fgsea` | 0 | 1e-10 | 9 pass |
-| Normalized scores, p-values, log2 errors and adjusted p-values, given fgsea's seeds, in 7 cases | `fgsea` | 2.2e-13 | 1e-10 | 7 pass |
-| The same of the mostly positive statistics, without the adjusted p-values | `fgsea` | 4.2e-16 | 1e-10 | 1 pass |
-| Mean log10 p-values and mean normalized scores over 20 runs, seeds of their own, for 36 sets and one set alone | `fgsea` | 2.4 standard errors | 4 | 4 pass |
-| `plot_enrichment`: the walk, the ticks and the extremes of 6 plots | `plotEnrichmentData` | 1.7e-14 | 1e-10 | 1 pass |
-| `plot_gsea_table`: the bars and numbers of 10 sets | `plotEnrichmentData`, `plotGseaTable` | 1.1e-13 | 1e-10 | 1 pass |
-| Adjusted p-values of the mostly positive statistics | `fgsea` | 0.16 (relative) | n/a | 1 deliberate difference |
+Deliberate differences:
-Numbers are compared relative to their absolute value, or 1e-300, and NaN must stand
-where fgsea gives NA; sets, sizes and leading edges must be identical. Given fgsea's
-seeds, the random scores and the counts of the permutations are the same to the last
-bit; the multilevel p-values differ in the last bits, through the digamma function of
-each program.
+- The p-values of `topTable`'s F-tests: dream computes those of every gene on the total
+ degrees of freedom of the first gene for the first coefficient listed, which the
+ script reproduces to 3e-14 in the 11 F-tests; CytePool gives each gene its own (see
+ `cytepool.stats.dream`). Its p-values were 0.14 to 11.5 times dream's.
+- Which genes fall back to Satterthwaite's method: dream decides from R's estimate of
+ the condition number (`kappa`), CytePool from the exact one; they can differ within
+ about 2% of 1e6. Here they decided alike for every gene tested, and the tests agree
+ to 3.3e-7.
-Deliberate difference:
+dream also computes `F.std` with the number of coefficients where they are linearly
+dependent, and CytePool with their rank, the numerator degrees of freedom of the F-test;
+the F-tests here have independent coefficients.
-- The adjusted p-values of the mostly positive statistics, where some sets have too few
- random scores of their sign and no set goes to the multilevel method: fgsea adjusts
- the p-values over the sets before it removes those that have some, but fewer than 10,
- random scores of their sign, as though they had been tested, so that its adjusted
- p-values of the sets it reports come out smaller or larger; CytePool adjusts over the
- sets with a p-value. The tests check that CytePool's are Benjamini and Hochberg's of
- the p-values it reports.
+(validation-dream)=
+### dream's weights, fits and tests
-## Pathway scores
+The script printed 63 checks (53 pass, 5 information and 5 deliberate differences) in
+30.5 s, with a peak memory of 390 MiB. [Mixed models and
+dream](statistics/mixed_models.md#dream) explains what it checks.
-[`validation/pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py)
-checks the scores of gene sets in every sample, and their heatmap, against GSVA 2.0.0,
-whose `gsva()` runs with a `gsvaParam`, `ssgseaParam`, `zscoreParam` or `plageParam`,
-and the CytePool function with the same options. The inputs are simulated from fixed
-seeds: log-expression values of 2,000 genes in 24 samples, 200 genes higher in the
-second 12, with two constant genes, a gene equal to another, a gene of whole numbers
-and a gene with an outlier sample; negative binomial counts of the same genes in 16
-samples, stored as integers and as decimals; Poisson counts with means from 0.3 to 6
-(low counts); 400 genes in 210 samples; the log-expression values rounded to one
-decimal; the same values with 2% of those of the first 12 samples missing; and a cube of
-one cell type with the counts, whose samples have 3 to 60 cells. The 66 gene sets have
-1 to 300 genes, a fifth of each outside the data: random sets, a set of one gene, one
-with the constant genes, one that lists genes twice, two identical sets and a set of
-every gene, which is left out for ssGSEA but in one case. Reference: R 4.4.3, GSVA
-2.0.0.
+[`validation/dream.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/dream.py)
+compares `voom_with_dream_weights`, `moderated_fixed_effect_tests` and `dream_voom`,
+and the maximum likelihood fits, fitted values and approximate residual degrees of
+freedom of `fit_mixed_model` they come from, with variancePartition's
+`voomWithDreamWeights`, `dream(ddf = "Satterthwaite")`, `eBayes` and `topTable`
+(`dream_voom` with `df_method="satterthwaite"`; [Kenward-Roger's tests and
+F-tests](#validation-kenward-roger-and-f-tests) checks its default, the other choices of degrees of freedom and the F-tests), on
+negative binomial counts of 60 genes from fixed seeds (20261025 to 20261029): a random
+donor intercept with a condition, an age and the contrast `conditionstim - age` (10
+donors x 3 samples, with two hypervariable genes), moderated also with `robust = TRUE`
+and `trend = TRUE`; crossed donor and batch intercepts (8 donors x 4 samples over 3
+batches) with sample prior weights; unbalanced donors with 1 to 4 samples, with prior
+weights for every observation; and `dream_voom` on three cell types of a cube of 8
+donors x 2 conditions, with prior weights from the numbers of cells, the samples with
+fewer than 10 cells left out (both samples of a donor and one of another in one cell
+type, and all but one sample of each of three donors in the third), and the moderation
+also with `trend = TRUE`. Reference: R 4.4.3, variancePartition 1.36.2,
+limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3, pbkrtest 0.5.3, Matrix 1.7-4, RcppEigen
+0.3.4.0.2 and minqa 1.2.8. dream is compared on lme4 1.1-38, while the
+[mixed models](#validation-mixed-models) are compared on lme4 2.0.6: variancePartition 1.36.2's
+`dream` fails for every gene with lme4 2.0, which reworked lme4's random-effect terms.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| GSVA's method: the Gaussian kernel, tau 0.5 with maxDiff FALSE, tau 2 with absRanking, no kernel, sizes 10 to 100, counts stored as integers (the Poisson kernel), as decimals (the Gaussian kernel) and 210 samples (no kernel), in 8 cases | `gsva` with `gsvaParam` | 0 | 1e-10 | 8 pass |
-| ssGSEA: the defaults, alpha 1 without normalization, alpha 0 on tied values, in 3 cases | `ssgseaParam` | 1.8e-15 | 1e-10 | 3 pass |
-| Combined z-scores | `zscoreParam` | 2.7e-15 | 1e-10 | 1 pass |
-| PLAGE, the sets of two genes or more, up to sign | `plageParam` | 8.7e-14 | 1e-10 | 1 pass |
-| Missing values: GSVA's method in the samples without any, ssGSEA and combined z-scores | `gsvaParam` and `ssgseaParam` with `use = "na.rm"`, `zscoreParam` | 2.0e-15 | 1e-10 | 3 pass |
-| A cube's cell type: its log-CPM and its counts | `gsva` with `gsvaParam` | 0 | 1e-10 | 2 pass |
-| `plot_pathway_scores`: the 1,584 cells of the standardized heatmap, the samples grouped | GSVA, R's `heatmap(scale = "row")` | 3.2e-16 | 1e-10 | 1 pass |
-| PLAGE: the share of sets whose sign is GSVA's | `plageParam` | 0.42 | n/a | 1 information |
-| GSVA's method with the Poisson kernel on low counts | `gsvaParam` | 2.2e-3 | n/a | 1 information |
-| GSVA's method in the samples with missing values; PLAGE's sets of one gene; ssGSEA with a set of every gene | `gsvaParam`, `plageParam`, `ssgseaParam` | 1.6 | n/a | 3 deliberate differences |
+| Precision weights and the square roots of the residual SDs in the trend; with prior weights for samples or observations, also the trend's weights alone | `voomWithDreamWeights` | 2.1e-8 | 1e-6 | 3 pass |
+| Coefficients and a contrast, relative to the larger of their absolute value and standard error, and standard errors | `dream` | 6.2e-8 | 1e-6 | 3 pass |
+| Satterthwaite df | `dream` | 2.2e-7 | 1e-5 | 3 pass |
+| Residual SD of an observation of average weight, and approximate residual df | `dream` `sigma`, `rdf` | 2.8e-8 | 1e-6 | 3 pass |
+| Prior df and variance, posterior variances (constant, robust and trend priors) | `eBayes` | 1.6e-8 | 1e-6 | 5 pass |
+| Moderated t-statistics and B-statistics, relative to max(\|x\|, 1) | `eBayes` | 1.2e-7 | 1e-6 | 5 pass |
+| Total df, p-values, adjusted p-values and z-statistics | `eBayes`, `topTable` | 7.9e-8 | 1e-5 | 5 pass |
+| Maximum likelihood and REML at CytePool's estimates: log-likelihood, residual variance, approximate residual df, and fitted values relative to their largest value | lme4 `logLik`, `sigma`, `fitted`; `rdf.merMod` | 5.9e-15 | 1e-10 | 4 pass |
+| Satterthwaite df there, of maximum likelihood and REML fits | lmerTest `summary` | 5.8e-7 | 1e-5 | 4 pass |
+| Maximum likelihood optimum: log-likelihood and residual variance | lme4 `logLik`, `sigma` | 2.5e-8 | 1e-6 | 2 pass |
+| Genes whose log-likelihood is below lme4's maximum by more than 1e-10, relative | lme4 `logLik` | 0 genes | 0 | 2 pass |
+| `dream_voom`, in two cell types: samples used and features tested | the rules of `limma_voom` | 0 differences | 0 | 2 pass |
+| `dream_voom`, with constant and trend priors: logFC and AveExpr | `topTable` | 1.5e-8 | 1e-6 | 4 pass |
+| `dream_voom`: t and B | `eBayes` | 2.7e-8 | 1e-6 | 4 pass |
+| `dream_voom`: df, P.Value, adj.P.Val and z.std | `eBayes`, `topTable` | 1.1e-7 | 1e-5 | 4 pass |
+| Prior df (median and smallest) and the estimator of the prior | CytePool | 8.4 to 15.2; robust 5.7 to 12.0; likelihood | n/a | 5 information |
+| Deliberate differences, listed below | variancePartition | 5 inputs | n/a | 5 deliberate differences |
-Differences are relative to the largest absolute score of a case, NaN must stand where
-GSVA gives NA, and the sets scored must be GSVA's, in its order. The scores depend on
-the ranks of the genes in each sample, which come from comparisons of their kernel
-estimates; CytePool evaluates the normal distribution function from a table at steps
-of 0.001, as GSVA does, and sums the estimates over the samples in their order, so that
-the ranks, and with them the scores, are GSVA's on continuous values and on counts of
-the sizes of the cases above. With low counts, many genes have kernel estimates that
-are equal in exact arithmetic, such as genes with the same counts in different
-samples, and GSVA orders them by the last bit of R's Poisson probabilities, which
-CytePool computes as accurately but not always to the same bit: the scores of the sets
-that hold them then differ by up to 2.2e-3 of the largest score. PLAGE's sign is that
-of R's singular value decomposition, which agrees with CytePool's convention for 42% of
-the sets.
+variancePartition fits every gene with lme4, whose optimizer stops at about 1e-6 of
+the optimum with its default tolerances. The script runs it with Powell's BOBYQA from
+minqa (lme4's `"bobyqa"`) with the trust region shrunk to 1e-10, which lme4's
+`optimizer` argument accepts by name as a function that reports convergence: at such
+tolerances lme4 warns for some genes, and variancePartition turns the warning into an
+error and drops the gene. BOBYQA from nloptr at tolerances near machine precision
+ended at worse points inside `voomWithDreamWeights` for crossed random effects, so it
+is not used. Estimates are then compared to the close tier, and Satterthwaite's
+degrees of freedom, with the p-values and z-statistics that use them, to 1e-5, as in
+the section on mixed models: lmerTest differentiates the deviance numerically. At
+CytePool's estimates, lme4 evaluated once (`maxeval = 1`) gives closed forms, compared
+to the exact tier. P-values are scaled as [above](#reading-the-tables). In the cube the features tested and the TMM factors are CytePool's
+(`filter_by_expression` and `tmm_factors`, which [Normalization and
+filtering](#validation-normalization) checks against edgeR), passed to R with the samples kept by the rule of `limma_voom`; R gets the
+effective library sizes and the numbers of cells as prior weights, with
+`rescaleWeightsAfter = TRUE`.
+
+The conventions of variancePartition that its documentation leaves open, found by
+probing it and listed in the documentation of `cytepool.stats.dream`, are all exercised
+here: `voomWithDreamWeights` fits the mixed models by maximum likelihood and reads the
+trend at the fitted values with the predicted random effects; it divides each gene's
+prior weights by their mean, and by default returns the trend's weights alone; `dream`
+fits each gene with its weights divided by their mean, so that `sigma` is the residual
+standard deviation of an observation of average weight; `eBayes` moderates these
+variances on `rdf` with limma's default estimator (here the likelihood estimator, as
+the `rdf` differ), and the p-values use Satterthwaite's df plus the prior's, without
+limma's cap. With `robust = TRUE` the two hypervariable genes got prior df of 5.7,
+against 12.0 for the others.
Deliberate differences:
-- GSVA's method in samples with missing values: with `use = "na.rm"`, GSVA's walk
- falls by 1/(p - k) at a sample's observed genes outside a set, p counting the genes
- missing in the sample too, so that it ends above zero, and it scores the set of every
- gene there; CytePool's walk falls by 1/(p_j - k_j), with p_j and k_j counting the
- genes observed in the sample, and ends at zero (largest difference 4.8e-2, where both
- give a score). The samples without missing values agree exactly.
-- PLAGE's sets of one gene: GSVA gives 1 in every sample; CytePool gives the gene's
- standardized values, scaled to length one, its first right singular vector (largest
- difference 1.6).
-- ssGSEA with a set of every gene: GSVA stops ("Cannot calculate normalizing factor for
- the enrichment scores in ssGSEA, likely due to NA values in the input expression
- data"); CytePool gives that set NaN and normalizes the other scores by their range.
+- The default span: `voom_with_dream_weights` chooses it from the number of genes, as
+ `voom` does (0.959 for 60 genes), and `voomWithDreamWeights` uses 0.5; with the
+ defaults, the weights differ by up to 26%. `span=0.5` reproduces variancePartition.
+- A z-statistic beyond p = 1e-300: `topTable` computes `z.std` from the p-value
+ floored at 1e-300, so it gives -37.07 for a contrast scaled up 1e13 times, where
+ CytePool's z-statistic, from the logarithm of the tail probability, is -44.59.
+- A gene that the fixed and random effects reproduce, constant within every donor:
+ `voomWithDreamWeights` keeps it in the trend with the square root of a residual SD
+ of 3.7e-4, and `dream` keeps it; CytePool leaves it out of the trend and returns its
+ tests as NaN.
+- A design without random effects: `dream` fits it with `lmFit` (class `MArrayLM`);
+ `dream_voom` refuses it and points to `limma_voom`.
+- A cell type left with one sample per donor: `voomWithDreamWeights` stops ("Initial
+ model failed"); `dream_voom` reports the status `random_effects_not_estimable` and
+ goes on with the other cell types.
+- The degrees of freedom of `topTable`'s F-tests, and the condition number that decides
+ which genes fall back to Satterthwaite's method: [Kenward-Roger's tests and
+ F-tests](#validation-kenward-roger-and-f-tests) checks both.
-## Pathway and regulator activity
+(validation-variance-partitioning)=
+### Variance partitioning
-[`validation/activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py)
-checks the activities of regulators and pathways, their bar plot and the networks
-against decoupler 2.2.0 and progeny 1.28.0. CytePool's loaders download the networks
-into its cache. CollecTRI's file and progeny's package are compared with the MD5
-digests and sizes their sources publish (Zenodo's record, Bioconductor's `PACKAGES`
-and the server's `Content-Length`); `collectri(licence="academic")` with decoupler's
-`op.collectri`; the commercial level with the licence purposes that OmniPath's
-resources service lists, applied to the file by the script; and the weights and
-p-values of `progeny` with progeny's `model_human_full` and `model_mouse_full`, and its
-`top` with `getModel` (100 human genes; 500 mouse genes, with ties). The inputs of the
-activities are simulated from fixed seeds: log-expression values of 6 samples over
-CollecTRI's 6,693 targets and 2,000 other genes, the targets of 6 regulators shifted in
-the last 3 samples, with 3 genes zero in every sample; a profile of t-statistics; the
-values with 3% missing in two samples, which decoupler, refusing missing values,
-scores one profile at a time over its observed genes; 6 samples over the targets of
-PROGENy's top 500 genes and 1,000 others; a cube of one cell type, scored as its
-log-CPM; and limma_voom's moderated t-statistics in two cell types that test different
-genes. `ulm`, `mlm` and `weighted_aggregate` run as decoupler's `mt.ulm`, `mt.mlm` and
-`mt.waggr` with `tmin` 5, or 3 for a network of weights of one sign. Reference:
-decoupler 2.2.0 with scipy 1.18.1, and progeny 1.28.0 on R 4.4.3.
+The script printed 31 checks (22 pass, 6 information and 3 deliberate differences) in
+63.1 s, with a peak memory of 394 MiB. [Mixed models and
+dream](statistics/mixed_models.md#variance-fractions) explains what it checks.
+
+[`validation/variance_partitioning.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/variance_partitioning.py)
+compares `variance_fractions` and `variance_partitioning`, and the figures
+`plot_variance_fractions` and `plot_feature_variance_fractions`, with variancePartition's
+`fitExtractVarPartModel`, `calcVarPart`, `plotVarPart` and `plotPercentBars`, on data
+from fixed seeds (20261101 to 20261108), 60 genes each: one random donor intercept (10
+donors x 3 samples, a fifth of the genes without a donor effect); random donor, batch
+and condition intercepts with a donor's age and a sample's RNA integrity as fixed
+effects and precision weights for every observation (8 donors x 4 samples), also
+fitted by REML; unbalanced donors with 1 to 4 samples and sample weights; nested
+donor and tissue intercepts (6 donors x 2 tissues x 2 replicates); a linear model of
+the age, the RNA integrity, the batch and the condition with the same weights (which
+variancePartition fits with `lm`); negative binomial counts of 80 genes with the
+weights of `voomWithDreamWeights(span = 0.5)`; and `variance_partitioning` on three
+cell types of a cube of 8 donors x 2 conditions, with prior weights from the numbers
+of cells, three samples left out of one cell type for having fewer than 10 cells, and
+one sample per donor in the third. Reference: R 4.4.3, variancePartition 1.36.2,
+lme4 1.1-38, minqa 1.2.8 and ggplot2 4.0.3, in the R environment of `dream.py`.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| ULM's t-statistics and slopes, in 10 cases | `mt.ulm` | 2.3e-13 | 1e-10 | 20 pass |
-| ULM's p-values, and their Benjamini-Hochberg adjustment, in 10 cases | `mt.ulm`; scipy's `false_discovery_control` of its p-values | 4.4e-11 | 1e-10 | 20 pass |
-| ULM's adjusted p-values, in single precision | `mt.ulm` | 0 | 1.2e-7 | 10 pass |
-| MLM's t-statistics and coefficients, in 6 cases | `mt.mlm` | 3.2e-14 | 1e-10 | 12 pass |
-| MLM's p-values | scipy's `t.sf` of `mt.mlm`'s t-statistics | 3.3e-12 | 1e-10 | 6 pass |
-| Weighted means and sums, and the normalized scores given decoupler's permutations (100 and 1,000) | `mt.waggr` | 1.1e-14 | 1e-10 | 4 pass |
-| Weighted sums of PROGENy's top 100 genes | progeny's `progeny(scale = FALSE)` | 6.5e-16 | 1e-10 | 1 pass |
-| `plot_top_activities`: the 15 bars of a sample, their sources and lengths | `pl.barplot` | 4.8e-14 | 1e-10 | 1 pass |
-| CollecTRI at the academic level; at the commercial level; PROGENy's weights and p-values, human and mouse; its top 100 human and 500 mouse genes | `op.collectri`, OmniPath's resources, progeny's models and `getModel` | 0 | 0 | 6 pass |
-| The files downloaded: sizes and MD5 digests | Zenodo, Bioconductor | 0 | 0 | 2 pass |
-| decoupler's `op.collectri` at its levels; PROGENy through OmniPath | `op.collectri`, `op.progeny` | n/a | n/a | 2 information |
-| The weighted aggregate's p-values; a sample of zeros; MLM with two sources alike; CollecTRI's interactions without a reference | `mt.waggr`, `mt.ulm`, `mt.mlm`, `op.collectri` | 0.97 | n/a | 5 deliberate differences |
-
-Scores and estimates are compared relative to the largest absolute value of a case,
-p-values relative to each value, and NaN must stand where the reference has none; the
-networks and the bars must be identical. decoupler keeps its adjusted p-values in
-single precision, so CytePool's are rounded to it first. decoupler computes MLM's
-p-values as one minus the distribution function, which loses their digits below about
-1e-12, so CytePool's are compared with scipy's upper tail at decoupler's t-statistics.
-The normalized scores are compared given decoupler's permutations: the genes in the
-order in which decoupler shuffles them, and its seed, so that both draw the same
-permutations. decoupler 2.2.0 accepts a licence for CollecTRI but returns the whole
-file at every level. Its `op.progeny` reads PROGENy from OmniPath's annotations, which
-name genes by their current symbols, leave out genes OmniPath does not map to a
-protein, and merge some genes: of its 62,421 edges with a p-value below 0.05, where the
-progeny package has 69,827, 59,617 are named alike, and 59,599 of them have the same
-weight and p-value.
+| Fractions and variances at the optimum, relative to the gene's total variance: one, several, unbalanced and nested random effects, and voom's weights | `fitExtractVarPartModel`, `calcVarPart` | 4.4e-8 | 1e-6 | 5 pass |
+| The same, fitted by REML | `fitExtractVarPartModel(REML = TRUE)` | 2.0e-7 | 1e-6 | 1 pass |
+| A linear model with factors and observation weights | `fitExtractVarPartModel` (`lm`) | 3.8e-15 | 1e-10 | 1 pass |
+| Variances at CytePool's estimates | lme4 (`maxeval = 1`), `calcVarPart` | 1.9e-13 | 1e-10 | 5 pass |
+| Random effects estimated at zero: CytePool's exact zeros and lme4's below 1e-10 | `fitExtractVarPartModel` | equal in every case (19 to 91) | n/a | 6 information |
+| `variance_partitioning`, in two cell types: samples used and features | the rules of `dream_voom` | 0 differences | 0 | 2 pass |
+| `variance_partitioning`: fractions | `voomWithDreamWeights`, `fitExtractVarPartModel` | 3.4e-8 | 1e-6 | 2 pass |
+| Figures: the variables and their order, the quartiles, whiskers and outliers of the violins, and the segments of the bars, in percent over 100 | `plotVarPart(sortCols())`, `plotPercentBars` | 5.3e-16 | 1e-10 | 3 pass |
+| The violins' half-widths at R's points, relative to the widest | `plotVarPart` | 5.9e-4 | 1e-3 | 1 pass |
+| Designs both refuse: a factor among the fixed effects of a mixed model, and a formula without an intercept | `fitExtractVarPartModel` | 0 differences | 0 | 2 pass |
+| Deliberate differences, listed below | variancePartition | 3 inputs | n/a | 3 deliberate differences |
-Deliberate differences:
+lme4 runs with Powell's BOBYQA with its trust region shrunk to 1e-10, as in `dream.py`,
+so the fractions at the optimum are compared to the close tier. Differences are taken
+relative to each gene's total variance, the scale of a fraction: lme4 leaves a variance
+component at the boundary at about 1e-16 of the total where CytePool holds it at zero,
+and the two found the same components at zero in every case. At CytePool's estimates,
+lme4 evaluated once and `calcVarPart` give closed forms, compared to the exact tier,
+as are linear models, which have closed forms. The figures are drawn by R from
+CytePool's fractions, so they compare the drawing alone. The violins agree to 1e-3 of
+their widest point: ggplot2 takes R's `density()`, which bins the values on a grid of
+512 points and convolves them by the fast Fourier transform, as its help page
+describes, and differs from the kernel density estimate that CytePool draws by up to
+5.9e-4 here. ggplot2 stacks a segment of no width anywhere in its bar, so only the
+segments with a width are compared by position.
-- The weighted aggregate's p-values (largest difference 0.97 with 100 permutations,
- 2.0e-3 with 1,000): decoupler counts the permutations whose absolute score exceeds
- the absolute observed score and then folds that share as it would fold a one-sided
- one, so that a score near zero gets a p-value near 2 / n for n permutations; CytePool
- doubles the smaller of the two one-sided p-values (1 + b) / (1 + n).
-- A sample whose values are all zero: decoupler leaves it out of its result; CytePool
- gives it NaN scores. The other samples agree exactly.
-- MLM with two sources of the same targets and weights: decoupler inverts a singular
- matrix and gives NaN t-statistics for both (12 in 6 samples); CytePool refuses,
- naming the source it cannot tell apart from the other.
-- CollecTRI's 185 interactions whose field of references is empty: decoupler's loader
- drops them, through pandas' `dropna`; CytePool keeps them, as the file holds them and
- the paper counts them (43,175 interactions).
+The conventions of variancePartition that its documentation leaves open, found by
+probing it and listed in the documentation of `cytepool.stats.variance_fractions`, are
+exercised here: the variance of a fixed effect is the weighted variance of its fitted
+contribution, with the denominator of a maximum likelihood variance; in a mixed model
+the terms share the variance of all the fitted fixed effects in proportion to their
+own, and in a linear model each term has its own; each gene's precision weights are
+divided by their mean. The two covariates of the second case have a correlation of
+-0.13 over its samples, and without the sharing its fractions would differ by up to
+0.034.
-## Multiple testing
+Deliberate differences:
-[`validation/multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py)
-runs every method of `adjust_p_values` (Holm, Hochberg, Hommel, Bonferroni,
-Benjamini-Hochberg, Benjamini-Yekutieli and none) on each case and compares it with
-R's `p.adjust`. Each check covers all seven methods. Reference: R 4.4.3,
-`stats::p.adjust`.
+- A gene constant within every donor: variancePartition reports the fractions of
+ lme4's fit with a residual variance near zero, a donor fraction of 1.0000; CytePool
+ leaves the gene unfitted, with NaN fractions, as its mixed models have no maximum
+ likelihood estimate for such a gene.
+- REML without random effects: variancePartition fits `lm` and takes the residual sum
+ of squares over n whatever `REML` says, so its fractions do not change; CytePool
+ takes REML's residual variance, over n - p, which changed a fraction by up to 0.052.
+- A cell type with one sample per donor: `voomWithDreamWeights` stops ("Initial model
+ failed"); `variance_partitioning` reports the status `random_effects_not_estimable`
+ and goes on with the other cell types.
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| 2,000 uniform values; rounded values with ties, 0 and 1; 10% NaN; 10% exactly 0 or 1; n = 2,000 for 500 values with ties and NaN | `p.adjust` | 0 | 1e-10 | 5 pass |
-| All 9,330 vectors of length 1 to 5 over {0, 0.01, 0.03, 0.5, 1, NaN}, with the default n and with n = 6 | `p.adjust` | 0 | 1e-10 | 2 pass |
-| A 2,000 x 4 matrix adjusted by column, 0 to 50% NaN | `p.adjust` | 0 | 1e-10 | 1 pass |
-| 50 random families of 2 to 2,000 values, with the default n and with n = 2,500 | `p.adjust` | 1.4e-16 | 1e-10 | 2 pass |
-| 100,000 values, 10% duplicated | `p.adjust` | 0 | 1e-10 | 1 pass |
-| The small cases written as test fixtures | `p.adjust` | 0 | 1e-10 | 4 pass |
+## Composition tests
-Of the 290,224 p-values each method adjusted, all but 5 Hommel values were identical
-to R's to the last bit.
+(validation-composition-tests)=
+### Cell-type composition
-## Differential expression
+The script printed 32 checks (25 pass and 7 deliberate differences) in 13.1 s, with a
+peak memory of 860 MiB. [Composition tests](statistics/composition.md#propeller-steps) explains what it
+checks.
-[`validation/differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py)
-simulates cells with a donor structure (seed 20261025), pools them into cubes with
-`from_cells`, and runs `limma_voom` and `limma_trend` on every cell type; R runs the
-workflow of the limma and edgeR User's Guides on the same pseudobulk counts, group by
-group: it leaves out the samples with fewer than 10 cells, keeps the rows of the
-remaining samples of a design built by `model.matrix` on the full sample table, and
-runs `filterByExpr`, `calcNormFactors`, `voom` (or `cpm(log = TRUE, prior.count = 3)`
-for limma-trend), `lmFit`, `contrasts.fit` one contrast at a time, `eBayes` and
-`topTable(sort.by = "none")`. voom chooses its span from the number of genes tested
-in each group, in both programs (`adaptive.span = TRUE` in R, limma's default since
-3.66), except in the case that fixes it at 0.5 (`span = 0.5, adaptive.span = FALSE`);
-limma's voom reports the span it chose (found by probing), and each group's span is
-compared. The cases are a paired design, `~ donor + condition` (6 donors x 2
-conditions, 4 cell types, 1,000 genes), by limma-voom and limma-trend, and an
-unpaired design with a covariate, `~ condition + age` (10 donors, 3 cell types), by
-limma-voom with robust moderation and a span of 0.5, and by limma-trend without
-filtering or normalization factors. The paired design tests the stimulation and
-three donor comparisons: `D2_vs_D1`, `D2_vs_D6` and `D3_vs_D2`. Some sample x cell
-type entries have fewer than 10 cells; in the NK cells both samples of donor D1, the
-reference level, are left out, which makes the column `donorD6` a combination of the
-others, and in the DC cells four samples remain, too few for any residual degrees of
-freedom.
-Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0.
+[`validation/composition_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/composition_tests.py)
+compares `propeller` with speckle's `propeller`, `propeller.ttest` and
+`propeller.anova`. It simulates the number of cells of every sample in every cell type
+from a Dirichlet-multinomial model (seed 20261025), holds them in cubes, and gives
+speckle one label per cell; `propeller.ttest` and `propeller.anova` get
+`getTransformedProps` and design matrices from `model.matrix`. Every case runs with
+the logit and the arcsine square-root transform, and with speckle's defaults (robust
+moderation, no trend) unless a row says otherwise. The cases are two groups of 5
+samples with 1,000 to 5,000 cells and 12 cell types, three of which change, also with
+`robust = FALSE` and `trend = TRUE`; three groups of 4 samples, by speckle's F-test;
+three groups of 5 samples and a covariate (age), `~ 0 + group + age`, with the
+contrasts B - A and (A + B)/2 - C, the F-test of `group` (`propeller.anova` with
+`coef = 1:3`), the slope of age and A + B, and `~ group + age` with the contrast
+`groupB`; a pairing of 6 donors in two conditions, `~ 0 + condition + donor` and
+`~ donor + condition`; 8 samples with 6 to 32 cells, in which most cell types are
+absent from some samples and two from all of them, in a cube that also holds a cell
+type and a sample without cells; two cell types, which limma does not moderate; and
+samples that are copies of one another within each condition. Reference: R 4.4.3,
+speckle 1.6.0, limma 3.62.1.
-| Check family | Reference | Largest difference | Tolerance | Outcome |
+| Check family | Reference | Largest difference: proportions, statistics | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Samples used, features tested, coefficients that cannot be estimated, contrasts tested (other than the deliberate differences), voom's span, residual and prior df, in the 12 fitted groups | `filterByExpr`, `voom`, `lmFit`, `contrasts.fit`, `eBayes` | 0 mismatches | 0 | 12 pass |
-| logFC, AveExpr, t, P.Value, adj.P.Val and B of every contrast both programs compute, and the moderated standard error of logFC (`sqrt(s2.post) * stdev.unscaled`) | `topTable`, `eBayes` | 6.3e-13 | 1e-10 | 9 pass |
-| The same with robust moderation | `topTable` | 1.0e-13 | 1e-6 | 3 pass |
-| `D2_vs_D6` in the NK cells, which limma refuses, against limma's values for `D2_vs_D1`, which there estimate the same difference | `topTable` | 2.8e-13 | 1e-10 | 2 pass |
-| DC cells, without residual degrees of freedom: both programs refuse every contrast | `eBayes` | both refuse | n/a | 2 pass |
-| Deliberate differences, listed below | `contrasts.fit` | 2 contrasts limma refuses and CytePool computes, 2 limma computes and CytePool reports as not estimable | n/a | 4 deliberate differences |
+| Two groups (t-test) and three groups (F-test), 12 cell types: `BaselineProp`, `PropMean`, `PropRatio`, `Tstatistic` or `Fstatistic`, `P.Value` and `FDR` | `propeller` | 1.7e-15, 3.9e-9 | 1e-10, 1e-6 | 4 pass |
+| Two groups with `robust = FALSE` and with `trend = TRUE` | `propeller` | 5.4e-16, 6.7e-15 | 1e-10, 1e-10 and 1e-6 | 2 pass |
+| B - A and (A + B)/2 - C with a covariate, and B - A as `groupB` in `~ group + age` | `propeller.ttest` | 6.1e-16, 9.4e-15 | 1e-10, 1e-6 | 6 pass |
+| F-test of `group` with a covariate | `propeller.anova` | 4.9e-16, 3.6e-15 | 1e-10, 1e-6 | 2 pass |
+| The slope of the covariate, which speckle refuses | limma `eBayes` on speckle's transformed proportions | none reported, 3.0e-15 | 1e-6 | 2 pass |
+| A pairing, in group means and in treatment coding | `propeller.ttest` | 4.7e-16, 7.7e-15 | 1e-10, 1e-6 | 4 pass |
+| Few cells and absent cell types; the sample without cells, with the arcsine transform | `propeller`, `propeller.ttest` | 3.7e-16, 3.5e-15 | 1e-10 | 3 pass |
+| Two cell types | `propeller` | 2.3e-16, 3.3e-15 | 1e-10 | 2 pass |
+| Deliberate differences, listed below | `propeller.ttest`, `propeller` | 2 contrasts speckle refuses, 2 ratios of contrasts that compare nothing, 2 unused levels speckle keeps, 1 data set CytePool refuses | n/a | 7 deliberate differences |
-Differences are scaled by max(\|reference\|, 1), and p-values by
-max(p, 1e-300) max(1, \|log p\|). limma-voom ran with
-`contrast_standard_errors="limma"`, except in the robust case, whose contrasts are
-single coefficients, for which the default exact standard errors are limma's. In
-the NK cells, `D3_vs_D2` combines two of the columns the group keeps, and agrees with
-limma's approximate standard errors too. The robust case keeps the close tier because
-limma solves the robust prior only to a tolerance; here it agreed to 1e-13.
-Deliberate differences, in the NK cells of both paired cases:
+Each row compares every column that speckle reports, matched by cell type, and the
+columns themselves must match: the mean proportions of the levels a contrast
+compares, and no others. Proportions and ratios are compared relative to speckle's
+values, in the exact tier; statistics, p-values and FDRs are scaled as [above](#reading-the-tables),
+in the exact tier, or in the
+close tier with robust moderation of 10 cell types or more, whose prior limma solves
+only to a tolerance. The largest difference, 3.9e-9, is in such a case; every other is
+below 1e-14. Deliberate differences:
-- `D2_vs_D6` (`donorD2 - donorD6`) is estimable without donor D1, but it weighs the
- aliased column `donorD6`, and limma's `contrasts.fit` stops with "trying to take
- contrast of non-estimable coefficient". CytePool computes it.
-- `D2_vs_D1` (`donorD2`, donor D2 against D1) cannot be estimated without donor D1's
- samples. limma computes it from the kept columns, where `donorD2` means donor D2
- against donor D6, the values of the third row of the table; CytePool reports it as
- not estimable.
+- speckle's `PropMean` are the coefficients of the proportions regressed on the
+ columns of the design matrix that a contrast weighs, and its `PropRatio` their
+ product raised to the contrast's weights (found by probing). Where those columns
+ indicate groups of samples, as in the group-means designs above, they are the
+ groups' mean proportions, and CytePool's values agree. Otherwise they are not. For
+ A + B, which compares nothing, speckle reports the product of the two means as a
+ ratio; CytePool reports no mean proportions and no ratio, and its t-statistics,
+ p-values and FDRs agree with speckle's. For the slope of the covariate,
+ `propeller.ttest` stops with "dim(X) must have a positive length", as it does for a
+ contrast of a single column such as `groupB` in `~ group + age`. CytePool finds the
+ levels a contrast compares from the design's coding: it tests the slope, without
+ mean proportions or ratio, and its statistics agree with limma's `eBayes` on
+ speckle's transformed proportions; `groupB` in `~ group + age` and `conditionstim`
+ in `~ donor + condition` agree with speckle's values for the same comparisons in
+ its group-means designs.
+- A cell type without cells: from cell labels speckle never sees it, and CytePool,
+ which leaves it out, agrees with it. From a factor of cell labels with an unused
+ level, speckle tests that level, and with the logit transform its half cells change
+ the transformed proportions of every other cell type (a scaled difference of 0.23
+ here).
+- A sample without cells, as an unused level of a factor of sample labels: with the
+ logit transform speckle gives each of its I cell types the proportion 1/I and fits
+ them as data (a scaled difference of 0.74 here); with the arcsine transform its
+ proportions are NaN and limma leaves it out, as CytePool does with either transform.
+- Samples that are copies of one another within each condition: the design fits the
+ transformed proportions of every cell type exactly, so no variance can be estimated.
+ speckle tests the rounding errors, with t-statistics up to 3.4e15 here; CytePool
+ raises an error.
+
+CytePool computes the transforms as log((x + 0.5) / (N - x + 0.5 (I - 1))) and
+arctan2(sqrt(x), sqrt(N - x)), equal to speckle's formulas but without the
+cancellation in 1 - q and the loss of digits of arcsin near 1; on these data the two
+agree to rounding.
-## Options for pseudobulk differential expression
+## Gene sets and pathways
-[`validation/differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py)
-checks the options of `limma_voom` and `limma_trend` for pseudobulk data against
-limma and edgeR. It simulates cells from 6 donors, each sampled before and after a
-stimulation (seed 20261027), in 3 cell types with 1,000 genes, pools them with
-`from_cells`, and fits `~ donor + condition` with two contrasts, `stim_vs_ctrl` and
-`D3_vs_D2`. One sample is an outlier in every cell type, two are sequenced shallowly
-in one cell type each, and in the NK cells both samples of donor D1, the reference
-level, have fewer than 10 cells, which makes the column `donorD6` a combination of the
-others. Six analyses each set some of the options, and R runs the workflow of the
-section above with the same options:
+(validation-gene-set-tests)=
+### Gene-set tests
-- sample quality weights (`quality_weights`): limma's `voomWithQualityWeights`, gene by
- gene and by REML, for limma-voom; `arrayWeights` on the log2 CPM, by REML and gene
- by gene, then `lmFit` with them as sample weights, for limma-trend;
-- prior weights from cell numbers (`prior_weights="n_cells"`): R counts the cells of
- each sample and cell type from the cells' labels, not from the cube, and uses them
- as prior weights: `voom(weights = n)`, whose weights times n enter `lmFit`, as
- voom's help page says its weights modify prior weights; independently, edgeR's
- `voomLmFit(prior.weights = n)`, which forms that product itself (found by
- probing); and `lmFit(weights = n)` for limma-trend;
-- a minimum total count per sample (`min_counts=30000`, in three analyses): R leaves
- out the samples whose column sum is below it, here the two shallow samples, besides
- those with fewer than 10 cells;
-- p-values adjusted across cell types (`adjust_across_groups`, in four analyses):
- `p.adjust(method = "BH")` of the `P.Value` of the three cell types stacked, one
- contrast at a time.
+The script printed 93 checks (79 pass, 8 deliberate differences and 6 information) in
+6.9 s, with a peak memory of 278 MiB. [Gene-set tests](statistics/gene_set_tests.md#camera)
+explains what it checks.
-Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0.
+[`validation/gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_tests.py)
+compares `camera` and `fry` with limma's `camera` and `fry` on log-expression of 1,500
+genes simulated from fixed seeds, with gene-specific variances from a scaled inverse
+chi-squared prior around a mean-variance trend. The gene sets change up, down or in
+both directions, or not at all, share random sample effects (inter-gene correlation)
+with or without a change, overlap others, hold one, two or five genes, or 40% of the
+genes. The cases are three groups with the contrast given as a design column, also
+with only sets of one or more than d genes (so that fry's adjusted mixed p-values can
+be compared); three groups in interleaved order with observation weights and the
+contrast given as weights (a named contrast); counts with voom weights from `voom`
+(limma gets the same weights matrix), with the contrast both ways; a paired design with
+sample weights; residual variances that are all equal, so that the prior df is
+infinite; 16 samples with sets of 2 to 30 unchanged genes and three sets with changed
+genes; a gene the design fits exactly and ten identical genes; and 8 genes, fewer than
+the residual df. Designs are `Design` objects with named contrasts and genes are named
+by id in CytePool; R gets the design matrices and the genes' positions. camera runs with
+the default inter-gene correlation of 0.01, with it estimated for each set, with
+negative correlations allowed, with ranks, with a trend and robustly; fry with each
+standardization, and with a trend and robustly. limma's camera has no robust option: its
+reference is `cameraPR` or `rankSumTestWithCorrelation` on the z-scores of the
+moderated t-statistics of `eBayes(robust = TRUE)`, limma's `zscoreT(approx = TRUE,
+method = "hill")`, with the design reparametrized by `contrastAsCoef`; with an estimated
+correlation, the parametric p-value of `cameraPR` is converted to the df of the
+estimate. Reference: R 4.4.3, limma 3.62.1.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Samples used, features tested, voom's span, residual and prior df, in the 18 fits (6 analyses x 3 cell types) | `filterByExpr`, `voom`, `lmFit`, `eBayes` | 0 mismatches | 0 | 18 pass |
-| Sample quality weights | `voomWithQualityWeights`, `arrayWeights` | 2.7e-11 | 1e-6 | 12 pass |
-| Prior weights against the number of cells R counts from the cells | `table` | 0 | 1e-10 | 6 pass |
-| logFC, AveExpr, t, P.Value, adj.P.Val and B of both contrasts, with quality weights | `topTable` | 1.1e-12 | 1e-6 | 12 pass |
-| The same with prior weights | `topTable` | 1.7e-12 | 1e-10 | 6 pass |
-| The same, limma-voom with prior weights, against edgeR | `voomLmFit` | 1.8e-13 | 1e-10 | 3 pass |
-| P-values adjusted across the cell types, with quality weights | `p.adjust` | 5.1e-13 | 1e-6 | 4 pass |
-| The same with prior weights | `p.adjust` | 7.6e-13 | 1e-10 | 4 pass |
-| Prior weights together with estimated sample weights: both programs refuse | `voomLmFit` | both refuse | n/a | 1 pass |
+| camera: numbers of genes, correlations, directions, p-values and FDR, with a given or estimated correlation, ranks and a trend | `camera` | 1.3e-14 | 1e-10 | 35 pass |
+| camera with robust moderation | `cameraPR`, `rankSumTestWithCorrelation` | 1.7e-12 | 1e-6 | 12 pass |
+| fry with `standardize` `"none"` and `"residual_sd"`: numbers of genes, directions, p-values and FDR, mixed p-values of sets of one or more than d genes, and their FDR | `fry` | 5.2e-13 | 1e-10 | 15 pass |
+| fry with `"posterior_sd"`, with and without trend or robust moderation | `fry` | 4.4e-5 | 1e-3 | 15 pass |
+| fry with `"posterior_sd"` and an infinite prior df, where both standardize as `"none"` | `fry` | 3.3e-14 | 1e-10 | 1 pass |
+| CytePool's mixed p-values of the sets of 2 to 14 genes against 200,000 random rotations of their effects | simulation | 0.025 (absolute) | 0.1 | 1 pass |
+| fry's mixed p-values of sets of 2 to d genes | `fry` | 0.88 (absolute) | n/a | 8 deliberate differences |
+| The rotation simulation: limma's mixed p-values of the sets of 2 to 14 genes; both programs' of the sets of 15 to 30 genes; both programs' of the three sets with changed genes (relative) | simulation | 0.59, 0.030, 0.99 | n/a | 3 information |
+| Samples sorted by group, observation weights and the contrast as weights: camera, fry with `"residual_sd"`, and the genes whose last pivot R's `qr()` signs by rounding noise | `camera`, `fry`, `qr` | 0.012, 0.48; 115 of 1,500 genes | n/a | 3 information |
+
+Correlations are compared as differences, p-values scaled as [above](#reading-the-tables)
+(limma's p-values above 1 count as 1), and a
+direction or a number of genes that differs counts as an infinite difference. Robust
+moderation is in the close tier because limma solves the robust prior df only to a
+tolerance. With `"posterior_sd"`, fry moderates each gene's mean square of its d + 1
+effects without the largest, divided by its expected value under the null hypothesis,
+as a variance on 0.92 d degrees of freedom, as limma does (found by probing); limma's
+expected value is about 7.3e-5 (d + 1) / d above the exact one that CytePool computes,
+so directional p-values differ by up to 4.9e-5 and mixed p-values by up to 5.7e-4 of
+their value, which the tolerance of 1e-3 allows. The rotation check uses sets without
+a change, whose p-values a simulation resolves: the beta approximation that both
+programs use for the mixed p-value is off by up to 0.03 at such p-values for sets of
+more than d genes, where the programs agree, and in the tail it can be much too small
+for both: sets with changed genes and rotation p-values of 3.2e-3 and about 6e-5 get
+6.3e-4 and 2.9e-7.
+
+In each gene's QR decomposition, CytePool treats a pivot that is zero up to rounding
+as zero, which R's `qr()` signs as positive. With the samples sorted by group and a
+contrast between groups given as weights, every gene's last pivot is such a zero.
+limma computes most of them exactly, but its arithmetic leaves rounding noise in a few
+percent of genes, which changes their residual bases, and camera and fry do so for
+different genes: fry's are the 115 of the 1,500 whose last pivot R's `qr()` signs by
+noise. camera's correlations of sets then differ by up to 0.012, and fry's p-values
+with `"residual_sd"` by up to a factor of 2. limma's results there change with the
+last bits of the weights, CytePool's do not; in every other case the two agree.
+
+Deliberate differences:
+
+- fry's mixed p-value of a set of 2 to d genes. Its statistic, the sum of the set's
+ squared standardized contrast effects, is r'(E'E)r under a random rotation r of the
+ d + 1 effects, which both programs approximate by a beta distribution with its first
+ two moments. CytePool uses all d + 1 dimensions, as for larger sets; limma uses only
+ the set's m nonzero eigenvalues of E'E as if r had m dimensions (found by probing).
+ Against 200,000 random rotations, CytePool's mixed p-values of the sets of 2 to 14
+ genes are within 0.025 and limma's off by up to 0.59.
+- A p-value above 1 from camera's rank test (its continuity correction can put both
+ tails above 1/2): limma reports twice the smaller tail, CytePool 1. A gene listed
+ twice in a set: limma counts it twice, CytePool once. A set without genes, or holding
+ every gene in camera: NaN p-values in CytePool, left out of the other sets'
+ adjustment; limma reports the direction "Up", in camera's rank test the p-value 2,
+ which it adjusts with the others, and in fry a mixed p-value of 0 for an empty set.
+ The tests check these.
-Differences are relative for the weights; the statistics are scaled as in the section
-above. Everything computed from sample quality weights is in the close tier, as they
-are iterative estimates; the rest is exact. limma-voom ran with
-`contrast_standard_errors="limma"`, since `D3_vs_D2` combines two coefficients. limma's
-quality weights on the rank-deficient design of the NK cells are those on its kept
-columns (found by probing), as CytePool's are. `voomLmFit` treats some genes with zero
-counts differently from `voom` and `lmFit`: it lowers their residual df, as its help
-page says, and can move their average log count in the trend (found by probing). In
-these data that happens only where `min_counts` leaves a donor with one sample, so
-`voomLmFit` is compared in the analysis of limma-voom with prior weights that does not
-set `min_counts`. limma's `voomWithQualityWeights` takes no prior weights, and edgeR's
-`voomLmFit` stops with "Can't specify prior.weights and estimate sample weights";
-CytePool refuses the two together as well.
+(validation-rotation-gene-set-tests)=
+### Rotation gene-set tests
-## dream
+The script printed 103 checks (81 pass, 20 deliberate differences and 2 information) in
+20.9 s, with a peak memory of 190 MiB. [Gene-set tests](statistics/gene_set_tests.md#roast)
+explains what it checks.
-[`validation/dream.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/dream.py)
-compares `voom_with_dream_weights`, `moderated_fixed_effect_tests` and `dream_voom`,
-and the maximum likelihood fits, fitted values and approximate residual degrees of
-freedom of `fit_mixed_model` behind them, with variancePartition's
-`voomWithDreamWeights`, `dream(ddf = "Satterthwaite")`, `eBayes` and `topTable`
-(`dream_voom` with `df_method="satterthwaite"`; [the next section](#validation-kenward-roger)
-checks its default, the other choices of degrees of freedom and the F-tests), on
-negative binomial counts of 60 genes from fixed seeds (20261025 to 20261029): a random
-donor intercept with a condition, an age and the contrast `conditionstim - age` (10
-donors x 3 samples, with two hypervariable genes), moderated also with `robust = TRUE`
-and `trend = TRUE`; crossed donor and batch intercepts (8 donors x 4 samples over 3
-batches) with sample prior weights; unbalanced donors with 1 to 4 samples, with prior
-weights for every observation; and `dream_voom` on three cell types of a cube of 8
-donors x 2 conditions, with prior weights from the numbers of cells, the samples with
-fewer than 10 cells left out (both samples of a donor and one of another in one cell
-type, and all but one sample of each of three donors in the third), and the moderation
-also with `trend = TRUE`. Reference: R 4.4.3, variancePartition 1.36.2,
-limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3, pbkrtest 0.5.3, Matrix 1.7-4, RcppEigen
-0.3.4.0.2 and minqa 1.2.8. dream is compared on lme4 1.1-38, while the
-[mixed models](#validation-mixed-models) are compared on lme4 2.0.6: variancePartition 1.36.2's
-`dream` fails for every gene with lme4 2.0, which reworked lme4's random-effect terms.
+[`validation/rotation_gene_set_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/rotation_gene_set_tests.py)
+compares `roast`, `mroast` and `romer` with limma's functions of the same names on
+log-expression of 1,500 genes simulated from fixed seeds, with the gene variances and
+gene sets of `gene_set_tests.py` and three sets with contribution weights: a signature
+whose genes go up or down, with weights +1 and -1; a set with weights of different
+sizes; and a set with half of its weights zero. The cases are three groups with the
+contrast given as a design column; interleaved groups with observation weights and a
+named contrast given as weights; the same with a trended prior estimated robustly;
+counts with voom weights; a paired design with sample weights; equal residual
+variances (an infinite prior df), also with effects so large that limma's log-odds of
+23 genes exceed 709.8; and contribution weights for every gene, drawn from -2, -1, 0.5,
+1 and 3. The exact checks give CytePool the rotations limma drew: a generator that
+replays the standard normal draws R gives limma after `set.seed`, in the order limma
+uses them (found by probing). limma tests each set with the same seed, which is how
+CytePool's `mroast` shares its rotations between sets. The calibrated checks let each
+program draw its own rotations, and allow a difference of 4 sd(p_1 - p_2) + 2 / (B + 1)
+for B rotations each, with the sd from the binomial variance of each estimate at their
+mean. Differences of p-values are absolute: each p-value counts rotations, so a
+difference of one count is at least 1 / (2B + 1). Reference: R 4.4.3, limma 3.62.1.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Precision weights and the square roots of the residual SDs in the trend; with prior weights for samples or observations, also the trend's weights alone | `voomWithDreamWeights` | 2.1e-8 | 1e-6 | 3 pass |
-| Coefficients and a contrast, relative to the larger of their absolute value and standard error, and standard errors | `dream` | 6.2e-8 | 1e-6 | 3 pass |
-| Satterthwaite df | `dream` | 2.2e-7 | 1e-5 | 3 pass |
-| Residual SD of an observation of average weight, and approximate residual df | `dream` `sigma`, `rdf` | 2.8e-8 | 1e-6 | 3 pass |
-| Prior df and variance, posterior variances (constant, robust and trend priors) | `eBayes` | 1.6e-8 | 1e-6 | 5 pass |
-| Moderated t-statistics and B-statistics, relative to max(\|x\|, 1) | `eBayes` | 1.2e-7 | 1e-6 | 5 pass |
-| Total df, p-values, adjusted p-values and z-statistics | `eBayes`, `topTable` | 7.9e-8 | 1e-5 | 5 pass |
-| Maximum likelihood and REML at CytePool's estimates: log-likelihood, residual variance, approximate residual df, and fitted values relative to their largest value | lme4 `logLik`, `sigma`, `fitted`; `rdf.merMod` | 5.9e-15 | 1e-10 | 4 pass |
-| Satterthwaite df there, of maximum likelihood and REML fits | lmerTest `summary` | 5.8e-7 | 1e-5 | 4 pass |
-| Maximum likelihood optimum: log-likelihood and residual variance | lme4 `logLik`, `sigma` | 2.5e-8 | 1e-6 | 2 pass |
-| Genes whose log-likelihood is below lme4's maximum by more than 1e-10, relative | lme4 `logLik` | 0 genes | 0 | 2 pass |
-| `dream_voom`, in two cell types: samples used and features tested | the rules of `limma_voom` | 0 differences | 0 | 2 pass |
-| `dream_voom`, with constant and trend priors: logFC and AveExpr | `topTable` | 1.5e-8 | 1e-6 | 4 pass |
-| `dream_voom`: t and B | `eBayes` | 2.7e-8 | 1e-6 | 4 pass |
-| `dream_voom`: df, P.Value, adj.P.Val and z.std | `eBayes`, `topTable` | 1.1e-7 | 1e-5 | 4 pass |
-| Prior df (median and smallest) and the estimator of the prior | CytePool | 8.4 to 15.2; robust 5.7 to 12.0; likelihood | n/a | 5 information |
-| Deliberate differences, listed below | variancePartition | 5 inputs | n/a | 5 deliberate differences |
-
-variancePartition fits every gene with lme4, whose optimiser stops at about 1e-6 of
-the optimum with its default tolerances. The script runs it with Powell's BOBYQA from
-minqa (lme4's `"bobyqa"`) with the trust region shrunk to 1e-10, which lme4's
-`optimizer` argument accepts by name as a function that reports convergence: at such
-tolerances lme4 warns for some genes, and variancePartition turns the warning into an
-error and drops the gene. BOBYQA from nloptr at tolerances near machine precision
-ended at worse points inside `voomWithDreamWeights` for crossed random effects, so it
-is not used. Estimates are then compared to the close tier, and Satterthwaite's
-degrees of freedom, with the p-values and z-statistics that use them, to 1e-5, as in
-the section on mixed models: lmerTest differentiates the deviance numerically. At
-CytePool's estimates, lme4 evaluated once (`maxeval = 1`) gives closed forms, compared
-to the exact tier. P-values are scaled by max(p, 1e-300) max(1, \|log p\|), as for
-voom. In the cube the features tested and the TMM factors are CytePool's
-(`filter_by_expression` and `tmm_factors`, which the sections above check against
-edgeR), passed to R with the samples kept by the rule of `limma_voom`; R gets the
-effective library sizes and the numbers of cells as prior weights, with
-`rescaleWeightsAfter = TRUE`.
+| roast given limma's rotations: the down, up, two-sided and mixed p-values, active proportions and numbers of genes of every set, for every set statistic and case | `roast` | 5.0e-16 | 1e-12 | 19 pass |
+| mroast given the same rotations for every set: two-sided p-values, and directions (mismatched sets) | `mroast` | 0; 0 sets | 1e-12; 0 | 38 pass |
+| romer given limma's rotations: up, down and mixed p-values and numbers of genes, for every set statistic, with and without shrinkage, with sample weights and with an infinite prior df | `romer` | 0 | 1e-12 | 7 pass |
+| limma's FDR and FDR.Mixed from its p-values by mroast's rule; numbers of genes and active proportions with each program's own rotations | `mroast` | 4.4e-16 | 1e-12 | 9 pass |
+| Each program's own rotations: mroast (mean, msq, mean50; B = 1,999) and romer (mean, mean50; B = 999) | `mroast`, `romer` | 0.54 of the bound | 1 | 5 pass |
+| mroast's mean statistic with 20,000 rotations against fry, its limit, with equal variances | `fry` | 0.34 of the bound | 1 | 1 pass |
+| CytePool's fry, mixed p-values of the sets of 2 to 6 genes, against the msq rotation test with 20,000 rotations | `mroast` | 0.015 (absolute) | 0.1 | 1 pass |
+| CytePool's msq p-values with every contribution weight multiplied by 10 (B = 1,999) | `roast` | 0 | 1e-12 | 1 pass |
+| limma's fry, mixed p-values of the sets of 2 to 6 genes, against the same | `mroast` | 0.45 (absolute) | n/a | 1 information |
+| Genes whose log-odds in limma exceed 709.8 (very large effects) | `romer` | 23 genes | n/a | 1 information |
+| p-values where the observed floor-mean or mean-square statistic is the smallest it can be | `roast` | 0.50 (absolute) | n/a | 6 deliberate differences |
+| floormean's mixed p-values of sets with negative weights; msq's up, down and two-sided p-values of sets with weights of both signs | `roast` | 0.65; 0.33 (absolute) | n/a | 6 deliberate differences |
+| limma's msq p-values with every weight multiplied by 10 (B = 1,999) | `roast` | 0.45 (absolute change) | n/a | 1 deliberate difference |
+| The set with weights of zero: its number of genes, and its mean50 p-values | `roast` | 8 against 16 genes; 0.039 | n/a | 6 deliberate differences |
+| romer with shrinkage where limma's posterior probabilities are NaN | `romer` | 0.15 (absolute) | n/a | 1 deliberate difference |
-The conventions of variancePartition that its documentation leaves open, found by
-probing it and listed in the documentation of `cytepool.stats.dream`, are all exercised
-here: `voomWithDreamWeights` fits the mixed models by maximum likelihood and reads the
-trend at the fitted values with the predicted random effects; it divides each gene's
-prior weights by their mean, and by default returns the trend's weights alone; `dream`
-fits each gene with its weights divided by their mean, so that `sigma` is the residual
-standard deviation of an observation of average weight; `eBayes` moderates these
-variances on `rdf` with limma's default estimator (here the likelihood estimator, as
-the `rdf` differ), and the p-values use Satterthwaite's df plus the prior's, without
-limma's cap. With `robust = TRUE` the two hypervariable genes got prior df of 5.7,
-against 12.0 for the others.
+The z-scores of the moderated t-statistics are Bailey's (1980) transformation on
+min(d_0 + d, 10^4) degrees of freedom; romer's shrinkage of each gene's contrast effect
+computes its log-odds on d_0 + d degrees of freedom, without the limit, and with the
+proportion of true null hypotheses of limma's `propTrueNull(method = "lfdr")`. These
+conventions, the set statistics' definitions where limma differs from the paper (the
+mean-50 statistic averages the ceiling(m / 2) largest values, and romer's floor-mean
+mixed statistic floors \|t\| at 1), and the order of limma's random draws were found
+by probing, and the exact checks confirm them, with an infinite prior df too. Without
+shrinkage, romer agrees with limma exactly on the data with very large effects, which
+shows that the difference with shrinkage comes from limma's NaN posterior
+probabilities. The fry checks give the reference that `gene_set_tests.py` approximated
+by simulation: the mixed p-values of fry for sets of 2 to d genes, where CytePool uses
+all d + 1 dimensions of the rotation and limma only the set's, against the rotation
+test of the same statistic, which `mroast`'s msq statistic is when all genes have the
+same residual variance (up to the limit on the df of the z-scores).
Deliberate differences:
-- The default span: `voom_with_dream_weights` chooses it from the number of genes, as
- `voom` does (0.959 for 60 genes), and `voomWithDreamWeights` uses 0.5; with the
- defaults, the weights differ by up to 26%. `span=0.5` reproduces variancePartition.
-- A z-statistic beyond p = 1e-300: `topTable` computes `z.std` from the p-value
- floored at 1e-300, so it gives -37.07 for a contrast scaled up 1e13 times, where
- CytePool's z-statistic, from the logarithm of the tail probability, is -44.59.
-- A gene that the fixed and random effects reproduce, constant within every donor:
- `voomWithDreamWeights` keeps it in the trend with the square root of a residual SD
- of 3.7e-4, and `dream` keeps it; CytePool leaves it out of the trend and returns its
- tests as NaN.
-- A design without random effects: `dream` fits it with `lmFit` (class `MArrayLM`);
- `dream_voom` refuses it and points to `limma_voom`.
-- A cell type left with one sample per donor: `voomWithDreamWeights` stops ("Initial
- model failed"); `dream_voom` reports the status `random_effects_not_estimable` and
- goes on with the other cell types.
-
-(validation-kenward-roger)=
-## Kenward-Roger's tests and F-tests
+- roast and mroast count a rotated statistic equal to the observed one as at least as
+ extreme, as Wu et al. (2010) and Phipson and Smyth (2010) define the p-value and as
+ limma's `romer` does; limma's `roast` counts only larger ones. Where the observed
+ floor-mean or mean-square statistic is the smallest it can be (a set without a gene
+ on one side of zero, or, for the floor-mean mixed statistic, without a gene whose
+ \|z\| exceeds 0.674), CytePool's p-value is 1 and limma's about 1/2. The script
+ checks that CytePool's p-value is 1 in each such case.
+- With contribution weights a_g, the mean-square statistic weighs each gene's z^2 by
+ \|a_g\|, as Wu et al. (2010) define it; limma's up, down and two-sided p-values differ
+ from it for sets with weights of both signs, and with more than 1,000 rotations its
+ p-values change when every weight is multiplied by the same number (here by up to
+ 0.45 for a factor of 10). The floor-mean mixed statistic weighs each gene by
+ \|a_g\|, where limma takes a negative weight as evidence against the set; the script
+ checks that CytePool's mixed p-value is limma's with the absolute weights.
+- A gene of contribution weight zero leaves the set: it counts neither in the number
+ of genes nor among the values that the mean-50 statistic averages. limma keeps it in
+ both, and leaves it out of the active proportions, as CytePool does.
+- romer's posterior probability that a gene is differentially expressed is
+ 1 / (1 + exp(-lods)). limma's is NaN beyond log-odds of 709.8 (\|t\| above about 38
+ with an infinite prior df), which makes the gene's shrunk contrast effect NaN and
+ changes the p-values of every set (found by probing).
+- A set without genes, or whose genes all have weight zero, has NaN p-values and no
+ direction in CytePool and is left out of the other sets' adjustment; limma reports
+ "down" and NA, or the smallest possible p-value when all weights are zero. A gene
+ listed twice counts once in CytePool, twice in limma. mroast tests every set with the
+ same rotations, where limma draws new ones for each set. The tests check these.
-[`validation/kenward_roger_and_f_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/kenward_roger_and_f_tests.py)
-compares `fixed_effect_tests(df_method="kenward_roger")` with lmerTest's `summary` and
-`contest1D` (`ddf = "Kenward-Roger"`) and pbkrtest's `vcovAdj`, and
-`fixed_effect_f_test` with pbkrtest's `KRmodcomp` and lmerTest's `contestMD` (by
-Kenward-Roger and by Satterthwaite), gene by gene, in 9 cases of 24 genes from fixed
-seeds: a time course of three time points within 6 donors with an age per donor, with
-the F-tests of two contrasts and of three that span two dimensions; unbalanced donors
-with 1 to 5 samples; crossed donor and batch intercepts; nested intercepts
-(`(1 | donor/sample)`); a design simulated without donor variance, so that 15 of the 24
-fits lie on the boundary; and observation weights like voom's, with one and with two
-random intercepts, each taken both ways: every observation at the average weight,
-against lme4 fits whose weights are divided by their mean, as dream fits them, and the
-precision weights, against pbkrtest on the same model without weights, its values,
-fixed effects and random effects multiplied by the square roots of the weights, which is
-the weighted model. It then compares dream with variancePartition's `dream` with
-`ddf = "Kenward-Roger"`, `"adaptive"` and `"Satterthwaite"`, `eBayes` and `topTable`,
-on negative binomial counts of 40 genes in studies of 12, 18, 19, 20 and 24 samples (a
-time course of three time points and an age), including `topTable`'s F-test of time,
-and `dream_voom` with its defaults on two cubes: cell types left with 12 and with 24
-samples, and one of 15 samples whose age, 32 years on average and about 1 apart, makes
-the adjusted covariance matrices ill conditioned. Reference: R 4.4.3, variancePartition
-1.36.2, limma 3.62.1, lme4 1.1-38, lmerTest 3.1-3, pbkrtest 0.5.3 and minqa 1.2.8.
+(validation-gene-set-files-and-ora)=
+### Gene-set files and over-representation
-| Check family | Reference | Largest difference | Tolerance | Outcome |
-| --- | --- | --- | --- | --- |
-| Adjusted covariance matrix of the coefficients, relative to each gene's largest entry | pbkrtest `vcovAdj` | 1.4e-13 | 1e-10 | 9 pass |
-| Kenward-Roger t-tests of every coefficient and a contrast: estimates (relative to the larger of their absolute value and standard error), standard errors and t-statistics (relative to max(\|t\|, 1)) | lmerTest `summary`, `contest1D` | 1.7e-12 | 1e-10 | 9 pass |
-| Their df and p-values | lmerTest `summary`, `contest1D` | 1.8e-12 | 1e-10 | 9 pass |
-| Kenward-Roger F-tests: F, numerator and denominator df, scaling and p-value, and lmerTest's F and df | pbkrtest `KRmodcomp`, lmerTest `contestMD` | 8.2e-13 | 1e-10 | 13 pass |
-| Satterthwaite F-statistics | lmerTest `contestMD` | 8.2e-13 | 1e-10 | 13 pass |
-| Satterthwaite F-tests: denominator df and p-values | lmerTest `contestMD` | 1.9e-7 | 1e-5 | 13 pass |
-| Kenward-Roger standard errors and df at lme4's own optimum | lmerTest `summary` | 1.2e-7 | 1e-6 | 9 pass |
-| dream: precision weights | `voomWithDreamWeights` | 2.1e-8 | 1e-6 | 5 pass |
-| dream with each `ddf`: standard errors and df of the tests | `dream` | 8.0e-8 | 1e-6 | 9 pass |
-| Moderated t-statistics, B-statistics and z-statistics (relative to max(\|x\|, 1)), total df and p-values | `eBayes`, `topTable` | 5.0e-8 | 1e-6 | 9 pass |
-| Moderated F-statistics of time | `topTable` | 4.1e-8 | 1e-6 | 11 pass |
-| `dream_voom`: samples used and features tested | the rules of `dream_voom` | 0 differences | 0 | 3 pass |
-| `dream_voom` with its defaults: t, B and z-statistics, df and p-values of every contrast | `dream`, `eBayes`, `topTable` | 3.3e-7 | 1e-6 | 3 pass |
-| Fits with a variance at zero; the two treatments of the weights; the default against Satterthwaite's method | CytePool | below | n/a | 16 information |
-| Deliberate differences, listed below | variancePartition | 12 inputs | n/a | 12 deliberate differences |
+The script printed 34 checks (29 pass and 5 deliberate differences) in 4.7 s, with a
+peak memory of 297 MiB. [Over-representation and
+GSEA](statistics/over_representation_and_gsea.md#over-representation) explains what it
+checks.
-At CytePool's estimates, which the script passes to lme4 as starting values with one
-evaluation allowed, as for [mixed models](#validation-mixed-models), pbkrtest computes
-Kenward and Roger's quantities from dense matrices without numerical derivatives, so
-they are compared to the exact tier; lmerTest's Satterthwaite F-tests differentiate the
-deviance numerically, compared to 1e-5. At lme4's own optimum, with Powell's BOBYQA and
-its trust region shrunk to 1e-10 as in the section on dream, and in dream's fits, which
-the script runs with that optimiser, the estimates agree to the close tier. P-values are
-scaled by max(p, 1e-300) max(1, \|log p\|), as for voom.
+[`validation/gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py)
+checks the reading and writing of GMT files, the matching of gene sets to an analysis,
+and over-representation analysis. Four GMT files of 60 sets are simulated from fixed
+seeds in the styles of MSigDB (names in capitals, links as descriptions, symbols),
+Reactome (names with spaces and punctuation, lines ending in a carriage return and a
+line feed, tabs at the ends of lines), WikiPathways (names joined by %, Entrez ids, a
+byte-order mark, blank lines) and the GMT exports of the Gene Ontology (descriptions
+"na" or empty, genes listed twice, a set without genes, a name outside ASCII,
+compressed by gzip). `read_gmt` reads each file and `write_gmt` writes it back; R reads
+both files by hand (`readLines`, fields split at tabs), and decoupler's `read_gmt`
+reads the MSigDB-style file. A file of 60 sets of symbols is matched to the 2,000 genes
+of an analysis by `GeneSets.restrict_to` and by limma's `ids2indices`, the sets of 15
+to 200 genes are kept, and `camera` tests them on simulated log-expression, given the
+sets that CytePool read, against limma's `camera` given those that R read.
-The checks confirm what probing pbkrtest, lmerTest and variancePartition as black boxes
-found, and the documentation of `cytepool.stats.mixed_models` and
-`cytepool.stats.dream` lists: pbkrtest takes the inverse of the expected information
-of REML, includes a variance component estimated at zero, which 15 fits of the
-boundary case have, and builds the covariance matrix of the observations without lme4's
-prior weights while it takes the covariance matrix of the coefficients from the
-weighted fit; lmerTest's `contest1D` and `summary` use the adjusted standard errors with
-pbkrtest's degrees of freedom; `dream` uses Kenward and Roger's method with the adjusted
-standard errors for 19 samples and Satterthwaite's for 20 under `ddf = "adaptive"`, and
-`topTable`'s F-statistic is limma's, from the moderated t-statistics and each gene's
-covariance matrix of the coefficients, the adjusted one under Kenward and Roger's
-method. In the ill-conditioned cell type `dream` tested 24 genes by Satterthwaite's
-method because R's estimate of the condition number of their adjusted covariance
-exceeded 1e6, and `dream_voom` the same 24, from the exact condition number.
+`over_representation` runs on simulated results of 4,000 genes, 8% up and 6% down at
+an FDR of 5%, with a block of tied p-values, and 305 sets of up to 600 genes, a fifth
+of each set outside the results: sets enriched for genes up, down or both, or for none,
+two identical sets, a set that lists genes twice, a set of every gene, a set outside
+the universe, and a set with two features of one symbol, one up and one down. Its table
+is compared with limma's `kegga` of a fit with the same p-values and log fold changes,
+which selects the genes up and down itself, and with kegga's default method given the
+genes of each direction and the universe: by feature id; by symbol, which 160 features
+share in pairs; by symbol where 131 features lack one; with a universe of 60% of the
+genes, 100 genes of the sets outside the results and 50 genes of no set; with 416 genes
+without a p-value; and for a list of 230 genes, 30 of them outside the universe and
+some listed twice. The adjusted p-values are compared with `p.adjust` of kegga's
+p-values, the order of the sets with `topKEGG`'s, the p-values also with R's `phyper`
+on a grid of 247 points that reaches 1e-300 and underflow, and the dot plot's sets,
+dots and colours with kegga's counts and adjusted p-values. Reference: R 4.4.3, limma
+3.62.1, decoupler 2.2.0.
-The information rows give the size of the two treatments of the weights. Against the
-precision weights, every observation at the average weight gave standard errors 0.976
-times as large at the median (0.51 to 1.05) and df 0.91 times (0.26 to 3.7) with one
-random intercept, and 0.977 (0.81 to 1.02) and 0.89 (0.27 to 3.4) with two; its standard
-errors were below the unadjusted ones for 61% and 33% of the genes, the precision
-weights' for none. And they give how dream's default changes the results of small
-studies against Satterthwaite's method, with which `dream_voom` tested them before:
-with 12 samples, p-values 1.12 times as large at the median (0.97 to 1.66 for 90% of the
-genes), with standard errors 1.06 times as large at the median and smaller for 20% of
-the genes; with 18 samples, 1.00 (0.91 to 1.07), and standard errors 1.00, smaller for
-48%; in the cube's cell type with 12 samples, 0.99 (0.73 to 1.12), with as many genes
-below an adjusted p-value of 0.05 (12) as before.
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| GMT files in four styles: the names, descriptions and genes of every set, read, written and read again | R, by hand | 0 sets differ | 0 | 4 pass |
+| The MSigDB-style file: names and genes | decoupler `read_gmt` | 0 sets differ | 0 | 1 pass |
+| `restrict_to`: the sets kept and the positions of their genes | `ids2indices` | identical | 0 | 1 pass |
+| `filter_by_size(15, 200)`, then `camera` of the 34 sets kept | `camera` | 3.1e-15 | 1e-10 | 1 pass |
+| P(X >= k) at 247 points | `phyper` | 2.1e-12 | 1e-10 | 1 pass |
+| Genes up and down that kegga selects from a fit, by feature id and by shared symbols: numbers of genes, overlaps, p-values, adjusted p-values and the order of the sets | `kegga` of a fit, `p.adjust`, `topKEGG` | 6.2e-14 | 1e-10 | 4 pass |
+| Genes up, down and either way, by feature id, by shared or missing symbols, with a universe given and without p-values for some genes, and a list of genes: the same, and the numbers selected, the size of the universe and the expected overlaps | `kegga`, `p.adjust`, `topKEGG` | 9.7e-14 | 1e-10 | 16 pass |
+| The dot plot: the 20 sets shown and their order, the area and colour of its 60 dots | `kegga` | identical | 0 | 1 pass |
+| Features without a symbol, which kegga of a fit leaves out of its adjustment | `kegga` of a fit | 1.9 (relative) | n/a | 2 deliberate differences |
+| Genes without a p-value, which kegga of a fit keeps in the universe | `kegga` of a fit | 5.3 (relative) | n/a | 2 deliberate differences |
+| Sets without genes in the universe: rows with NaN p-values here, none in kegga's table | `kegga` | 1 to 23 sets per case | n/a | 1 deliberate difference |
+
+p-values, adjusted p-values and expected overlaps are compared relative to max(p,
+1e-300); numbers of genes, overlaps, names and orders must be identical. kegga selects
+the genes of a fit whose adjusted p-value is below its `FDR`, strictly, up or down by
+the sign of the coefficient; it counts a gene id on several features once, in both
+lists when one feature goes up and another down; it leaves out features without an id;
+and `topKEGG` orders sets by p-value, ties by the number of genes. These conventions
+were found by probing, and the checks by symbol confirm them. Half of the features
+without a symbol are changed genes, so that kegga's adjustment without them moves
+other genes across the cutoff and the difference shows.
Deliberate differences:
-- The p-values of `topTable`'s F-tests: dream computes those of every gene on the total
- degrees of freedom of the first gene for the first coefficient listed, which the
- script reproduces to 3e-14 in the 11 F-tests; CytePool gives each gene its own (see
- `cytepool.stats.dream`). Its p-values were 0.14 to 11.5 times dream's.
-- Which genes fall back to Satterthwaite's method: dream decides from R's estimate of
- the condition number (`kappa`), CytePool from the exact one; they can differ within
- about 2% of 1e6. Here they decided alike for every gene tested, and the tests agree
- to 3.3e-7.
+- A set without genes in the universe has a row with NaN p-values in CytePool, left
+ out of the adjustment of the others; kegga leaves the set out of its table.
+- Genes without a p-value, such as those DESeq2's independent filtering leaves out:
+ kegga's method for a fit keeps them in the universe, where CytePool, which takes them
+ as not tested, leaves them out. Given the universe of every gene, CytePool gives
+ kegga's values; the tests check this.
+- Features without an id, such as a symbol: kegga's method for a fit leaves them out
+ of its Benjamini-Hochberg adjustment, so that the genes it selects depend on which
+ features have a symbol. CytePool takes the adjusted p-values of the results, the
+ genes the analysis called significant. With the p-values adjusted over the features
+ with a symbol, CytePool gives kegga's values; the tests check this.
-dream also computes `F.std` with the number of coefficients where they are linearly
-dependent, and CytePool with their rank, the numerator degrees of freedom of the F-test;
-the F-tests here have independent coefficients.
+(validation-gsea)=
+### Gene set enrichment analysis
-(validation-variance-partitioning)=
-## Variance partitioning
+The script printed 24 checks (23 pass and 1 deliberate difference) in 7 min 3 s, with a
+peak memory of 323 MiB. [Over-representation and
+GSEA](statistics/over_representation_and_gsea.md#gsea) explains what it checks.
-[`validation/variance_partitioning.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/variance_partitioning.py)
-compares `variance_fractions` and `variance_partitioning`, and the figures
-`plot_variance_fractions` and `plot_feature_variance_fractions`, with variancePartition's
-`fitExtractVarPartModel`, `calcVarPart`, `plotVarPart` and `plotPercentBars`, on data
-from fixed seeds (20261101 to 20261108), 60 genes each: one random donor intercept (10
-donors x 3 samples, a fifth of the genes without a donor effect); random donor, batch
-and condition intercepts with a donor's age and a sample's RNA integrity as fixed
-effects and precision weights for every observation (8 donors x 4 samples), also
-fitted by REML; unbalanced donors with 1 to 4 samples and sample weights; nested
-donor and tissue intercepts (6 donors x 2 tissues x 2 replicates); a linear model of
-the age, the RNA integrity, the batch and the condition with the same weights (which
-variancePartition fits with `lm`); negative binomial counts of 80 genes with the
-weights of `voomWithDreamWeights(span = 0.5)`; and `variance_partitioning` on three
-cell types of a cube of 8 donors x 2 conditions, with prior weights from the numbers
-of cells, three samples left out of one cell type for having fewer than 10 cells, and
-one sample per donor in the third. Reference: R 4.4.3, variancePartition 1.36.2,
-lme4 1.1-38, minqa 1.2.8 and ggplot2 4.0.3, in the R environment of `dream.py`.
+[`validation/gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py)
+checks gene set enrichment analysis and its figures against fgsea. Statistics of 4,000
+genes are simulated from fixed seeds like moderated t-statistics, 6% of the genes up and
+5% down, with 60 genes tied at 1.25 and 30 at zero, with 90 gene sets of 1 to 1,000
+genes, a fifth of each outside the ranking: sets enriched for genes up, down or both, or
+for none, a set of every gene, which neither program tests, a set of one gene, half of
+the tied genes, a set that lists genes twice and two identical sets. `gsea` and fgsea's
+`fgsea` run with the same options: fgsea's defaults (its multilevel method, 1,000
+permutations, 101 sets per level, eps 1e-50); gseaParam 0 and 0.5; scoreType "neg";
+scoreType "pos" on -log10 p-values, which are all positive; sets of 15 to 500 genes,
+with eps 0, 51 sets per level and 2,000 permutations; permutations alone (fgsea's
+`nperm`, 2,500 in three chunks); statistics mostly positive, with 6 sets spread evenly
+along the ranking, a set of a few genes near its top and others spread along it, whose
+p-value is about 0.05, and 3 sets of 25 genes from its bottom, whose negative scores
+fewer than 10 random sets of their size reach, so that no set goes to the multilevel
+method; and one set alone.
+
+The R script records the seeds that fgsea draws where it hands them to its compiled code
+(`trace` of `fgseaSimpleImpl` and `multilevelImpl`), and `gsea` draws the same through
+`rng`; the two programs then draw the same random sets, and must agree exactly. fgsea
+draws the random sets of a set tested alone with R's `sample.int`, which cannot be
+replayed: that case is exact for the enrichment score and leading edge, and calibrated
+for the rest, with seeds of their own (R's 1 to 20 and numpy's 1 to 20): the mean
+log10 p-value and the mean normalized score of each set over 20 runs must agree within
+4 standard errors of their difference, for 36 sets and for the set alone. The figures:
+what `plot_enrichment` draws against `plotEnrichmentData`, for three sets with gseaParam
+1 and 0.5, and the bars and numbers of `plot_gsea_table` against the ticks of
+`plotEnrichmentData`, scaled as `plotGseaTable` scales them, and its numbers as it
+writes them. Reference: R 4.4.3, fgsea 1.32.2.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Fractions and variances at the optimum, relative to the gene's total variance: one, several, unbalanced and nested random effects, and voom's weights | `fitExtractVarPartModel`, `calcVarPart` | 4.4e-8 | 1e-6 | 5 pass |
-| The same, fitted by REML | `fitExtractVarPartModel(REML = TRUE)` | 2.0e-7 | 1e-6 | 1 pass |
-| A linear model with factors and observation weights | `fitExtractVarPartModel` (`lm`) | 3.8e-15 | 1e-10 | 1 pass |
-| Variances at CytePool's estimates | lme4 (`maxeval = 1`), `calcVarPart` | 1.9e-13 | 1e-10 | 5 pass |
-| Random effects estimated at zero: CytePool's exact zeros and lme4's below 1e-10 | `fitExtractVarPartModel` | equal in every case (19 to 91) | n/a | 6 information |
-| `variance_partitioning`, in two cell types: samples used and features | the rules of `dream_voom` | 0 differences | 0 | 2 pass |
-| `variance_partitioning`: fractions | `voomWithDreamWeights`, `fitExtractVarPartModel` | 3.4e-8 | 1e-6 | 2 pass |
-| Figures: the variables and their order, the quartiles, whiskers and outliers of the violins, and the segments of the bars, in percent over 100 | `plotVarPart(sortCols())`, `plotPercentBars` | 5.3e-16 | 1e-10 | 3 pass |
-| The violins' half-widths at R's points, relative to the widest | `plotVarPart` | 5.9e-4 | 1e-3 | 1 pass |
-| Designs both refuse: a factor among the fixed effects of a mixed model, and a formula without an intercept | `fitExtractVarPartModel` | 0 differences | 0 | 2 pass |
-| Deliberate differences, listed below | variancePartition | 3 inputs | n/a | 3 deliberate differences |
-
-lme4 runs with Powell's BOBYQA with its trust region shrunk to 1e-10, as in `dream.py`,
-so the fractions at the optimum are compared to the close tier. Differences are taken
-relative to each gene's total variance, the scale of a fraction: lme4 leaves a variance
-component at the boundary at about 1e-16 of the total where CytePool holds it at zero,
-and the two found the same components at zero in every case. At CytePool's estimates,
-lme4 evaluated once and `calcVarPart` give closed forms, compared to the exact tier,
-as are linear models, which have closed forms. The figures are drawn by R from
-CytePool's fractions, so they compare the drawing alone. The violins agree to 1e-3 of
-their widest point: ggplot2 takes R's `density()`, which bins the values on a grid of
-512 points and convolves them by the fast Fourier transform, as its help page
-describes, and differs from the kernel density estimate that CytePool draws by up to
-5.9e-4 here. ggplot2 stacks a segment of no width anywhere in its bar, so only the
-segments with a width are compared by position.
+| The sets tested, their sizes, leading edges and enrichment scores, in all 9 cases | `fgsea` | 0 | 1e-10 | 9 pass |
+| Normalized scores, p-values, log2 errors and adjusted p-values, given fgsea's seeds, in 7 cases | `fgsea` | 2.2e-13 | 1e-10 | 7 pass |
+| The same of the mostly positive statistics, without the adjusted p-values | `fgsea` | 4.2e-16 | 1e-10 | 1 pass |
+| Mean log10 p-values and mean normalized scores over 20 runs, seeds of their own, for 36 sets and one set alone | `fgsea` | 2.4 standard errors | 4 | 4 pass |
+| `plot_enrichment`: the walk, the ticks and the extremes of 6 plots | `plotEnrichmentData` | 1.7e-14 | 1e-10 | 1 pass |
+| `plot_gsea_table`: the bars and numbers of 10 sets | `plotEnrichmentData`, `plotGseaTable` | 1.1e-13 | 1e-10 | 1 pass |
+| Adjusted p-values of the mostly positive statistics | `fgsea` | 0.16 (relative) | n/a | 1 deliberate difference |
-The conventions of variancePartition that its documentation leaves open, found by
-probing it and listed in the documentation of `cytepool.stats.variance_fractions`, are
-exercised here: the variance of a fixed effect is the weighted variance of its fitted
-contribution, with the denominator of a maximum likelihood variance; in a mixed model
-the terms share the variance of all the fitted fixed effects in proportion to their
-own, and in a linear model each term has its own; each gene's precision weights are
-divided by their mean. The two covariates of the second case have a correlation of
--0.13 over its samples, and without the sharing its fractions would differ by up to
-0.034.
+Numbers are compared relative to their absolute value, or 1e-300, and NaN must stand
+where fgsea gives NA; sets, sizes and leading edges must be identical. Given fgsea's
+seeds, the random scores and the counts of the permutations are the same to the last
+bit; the multilevel p-values differ in the last bits, through the digamma function of
+each program.
-Deliberate differences:
+Deliberate difference:
-- A gene constant within every donor: variancePartition reports the fractions of
- lme4's fit with a residual variance near zero, a donor fraction of 1.0000; CytePool
- leaves the gene unfitted, with NaN fractions, as its mixed models have no maximum
- likelihood estimate for such a gene.
-- REML without random effects: variancePartition fits `lm` and takes the residual sum
- of squares over n whatever `REML` says, so its fractions do not change; CytePool
- takes REML's residual variance, over n - p, which changed a fraction by up to 0.052.
-- A cell type with one sample per donor: `voomWithDreamWeights` stops ("Initial model
- failed"); `variance_partitioning` reports the status `random_effects_not_estimable`
- and goes on with the other cell types.
+- The adjusted p-values of the mostly positive statistics, where some sets have too few
+ random scores of their sign and no set goes to the multilevel method: fgsea adjusts
+ the p-values over the sets before it removes those that have some, but fewer than 10,
+ random scores of their sign, as though they had been tested, so that its adjusted
+ p-values of the sets it reports come out smaller or larger; CytePool adjusts over the
+ sets with a p-value. The tests check that CytePool's are Benjamini and Hochberg's of
+ the p-values it reports.
-## Differential expression on published data
+(validation-pathway-scores)=
+### Pathway scores
-[`validation/interferon_stimulated_pbmcs.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/interferon_stimulated_pbmcs.py)
-runs the whole path from cells to differential-expression tables on published
-single-cell data, and compares it with limma on the same pseudobulk counts. The data
-are batch 2 of GEO series
-[GSE96583](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE96583) (Kang et al.
-2018, *Nature Biotechnology* 36, 89-94): peripheral blood mononuclear cells (PBMCs) of
-8 lupus patients, each split into an untreated culture and one stimulated with
-interferon-beta for 6 hours, sequenced with 10x Genomics, the cells assigned to their
-donors by genotype. The script downloads the three files it needs (77 MB) into a cache
-directory outside the repository and checks their SHA-256 checksums. It reads them as
-data, without extracting the archive, and stops if a fact it relies on fails: the
-dimensions of the two count matrices, the gene identifiers, the number of cells of
-each type, and the rule that joins each cell to the published cell table by its
-condition and barcode (every cell matches one row and every row one cell; nothing is
-joined by position).
+The script printed 24 checks (19 pass, 3 deliberate differences and 2 information) in
+10.7 s, with a peak memory of 949 MiB. [Pathway scores per
+sample](statistics/pathway_scores.md#gsva) explains what it checks.
-`from_cells` pools the 24,673 singlets that have a cell type into a cube of 16 samples
-(8 donors, control and stimulated) x 8 cell types x 35,635 genes, keyed by Ensembl id
-with symbols, and its counts are checked against sums of the cells that scipy computes
-independently. `limma_voom` then tests stimulated against control in every cell type,
-with the paired design `~ donor + condition` and its defaults: samples with fewer than
-10 cells (`min_cells`) are left out, then come `filterByExpr`, TMM and voom, whose
-LOWESS span is chosen from the number of genes tested by limma's `chooseLowessSpan`
-rule, as limma's voom does by default since limma 3.66. R runs the limma User's Guide
-workflow on the counts, samples and cell numbers exported from the cube: the same
-`min_cells` rule, then `filterByExpr`, `calcNormFactors`, `voom`, `lmFit`,
-`contrasts.fit`, `eBayes` and `topTable(sort.by = "none")`, all with R's defaults
-except two settings that differ between limma versions: voom gets the span that the
-cell type's record in the CytePool result gives, with `adaptive.span = FALSE`, and
-`eBayes` gets `legacy = TRUE`. R also computes `chooseLowessSpan` of the number of
-genes tested, the span its voom would choose itself, and that span must be the same.
-Reference: R 4.4.3, limma 3.62.1, edgeR 4.4.0; CytePool ran with Python 3.13.12,
-numpy 2.5.4 and numba 0.68.0, and scipy 1.18.1 read the count matrices.
+[`validation/pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py)
+checks the scores of gene sets in every sample, and their heatmap, against GSVA 2.0.0,
+whose `gsva()` runs with a `gsvaParam`, `ssgseaParam`, `zscoreParam` or `plageParam`,
+and the CytePool function with the same options. The inputs are simulated from fixed
+seeds: log-expression values of 2,000 genes in 24 samples, 200 genes higher in the
+second 12, with two constant genes, a gene equal to another, a gene of whole numbers
+and a gene with an outlier sample; negative binomial counts of the same genes in 16
+samples, stored as integers and as decimals; Poisson counts with means from 0.3 to 6
+(low counts); 400 genes in 210 samples; the log-expression values rounded to one
+decimal; the same values with 2% of those of the first 12 samples missing; and a cube of
+one cell type with the counts, whose samples have 3 to 60 cells. The 66 gene sets have
+1 to 300 genes, a fifth of each outside the data: random sets, a set of one gene, one
+with the constant genes, one that lists genes twice, two identical sets and a set of
+every gene, which is left out for ssGSEA but in one case. Reference: R 4.4.3, GSVA
+2.0.0.
| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Counts of the 16 samples x 8 cell types x 35,635 genes of the cube | sums of the cells, by scipy | 0 counts differ | 0 | 1 pass |
-| Samples used, genes tested, aliased columns, residual and prior df, and voom's span, in the 8 cell types | `filterByExpr`, `lmFit`, `eBayes`, `chooseLowessSpan` | 0 mismatches | 0 | 8 pass |
-| logFC, AveExpr, t, P.Value, adj.P.Val and B of every gene tested in the 8 cell types | `topTable` | 6.6e-13 | 1e-10 | 8 pass |
-| Per cell type: genes tested and called, the interferon response, the negative control (tables below) | MSigDB, none | n/a | n/a | 24 information |
+| GSVA's method: the Gaussian kernel, tau 0.5 with maxDiff FALSE, tau 2 with absRanking, no kernel, sizes 10 to 100, counts stored as integers (the Poisson kernel), as decimals (the Gaussian kernel) and 210 samples (no kernel), in 8 cases | `gsva` with `gsvaParam` | 0 | 1e-10 | 8 pass |
+| ssGSEA: the defaults, alpha 1 without normalization, alpha 0 on tied values, in 3 cases | `ssgseaParam` | 1.8e-15 | 1e-10 | 3 pass |
+| Combined z-scores | `zscoreParam` | 2.7e-15 | 1e-10 | 1 pass |
+| PLAGE, the sets of two genes or more, up to sign | `plageParam` | 8.7e-14 | 1e-10 | 1 pass |
+| Missing values: GSVA's method in the samples without any, ssGSEA and combined z-scores | `gsvaParam` and `ssgseaParam` with `use = "na.rm"`, `zscoreParam` | 2.0e-15 | 1e-10 | 3 pass |
+| A cube's cell type: its log-CPM and its counts | `gsva` with `gsvaParam` | 0 | 1e-10 | 2 pass |
+| `plot_pathway_scores`: the 1,584 cells of the standardized heatmap, the samples grouped | GSVA, R's `heatmap(scale = "row")` | 3.2e-16 | 1e-10 | 1 pass |
+| PLAGE: the share of sets whose sign is GSVA's | `plageParam` | 0.42 | n/a | 1 information |
+| GSVA's method with the Poisson kernel on low counts | `gsvaParam` | 2.2e-3 | n/a | 1 information |
+| GSVA's method in the samples with missing values; PLAGE's sets of one gene; ssGSEA with a set of every gene | `gsvaParam`, `plageParam`, `ssgseaParam` | 1.6 | n/a | 3 deliberate differences |
-Differences are scaled as in the section above. The dendritic cells are fitted without
-2 of the 16 samples, and the megakaryocytes without 4, among them both samples of donor
-107: that donor's column of the design is then zero, and both programs leave it out of
-the fit.
+Differences are relative to the largest absolute score of a case, NaN must stand where
+GSVA gives NA, and the sets scored must be GSVA's, in its order. The scores depend on
+the ranks of the genes in each sample, which come from comparisons of their kernel
+estimates; CytePool evaluates the normal distribution function from a table at steps
+of 0.001, as GSVA does, and sums the estimates over the samples in their order, so that
+the ranks, and with them the scores, are GSVA's on continuous values and on counts of
+the sizes of the cases above. With low counts, many genes have kernel estimates that
+are equal in exact arithmetic, such as genes with the same counts in different
+samples, and GSVA orders them by the last bit of R's Poisson probabilities, which
+CytePool computes as accurately but not always to the same bit: the scores of the sets
+that hold them then differ by up to 2.2e-3 of the largest score. PLAGE's sign is that
+of R's singular value decomposition, which agrees with CytePool's convention for 42% of
+the sets.
+
+Deliberate differences:
+
+- GSVA's method in samples with missing values: with `use = "na.rm"`, GSVA's walk
+ falls by 1/(p - k) at a sample's observed genes outside a set, p counting the genes
+ missing in the sample too, so that it ends above zero, and it scores the set of every
+ gene there; CytePool's walk falls by 1/(p_j - k_j), with p_j and k_j counting the
+ genes observed in the sample, and ends at zero (largest difference 4.8e-2, where both
+ give a score). The samples without missing values agree exactly.
+- PLAGE's sign: GSVA returns the sign that R's singular value decomposition gives,
+ which depends on the linear algebra library; CytePool makes a set's scores rise with
+ the mean standardized expression of its genes. The scores are compared up to sign,
+ and the information row gives the share of sets whose signs agree.
+- PLAGE's sets of one gene: GSVA gives 1 in every sample; CytePool gives the gene's
+ standardized values, scaled to length one, its first right singular vector (largest
+ difference 1.6).
+- ssGSEA with a set of every gene: GSVA stops ("Cannot calculate normalizing factor for
+ the enrichment scores in ssGSEA, likely due to NA values in the input expression
+ data"); CytePool gives that set NaN and normalizes the other scores by their range.
-The recovery of a pre-specified list of interferon-stimulated genes is measured with
-MSigDB's Hallmark
-[interferon-alpha response](https://www.gsea-msigdb.org/gsea/msigdb/human/geneset/HALLMARK_INTERFERON_ALPHA_RESPONSE.html)
-gene set (97 genes, MSigDB v2026.2.Hs; Liberzon et al. 2015, *Cell Systems* 1,
-417-425; CC BY 4.0), matched to the data by symbol; two of its genes have earlier
-symbols in the data, FAM46A for TENT5A and WARS for WARS1. Up and down are calls at a
-5% false discovery rate:
+(validation-activity)=
+### Pathway and regulator activity
-| Cell type | Cells | Samples used (left out) | Genes tested | voom span | Up | Down | Hallmark genes tested | Up | Down | Median log2 fold change |
-| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
-| B cells | 2,573 | 16 (0) | 2,768 | 0.484 | 498 | 399 | 76 | 73 | 2 | 2.73 |
-| CD14+ monocytes | 5,385 | 16 (0) | 5,125 | 0.450 | 1,619 | 1,549 | 92 | 89 | 3 | 3.26 |
-| CD4 T cells | 10,389 | 16 (0) | 6,295 | 0.440 | 991 | 826 | 86 | 82 | 1 | 2.41 |
-| CD8 T cells | 2,042 | 16 (0) | 1,453 | 0.528 | 233 | 147 | 55 | 54 | 0 | 2.51 |
-| Dendritic cells | 432 | 14 (2) | 2,281 | 0.496 | 454 | 285 | 76 | 68 | 0 | 3.27 |
-| FCGR3A+ monocytes | 1,599 | 16 (0) | 2,947 | 0.480 | 814 | 663 | 84 | 82 | 1 | 2.42 |
-| Megakaryocytes | 260 | 12 (4) | 704 | 0.590 | 63 | 16 | 48 | 35 | 0 | 2.48 |
-| NK cells | 1,993 | 16 (0) | 2,292 | 0.496 | 374 | 300 | 68 | 66 | 0 | 2.13 |
+The script printed 89 checks (82 pass, 5 deliberate differences and 2 information) in 2
+min 39 s, with a peak memory of 987 MiB. [Activities from
+networks](statistics/activities.md#ulm) explains what it checks.
-In the six cell types with all 16 samples, 95% to 98% of the Hallmark genes tested are
-called up, with median log2 fold changes of 2.1 to 3.3, that is 4 to 10 times the
-expression of the controls; in the dendritic cells and megakaryocytes, with fewer cells
-and samples, 89% and 73% are. The only Hallmark genes called down are IL4R, in four
-cell types, CD74 in the B cells, and PROCR and IFI30 in the CD14+ monocytes.
+[`validation/activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py)
+checks the activities of regulators and pathways, their bar plot and the networks
+against decoupler 2.2.0 and progeny 1.28.0. CytePool's loaders download the networks
+into its cache. CollecTRI's file and progeny's package are compared with the MD5
+digests and sizes their sources publish (Zenodo's record, Bioconductor's `PACKAGES`
+and the server's `Content-Length`); `collectri(licence="academic")` with decoupler's
+`op.collectri`; the commercial level with the licence purposes that OmniPath's
+resources service lists, applied to the file by the script; and the weights and
+p-values of `progeny` with progeny's `model_human_full` and `model_mouse_full`, and its
+`top` with `getModel` (100 human genes; 500 mouse genes, with ties). The inputs of the
+activities are simulated from fixed seeds: log-expression values of 6 samples over
+CollecTRI's 6,693 targets and 2,000 other genes, the targets of 6 regulators shifted in
+the last 3 samples, with 3 genes zero in every sample; a profile of t-statistics; the
+values with 3% missing in two samples, which decoupler, refusing missing values,
+scores one profile at a time over its observed genes; 6 samples over the targets of
+PROGENy's top 500 genes and 1,000 others; a cube of one cell type, scored as its
+log-CPM; and limma_voom's moderated t-statistics in two cell types that test different
+genes. `ulm`, `mlm` and `weighted_aggregate` run as decoupler's `mt.ulm`, `mt.mlm` and
+`mt.waggr` with `tmin` 5, or 3 for a network of weights of one sign. Reference:
+decoupler 2.2.0 with scipy 1.18.1, and progeny 1.28.0 on R 4.4.3.
-The response to interferon is so large that recovering it says little about error
-control; a negative control, which needs no reference, does. In each cell type, the
-control samples that its fit used are split into two halves in every possible way,
-each split once, and every split is fitted with `~ split`, so that a call compares
-donors, not conditions: 35 splits of 4 against 4 samples, or 10 splits of 3 against 3
-in the dendritic cells and megakaryocytes, where 2 control samples have fewer than 10
-cells. If the donors did not differ, the Benjamini-Hochberg procedure at a 5% false
-discovery rate would make any call in an analysis with a probability of at most about
-5%, so in about 12 of these 230 analyses or fewer.
+| Check family | Reference | Largest difference | Tolerance | Outcome |
+| --- | --- | --- | --- | --- |
+| ULM's t-statistics and slopes, in 10 cases | `mt.ulm` | 2.3e-13 | 1e-10 | 20 pass |
+| ULM's p-values, and their Benjamini-Hochberg adjustment, in 10 cases | `mt.ulm`; scipy's `false_discovery_control` of its p-values | 4.4e-11 | 1e-10 | 20 pass |
+| ULM's adjusted p-values, in single precision | `mt.ulm` | 0 | 1.2e-7 | 10 pass |
+| MLM's t-statistics and coefficients, in 6 cases | `mt.mlm` | 3.2e-14 | 1e-10 | 12 pass |
+| MLM's p-values | scipy's `t.sf` of `mt.mlm`'s t-statistics | 3.3e-12 | 1e-10 | 6 pass |
+| Weighted means and sums, and the normalized scores given decoupler's permutations (100 and 1,000) | `mt.waggr` | 1.1e-14 | 1e-10 | 4 pass |
+| Weighted sums of PROGENy's top 100 genes | progeny's `progeny(scale = FALSE)` | 6.5e-16 | 1e-10 | 1 pass |
+| `plot_top_activities`: the 15 bars of a sample, their sources and lengths | `pl.barplot` | 4.8e-14 | 1e-10 | 1 pass |
+| CollecTRI at the academic level; at the commercial level; PROGENy's weights and p-values, human and mouse; its top 100 human and 500 mouse genes | `op.collectri`, OmniPath's resources, progeny's models and `getModel` | 0 | 0 | 6 pass |
+| The files downloaded: sizes and MD5 digests | Zenodo, Bioconductor | 0 | 0 | 2 pass |
+| decoupler's `op.collectri` at its levels; PROGENy through OmniPath | `op.collectri`, `op.progeny` | n/a | n/a | 2 information |
+| The weighted aggregate's p-values; a sample of zeros; MLM with two sources alike; CollecTRI's interactions without a reference | `mt.waggr`, `mt.ulm`, `mt.mlm`, `op.collectri` | 0.97 | n/a | 5 deliberate differences |
-| Cell type | Splits | Splits with calls | Calls | Most in one split | Genes called in more than one split |
-| --- | --- | --- | --- | --- | --- |
-| B cells | 35 | 4 | 4 | 1 | none |
-| CD14+ monocytes | 35 | 1 | 6 | 6 | none |
-| CD4 T cells | 35 | 7 | 123 | 44 | OASL, LMNA, DDIT4, ANXA2R (3 splits); YIPF5, FBLN5, RGS1, TPRG1, CD27, C16orf54, SCML4, NUCB2, SUN2, PASK, FASLG, FAM46C (2) |
-| CD8 T cells | 35 | 5 | 13 | 6 | TMSB10 (2) |
-| Dendritic cells | 10 | 2 | 2 | 1 | none |
-| FCGR3A+ monocytes | 35 | 2 | 5 | 3 | none |
-| Megakaryocytes | 10 | 0 | 0 | 0 | none |
-| NK cells | 35 | 2 | 3 | 2 | none |
+Scores and estimates are compared relative to the largest absolute value of a case,
+p-values relative to each value, and NaN must stand where the reference has none; the
+networks and the bars must be identical. decoupler keeps its adjusted p-values in
+single precision, so CytePool's are rounded to it first. decoupler computes MLM's
+p-values as one minus the distribution function, which loses their digits below about
+1e-12, so CytePool's are compared with scipy's upper tail at decoupler's t-statistics.
+The normalized scores are compared given decoupler's permutations: the genes in the
+order in which decoupler shuffles them, and its seed, so that both draw the same
+permutations. decoupler 2.2.0 accepts a licence for CollecTRI but returns the whole
+file at every level. Its `op.progeny` reads PROGENy from OmniPath's annotations, which
+name genes by their current symbols, leave out genes OmniPath does not map to a
+protein, and merge some genes: of its 62,421 edges with a p-value below 0.05, where the
+progeny package has 69,827, 59,617 are named alike, and 59,599 of them have the same
+weight and p-value.
-23 of the 230 analyses made a call, 156 calls in all, and most of them follow
-differences between the donors. Of the five splits that put donors 1244, 1256 and 1488
-in one group with a fourth donor, the three in which that donor is 1016, 1039 or 107
-make 117 calls in the CD4 T cells, among them genes of regulatory and memory T cells
-(TIGIT, TNFRSF4, TNFRSF18, FASLG, LMNA, RGS1), which suggests that the CD4 T cells of
-these donors differ in composition. The split of donors 101, 107, 1244 and 1488 against
-the others calls interferon-stimulated genes (ISG15, IFI6, IFIT1, IFIT3, LY6E, IFITM3)
-in the monocytes and the CD4 T cells of the untreated cultures, consistent with lupus
-patients differing in interferon activity. Apart from those three CD4 T splits, no
-analysis makes more than 6 calls. The donors appear to be all female, so sex does not
-divide them: XIST is counted in all 16 samples, and none of the Y-chromosome genes
-RPS4Y1, DDX3Y, KDM5D, UTY and EIF1AY in any.
+Deliberate differences:
-The script times each part on the machine described under [](#validation-timings),
-with numba, OpenMP, OpenBLAS and MKL limited to 2 threads, the data downloaded already
-and numba's cache of compiled routines filled:
+- The weighted aggregate's p-values (largest difference 0.97 with 100 permutations,
+ 2.0e-3 with 1,000): decoupler counts the permutations whose absolute score exceeds
+ the absolute observed score and then folds that share as it would fold a one-sided
+ one, so that a score near zero gets a p-value near 2 / n for n permutations; CytePool
+ doubles the smaller of the two one-sided p-values (1 + b) / (1 + n).
+- A sample whose values are all zero: decoupler leaves it out of its result; CytePool
+ gives it NaN scores. The other samples agree exactly.
+- MLM with two sources of the same targets and weights: decoupler inverts a singular
+ matrix and gives NaN t-statistics for both (12 in 6 samples); CytePool refuses,
+ naming the source it cannot tell apart from the other.
+- CollecTRI's 185 interactions whose field of references is empty: decoupler's loader
+ drops them, through pandas' `dropna`; CytePool keeps them, as the file holds them and
+ the paper counts them (43,175 interactions).
+- MLM's adjusted p-values: decoupler reports MLM's p-values unadjusted, under the name
+ of its adjusted p-values; CytePool adjusts them by Benjamini and Hochberg's method
+ over the sources of each profile, as for the other methods, so the script compares
+ MLM's p-values before adjustment only.
-| Part | Seconds |
-| --- | --- |
-| Reading and checking the files, and keeping the 24,673 singlets with a cell type | 1.30 |
-| `from_cells`: 24,673 cells x 35,635 genes (14.2 million stored counts) into the cube | 0.24 |
-| `limma_voom` on the 8 cell types, first call in the process | 0.32 |
-| `limma_voom` on the 8 cell types, second call | 0.26 |
-| R: the limma workflow on the 8 cell types, from the counts in memory | 1.42 |
-| The negative control: `limma_voom` on the 230 splits, one cell type each | 3.61 |
+## Several studies
-With an empty numba cache, as after installing or changing the package, the first call
-of `limma_voom` compiles its routines too, and took 17 s in a run with
-`NUMBA_CACHE_DIR` set to an empty directory; later processes load them from the cache.
-The Python process held at most 617 MiB while it read the files and pooled the
-cells. Once the cell-level matrices were released it held 232 MiB, and while
-`limma_voom` ran at most 252 MiB, so the analysis of the 8 cell types added
-20 MiB to a process that held the cube; from then to the end, with the negative
-control and the export to R, it held at most 321 MiB. R peaked at 227 MiB.
+(validation-cube-collection)=
+### Studies aligned in a collection
-## Cell-type composition
+The script printed 15 checks (14 pass and 1 control) in 26.7 s, with a peak memory of
+495 MiB. [Aligning studies](statistics/aligning_studies.md#aligning-samples) explains what it
+checks.
-[`validation/composition_tests.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/composition_tests.py)
-compares `propeller` with speckle's `propeller`, `propeller.ttest` and
-`propeller.anova`. It simulates the number of cells of every sample in every cell type
-from a Dirichlet-multinomial model (seed 20261025), holds them in cubes, and gives
-speckle one label per cell; `propeller.ttest` and `propeller.anova` get
-`getTransformedProps` and design matrices from `model.matrix`. Every case runs with
-the logit and the arcsine square-root transform, and with speckle's defaults (robust
-moderation, no trend) unless a row says otherwise. The cases are two groups of 5
-samples with 1,000 to 5,000 cells and 12 cell types, three of which change, also with
-`robust = FALSE` and `trend = TRUE`; three groups of 4 samples, by speckle's F-test;
-three groups of 5 samples and a covariate (age), `~ 0 + group + age`, with the
-contrasts B - A and (A + B)/2 - C, the F-test of `group` (`propeller.anova` with
-`coef = 1:3`), the slope of age and A + B, and `~ group + age` with the contrast
-`groupB`; a pairing of 6 donors in two conditions, `~ 0 + condition + donor` and
-`~ donor + condition`; 8 samples with 6 to 32 cells, in which most cell types are
-absent from some samples and two from all of them, in a cube that also holds a cell
-type and a sample without cells; two cell types, which limma does not moderate; and
-samples that are copies of one another within each condition. Reference: R 4.4.3,
-speckle 1.6.0, limma 3.62.1.
+[`validation/cube_collection.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/cube_collection.py)
+simulates the cells of three studies of one stimulation (seed 20261026) and pools each
+with `from_cells`: `lupus`, 8 samples of 4 donors, each control and stimulated, 4 cell
+types with Cell Ontology ids and 700 genes; `covid`, 10 samples of 5 donors whose
+sample and donor ids repeat those of `lupus`, 5 cell types of which two share an
+ontology id and merge, and 750 genes, 40 of them filled with zeros and marked as not
+measured; and `atlas`, 6 unpaired samples with integer donor ids, 5 cell types without
+ontology ids, aligned by a mapping that leaves one out, and 950 genes. The collection
+aligns the cell types by ontology id and writes donor ids `study::donor`. anndata and
+pandas run as a separate program: each study's cube is exported with `to_anndata`, the
+genes marked as not measured are dropped, the rows of each sample and aligned cell type
+are summed, and the studies are joined with `anndata.concat` (outer and inner joins,
+labelled by study); pandas joins the sample tables. R runs limma-voom on each study's
+own counts, summing the cell types that share an aligned id, and on the stacked counts
+of `lupus` and `covid` with `~ donor + condition`, by the steps of
+`differential_expression.py`. Reference: anndata 0.13.4, pandas 3.1.0, numpy 2.5.4,
+Python 3.13.12; R 4.4.3, limma 3.62.1, edgeR 4.4.0. CytePool ran with numpy 2.5.4 and
+numba 0.68.0.
-| Check family | Reference | Largest difference: proportions, statistics | Tolerance | Outcome |
+| Check family | Reference | Largest difference | Tolerance | Outcome |
| --- | --- | --- | --- | --- |
-| Two groups (t-test) and three groups (F-test), 12 cell types: `BaselineProp`, `PropMean`, `PropRatio`, `Tstatistic` or `Fstatistic`, `P.Value` and `FDR` | `propeller` | 1.7e-15, 3.9e-9 | 1e-10, 1e-6 | 4 pass |
-| Two groups with `robust = FALSE` and with `trend = TRUE` | `propeller` | 5.4e-16, 6.7e-15 | 1e-10, 1e-10 and 1e-6 | 2 pass |
-| B - A and (A + B)/2 - C with a covariate, and B - A as `groupB` in `~ group + age` | `propeller.ttest` | 6.1e-16, 9.4e-15 | 1e-10, 1e-6 | 6 pass |
-| F-test of `group` with a covariate | `propeller.anova` | 4.9e-16, 3.6e-15 | 1e-10, 1e-6 | 2 pass |
-| The slope of the covariate, which speckle refuses | limma `eBayes` on speckle's transformed proportions | none reported, 3.0e-15 | 1e-6 | 2 pass |
-| A pairing, in group means and in treatment coding | `propeller.ttest` | 4.7e-16, 7.7e-15 | 1e-10, 1e-6 | 4 pass |
-| Few cells and absent cell types; the sample without cells, with the arcsine transform | `propeller`, `propeller.ttest` | 3.7e-16, 3.5e-15 | 1e-10 | 3 pass |
-| Two cell types | `propeller` | 2.3e-16, 3.3e-15 | 1e-10 | 2 pass |
-| Deliberate differences, listed below | `propeller.ttest`, `propeller` | 2 contrasts speckle refuses, 2 ratios of contrasts that compare nothing, 2 unused levels speckle keeps, 1 data set CytePool refuses | n/a | 7 deliberate differences |
-
-Each row compares every column that speckle reports, matched by cell type, and the
-columns themselves must match: the mean proportions of the levels a contrast
-compares, and no others. Proportions and ratios are compared relative to speckle's
-values, in the exact tier; statistics are scaled by max(\|reference\|, 1), and
-p-values and FDRs by max(p, 1e-300) max(1, \|log p\|), in the exact tier, or in the
-close tier with robust moderation of 10 cell types or more, whose prior limma solves
-only to a tolerance. The largest difference, 3.9e-9, is in such a case; every other is
-below 1e-14. Deliberate differences:
+| The sample table: ids `study::sample_id`, donors `study::donor`, the union of the columns, ages that are integers in two studies and floats in one as float64, and missing values where a study lacks a column | `pandas.concat` | 0 mismatches | 0 | 1 pass |
+| The genes, as a set and in the order in which the studies first list them, and which study measured which | `anndata.concat`, outer join (NaN where a study lacks a gene), `pandas.unique` | identical; 0 mismatches | 0 | 2 pass |
+| Every layer of each study's aligned cube: merged cell types, measured genes | the outer join's values | 0 | 1e-10 | 1 pass |
+| `stack()`: genes in order, cell types and samples; every layer; the cells of each entry, and an entry without cells | `anndata.concat`, inner join, kept to the cell types of every study | 0 | 0 and 1e-10 | 3 pass |
+| `limma_voom` on each study's aligned cube, lined up by `StudyResults.table`: logFC, AveExpr, t, P.Value, adj.P.Val, B and the standard error, in 12 cell types of 3 studies | limma, study by study | 3.8e-13 | 1e-10 | 3 pass |
+| The status of every gene of every study: tested, measured and left out, or not measured | limma's tables and each study's measured genes | 0 mismatches | 0 | 3 pass |
+| `limma_voom` on the stack of `lupus` and `covid`, `~ donor + condition` with donors `study::donor` | limma on the stacked counts | 1.4e-13 | 1e-10 | 1 pass |
+| The same with the donor ids as the studies give them | limma on the stacked counts, donors `study::donor` | 1.9 (log2 fold changes) | must exceed 1e-10 | 1 control |
-- speckle's `PropMean` are the coefficients of the proportions regressed on the
- columns of the design matrix that a contrast weighs, and its `PropRatio` their
- product raised to the contrast's weights (found by probing). Where those columns
- indicate groups of samples, as in the group-means designs above, they are the
- groups' mean proportions, and CytePool's values agree. Otherwise they are not. For
- A + B, which compares nothing, speckle reports the product of the two means as a
- ratio; CytePool reports no mean proportions and no ratio, and its t-statistics,
- p-values and FDRs agree with speckle's. For the slope of the covariate,
- `propeller.ttest` stops with "dim(X) must have a positive length", as it does for a
- contrast of a single column such as `groupB` in `~ group + age`. CytePool finds the
- levels a contrast compares from the design's coding: it tests the slope, without
- mean proportions or ratio, and its statistics agree with limma's `eBayes` on
- speckle's transformed proportions; `groupB` in `~ group + age` and `conditionstim`
- in `~ donor + condition` agree with speckle's values for the same comparisons in
- its group-means designs.
-- A cell type without cells: from cell labels speckle never sees it, and CytePool,
- which leaves it out, agrees with it. From a factor of cell labels with an unused
- level, speckle tests that level, and with the logit transform its half cells change
- the transformed proportions of every other cell type (a scaled difference of 0.23
- here).
-- A sample without cells, as an unused level of a factor of sample labels: with the
- logit transform speckle gives each of its I cell types the proportion 1/I and fits
- them as data (a scaled difference of 0.74 here); with the arcsine transform its
- proportions are NaN and limma leaves it out, as CytePool does with either transform.
-- Samples that are copies of one another within each condition: the design fits the
- transformed proportions of every cell type exactly, so no variance can be estimated.
- speckle tests the rounding errors, with t-statistics up to 3.4e15 here; CytePool
- raises an error.
+limma ran on each study's own counts, with the genes filled with zeros, and its tables
+equal those of the aligned cubes, which leave those genes out: genes without counts
+change no library size, expression filter or normalization factor, so a gene that a
+study did not measure can be marked missing without changing any other gene's result.
+The control shows why donor ids are written with their study: taken for one person,
+donor D1 of two studies changes the fit of every gene.
-CytePool computes the transforms as log((x + 0.5) / (N - x + 0.5 (I - 1))) and
-arctan2(sqrt(x), sqrt(N - x)), equal to speckle's formulas but without the
-cancellation in 1 - q and the loss of digits of arcsin near 1; on these data the two
-agree to rounding.
+(validation-meta-analysis)=
+### Meta-analysis
-## Meta-analysis
+The script printed 75 checks (52 pass, 20 information and 3 deliberate differences) in
+59.5 s, with a peak memory of 256 MiB. [Meta-analysis](statistics/meta_analysis.md#meta-analysis-model)
+explains what it checks.
[`validation/meta_analysis.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/meta_analysis.py)
compares `fit_meta_analysis` with metafor's `rma.uni`, run feature by feature in R on the
@@ -2050,8 +2256,8 @@ R 4.4.3, metafor 5.0.1, jsonlite 2.0.0.
| A Paule-Mandel estimate above metafor's search interval: against metafor with `tau2.max = 1e6`, and with its default, where it stops | `rma.uni` | 1.7e-15 | 1e-6 | 1 pass, 1 deliberate difference |
| Zero and infinite standard errors, which CytePool refuses | `rma.uni` | metafor stops for an infinite one, and for a zero one with the fixed effect, DerSimonian-Laird and Paule-Mandel; with REML it fits without Q, I² and H² | n/a | 1 pass, 1 deliberate difference |
-Differences are scaled by max(\|reference\|, 1), and p-values by
-max(p, 1e-300) max(1, \|log p\|); I² is compared in percent, as metafor reports it
+Differences are scaled as [above](#reading-the-tables), values as statistics and p-values as
+p-values; I² is compared in percent, as metafor reports it
(CytePool reports a proportion), and the number of studies must be equal. A row covers
both tests of one case and method. The REML rows' largest differences, near 1e-10, are
in I²: metafor stops when tau² changes by less than its threshold of 1e-13, which I² in
@@ -2084,7 +2290,12 @@ CytePool's REML estimate is zero; metafor's step halving approaches zero and sto
its threshold, at about 1e-6 by default. With standard errors 1e-8 of the spread of the
effects, the two programs agree to 1.5e-14.
-## Combined p-values and replication
+(validation-combined-p-values-and-replication)=
+### Combined p-values and replication
+
+The script printed 54 checks (47 pass and 7 deliberate differences) in 67.4 s, with a
+peak memory of 444 MiB. [Replication](statistics/replication.md#combined-p-values) explains what it
+checks.
[`validation/combined_p_values_and_replication.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/combined_p_values_and_replication.py)
checks combined p-values, partial conjunctions, the proportion of true nulls and the
@@ -2130,8 +2341,8 @@ figures are compared with what R computes for them. Reference: scipy 1.18.1, mpm
| `plot_study_agreement` and `plot_replicated_genes`: the values and bars drawn | `pi0est`, R | 0 | 1e-10, 0 mismatches | 2 pass |
| metap at a p-value of 0 and for one study; pi0est where no p-value reaches the largest lambda (smoother, bootstrap), where its estimate is at most 0, for pi1 of 23 pairs of studies, and for 96 values of lambda | metap, `pi0est` | listed below | n/a | 7 deliberate differences |
-Differences are scaled by max(\|reference\|, 1), and p-values by
-max(p, 1e-300) max(1, \|log p\|); counts and directions must be equal. qvalue's
+Differences are scaled as [above](#reading-the-tables), values as statistics and p-values as
+p-values; counts and directions must be equal. qvalue's
smoother is R's `smooth.spline`, which CytePool follows step by step in its search for
the smoothing parameter: the estimates agree to about 1e-12. Deliberate differences:
@@ -2149,7 +2360,12 @@ the smoothing parameter: the estimates agree to about 1e-12. Deliberate differen
only; CytePool keeps one at every lambda. For 96 values (0, 0.01, ..., 0.95) the
estimates differed by 2.6e-5, relative.
-## SumRank and held-out prediction
+(validation-sumrank)=
+### SumRank and held-out prediction
+
+The script printed 23 checks (13 pass, 9 information and 1 deliberate difference) in
+44.9 s, with a peak memory of 399 MiB. [Replication](statistics/replication.md#sum-of-ranks)
+explains what it checks.
[`validation/sumrank.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/sumrank.py)
runs the authors' R code of SumRank, under the Apache-2.0 licence, as they publish it:
@@ -2208,6 +2424,13 @@ gave a held-out AUC of 1 in every study; chosen by the meta-analysis with Knapp-
## Figures
+(validation-plotting)=
+### Figures of limma, edgeR, DESeq2, speckle and metafor
+
+The script printed 26 checks (23 pass, 2 information and 1 deliberate difference) in
+25.4 s, with a peak memory of 810 MiB. The API reference of {api}`cytepool.plotting`
+documents what it checks.
+
[`validation/plotting.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/plotting.py)
draws each figure of `cytepool.plotting` that follows an R function with CytePool and
with R on the same data, and compares what the two drew: R's base graphics as the
@@ -2235,15 +2458,17 @@ composition. limma-voom runs in R as the limma User's Guide runs it, and voom wi
| Forest plots of 3 genes, one of them tested by two studies, by REML with z-tests and by DerSimonian-Laird with the Knapp-Hartung adjustment: the studies' intervals, and the pooled estimate and interval | `forest`, `rma.uni` | 4.5e-15 | 1e-6 (REML), 1e-10 | 6 pass |
| Composition: the bar segment of every sample and cell type; proportions by condition, their means and adjusted p-values | `plotCellTypeProps`, `propeller` | 5.0e-16 | 1e-10; 1e-6 | 2 pass |
-Differences are scaled by max(\|reference\|, 1). R writes numbers with 15 significant
+Every difference, p-values included, is scaled as a statistic is [above](#reading-the-tables). R
+writes numbers with 15 significant
digits, which the exact rows reach. The residual variances agree to 2.6e-9 because
-limma solves the prior's degrees of freedom to a tolerance (see Empirical Bayes
-moderation above). After robust moderation, `plotSA` marks the variances whose
+limma solves the prior's degrees of freedom to a tolerance (see [Empirical Bayes
+moderation](#validation-empirical-bayes)). After robust moderation, `plotSA` marks the variances whose
two-sided p-value as a variance of the prior, adjusted by Benjamini and Hochberg, is
below 0.5 (found by probing), and both mark the same 14. CytePool's dispersions differ
from DESeq2's where DESeq2's line search stops short of the maximum, and its
-quasi-dispersions from edgeR's by the exact moments of the unit deviance (see the
-sections on DESeq2 and on quasi-likelihood F-tests above), so these figures are
+quasi-dispersions from edgeR's by the exact moments of the unit deviance (see [DESeq2's
+dispersions and tests](#validation-deseq2-tests) and [Quasi-likelihood
+F-tests](#validation-quasi-likelihood)), so these figures are
compared drawn from R's own estimates, and from CytePool's fits for information.
metafor clips its diamond at the axis limits, which a pooled interval of two studies
with the Knapp-Hartung adjustment exceeds here, so the pooled estimate and interval
@@ -2252,7 +2477,13 @@ difference: a gene without residual degrees of freedom, such as one without coun
has no quasi-dispersion (its deviance and its degrees of freedom are 0); edgeR draws it
at 0, and CytePool leaves it out.
-## Principal components and dot plots
+(validation-pca-and-dot-plots)=
+### Principal components and dot plots
+
+The script printed 13 checks (11 pass, 1 deliberate difference and 1 information) in
+19.8 s, with a peak memory of 671 MiB. [Normalization and
+filtering](statistics/normalization_and_filtering.md#samples-at-a-glance), with
+[Pseudobulk profiles and cubes](statistics/cubes.md#dot-plot), explains what it checks.
[`validation/pca_and_dot_plots.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pca_and_dot_plots.py)
compares `principal_components` and `plot_pca` with DESeq2's `plotPCA`, run as a black
@@ -2278,9 +2509,10 @@ counts of 40 genes; scanpy normalizes them (`normalize_total`, `log1p`) and
| Dot plots: the areas and the fractions they encode, and the values the colours show, plain, scaled by gene or by group and over the expressing cells only, for every sample and for 3 samples and 3 cell types in a chosen order, and for genes in groups; the names of the rows and columns, and the key of the areas | `scanpy.pl.dotplot` | 1.0e-15 | 1e-10 | 6 pass |
| The brackets of genes given in groups: their columns and names | `scanpy.pl.dotplot` | identical | 0 | 1 pass |
-Differences are scaled by max(\|reference\|, 1). R's `prcomp`, whose coordinates
-`plotPCA` returns, leaves the sign of each component arbitrary; CytePool makes each
-component's largest coordinate positive, so the coordinates are compared after each
+Every difference is scaled as a statistic is [above](#reading-the-tables). R's `prcomp`, whose
+coordinates `plotPCA` returns, leaves the sign of each component arbitrary; CytePool
+makes each component's coordinate of largest absolute value positive, so the
+coordinates are compared after each
component takes the sign that matches R's. What DESeq2's help page leaves open was
found by probing: the genes are ranked by their variance, genes of equal variance keep
their order (the first are kept), `ntop` beyond the number of genes takes every gene,
@@ -2293,7 +2525,13 @@ Deliberate difference: with two samples, DESeq2's `plotPCA` returns, and draws,
second component of rounding noise; CytePool returns one component, since the centred
values of n samples have at most n - 1 dimensions, and refuses to draw a second.
-## Distributions and special functions
+## Building blocks on arrays
+
+(validation-distributions)=
+### Distributions and special functions
+
+The script printed 51 checks (51 pass) in 12 min 28 s, with a peak memory of 174 MiB.
+The API reference of {api}`cytepool.stats.distributions` documents what it checks.
[`validation/distributions.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/distributions.py)
checks every public function of `cytepool.stats.special` and
@@ -2344,7 +2582,12 @@ distribution's cdf and 0.193 for `gammainc` (1.94e-13, 1.95e-13 and 1.26e-13 for
CytePool), and it returns non-finite values at up to 493 points of a check
(`f_isf`).
-## LOWESS and spline bases
+(validation-smoothing)=
+### LOWESS and spline bases
+
+The script printed 90 checks (87 pass, 1 information and 2 deliberate differences) in
+1.5 s, with a peak memory of 175 MiB. [limma-voom and
+limma-trend](statistics/limma_voom.md#voom-span) explains what it checks.
[`validation/smoothing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/smoothing.py)
compares `lowess` with R's `stats::lowess` and `natural_spline_basis` with
@@ -2418,18 +2661,25 @@ observation weights or missing values fit each gene on its own, on 2 threads her
| Block correlation | 0.13 | 0.090 |
| Block correlation and observation weights | 10.81 | 0.194 |
-`mixed_models.py --benchmark` on 20,000 genes x 120 samples with observation weights
-and the fixed effects `~ condition + age` (3 conditions): CytePool on one thread, the
-median of three runs after a first call that compiled it, and lmerTest's `lmer` and
-`summary` (Satterthwaite's df for every coefficient, with lme4's default tolerances)
-on 100 of the genes, after one fit that was not timed. The peak resident memory of
-the run was 424 MiB.
+`mixed_models.py --benchmark`, run on 2026-10-28, on 20,000 genes with observation
+weights and the fixed effects `~ condition + age` (3 conditions), at 120 samples with three structures of
+random effects and at 18 samples, a study small enough for dream's default to use
+Kenward-Roger's method, with one random intercept: CytePool on one thread, the fits the
+median of three runs and the tests one run, after a first call that compiled them, and
+lmerTest's `lmer` and `summary` (Satterthwaite's df for every coefficient, with lme4's
+default tolerances) on 100 of the genes, after one fit that was not timed. The peak
+resident memory of the run was 729 MiB.
-| Random effects | `fit_mixed_model` (s) | `fixed_effect_tests` (s) | lmerTest, per gene (s) | lmerTest, 20,000 genes at that rate (min) |
-| --- | --- | --- | --- | --- |
-| One random intercept: 40 donors x 3 samples | 0.63 | 0.06 | 0.045 | 15.1 |
-| Two crossed: 40 donors, 6 batches | 1.50 | 0.06 | 0.052 | 17.5 |
-| Two nested: 20 donors x 3 samples x 2 replicates | 4.12 | 0.06 | 0.051 | 17.0 |
+| Random effects | Samples | `fit_mixed_model` (s) | Satterthwaite's tests (s) | Kenward-Roger's tests (s) | lmerTest, per gene (s) | lmerTest, 20,000 genes at that rate (min) |
+| --- | --- | --- | --- | --- | --- | --- |
+| One random intercept: 40 donors x 3 samples | 120 | 0.49 | 0.06 | 8.49 | 0.026 | 8.8 |
+| Two crossed: 40 donors, 6 batches | 120 | 1.18 | 0.07 | 9.70 | 0.033 | 11.1 |
+| Two nested: 20 donors x 3 samples x 2 replicates | 120 | 3.03 | 0.08 | 9.17 | 0.029 | 9.7 |
+| One random intercept: 6 donors x 3 samples | 18 | 0.20 | 0.06 | 0.15 | 0.021 | 7.1 |
+
+Kenward-Roger's tests cost O(n³) operations per gene for n samples: in the small study
+they took 0.15 s, against 0.06 s for Satterthwaite's, and at 120 samples more than 100
+times as long as Satterthwaite's.
On fgsea's example data, 12,000 genes and 586 sets of 15 to 500 genes, with its
defaults, fgsea's `fgsea` took 2.9 s and `gsea` 4.1 s, both on one thread, after a
@@ -2478,9 +2728,10 @@ one:
A read holds what it returns and about one decompressed chunk (3.3 MB of `log_sum`).
Genes at random lie in every chunk, so they decompress the whole file, and so does a
-selection of samples, since every chunk holds every sample. A read that covers a whole
-group is hashed, to check it against the digest stored with the file, and a selection's
-groups are hashed as they are read, for the selection's own fingerprint.
+selection of samples, since every chunk of this file holds every sample. A read that
+covers a whole group is hashed, to check it against the digest stored with the file,
+and a selection's groups are hashed as they are read, for the selection's own
+fingerprint.
(reproduce-validation)=
## Reproducing this page
diff --git a/docs/source/why_pseudobulk.ipynb b/docs/source/why_pseudobulk.ipynb
index 4b1dd1e..1e02ca6 100644
--- a/docs/source/why_pseudobulk.ipynb
+++ b/docs/source/why_pseudobulk.ipynb
@@ -44,7 +44,7 @@
"id": "547cc1b3",
"metadata": {
"figure_descriptions": [
- "Two histograms of the p-values of the 2,000 genes of the CD4 T cells, in 20 bins of width 0.05, between two random groups of six donors that do not differ, each with a dashed line at the count expected without differences, 100. Left, cells as replicates: the first bin, p-values below 0.05, holds about 750 genes, the second about 150, and the other bins fewer than the dashed line. Right, samples as replicates: every bin holds close to the expected count."
+ "Two histograms of the p-values of the genes of the CD4 T cells, 2,000 tested as cells and the 1,991 that pass the filter as samples, in 20 bins of width 0.05, between two random groups of six donors that do not differ, each with a dashed line at the count expected without differences, 100. Left, cells as replicates: the first bin, p-values below 0.05, holds about 750 genes, the second about 150, and the other bins fewer than the dashed line. Right, samples as replicates: every bin holds close to the expected count."
]
},
"outputs": [
@@ -52,7 +52,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "CytePool 0.1.dev63+g8afa32afb\n",
+ "CytePool 0.1.dev70+g194507939\n",
"group A: D01, D05, D06, D08, D09, D12\n",
"group B: D02, D03, D04, D07, D10, D11\n"
]
@@ -280,6 +280,17 @@
"- @crowell2020 surveyed cell-level mixed models and methods on pseudobulk data for\n",
" differential state analysis, with a simulation of single-cell data of several\n",
" samples and conditions.\n",
+ "- @lun2017 showed that ignoring plate effects loses control of the type 1 error, and\n",
+ " that summing the counts of the cells of each plate restores it, without losing power\n",
+ " in their simulations.\n",
+ "- @hafner2025 found that methods built for single cells offered no advantage over\n",
+ " pseudobulk DESeq2 on individual datasets in nested designs, and suggested dream as a\n",
+ " compromise at atlas scale.\n",
+ "- @nguyen2023 benchmarked 46 workflows for data from several batches: batch-corrected\n",
+ " data rarely improved the analysis of sparse data, while modelling the batch as a\n",
+ " covariate helped where batch effects were large.\n",
+ "- @heumos2023 summarized independent benchmarks into best-practice workflows for\n",
+ " single-cell analysis.\n",
"\n",
"## Results that replicate\n",
"\n",
@@ -299,20 +310,27 @@
" of 20 published results across datasets.\n",
"\n",
"Pooling the cells removes the false positives of pseudoreplication. Small studies and\n",
- "differences between cohorts remain, which combining studies addresses\n",
- "([Meta-analysis](statistics/meta_analysis.md)).\n",
+ "differences between studies remain, which the chapters on several studies take up\n",
+ "([Why findings fail to\n",
+ "replicate](statistics/aligning_studies.md#why-findings-fail-to-replicate)).\n",
"\n",
"## Choices\n",
"\n",
"- **Pool, then test.** {api}`from_cells` pools the cells of every sample and cell type\n",
- " ([Pool cells into pseudobulk profiles](guides/pool_cells.ipynb)), and the tests of\n",
- " [Compare conditions](guides/genes_per_cell_type.ipynb) compare the profiles.\n",
+ " ([Pool cells into pseudobulk profiles](guides/pool_cells.ipynb)), and\n",
+ " {api}`limma_voom` tests the profiles of every cell type ([Genes that change in each\n",
+ " cell type](guides/genes_per_cell_type.ipynb)).\n",
"- **Profiles of few cells.** A profile of a handful of cells is noisy: `min_cells` (10\n",
- " by default) leaves such profiles out of a fit, and `prior_weights=\"n_cells\"` instead\n",
- " weights every profile by its number of cells [@hoffman2026a].\n",
+ " by default) leaves such profiles out of a fit ([Samples with few cells or\n",
+ " counts](statistics/normalization_and_filtering.md#samples-with-few-cells)), and\n",
+ " `prior_weights=\"n_cells\"` instead weights every profile by its number of cells\n",
+ " ([Prior weights from the number of\n",
+ " cells](statistics/limma_voom.md#prior-weights-from-cells)).\n",
"- **Several samples of a donor.** They are not independent either: pair them in the\n",
" design, `~ donor + condition`, or give the donor a random effect with\n",
- " {api}`dream_voom` ([Repeated measures and donors](guides/repeated_measures.ipynb)).\n",
+ " {api}`dream_voom` ([A fixed or a random donor\n",
+ " effect](statistics/mixed_models.md#fixed-or-random-donor), and [Repeated measures and\n",
+ " donors](guides/repeated_measures.ipynb) on data).\n",
"- **Models of the cells.** Generalized linear mixed models with a random effect for\n",
" each individual model the cells themselves [@zimmerman2021]; CytePool does not fit\n",
" them.\n",
@@ -326,7 +344,7 @@
"datasets": [
"simulated_pbmc"
],
- "executed_at": "2026-10-27T15:23:55+00:00",
+ "executed_at": "2026-10-28T00:18:13+00:00",
"executed_code_sha256": "004b91136d2d3f0defea4a3d05f6753dc47fa05ba253d23be5851f01e588c3b8",
"executed_datasets": {
"simulated_pbmc": {
@@ -334,8 +352,8 @@
"simulated_pbmc_cells.npz": "sha256:0d4c9154b87dbf8e9796232c2afec4a8340368fd0f5d386df4dd780c4e478129"
}
},
- "executed_with": "0.1.dev63+g8afa32afb",
- "execution_seconds": 2.1,
+ "executed_with": "0.1.dev70+g194507939",
+ "execution_seconds": 2.0,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
diff --git a/tests/test_cube.py b/tests/test_cube.py
index db01dd5..3806316 100644
--- a/tests/test_cube.py
+++ b/tests/test_cube.py
@@ -1847,7 +1847,7 @@ def test_group_and_layer_lookup(cube: Cube, layers: dict[str, np.ndarray]) -> No
IndexError, match="group position 4 is out of range for 4 groups"
):
cube.mean(4)
- with pytest.raises(TypeError, match="not bool"):
+ with pytest.raises(TypeError, match="got bool"):
cube.mean(True)
with pytest.raises(TypeError, match="got float"):
cube.mean(2.0) # type: ignore[arg-type]
diff --git a/tests/test_design_checks.py b/tests/test_design_checks.py
index bb067c4..20e1500 100644
--- a/tests/test_design_checks.py
+++ b/tests/test_design_checks.py
@@ -431,9 +431,11 @@ def test_labels_that_differ_only_in_case_or_spaces() -> None:
def test_many_single_sample_levels_are_reported_once() -> None:
- labels = [f"L{i}" for i in range(8)] + ["L9", "L9"]
+ # Ten levels of one sample: the message names eight, as every list of a message
+ # does, and counts the others.
+ labels = [f"L{i}" for i in range(10)] + ["L10", "L10"]
report = Design(
- {"g": labels, "sample_id": [f"S{i}" for i in range(10)]}, "~ g"
+ {"g": labels, "sample_id": [f"S{i}" for i in range(12)]}, "~ g"
).check()
assert report.kinds() == ("single_sample_level",)
assert report.warnings[0].message.endswith(
@@ -587,3 +589,20 @@ def test_high_leverage_from_few_residual_degrees_of_freedom() -> None:
assert message.startswith("the model nearly fits S1 (leverage 0.99")
assert "with 1 residual degree of freedom" in message
assert "rin =" not in message and "log scale" not in message
+
+
+def test_many_aliased_columns_are_named_up_to_eight_and_counted() -> None:
+ # Eleven multiples of one covariate leave ten columns aliased: the message
+ # names the relations of eight, as every list of a message does, and counts the
+ # others after the separator of the relations.
+ values = [0.5, 1.5, 2.0, 3.5, 4.0, 6.0, 7.0, 8.5, 9.0, 11.0, 12.5, 13.0, 15.0]
+ samples: dict[str, list[Any]] = {
+ f"c{i}": [value * (i + 1) for value in values] for i in range(11)
+ }
+ samples["sample_id"] = [f"S{i}" for i in range(len(values))]
+ report = Design(samples, "~ " + " + ".join(f"c{i}" for i in range(11))).check()
+
+ message = _issue(report, "rank_deficient").message
+ named = re.findall(r"c(\d+) = (\d+)\*c0", message)
+ assert named == [(str(i), str(i + 1)) for i in range(1, 9)]
+ assert "c8 = 9*c0; and 2 more. The columns" in message
diff --git a/tests/test_design_model_matrix.py b/tests/test_design_model_matrix.py
index 67bedcf..b39a11a 100644
--- a/tests/test_design_model_matrix.py
+++ b/tests/test_design_model_matrix.py
@@ -260,7 +260,7 @@ def test_missing_values_name_the_column_and_rows() -> None:
}
with pytest.raises(
ValueError,
- match=r"'age' in rows 1, 3 \(sample_id S2, S4\); 'g' in row 1 "
+ match=r"'age' in rows 1 and 3 \(sample_id S2 and S4\); 'g' in row 1 "
r"\(sample_id S2\)\. Samples are not dropped silently",
):
model_matrix(samples, "~ age + g")
diff --git a/tests/test_differential_expression.py b/tests/test_differential_expression.py
index ad80368..d98bdad 100644
--- a/tests/test_differential_expression.py
+++ b/tests/test_differential_expression.py
@@ -898,9 +898,9 @@ def test_contrasts_are_required_and_matched_by_name() -> None:
({"groups": ["T", "T"]}, ValueError, "'T' is listed more than once"),
({"groups": 3}, TypeError, "groups must be a group id"),
({"groups": []}, ValueError, "groups is empty"),
- ({"min_cells": -1}, ValueError, "min_cells must be at least 0"),
+ ({"min_cells": -1}, ValueError, "min_cells must be non-negative"),
({"min_cells": 2.5}, TypeError, "min_cells must be a whole number"),
- ({"min_cells": True}, TypeError, "not bool"),
+ ({"min_cells": True}, TypeError, "got bool"),
(
{"expression_filter": {"min_counts": 5}},
ValueError,
diff --git a/tests/test_differential_expression_options.py b/tests/test_differential_expression_options.py
index 697805e..c2a3ef9 100644
--- a/tests/test_differential_expression_options.py
+++ b/tests/test_differential_expression_options.py
@@ -630,7 +630,7 @@ def test_none_or_false_turns_quality_weights_off() -> None:
(
{"quality_weights": {"prior_n": -1}},
ValueError,
- r"quality_weights\['prior_n'\] must be finite and at least 0",
+ r"quality_weights\['prior_n'\] must be non-negative",
),
(
{"quality_weights": {"max_iterations": 0}},
diff --git a/tests/test_documentation.py b/tests/test_documentation.py
index 2d36690..fcb56be 100644
--- a/tests/test_documentation.py
+++ b/tests/test_documentation.py
@@ -538,17 +538,41 @@ def test_every_public_name_is_documented_under_its_own_name() -> None:
def test_every_api_page_is_in_the_api_index() -> None:
index = (API_SOURCE / "index.md").read_text()
- toctree = re.search(r"```\{toctree\}\n(.*?)```", index, re.S)
- assert toctree is not None
+ toctrees = re.findall(r"```\{toctree\}\n(.*?)```", index, re.S)
listed = [
line.strip()
- for line in toctree[1].splitlines()
+ for toctree in toctrees
+ for line in toctree.splitlines()
if line.strip() and not line.startswith(":")
]
+ captions = re.findall(r"^:caption: (.+)$", index, re.M)
assert sorted(listed) == sorted(
path.stem for path in API_SOURCE.glob("*.md") if path.stem != "index"
)
+ # Every page is in one group, and the groups follow the parts of the book.
+ assert len(captions) == len(toctrees)
+ assert captions == [
+ "Cubes and files",
+ "Designs and linear models",
+ "Differential expression",
+ "Composition",
+ "Gene sets and pathways",
+ "Several studies",
+ "Figures",
+ "Building blocks on arrays",
+ ]
+
+
+def _validation_sections() -> dict[str, str]:
+ """The sections of the Validation page by label, each up to the next label or
+ heading of its level or above."""
+ page = (SOURCE / "validation.md").read_text()
+ parts = re.split(r"(?m)^\(([\w-]+)\)=\n", page)
+ return {
+ label: re.split(r"(?m)^##? ", body)[0] if body.startswith("###") else body
+ for label, body in zip(parts[1::2], parts[2::2], strict=True)
+ }
def test_every_validation_script_is_listed_on_the_validation_page() -> None:
@@ -557,11 +581,17 @@ def test_every_validation_script_is_listed_on_the_validation_page() -> None:
re.escape(REPOSITORY) + r"blob/master/validation/(\w+\.py)\)", page
)
scripts = {path.name for path in (ROOT / "validation").glob("*.py")}
+ sections = _validation_sections()
+ summary = page.split("## Summary", 1)[1].split("\n## ", 1)[0]
assert set(linked) == scripts
- # Each script has a row in the summary table and a section of its own.
+ # Each script has a section of its own, labelled after it, so that chapters can
+ # link it, which links the script and is linked from the summary.
for script in scripts:
- assert linked.count(script) >= 2, script
+ label = "validation-" + script.removesuffix(".py").replace("_", "-")
+ assert label in sections, script
+ assert f"validation/{script})" in sections[label], script
+ assert f"](#{label})" in summary, script
def _anchors(page: Path) -> set[str]:
@@ -625,6 +655,27 @@ def test_links_between_pages_resolve() -> None:
assert {"validation", "reproduce-validation", "quickstart"} <= labels
+def test_labels_are_unique_and_unlike_the_headings_of_other_pages() -> None:
+ # MyST resolves a link to #name against every page: a label that is also the id
+ # of a heading elsewhere can link to that heading instead.
+ labels: dict[str, list[str]] = defaultdict(list)
+ headings: dict[str, set[str]] = defaultdict(set)
+ for page in _site_pages():
+ text = _text(page)
+ where = page.relative_to(SOURCE).as_posix()
+ for label in LABEL.findall(text):
+ labels[label].append(where)
+ for heading in re.findall(r"^#+\s+(.+)$", text, re.M):
+ headings[re.sub(r"[^a-z0-9]+", "-", heading.lower()).strip("-")].add(where)
+
+ assert {label: pages for label, pages in labels.items() if len(pages) > 1} == {}
+ assert {
+ label: sorted(headings[label] - set(pages))
+ for label, pages in labels.items()
+ if headings[label] - set(pages)
+ } == {}
+
+
def test_link_check_finds_broken_links() -> None:
page = SOURCE / "index.md"
api = _documented_objects()
@@ -917,6 +968,408 @@ def f():
assert _variants(same_doi[:1] + same_work[:1]) == []
+# The copy rules (AGENTS.md, "Writing"): what a machine can check of them.
+
+# An em dash, or an en dash between spaces, as punctuation; ranges such as 1–5 are
+# not punctuation.
+PUNCTUATION_DASH = re.compile("\u2014|(?<=\\s)\u2013(?=\\s)")
+# A centred dot between words; 1.2·10^-5, as fgsea writes p-values, is a product.
+CENTRED_DOT = re.compile(r"[^\W\d_]\s*\u00b7\s*[^\W\d_]")
+STOCK_PHRASES = re.compile(r"\bunder the hood\b|\blives? in\b", re.I)
+
+
+def _copy_problems(text: str, where: str) -> list[str]:
+ """Where a text breaks the copy rules that a machine can check: dashes used as
+ punctuation, centred dots between words, stock phrases outside quotation marks,
+ and all-caps headings of more than one word."""
+ problems = []
+
+ def found(match: re.Match[str], rule: str) -> None:
+ line = text.count("\n", 0, match.start()) + 1
+ problems.append(f"{where}:{line}: {rule}: {match[0]!r}")
+
+ for match in PUNCTUATION_DASH.finditer(text):
+ found(match, "a dash used as punctuation")
+ for match in CENTRED_DOT.finditer(text):
+ found(match, "a centred dot between words")
+ # Quoting a phrase names it, as the rules themselves do.
+ unquoted = re.sub(r'"[^"\n]*"', lambda m: " " * len(m[0]), text)
+ for match in STOCK_PHRASES.finditer(unquoted):
+ found(match, "a stock metaphor")
+ for match in re.finditer(r"^#+\s+(.+)$", text, re.M):
+ words = re.sub(r"[^A-Za-z ]", "", match[1]).split()
+ if len(words) > 1 and all(word.isupper() for word in words):
+ found(match, "an all-caps heading")
+ return problems
+
+
+def _readable_texts() -> dict[str, str]:
+ """The prose that people read outside the package: every page, notebook prose
+ included, the API pages, the README and AGENTS.md."""
+ texts = {
+ path.relative_to(ROOT).as_posix(): _text(path)
+ for path in [*_site_pages(), *sorted(API_SOURCE.glob("*.md"))]
+ }
+ for name in ("README.md", "AGENTS.md"):
+ texts[name] = (ROOT / name).read_text()
+ return texts
+
+
+def _package_texts() -> dict[str, str]:
+ """The strings and comments of the package: docstrings, messages, figure labels
+ and reprs, by file."""
+ texts = {}
+ for path in sorted(PACKAGE.rglob("*.py")):
+ source = path.read_text()
+ strings = [
+ node.value
+ for node in ast.walk(ast.parse(source))
+ if isinstance(node, ast.Constant) and isinstance(node.value, str)
+ ]
+ comments = [
+ token.string
+ for token in tokenize.generate_tokens(io.StringIO(source).readline)
+ if token.type == tokenize.COMMENT
+ ]
+ texts[path.relative_to(ROOT).as_posix()] = "\n".join(strings + comments)
+ return texts
+
+
+def test_pages_the_readme_and_agents_follow_the_copy_rules() -> None:
+ problems = [
+ problem
+ for where, text in _readable_texts().items()
+ for problem in _copy_problems(text, where)
+ ]
+
+ assert problems == []
+
+
+def test_docstrings_messages_and_figure_labels_follow_the_copy_rules() -> None:
+ problems = [
+ problem
+ for where, text in _package_texts().items()
+ for problem in _copy_problems(text, where)
+ ]
+
+ assert problems == []
+ # The scan read the package: its docstrings and messages are long.
+ assert sum(map(len, _package_texts().values())) > 1_000_000
+
+
+def test_the_copy_rule_scan_finds_what_is_wrong() -> None:
+ wrong = textwrap.dedent(
+ """\
+ # TWO WORDS
+ A gene \u2014 or a set.
+ Counts \u2013 and their sums.
+ Genes \u00b7 sets.
+ The model lives in a module, and the fit runs under the hood.
+ """
+ )
+ right = textwrap.dedent(
+ """\
+ # GSVA and ULM
+ A p-value of 1.2\u00b710^-5 for genes 1\u20135, a per-sample
+ Kenward-Roger test; avoid "X lives in Y".
+ """
+ )
+
+ assert [problem.split(": ")[1] for problem in _copy_problems(wrong, "x")] == [
+ "a dash used as punctuation",
+ "a dash used as punctuation",
+ "a centred dot between words",
+ "a stock metaphor",
+ "a stock metaphor",
+ "an all-caps heading",
+ ]
+ assert _copy_problems(right, "x") == []
+
+
+# Addresses of the site, as docstrings and the README give them.
+
+# An address ends before the punctuation of the sentence that gives it.
+SITE_ADDRESS = re.compile(
+ r"https://docs\.nygen\.io/CytePool/((?:[\w-]+(?:[./][\w-]+)*)?/?)"
+ r"(?:#((?:[\w-]+\.)*[\w-]+))?"
+)
+
+
+def _address_problem(path: str, anchor: str | None) -> str | None:
+ """Why an address of the site names no page, or no label or heading of its page,
+ or None when it resolves."""
+ pages = {page.url: SOURCE / page.file for page in page_index.pages()}
+ route = "/" + path.strip("/")
+ if route.startswith("/api/"):
+ api = _documented_objects()
+ page = route.removeprefix("/api/").removesuffix(".html") + ".md"
+ if not route.endswith(".html") or page not in api:
+ return f"no API page {route}"
+ names = api[page] | {f"module-{name}" for name in api[page]}
+ return None if anchor is None or anchor in names else f"no {anchor} on {route}"
+ if route not in pages:
+ return f"no page {route}"
+ if anchor is not None and anchor not in _anchors(pages[route]):
+ return f"no label or heading {anchor} on {route}"
+ return None
+
+
+def test_every_address_of_the_site_resolves() -> None:
+ pytest.importorskip("yaml")
+ texts = {**_package_texts(), **_readable_texts()}
+ found = [
+ (where, match)
+ for where, text in texts.items()
+ for match in SITE_ADDRESS.finditer(text)
+ ]
+ # The API reference, served under api/, links the pages relative to it.
+ found += [
+ (path.name, match)
+ for path in sorted(API_SOURCE.glob("*.md"))
+ for match in re.finditer(
+ r"\]\(\.\./([\w/-]*?)/?(?:#([\w-]+))?\)", path.read_text()
+ )
+ ]
+ problems = [
+ f"{where}: {match[0]}: {problem}"
+ for where, match in found
+ if (problem := _address_problem(match[1], match[2]))
+ ]
+
+ assert problems == []
+ # Docstrings give the addresses of the chapters that explain them, and API pages
+ # link their chapters.
+ assert any(where.startswith("cytepool/") for where, _ in found)
+ assert any(where == "voom.md" for where, _ in found)
+
+
+def test_the_address_check_finds_addresses_that_do_not_resolve() -> None:
+ pytest.importorskip("yaml")
+
+ assert _address_problem("statistics/limma-voom", "voom-span") is None
+ assert _address_problem("", None) is None
+ assert _address_problem("api/voom.html", "cytepool.stats.voom.voom") is None
+ assert _address_problem("statistics/limma-voom", "no-such-label") is not None
+ assert _address_problem("statistics/precision-weights", None) is not None
+ assert _address_problem("api/voom.html", "cytepool.stats.voom.nothing") is not None
+
+
+# The chapters of the book.
+
+DRAFT = re.compile(r"^% draft:", re.M)
+CLOSING_SECTIONS = [
+ "Defaults and choices",
+ "Limits and differences",
+ "Checks and further reading",
+]
+
+
+def _book_files() -> list[str]:
+ """The pages of "Understand the methods", in reading order."""
+ book = "Understand the methods"
+ return [page.file for page in page_index.pages() if page.section == book]
+
+
+def _paragraphs(text: str) -> list[str]:
+ """The paragraphs of a page's prose after its title, without front matter,
+ labels, comments and code."""
+ prose, _ = page_index.split_markdown(
+ re.sub(r"\A---\n.*?\n---\n", "", text, flags=re.S)
+ )
+ body = prose.split("\n# ", 1)[-1].split("\n", 1)[-1]
+ lines = [
+ line for line in body.splitlines() if not re.match(r"\(\S+\)=$|%", line.strip())
+ ]
+ return [p.strip() for p in re.split(r"\n\s*\n", "\n".join(lines)) if p.strip()]
+
+
+@pytest.mark.parametrize(
+ "file",
+ sorted(
+ path.relative_to(SOURCE).as_posix()
+ for path in (SOURCE / "statistics").glob("*.md")
+ if path.name != "choosing_a_method.md"
+ ),
+)
+def test_every_chapter_has_the_shape_of_the_book(file: str) -> None:
+ pytest.importorskip("yaml")
+ text = (SOURCE / file).read_text()
+ # Every chapter is final: a draft marker left in one fails.
+ assert not DRAFT.search(text), f"{file} still carries a '% draft:' line"
+ book = _book_files()
+ position = book.index(file)
+ previous = Path(book[position - 1]).name
+ following = Path(book[position + 1]).name if position + 1 < len(book) else None
+ description = re.match(r"\A---\ndescription: (.+)\n---\n", text)
+ headings = re.findall(r"^## (.+)$", text, re.M)
+ paragraphs = _paragraphs(text)
+ opening, closing = paragraphs[0], paragraphs[-1]
+
+ # One sentence that a link preview can show.
+ assert description and len(description[1]) <= 200
+ assert headings[-3:] == CLOSING_SECTIONS
+ assert len(headings) > 3, (
+ "a chapter has sections of its own before the closing ones"
+ )
+ # The opening places the chapter after the one before, names the default, and
+ # points to the guide that runs it; the end leads to the next chapter.
+ assert f"]({previous}" in opening, f"the opening links {previous}"
+ assert "{api}`" in opening
+ assert re.search(r"\]\(\.\./(guides/\w+|quickstart|why_pseudobulk)\.ipynb", opening)
+ if following:
+ assert f"]({following}" in closing, f"the last paragraph links {following}"
+ # The checks link the sections of the Validation page that give them.
+ checks = text.split("## Checks and further reading", 1)[1]
+ assert re.search(r"\]\(#validation-[\w-]+\)", checks)
+
+
+def test_the_map_opens_the_book_and_leads_to_its_first_chapter() -> None:
+ pytest.importorskip("yaml")
+ book = _book_files()
+ text = (SOURCE / book[0]).read_text()
+
+ assert book[0] == "statistics/choosing_a_method.md"
+ # It links every chapter, and ends with the first.
+ assert sorted(Path(file).name for file in book[1:]) == sorted(
+ name for name in {Path(file).name for file in book[1:]} if f"]({name}" in text
+ )
+ assert f"]({Path(book[1]).name}" in _paragraphs(text)[-1]
+ assert "(not-implemented-yet)=" in text
+
+
+# The specification of cube files.
+
+
+def _record_keys(record: dict[str, Any]) -> set[str]:
+ """The keys of a provenance record that CytePool writes, at every depth, without
+ the caller's source record and the maps whose keys are ids or layer names."""
+ keys = set(record)
+ construction = record.get("construction") or {}
+ keys |= set(construction)
+ for axis in ("samples", "groups", "obs_columns"):
+ if isinstance(construction.get(axis), dict):
+ keys |= set(construction[axis])
+ for study in (construction.get("studies") or {}).values(): # by study name
+ keys |= set(study)
+ for operation in record.get("operations", []):
+ keys |= set(operation) | {operation["operation"]}
+ if operation["operation"] == "with_columns":
+ for axis in ("samples", "groups", "features"):
+ keys |= set(operation[axis] or {})
+ return keys
+
+
+def test_the_cube_files_page_names_every_key_that_cytepool_writes(
+ tmp_path: Path,
+) -> None:
+ from cytepool import CubeCollection, from_bulk, from_cells, write_cube
+
+ page = (SOURCE / "reference" / "cube_files.md").read_text()
+ # A key is named when it is written as code: in a code span or a code block.
+ code = re.findall(r"```\w*\n(.*?)```|`([^`]+)`", page, re.S)
+ named = {
+ word
+ for block, span in code
+ for word in re.findall(r"[A-Za-z_][\w.]*", block or span)
+ }
+ counts = np.array([[1, 0, 2], [3, 2, 0], [0, 4, 1], [2, 2, 2], [5, 0, 1]])
+ cube = from_cells(
+ counts,
+ sample_labels=["S1", "S1", "S2", "S2", "S3"],
+ group_labels=["B", "T", "B", "T", "B"],
+ layers=["counts", "detected", "log_sum", "log_sum_sq", "linear_sum"],
+ source={"tool": "a test"},
+ )
+ derived = (
+ cube.select(features=["feature_0", "feature_1"])
+ .regroup({"lymphocytes": ["B", "T"]})
+ .with_columns(samples={"donor": ["D1", "D1", "D2"]})
+ .collapse_samples("donor")
+ )
+ write_cube(
+ derived.with_columns(
+ samples={"visit": np.array(["2026-01-02", "NaT"], dtype="datetime64[D]")}
+ ),
+ tmp_path / "derived.cube.zarr",
+ )
+ root = json.loads((tmp_path / "derived.cube.zarr" / "zarr.json").read_text())
+ attributes = root["attributes"]
+ samples = json.loads(
+ (tmp_path / "derived.cube.zarr" / "samples" / "zarr.json").read_text()
+ )
+ visit = json.loads(
+ (tmp_path / "derived.cube.zarr" / "samples" / "visit" / "zarr.json").read_text()
+ )
+ sample_id = json.loads(
+ (
+ tmp_path / "derived.cube.zarr" / "samples" / "sample_id" / "zarr.json"
+ ).read_text()
+ )
+ bulk = from_bulk(np.array([[1, 2], [3, 4]]))
+ stacked = CubeCollection(
+ {"a": cube, "b": cube}, group_names={"B": "B cells"}
+ ).stack()
+ digests = attributes["digests"]
+ written = (
+ set(attributes)
+ | set(attributes["layers"])
+ | set(samples["attributes"])
+ | set(visit["attributes"])
+ | set(sample_id["attributes"])
+ | _record_keys(attributes["provenance"])
+ | _record_keys(bulk.provenance)
+ | _record_keys(stacked.provenance)
+ | set(digests)
+ | {key for layer in digests["layers"].values() for key in layer}
+ )
+ assert sorted(written - named) == [], sorted(written - named)
+ assert {"construction", "operations", "with_columns", "numpy_dtype"} <= written
+ assert {"studies", "group_names"} <= written
+ # The import and export of AnnData and MuData, when they are installed.
+ ad = pytest.importorskip("anndata")
+ pd = pytest.importorskip("pandas")
+ from cytepool import from_anndata, from_mudata, to_anndata
+
+ exported = to_anndata(cube)
+ record = exported.uns["cytepool"]
+ written |= set(record) | _record_keys(from_anndata(exported).provenance)
+ written |= {key for column in json.loads(record["features"]) for key in column}
+ written |= {
+ key
+ for column in json.loads(record["samples"])["columns"]
+ for key in [*column, "columns"]
+ }
+ long = ad.AnnData(
+ obs=pd.DataFrame(
+ {
+ "sample": ["S1", "S2", "S1"],
+ "group": ["B", "B", "T"],
+ "n_obs_aggregated": [2, 1, 3],
+ },
+ index=["r0", "r1", "r2"],
+ ),
+ var=pd.DataFrame(index=["G0", "G1"]),
+ layers={"sum": np.array([[4.0, 0.0], [3.0, 1.0], [0.0, 2.0]])},
+ )
+ imported = from_anndata(long, sample_column="sample", group_column="group")
+ written |= _record_keys(imported.provenance)
+ mudata = pytest.importorskip("mudata")
+ views = mudata.MuData(
+ {
+ "B": ad.AnnData(
+ np.array([[4.0, 0.0], [3.0, 1.0]]),
+ obs=pd.DataFrame(index=["S1", "S2"]),
+ var=pd.DataFrame(index=["B:G0", "B:G1"]),
+ )
+ }
+ )
+ written |= _record_keys(from_mudata(views, layers={"counts": "X"}).provenance)
+
+ assert sorted(written - named) == [], sorted(written - named)
+ # The keys were read: the provenance of each construction and operation.
+ assert {"membership", "studies", "layout", "obs_columns", "modalities"} <= written
+
+
# The navigation.
@@ -929,7 +1382,7 @@ def _toc() -> list[dict[str, Any]]:
def test_the_navigation_has_the_agreed_sections() -> None:
- overview, *sections = _toc()
+ overview, *sections, api = _toc()
# MyST needs a file first: the Overview.
assert overview == {"file": "index.md", "title": "Overview"}
@@ -939,6 +1392,28 @@ def test_the_navigation_has_the_agreed_sections() -> None:
assert all(
any("file" in child for child in entry["children"]) for entry in sections
)
+ # The API reference is a section of its own, last, whose pages the site build
+ # adds from the API index in its groups.
+ assert api == {
+ "title": page_index.API_REFERENCE,
+ "url": "/api/index.html",
+ "open_in_same_tab": True,
+ }
+ # The book's parts follow the map of questions, in reading order.
+ book = sections[page_index.SECTIONS.index("Understand the methods")]["children"]
+ assert [entry["title"] for entry in book] == [
+ "Choosing a method",
+ "Foundations",
+ "Differential expression",
+ "Composition tests",
+ "Gene sets and pathways",
+ "Several studies",
+ ]
+ # No two pages share a title.
+ titles = [
+ title for _, title, _ in page_index.toc_pages({"project": {"toc": _toc()}})
+ ]
+ assert len(titles) == len(set(titles))
def test_every_page_has_a_kind_and_the_lists_of_kinds_name_pages() -> None:
@@ -948,8 +1423,10 @@ def test_every_page_has_a_kind_and_the_lists_of_kinds_name_pages() -> None:
assert sorted(page_index.EXPLANATION_PAGES - files) == []
kinds = {page.file: page.kind for page in page_index.pages()}
assert kinds["statistics/choosing_a_method.md"] == page_index.EXPLANATION
+ # A chapter in a part of the book is in the book's section.
+ assert kinds["statistics/limma_voom.md"] == page_index.EXPLANATION
assert kinds["reference/cube_files.md"] == page_index.EXPLANATION
- assert kinds["reference/capabilities.md"] == page_index.INDEX
+ assert kinds["validation.md"] == page_index.INDEX
assert kinds["quickstart.ipynb"] == page_index.GUIDE
@@ -1391,6 +1868,36 @@ def test_the_notices_and_the_datasets_page_name_every_dataset() -> None:
assert "use no dataset" not in notices
+def _missing_paths(text: str) -> list[str]:
+ """The paths of the repository that a text names in backticks, such as
+ `cytepool/plotting.py` or `tests/data/`, and that do not exist."""
+ named = re.findall(r"`((?:cytepool|validation|docs|tests)/[^`\s]*)`", text)
+ return [path for path in named if not (ROOT / path).exists()]
+
+
+def test_the_notices_name_files_that_exist() -> None:
+ # A file renamed or removed must not leave its notice behind, naming no file.
+ notices = (ROOT / "THIRD_PARTY_NOTICES.md").read_text()
+
+ assert "`cytepool/plotting.py`" in notices
+ assert _missing_paths(notices) == []
+
+
+def test_the_notice_check_finds_files_that_do_not_exist() -> None:
+ # The notices of master before the files were renamed named these three.
+ text = (
+ "Adapted from fgsea in `cytepool/gsea.py`, checked by `validation/gsea.py`; "
+ "SumRank in `cytepool/sumrank.py` and `cytepool/held_out_prediction.py`, "
+ "with data in `tests/data/`."
+ )
+
+ assert _missing_paths(text) == [
+ "cytepool/gsea.py",
+ "cytepool/sumrank.py",
+ "cytepool/held_out_prediction.py",
+ ]
+
+
def test_the_cache_must_be_outside_the_repository(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -1561,7 +2068,7 @@ def write(entries: dict[str, str], more: str = "") -> None:
coverage.read_exemptions(path)
-# The capability matrix.
+# The summary of the Validation page.
def _table_rows(text: str, header: str) -> list[list[str]]:
@@ -1576,38 +2083,36 @@ def _table_rows(text: str, header: str) -> list[list[str]]:
return rows
-def _script(cell: str) -> str:
- """The validation script a table cell links to."""
- match = re.search(r"`(\w+\.py)`", cell)
- assert match is not None, cell
- return match[1]
-
-
-def test_the_capability_matrix_agrees_with_the_validation_summary() -> None:
- summary = {
- _script(row[0]): row[2]
- for row in _table_rows((SOURCE / "validation.md").read_text(), "| Script |")
- if "`" in row[0]
- }
- matrix = _table_rows(
- (SOURCE / "reference" / "capabilities.md").read_text(), "| Method |"
- )
+def test_the_validation_summary_agrees_with_its_sections() -> None:
+ sections = _validation_sections()
+ rows = _table_rows((SOURCE / "validation.md").read_text(), "| Method |")
problems = []
- for method, _, checked, tier, script_cell in matrix:
- script = _script(script_cell)
- if script not in summary:
- problems.append(f"{method}: {script} is not on the Validation page")
+ for method, functions, checked, tier, where in rows:
+ labels = re.findall(r"\]\(#([\w-]+)\)", where)
+ if not labels or not set(labels) <= set(sections):
+ problems.append(f"{method}: links no section of the page: {where}")
continue
- # Every reference and version named is one the script was run against.
+ text = " ".join("".join(sections[label] for label in labels).split())
+ # Every reference and version named is one the section was run against.
for tool in re.findall(r"[\w-]+ \d[\w.-]*\d", checked):
- if tool not in summary[script]:
- problems.append(f"{method}: {tool} is not among {summary[script]}")
- if not re.match(r"(exact|close|calibrated)\b", tier):
- problems.append(f"{method}: the tier {tier!r} is not a tier")
+ if tool not in text:
+ problems.append(f"{method}: {tool} is not in its section")
+ # A tier, with the bound of its checks.
+ if not re.match(r"(exact|close|calibrated): ", tier) or not re.search(
+ r"\de-\d|identical|standard (deviations|errors)", tier
+ ):
+ problems.append(f"{method}: {tier!r} is not a tier with its bound")
+ if "{api}`" not in functions:
+ problems.append(f"{method}: names no function")
assert problems == []
- # Every script of the Validation page has a row.
- assert sorted(set(summary) - {_script(row[4]) for row in matrix}) == []
+ # Every section of a script is in the summary.
+ linked = {label for row in rows for label in re.findall(r"#([\w-]+)\)", row[4])}
+ scripts = {
+ "validation-" + path.stem.replace("_", "-")
+ for path in (ROOT / "validation").glob("*.py")
+ }
+ assert sorted(scripts - linked) == []
@pytest.mark.parametrize("base_url", ["", "/CytePool"])
@@ -1618,11 +2123,16 @@ def test_api_pages_join_the_staged_guide_navigation(
api = tmp_path / "docs" / "build" / "api"
api.mkdir(parents=True)
+ # Sphinx writes a caption before the list of each captioned table of contents.
(api / "index.html").write_text(
'"
+ '
'
)
site = tmp_path / "site"
site.mkdir()
@@ -1630,12 +2140,8 @@ def test_api_pages_join_the_staged_guide_navigation(
"project": {
"toc": [
{"file": "index.md"},
- {
- "title": "Reference",
- "children": [
- {"url": f"{base_url}/api/index.html", "title": "API reference"}
- ],
- },
+ {"title": "Reference", "children": [{"file": "glossary.md"}]},
+ {"title": "API reference", "url": f"{base_url}/api/index.html"},
]
}
}
@@ -1646,19 +2152,31 @@ def test_api_pages_join_the_staged_guide_navigation(
build_docs.include_api_navigation(site)
toc = yaml.safe_load((site / "myst.yml").read_text())["project"]["toc"]
- assert toc[0] == {"file": "index.md"}
- assert toc[1]["children"][0]["children"] == [
- {
- "title": "Cubes API reference",
- "url": f"{base_url}/api/cubes.html",
- "open_in_same_tab": True,
- },
+ assert toc[:2] == config["project"]["toc"][:2]
+
+ def page(title: str, file: str) -> dict[str, Any]:
+ url = f"{base_url}/api/{file}"
+ return {"title": title, "url": url, "open_in_same_tab": True}
+
+ # Pages before the first caption come first; each caption is a group of its pages.
+ assert toc[2]["children"] == [
+ page("Overview", "overview.html"),
{
- "title": "Import & export",
- "url": f"{base_url}/api/import_export.html",
- "open_in_same_tab": True,
+ "title": "Cubes & files",
+ "children": [
+ page("Cubes API reference", "cubes.html"),
+ page("Import & export", "import_export.html"),
+ ],
},
+ {"title": "Figures", "children": [page("Figures", "plotting.html")]},
]
+ # A caption without pages is an error, not an empty group.
+ (api / "index.html").write_text(
+ ''
+ )
+ with pytest.raises(ValueError, match="groups without pages"):
+ build_docs.include_api_navigation(site)
@pytest.mark.parametrize("base_url", ["", "/CytePool"])
diff --git a/tests/test_dream.py b/tests/test_dream.py
index b4d1444..088533a 100644
--- a/tests/test_dream.py
+++ b/tests/test_dream.py
@@ -185,7 +185,7 @@ def test_dream_voom_matches_variance_partition_on_every_cell_type(variant: str)
data, _ = fixture("cube")
cube, design = fixture_cube()
# The fixture is dream(ddf = "Satterthwaite"); the default, "adaptive", would
- # use Kenward and Roger's method in these groups of fewer than 20 samples.
+ # use Kenward-Roger's method in these groups of fewer than 20 samples.
result = dream_voom(
cube,
design,
@@ -531,7 +531,7 @@ def test_dream_voom_records_its_settings_and_the_features_not_fitted() -> None:
group = record["groups"][0]
assert group["features_not_fitted"] == 1 and group["features_not_converged"] == 0
assert group["voom_span"] == 1.0
- # 12 samples: dream's adaptive choice is Kenward and Roger's method.
+ # 12 samples: dream's adaptive choice is Kenward-Roger's method.
assert group["df_method"] == "kenward_roger"
assert group["features_satterthwaite_fallback"] == 0 and group["f_test_df1"] == {}
assert pickle.loads(pickle.dumps(result)).provenance == record
diff --git a/tests/test_empirical_bayes.py b/tests/test_empirical_bayes.py
index 0b237d6..d0ea3df 100644
--- a/tests/test_empirical_bayes.py
+++ b/tests/test_empirical_bayes.py
@@ -936,7 +936,7 @@ def test_exact_f_statistics_need_exact_standard_errors() -> None:
([0.1, 0.2, 0.3], 4.0),
{"winsor_tail_p": (0.05, 0.6)},
ValueError,
- "strictly between 0 and 0.5",
+ "winsor_tail_p must be in \\(0, 0.5\\)",
),
(
fit_f_distribution_robust,
@@ -985,7 +985,7 @@ def test_exact_f_statistics_need_exact_standard_errors() -> None:
([0.1, 0.2, 0.3], 4.0),
{"robust": "yes"},
TypeError,
- "robust must be a bool",
+ "robust must be True or False",
),
(
fit_f_distribution_likelihood,
@@ -1057,7 +1057,7 @@ def test_hyperparameter_functions_reject_invalid_input(
moderated_statistics,
{"proportion": 0.0},
ValueError,
- "strictly between 0 and 1",
+ "must be in \\(0, 1\\)",
),
(moderated_statistics, {"proportion": True}, TypeError, "real number"),
(
@@ -1088,7 +1088,7 @@ def test_hyperparameter_functions_reject_invalid_input(
(treat, {"lfc": 1.0, "fc": 2.0}, ValueError, "not both"),
(treat, {"fc": -1.0}, ValueError, "positive and finite"),
(treat, {"lfc": np.nan}, ValueError, "finite"),
- (treat, {"robust": "yes"}, TypeError, "robust must be a bool"),
+ (treat, {"robust": "yes"}, TypeError, "robust must be True or False"),
(treat, {"prior_estimator": "fitFDist"}, ValueError, "prior_estimator must"),
(
moderated_statistics,
diff --git a/tests/test_gene_set_tests.py b/tests/test_gene_set_tests.py
index 24c41aa..987600b 100644
--- a/tests/test_gene_set_tests.py
+++ b/tests/test_gene_set_tests.py
@@ -640,7 +640,7 @@ def test_invalid_arguments_are_refused_with_a_reason() -> None:
({"feature_ids": genes[:3]}, ValueError, "one id per gene"),
({"inter_gene_correlation": 1.0}, ValueError, r"lie in \(-1, 1\)"),
({"inter_gene_correlation": True}, TypeError, "real number or None"),
- ({"use_ranks": 1}, TypeError, "use_ranks must be a bool"),
+ ({"use_ranks": 1}, TypeError, "use_ranks must be True or False"),
]
for changes, error, message in cases:
arguments = {"data": y, "design": x, "contrast": 1, "gene_sets": sets} | changes
@@ -655,7 +655,7 @@ def test_fry_refuses_options_without_effect_and_unknown_standardizations() -> No
fry(y, sets, design=x, contrast=1, standardize="none", trend=True)
with pytest.raises(ValueError, match="standardize must be one of"):
fry(y, sets, design=x, contrast=1, standardize="posterior.sd")
- with pytest.raises(TypeError, match="robust must be a bool"):
+ with pytest.raises(TypeError, match="robust must be True or False"):
fry(y, sets, design=x, contrast=1, robust="yes")
diff --git a/tests/test_gene_sets.py b/tests/test_gene_sets.py
index 7190624..bcb9a79 100644
--- a/tests/test_gene_sets.py
+++ b/tests/test_gene_sets.py
@@ -338,7 +338,7 @@ def test_filter_by_size_keeps_the_sizes_in_its_range() -> None:
assert list(sets.filter_by_size(17)) == []
with pytest.raises(ValueError, match="max_size \\(4\\) is smaller than min_size"):
sets.filter_by_size(5, 4)
- with pytest.raises(ValueError, match="min_size must be at least 0"):
+ with pytest.raises(ValueError, match="min_size must be non-negative"):
sets.filter_by_size(-1)
with pytest.raises(TypeError, match="min_size must be an integer"):
sets.filter_by_size(True)
diff --git a/tests/test_gsea.py b/tests/test_gsea.py
index 7d5f5ff..8b821b6 100644
--- a/tests/test_gsea.py
+++ b/tests/test_gsea.py
@@ -537,7 +537,7 @@ def test_more_permutations_are_suggested_only_where_they_can_help() -> None:
({"min_size": 0}, ValueError, "min_size must be at least 1"),
({"min_size": 1.5}, TypeError, "min_size must be an integer"),
({"min_size": 5, "max_size": 4}, ValueError, r"max_size \(4\) is smaller"),
- ({"exponent": -1.0}, ValueError, "exponent must be finite and at least 0"),
+ ({"exponent": -1.0}, ValueError, "exponent must be non-negative"),
({"score_type": "up"}, ValueError, "score_type must be one of 'std'"),
({"method": "simple"}, ValueError, "method must be one of 'multilevel'"),
({"n_permutations": 0}, ValueError, "n_permutations must be at least 1"),
diff --git a/tests/test_jit.py b/tests/test_jit.py
index 5f7620d..17d765a 100644
--- a/tests/test_jit.py
+++ b/tests/test_jit.py
@@ -217,9 +217,9 @@ def test_thread_limit_restores_threads_after_an_exception() -> None:
[
(0, ValueError, "n_threads must be at least 1, got 0"),
(-2, ValueError, "at least 1, got -2"),
- (1.5, TypeError, "positive integer or None, got float"),
+ (1.5, TypeError, "an integer or None, got float"),
("2", TypeError, "got str"),
- (True, TypeError, "not bool"),
+ (True, TypeError, "got bool"),
],
)
def test_thread_limit_rejects_invalid_counts(
diff --git a/tests/test_kenward_roger_and_f_tests.py b/tests/test_kenward_roger_and_f_tests.py
index f3f90c9..5f8fb5c 100644
--- a/tests/test_kenward_roger_and_f_tests.py
+++ b/tests/test_kenward_roger_and_f_tests.py
@@ -4,7 +4,7 @@
lmerTest 3.1-3 on lme4 1.1-38, evaluated at CytePool's REML estimates, and from
variancePartition 1.36.2's ``dream``, ``eBayes`` and ``topTable``, written by
``validation/kenward_roger_and_f_tests.py``. The other tests check closed forms of
-balanced designs, where Kenward and Roger's degrees of freedom are those of the analysis
+balanced designs, where Kenward-Roger's degrees of freedom are those of the analysis
of variance and the adjustment vanishes, and invariants that a wrong implementation
breaks: reparametrised designs and equivalent contrasts, relabelled donors and
reordered samples, the scale of the weights, and an F-test of one contrast, which is the
@@ -196,7 +196,7 @@ def study(name: str, method: str) -> tuple[Any, Any]:
],
)
def test_moderated_tests_match_dream(name: str, ddf: str, method: str) -> None:
- """dream's adaptive choice is Kenward and Roger's method for 12 samples and
+ """dream's adaptive choice is Kenward-Roger's method for 12 samples and
Satterthwaite's for 20, as the tests of each method reproduce."""
data, _ = fixture(f"study_{name}")
moderated, time = study(name, method)
@@ -250,7 +250,7 @@ def cube_result() -> Any:
def test_dream_voom_by_default_chooses_the_degrees_of_freedom_as_dream_does() -> None:
- """12 samples left in "small": Kenward and Roger's method; 24 in "large":
+ """12 samples left in "small": Kenward-Roger's method; 24 in "large":
Satterthwaite's; the tables are dream's with ddf = "adaptive"."""
data, _ = fixture("cube")
result = cube_result()
@@ -343,8 +343,8 @@ def test_a_balanced_one_way_design_has_kenward_rogers_closed_forms() -> None:
def test_a_randomised_block_design_has_the_classical_degrees_of_freedom() -> None:
"""Donors as random blocks with each treatment once per donor: a difference of
treatments lies within donors, on (q - 1)(t - 1) df, and a treatment's mean has
- Satterthwaite's combination of the donor and residual mean squares, as Kenward
- and Roger's method gives; the adjustment vanishes. The F-test of the treatments
+ Satterthwaite's combination of the donor and residual mean squares, as
+ Kenward-Roger's method gives; the adjustment vanishes. The F-test of the treatments
is the analysis of variance's, MS treatments / MSE on (t - 1, (q - 1)(t - 1))."""
q, t = 7, 3
rng = np.random.default_rng(5)
@@ -615,7 +615,7 @@ def ill_conditioned_cube() -> tuple[Cube, Design]:
def test_dream_voom_falls_back_to_satterthwaite_where_dream_does() -> None:
"""Features whose adjusted covariance has a condition number above 1e6 take
- Satterthwaite's tests; the others Kenward and Roger's."""
+ Satterthwaite's tests; the others Kenward-Roger's."""
cube, design = ill_conditioned_cube()
kr = dream_voom(cube, design, df_method="kenward_roger", expression_filter=False)
satterthwaite = dream_voom(
@@ -629,7 +629,7 @@ def test_dream_voom_falls_back_to_satterthwaite_where_dream_does() -> None:
np.testing.assert_allclose(
np.asarray(first["t"])[same], np.asarray(second["t"])[same], rtol=1e-10
)
- # dream_voom with its default is Kenward and Roger's for 15 samples.
+ # dream_voom with its default is Kenward-Roger's for 15 samples.
default = dream_voom(cube, design, expression_filter=False)
np.testing.assert_array_equal(default.table("bulk", "t1")["df"], first["df"])
assert default.summary["df_method"][0] == "kenward_roger"
diff --git a/tests/test_linear_models.py b/tests/test_linear_models.py
index cab2202..74e2e37 100644
--- a/tests/test_linear_models.py
+++ b/tests/test_linear_models.py
@@ -1070,8 +1070,8 @@ def test_per_feature_fits_do_not_depend_on_threads_or_chunks() -> None:
("n_threads", "error", "message"),
[
(0, ValueError, "n_threads must be at least 1, got 0"),
- (1.5, TypeError, "n_threads must be a positive integer or None, got float"),
- (True, TypeError, "n_threads must be a positive integer or None, not bool"),
+ (1.5, TypeError, "n_threads must be an integer or None, got float"),
+ (True, TypeError, "n_threads must be an integer or None, got bool"),
],
)
def test_thread_numbers_are_checked_before_any_fit(
@@ -1396,7 +1396,7 @@ def test_fit_object_checks_its_fields() -> None:
}
with pytest.raises(ValueError, match="no per-feature covariance matrices"):
LinearModelFit(**{**stored, "exact_standard_errors": False})
- with pytest.raises(TypeError, match="must be a bool"):
+ with pytest.raises(TypeError, match="must be True or False"):
LinearModelFit(**{**fields, "exact_standard_errors": 1})
flagged = LinearModelFit(**{**fields, "exact_standard_errors": np.True_})
assert flagged.exact_standard_errors is True
diff --git a/tests/test_negative_binomial_models.py b/tests/test_negative_binomial_models.py
index 64605e8..edcf3cb 100644
--- a/tests/test_negative_binomial_models.py
+++ b/tests/test_negative_binomial_models.py
@@ -861,7 +861,7 @@ def test_prior_count_zero_gives_the_maximum_as_shrunk_coefficients() -> None:
({"library_sizes": np.r_[0.0, np.ones(5)]}, ValueError, "finite and positive"),
({"prior_count": -1.0}, ValueError, "prior_count must be non-negative"),
({"max_iterations": 0}, ValueError, "at least 1"),
- ({"max_iterations": 2.5}, TypeError, "positive integer"),
+ ({"max_iterations": 2.5}, TypeError, "must be an integer, got float"),
({"coefficient_names": ["a"]}, ValueError, "2 entries"),
],
)
diff --git a/tests/test_normalization.py b/tests/test_normalization.py
index ada2f17..1c4f4d6 100644
--- a/tests/test_normalization.py
+++ b/tests/test_normalization.py
@@ -660,7 +660,7 @@ def test_empty_samples_are_rejected() -> None:
(dict(sum_trim=-0.1), ValueError, r"sum_trim must be in \[0, 0.5\)"),
(dict(sum_trim=np.nan), ValueError, "sum_trim must be finite"),
(dict(log_ratio_trim="0.3"), TypeError, "log_ratio_trim must be a real number"),
- (dict(weighted=1), TypeError, "weighted must be a bool"),
+ (dict(weighted=1), TypeError, "weighted must be True or False"),
(dict(a_cutoff=np.nan), ValueError, "a_cutoff must be a number"),
(dict(a_cutoff=None), TypeError, "a_cutoff must be a real number"),
],
diff --git a/tests/test_pathway_scores.py b/tests/test_pathway_scores.py
index 50f9249..82aabd8 100644
--- a/tests/test_pathway_scores.py
+++ b/tests/test_pathway_scores.py
@@ -606,7 +606,7 @@ def test_arguments_are_checked() -> None:
gsva(values, sets, feature_ids=genes, kernel="Gaussian")
with pytest.raises(ValueError, match="the Poisson kernel takes counts"):
gsva(values, sets, feature_ids=genes, kernel="poisson")
- with pytest.raises(ValueError, match="exponent must be finite and at least 0"):
+ with pytest.raises(ValueError, match="exponent must be non-negative"):
ssgsea(values, sets, feature_ids=genes, exponent=-1.0)
with pytest.raises(ValueError, match=r"max_size \(2\) is smaller than min_size"):
plage(values, sets, feature_ids=genes, min_size=3, max_size=2)
diff --git a/tests/test_pca_and_dot_plots.py b/tests/test_pca_and_dot_plots.py
index be22472..9866c48 100644
--- a/tests/test_pca_and_dot_plots.py
+++ b/tests/test_pca_and_dot_plots.py
@@ -449,7 +449,7 @@ def test_a_cube_needs_its_group_and_enough_samples() -> None:
principal_components(cube, group="Mono")
with pytest.raises(ValueError, match="principal components need two or more"):
principal_components(cube, group="pDC", min_cells=10_000)
- with pytest.raises(ValueError, match="min_cells must be at least 0"):
+ with pytest.raises(ValueError, match="min_cells must be non-negative"):
principal_components(cube, group="B", min_cells=-1)
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index afd483d..7e2dfd3 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -782,7 +782,7 @@ def test_cell_numbers_are_the_cubes_with_entries_left_out_pale() -> None:
assert np.ma.getmaskarray(artists(ax, "cell numbers")[0].get_array()).sum() == 2
with pytest.raises(ValueError, match="does not record cell numbers"):
plotting.plot_cell_numbers(bulk_cube())
- with pytest.raises(ValueError, match="min_cells must be at least 0"):
+ with pytest.raises(ValueError, match="min_cells must be non-negative"):
plotting.plot_cell_numbers(cube, min_cells=-1)
with pytest.raises(TypeError, match="cube must be a Cube"):
plotting.plot_cell_numbers(cells) # type: ignore[arg-type]
diff --git a/tests/test_quasi_likelihood.py b/tests/test_quasi_likelihood.py
index b186ef6..af2bcba 100644
--- a/tests/test_quasi_likelihood.py
+++ b/tests/test_quasi_likelihood.py
@@ -703,9 +703,9 @@ def small():
[
({"dispersion": 0.1, "top_proportion": 0.5}, ValueError, "not both"),
({"top_proportion": 1.5}, ValueError, r"top_proportion must be in \(0, 1\]"),
- ({"robust": 1}, TypeError, "robust must be a bool"),
+ ({"robust": 1}, TypeError, "robust must be True or False"),
({"trend": [1.0, 2.0]}, ValueError, "one covariate value per feature"),
- ({"trend": ["a"] * 20}, TypeError, "trend must be a bool or real"),
+ ({"trend": ["a"] * 20}, TypeError, "trend must be True, False or real"),
({"dispersion": -0.1}, ValueError, "dispersion must be finite"),
({"dispersion": 2e4}, ValueError, "dispersion must be at most 10000"),
({"prior_count": -1.0}, ValueError, "prior_count must be non-negative"),
diff --git a/tests/test_rotation_gene_set_tests.py b/tests/test_rotation_gene_set_tests.py
index 8244333..09e8b32 100644
--- a/tests/test_rotation_gene_set_tests.py
+++ b/tests/test_rotation_gene_set_tests.py
@@ -640,8 +640,8 @@ def test_invalid_arguments_are_refused_with_a_reason() -> None:
(mroast, {"set_statistic": "median"}, ValueError, "set_statistic must be one"),
(romer, {"set_statistic": "msq"}, ValueError, "set_statistic must be one"),
(mroast, {"n_rotations": 0}, ValueError, "at least 1"),
- (mroast, {"n_rotations": 9.5}, TypeError, "positive integer"),
- (mroast, {"n_rotations": True}, TypeError, "positive integer"),
+ (mroast, {"n_rotations": 9.5}, TypeError, "must be an integer, got float"),
+ (mroast, {"n_rotations": True}, TypeError, "must be an integer, got bool"),
(mroast, {"rng": -1}, ValueError, "non-negative seed"),
(mroast, {"rng": "seed"}, TypeError, "numpy.random.Generator"),
(mroast, {"gene_weights": np.ones(5)}, ValueError, "one weight per gene"),
@@ -656,9 +656,9 @@ def test_invalid_arguments_are_refused_with_a_reason() -> None:
(mroast, {"gene_sets": {"a": {0: "up"}}}, TypeError, "real numbers"),
(mroast, {"gene_sets": {"a": {0: math.inf}}}, ValueError, "must be finite"),
(mroast, {"gene_sets": {"a": {60: 1.0}}}, ValueError, "position 60"),
- (mroast, {"mid_p": "yes"}, TypeError, "mid_p must be a bool"),
- (mroast, {"trend": 1}, TypeError, "trend must be a bool"),
- (romer, {"shrink_contrast_effects": None}, TypeError, "must be a bool"),
+ (mroast, {"mid_p": "yes"}, TypeError, "mid_p must be True or False"),
+ (mroast, {"trend": 1}, TypeError, "trend must be True or False"),
+ (romer, {"shrink_contrast_effects": None}, TypeError, "must be True or False"),
(romer, {"weights": np.ones((9, 60))}, ValueError, "sample weights only"),
(romer, {"gene_sets": {"a": {0: 1.0}}}, TypeError, "romer does not take"),
(romer, {"n_threads": 0}, ValueError, "n_threads must be at least 1"),
diff --git a/tests/test_sample_quality_weights.py b/tests/test_sample_quality_weights.py
index 7e0dec8..04c5dc8 100644
--- a/tests/test_sample_quality_weights.py
+++ b/tests/test_sample_quality_weights.py
@@ -466,7 +466,7 @@ def test_a_sample_without_information_needs_the_prior(method: str) -> None:
ValueError,
"not both",
),
- ({"prior_n": -1.0}, ValueError, "prior_n must be finite and at least 0"),
+ ({"prior_n": -1.0}, ValueError, "prior_n must be non-negative"),
({"prior_n": np.inf}, ValueError, "prior_n must be finite"),
({"prior_n": True}, TypeError, "prior_n must be a real number"),
({"method": "genebygene"}, ValueError, "method must be one of 'auto'"),
@@ -646,7 +646,7 @@ def test_quality_weights_with_one_residual_df_are_voom_weights() -> None:
[
({"method": "auto"}, "method must be one of 'gene_by_gene', 'reml'"),
({"variance_groups": ["a"] * 8}, "at least two groups"),
- ({"prior_n": -2.0}, "prior_n must be finite and at least 0"),
+ ({"prior_n": -2.0}, "prior_n must be non-negative"),
({"span": 2.0}, r"span must be a fraction of the features in \(0, 1\]"),
],
)
diff --git a/tests/test_sumrank.py b/tests/test_sumrank.py
index 3f5e3fe..c7a9451 100644
--- a/tests/test_sumrank.py
+++ b/tests/test_sumrank.py
@@ -231,7 +231,7 @@ def test_a_number_of_studies_summed_is_the_proportion_that_gives_it(
"n_studies_summed must be from 1 to the number of studies, 2, got 0",
),
({"n_studies_summed": 3}, ValueError, "number of studies, 2, got 3"),
- ({"n_studies_summed": True}, TypeError, "whole number of studies, not bool"),
+ ({"n_studies_summed": True}, TypeError, "whole number of studies, got bool"),
({"n_studies_summed": 1.5}, TypeError, "whole number of studies, got float"),
],
)
@@ -662,9 +662,9 @@ def test_permutations_need_a_variable_of_each_design() -> None:
sumrank(results, "T", CONTRAST, permute="condition")
with pytest.raises(ValueError, match="not a variable of the design of study"):
sumrank(results, "T", CONTRAST, n_permutations=1, permute="donor")
- with pytest.raises(ValueError, match="at least 0"):
+ with pytest.raises(ValueError, match="must be non-negative"):
sumrank(results, "T", CONTRAST, n_permutations=-1)
- with pytest.raises(TypeError, match="not bool"):
+ with pytest.raises(TypeError, match="got bool"):
sumrank(results, "T", CONTRAST, n_permutations=True)
diff --git a/validation/cube_fingerprints.py b/validation/cube_fingerprints.py
index e1f7af1..1131032 100644
--- a/validation/cube_fingerprints.py
+++ b/validation/cube_fingerprints.py
@@ -2,8 +2,9 @@
No other tool computes a cube's fingerprint, so the reference is a separate program
that reads cube files with zarr-python and recomputes every digest and the
-fingerprint with numpy and hashlib, following only the encoding documented in
-cytepool/cube/fingerprints.py; it imports nothing from CytePool. The script checks:
+fingerprint with numpy and hashlib, following only the encoding that the Cube files
+page of the documentation specifies (docs/source/reference/cube_files.md); it imports
+nothing from CytePool. The script checks:
1. The digests that write_cube stores with each cube file equal the recomputed ones,
part by part, and Cube.fingerprint() of each cube equals the recomputed
diff --git a/validation/mixed_models.py b/validation/mixed_models.py
index 5663704..b2d3e11 100644
--- a/validation/mixed_models.py
+++ b/validation/mixed_models.py
@@ -27,9 +27,12 @@
lme4's own optimum, which inherit the optimiser's precision where a variance is small,
and the variance components and degrees of freedom with lme4's default tolerances.
-With ``--benchmark``, the script times ``fit_mixed_model`` on one thread on 20,000
-genes x 120 samples with observation weights, for one, two crossed and two nested
-random intercepts, and lmerTest's ``lmer`` and ``summary`` on 100 of the genes.
+With ``--benchmark``, the script times ``fit_mixed_model`` and ``fixed_effect_tests``,
+by Satterthwaite's and by Kenward-Roger's method, on one thread on 20,000 genes x 120
+samples with observation weights, for one, two crossed and two nested random
+intercepts, and on 20,000 genes x 18 samples with one random intercept, a study small
+enough for dream's default to take Kenward-Roger's method; and lmerTest's ``lmer`` and
+``summary`` on 100 of the genes of each.
Deliberate differences, printed as such: CytePool refuses a zero weight (lme4's REML
criterion is then infinite) and a missing value (lme4 drops the sample); it leaves
@@ -832,7 +835,8 @@ def write_fixtures(
def benchmark(n_genes: int = 20_000, n_timed: int = 100) -> None:
- """Time fit_mixed_model on one thread, and lmerTest on ``n_timed`` of the genes."""
+ """Time fit_mixed_model and its tests on one thread, and lmerTest on ``n_timed`` of
+ the genes."""
rng = np.random.default_rng(20261028)
donor = np.repeat(np.arange(40), 3)
samples = {
@@ -855,6 +859,18 @@ def benchmark(n_genes: int = 20_000, n_timed: int = 100) -> None:
for case in cases:
y = simulate(rng, case.design, n_genes, random_sd=2.0)
case.y, case.weights = with_weights(rng, y)
+ # A small study, of 6 donors x 3 samples, in which dream's default takes
+ # Kenward-Roger's method (fewer than 20 samples).
+ small_donor = np.repeat(np.arange(6), 3)
+ small_samples = {
+ "donor": labels("D", small_donor),
+ "condition": np.tile(np.array(["a", "b", "c"], dtype=object), 6),
+ "age": rng.normal(50.0, 10.0, small_donor.size),
+ }
+ small = Case("small", small_samples, formula, "(1 | donor)", None)
+ y = simulate(rng, small.design, n_genes, random_sd=2.0)
+ small.y, small.weights = with_weights(rng, y)
+ cases.append(small)
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for case in cases:
@@ -871,18 +887,19 @@ def benchmark(n_genes: int = 20_000, n_timed: int = 100) -> None:
)
lines = (root / "seconds.txt").read_text().splitlines()
theirs = {line.split()[0]: float(line.split()[1]) for line in lines}
- n = cases[0].y.shape[0]
- print(f"Benchmark: {n_genes} genes x {n} samples, observation weights, one thread")
+ print(f"Benchmark: {n_genes} genes, observation weights, one thread")
print(
- "| random effects | CytePool fit (s) | CytePool tests (s) | lmerTest per gene "
- "(s) | lmerTest for every gene (min) |"
+ "| random effects | samples | CytePool fit (s) | Satterthwaite's tests (s) | "
+ "Kenward-Roger's tests (s) | lmerTest per gene (s) | lmerTest for every gene "
+ "(min) |"
)
- print("| --- | --- | --- | --- | --- |")
+ print("| --- | --- | --- | --- | --- | --- | --- |")
for case in cases:
warm = fit_mixed_model(
case.y[:, :10], case.design, weights=case.weights[:, :10]
)
fixed_effect_tests(warm)
+ fixed_effect_tests(warm, df_method="kenward_roger")
seconds = []
for _ in range(3):
start = time.perf_counter()
@@ -893,11 +910,14 @@ def benchmark(n_genes: int = 20_000, n_timed: int = 100) -> None:
start = time.perf_counter()
fixed_effect_tests(fit)
tests = time.perf_counter() - start
+ start = time.perf_counter()
+ fixed_effect_tests(fit, df_method="kenward_roger")
+ kenward_roger = time.perf_counter() - start
per_gene = theirs[case.name]
random = case.random.replace("|", "\\|")
print(
- f"| {random} | {np.median(seconds):.2f} | {tests:.2f} | "
- f"{per_gene:.3f} | {per_gene * n_genes / 60:.1f} |"
+ f"| {random} | {case.y.shape[0]} | {np.median(seconds):.2f} | {tests:.2f} | "
+ f"{kenward_roger:.2f} | {per_gene:.3f} | {per_gene * n_genes / 60:.1f} |"
)
@@ -907,7 +927,9 @@ def main() -> int:
"--write-fixtures", action="store_true", help="write test fixtures"
)
parser.add_argument(
- "--benchmark", action="store_true", help="time 20,000 genes x 120 samples"
+ "--benchmark",
+ action="store_true",
+ help="time 20,000 genes x 120 and x 18 samples",
)
arguments = parser.parse_args()
if arguments.benchmark:
diff --git a/validation/quasi_likelihood.py b/validation/quasi_likelihood.py
index dc77e5e..4d956dd 100644
--- a/validation/quasi_likelihood.py
+++ b/validation/quasi_likelihood.py
@@ -508,8 +508,8 @@ def check(label, reference, metric, value, tolerance, robust_prior=False):
)
label = "the same from edgeR's adjusted deviances"
check(label, "glmQLFit, squeezeVar", MAX_RELATIVE, value, CLOSE, True)
- # Observations whose fitted values tend to zero (near 1e-8 in edgeR's fits) get
- # the leverages of the limit in both.
+ # Observations whose fitted values tend to zero (between about 1e-8 and 1e-7 in
+ # edgeR's fits) get the leverages of the limit in both.
value = float(np.max(np.abs(fit.leverage - edger["leverage"].reshape(n, n_genes))))
check("leverages", FIT, "max difference", value, CLOSE if exact else LEVENBERG)
for i, (test, result) in enumerate(zip(case["tests"], tests), start=1):