Repository navigation
Align several studies' cubes by sample, gene and cell type - #55
Merged
Merged
Conversation
parashardhapola
force-pushed
the
46-gsea
branch
from
October 26, 2026 17:59
3c99fd1 to
d812f2e
Compare
CubeCollection aligns the cubes of several named studies, each kept as it was given: samples keyed study::sample_id (and donor columns the caller names), genes by feature id with a per-study measured mask so that a gene a study did not measure is missing rather than zero, and cell types by Cell Ontology id, group id or a mapping per study. It gives each study's cube on the collection's axes, selections, and one stacked cube for a joint model, which refuses anything a study lacks rather than filling it with zeros. align_study_results lines up the studies' differential expression gene by gene, with the reason a study has no result for a gene. Checked against anndata 0.13.4, pandas 3.1.0 and limma 3.62.1 (validation/cube_collection.py: 15 checks, none failed).
parashardhapola
marked this pull request as ready for review
October 26, 2026 18:02
parashardhapola
force-pushed
the
48-cube-collection
branch
from
October 26, 2026 18:02
e12e5c9 to
65aa56c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Results of one single-cell study often do not replicate in the next, 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.
CubeCollectionaligns the cubes of several studies,each kept as it was given, with what each study measured and has, and gives each
study's cube on the collection's axes for analyses study by study, their results lined
up gene by gene, and one stacked cube for a joint model. Combined p-values, replication
metrics and SumRank, the next pull requests, build on it.
Changes
cytepool.cube.CubeCollection(cubes, *, align_groups_by=None, group_mapping=None, within_study=()):study name to cube, aligned. Properties
studies,n_studies,n_samples,n_groups,n_features,cubes(as given),samples(sample_idwrittenstudy::sample_id,study, every study's columns),groups(aligned ids and thecolumns the studies agree on),
features(feature_id,symbol),measured(studies x features booleans),
group_members(each study's groups under theiraligned ids) and
provenance.CubeCollection.aligned_cube(study): the study's cube with its groups under theiraligned ids (merged where several share one) and the features it measured, in the
collection's order, with its own samples; derived with
Cube.selectandCube.regroup, so lazy for cube files and recorded in its provenance.CubeCollection.select(studies, groups, features): a collection of some studies,groups and features, as
Cube.selectreads selections.CubeCollection.stack(*, groups, features, layers): one cube of every study'ssamples, with the groups every study has and the features every study measured by
default; anything a study lacks is refused, never filled with zeros.
cytepool.meta_analysis.align_study_results(collection, results, group, contrast):the results of every study for one cell type and contrast, a row per study and gene,
with
status(tested,not_tested,not_measured,not_analysed),reasonandevery statistic of the studies' tables (NaN where not tested).
describe_provenancedescribes a stacked cube's construction; the provenancedocumentation lists its keys.
cube._axes.selected_positions(private):Cube.select's reading of ids, masks andpositions, moved out of
cube.pyto be shared; behaviour unchanged.validation/cube_collection.pyand fixtures intests/data/cube_collection/(67 KB).
results on the meta-analysis page, the API pages' introductions, a capability matrix
row (and an item under "Not implemented yet"), the Validation page (summary row,
totals, date, section, command), the Cell Ontology paper (Diehl et al. 2016, checked
against Crossref) in
references.biband on the Bibliography page,selectunder[planned]for the combining-studies guide, the provenance keys page, the overview'stwo questions on combining studies, and the README status table.
Design notes
alignment, selection, each study's aligned cube (any per-group analysis then runs
study by study with its own design), results lined up gene by gene, and stacking.
Combined p-values and replication metrics are statistics over aligned results, so
they come next, reading
align_study_resultsandmeasured. Both of the data'sparts belong here because they decide what a missing value means (a gene not
measured is not a gene measured and filtered out, and neither is a zero), which the
validation checks against tools that know nothing of the statistics; a pull request
that added the statistics on its own alignment would have validated both at once.
There is no runner that analyses every study: designs differ by study, and the loop
is one line,
{s: limma_voom(c.aligned_cube(s), designs[s]) for s in c.studies},whose result
differential_expression_meta_analysisreads unchanged.An aligned cube is derived when asked for (a copy for a cube in memory, lazy for a
cube file, the cube itself when nothing changes), and its provenance chains to the
cube as given by fingerprint.
existing
measuredcolumn does not say otherwise; the collection's features arethose some study measured.
stacktakes only the features every study measured andthe groups every study has, by default or as given: a cube holds no missing values,
and a study without a group did not resolve that cell type, so cells counted as zero
would mislead a composition test. Stacking needs cell numbers in every study or in
none, and normalized layers only from studies normalized alike.
cell_ontology_idwhen every cube has it,group_idotherwise, or a column named.group_mappinggives some of a study's groups their aligned ids instead (None leavesa group out). Keys are equal values only, never mixed per group, which would not be
transitive. Groups of one study with the same key are merged with
Cube.regroup,exactly; aligned groups are named by their key, so ontology ids by default, and a
mapping gives readable names. A group without a key is an error that says how to map
it, never dropped silently. Ontology-aware roll-up to a common ancestor needs the
Cell Ontology as a resource and is left for later; the mapping covers it meanwhile.
study::sample_id; which othercolumns hold ids cannot be guessed, so
within_studynames them (donors), and thevalidation's control shows what goes wrong without it. Sample columns combine as
pandas combines them (union in order of appearance, integers and floats to float64,
missing where a study lacks a column), except that booleans that need a missing value
become float64 with NaN, as in
Table, and a column of two kinds of values is refusedwith its studies and types. A per-study aligned cube keeps its own sample ids, so
designs built from its table apply.
them, as the meta-analysis orders features; a selection sets its own order.
symbolis the first one a study gives. Feature ids are compared as they are: twostudies that share no feature are refused, with a hint when the ids differ only by an
Ensembl version.
align_study_resultsrefuses results that did not come fromthe studies' aligned cubes (unaligned group ids, a tested feature the study did not
measure), and gives a complete table, study by study and gene by gene, so that
column.reshape(n_studies, n_features)gives a matrix for the statistics to come.stacking, Ensembl version and ortholog mapping, and the guide.
Validation
validation/cube_collection.py: three simulated studies pooled withfrom_cells(8, 10 and 6 samples; 700, 750 and 950 genes, 40 of them zero-filled and marked not
measured; cell types aligned by ontology id, two of them merged, one study by a mapping
that leaves one out; sample and donor ids that repeat across studies). anndata and
pandas join each study's
to_anndataexport themselves (measured genes only, rows of asample and aligned cell type summed); R runs limma-voom on each study's own counts and
on the stacked counts of two studies.
pandas.concatanndata.concatouter join (NaN fill),pandas.uniquestack(): axes, every layer, cells per entry and an entry without cellsanndata.concatinner joinlimma_voomon each aligned cube viaalign_study_results, 12 cell types~ donor + conditionon the stack, donorsstudy::donor15 checks: 14 pass, 1 control. limma ran on the counts with the zero-filled genes and
its tables equal those of the aligned cubes, which drop them: marking a gene missing
changes no other gene's result. References: 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. 26.7 s, 495 MiB peak.
Tests
tests/test_cube_collection.py, 46 tests, 0.9 s warm: against the fixtures (samples aspandas joins them; features, order and
measuredas anndata's outer join, and alignedcounts as its values;
stackas its inner join; each study's limma-voom lined up andthe joint model as limma, to 1e-10, with every gene's status); a two-study example
worked out by hand (merged cell types, a zero-filled gene, repeated donor ids, symbols,
the stacked layers and cells); mappings that rename, merge and leave out, a column the
studies agree or disagree on, ontology ids given by a mapping; invariants (the order of
studies and of genes within a cube, select then stack against stack then select, a
single study against its own cube, cube files against cubes in memory); layer types;
normalizations; provenance of the collection, of aligned and stacked cubes and their
history; pickling, copying and immutability; and every message. The masked-input
inventory gains the constructor,
select,stack,aligned_cubeandalign_study_results(16 cases).Checks
uv run pytest -q --durations=15: 4,306 passed, 18 skipped (27 s warm, 155 s cold).OPENBLAS_CORETYPE=Sandybridge OPENBLAS_NUM_THREADS=4 NUMBA_NUM_THREADS=4 NUMBA_CPU_NAME=genericand a fresh cache: 1,489 passed.ruff checkandruff format --checkon cytepool, tests, validation and docs;mypy cytepool: clean.make -C docs api(warnings as errors) andmake -C docs html(Node.js 24): clean;tests/test_documentation.py: 74 passed.