Repository navigation
Conversation
Add a pytest suite for the numerical core: size factors and normalization, the variance-stabilizing transform, dispersion estimation, GLM fitting, Cook's distances and refitting, Wald tests, p-value adjustment and apeGLM shrinkage, through DeseqDataSet, DeseqStats, DefaultInference and the module-level functions behind them. The 300 tests sit in nine modules under tests/, with shared helpers in tests/helpers.py (a negative binomial simulator that records its true parameters, hand-worked inputs and reference implementations), fixtures in tests/conftest.py, DESeq2 1.50.2 reference outputs and the R scripts that wrote them under tests/data/unit_a/ and tests/data/unit_b/, tests/README_NEW_TESTS.md on running and extending the suite, and tests/TARGETS.md mapping every public class, method and function to its tests. No source, build or workflow file changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
These tests were written with auto-optimize, a tool developed by the Programmable Genomics Laboratory at UMass Chan Medical School: this pull request adds a pytest suite for PyDESeq2's numerical core: size factors and normalization, the variance-stabilizing transform, dispersion estimation, GLM fitting, Cook's distances and refitting, Wald tests, p-value adjustment and apeglm shrinkage, through
DeseqDataSet,DeseqStats,DefaultInferenceand the module-level functions behind them. It holds 300 tests in nine modules undertests/, with shared helpers intests/helpers.py(a negative binomial count simulator that records its true parameters, hand-worked inputs, and reference implementations of the quantities PyDESeq2 computes), fixtures intests/conftest.py, DESeq2 1.50.2 outputs with the R scripts that wrote them undertests/data/unit_a/andtests/data/unit_b/, atests/README_NEW_TESTS.mdon running and extending the suite, and atests/TARGETS.mdthat lists every public class, method and function with the tests that cover it. It changes no source, build or workflow file.Tests and line coverage by part
pydeseq2/datasets.pypydeseq2/dds.pypydeseq2/default_inference.pypydeseq2/dispersions.pypydeseq2/distributions.pypydeseq2/ds.pypydeseq2/glm.pypydeseq2/grid_search.pypydeseq2/inference.pypydeseq2/__init__.pypydeseq2/misc.pypydeseq2/plotting.pypydeseq2/preprocessing.pypydeseq2/stats.pypydeseq2/__version__.py"Before" is the existing tests, and "after" adds this suite. Tests per part count the tests that ran at least one line of the part, from per-test coverage; a test counts once per part, so the rows do not sum to the total.
Running it
PYDESEQ2_TEST_CPUSat its default of 1. Theexportline keeps BLAS and OpenMP to one thread per process.testdependency group;-nneeds pytest-xdist, which hatch's test environments install.PYDESEQ2_TEST_CPUS(default 1) is then_cpusevery new test passes to PyDESeq2. At 1 the per-gene solvers run in the test process; above 1 joblib starts that many workers for each call. No new test leavesn_cpusat its default of one worker per core.pytestmark = helpers.PANDAS_DEPRECATION_FILTERS, which ignores two pandas 3.1 deprecation messages that the repository'sfilterwarnings = errorwould otherwise turn into failures: anndata reading thefuture.infer_stringoption, and theinplacekeyword ofSeries.dropinDeseqDataSet._fit_parametric_dispersion_trend. Nothing else is filtered, and the filters match by text, so they are also valid on pandas 2. The hatch environments resolve pandas 3.1.0 today, under which the existing tests fail onmain(see Checked locally).No workflow changes.
testpathsistests, so theTestworkflow'shatch run hatch-test.<env>:run-cov -v --color=yes -n autoalready collects the new modules in all three hatch environments.How expected values are derived
Expected values are derived without PyDESeq2's code wherever practical:
tests/helpers.py, drawn from known size factors, coefficients and dispersions on a parametric trend, with all-zero genes, genes positive in a single sample, genes with means near 1e7 and planted outliers on request. It draws from NumPy's legacyRandomStatestreams, which NumPy keeps fixed across versions. Also small hand-worked matrices, and simulated datasets with DESeq2 outputs undertests/data/.tests/helpers.py, written from DESeq2's definitions: the Cox-Reid adjusted dispersion objective minimized over a dense grid and then by bounded Brent minimization, the ridge-penalized GLM objective minimized by Newton's method, DESeq2'sparametricDispersionFit, the Wald test with its sandwich covariance, apeglm's negative log posterior minimized by trust-region Newton from several starts, Cleveland's robust lowess, and DESeq2's independent filtering. Each has its own tests intests/test_unit_harness.py.poscountsanditerativesize factors, the variance-stabilizing transform, the dispersion trend, the dispersion prior variance and the dispersion outlier flags, with the R scripts that wrote them. DESeq2 1.50.2 reproduces the repository's existing single-factor fixtures, made with DESeq2 1.34.0, to within 6e-13 relative.rtol=1e-3on dispersions from L-BFGS-B,atol=1e-3on natural-log LFCs from IRLS,atol=2e-2on apeglm-shrunk LFCs, and 1e-8 relative on the Wald statistics, Cook's distances and hat diagonals recomputed from a fit's outputs. Integers, masks, NaN positions, shapes, index labels and dtypes are compared exactly. Tests that refit the same data with another gene order,n_cpusorbatch_sizecompare at the solvers' precision, and leave out the genes whose MAP dispersion takes the grid fallback in one fit only. Whether L-BFGS-B reports failure on a gene-wise fit, and the gene takes the grid fallback, depends on rounding for a few genes and so on the platform; the VST tests compare at most two such genes at one step of that grid.obs,var,varm,obsm,uns,layersor a results table. A customInferencethat implements only the abstract methods runsdeseq2,summaryandlfc_shrink.What it covers
test_unit_stats.pyDeseqStats(Wald tests,summaryfilters and hypotheses, contrasts,lfc_shrink) andDeseqDataSet.cond,contrastandcooks_outlier, recomputed from public fields, unchanged byn_cpusor gene order.test_unit_glm.pynb_nll, the trimmed statistics,irls_solverand its fallbacks,grid_fit_beta,fit_LFC, Cook's distances,refit, anddeseq2end to end, unchanged by gene order orn_cpus.test_unit_dispersions.pyfit_alpha_mle, rough and moments dispersions,dispersion_trend, the likelihoods and their gradient,grid_fit_alphaand theDefaultInferencedispersion methods, against the reference minimizer.test_unit_harness.pytest_unit_data.pyfit_size_factorswith every method and control genes, against hand-worked values, true size factors and DESeq2 1.50.2;deseq2_norm, fitted and applied; and themischelpers.test_unit_dispersions_dds.pyDeseqDataSetdispersion steps: gene-wise, trend and MAP dispersions, the prior, outliers, all-zero and tiny datasets, and DESeq2 1.50.2's trend, prior and outlier flags.test_unit_stats_functions.pyglm.wald_testagainst hand-worked values and the sandwich formula,nbinomFnandnbinomGLMagainst apeglm's posterior and its minimizer,grid_fit_shrink_beta, andstats.lowessagainst Cleveland's lowess.test_unit_data_dataset.pyDeseqDataSetconstructor, its errors and deprecated arguments,variables, the design matrix against formulaic,to_picklable_anndata,n_cpus, and a customInferencerunning the pipeline.test_unit_data_vst.pyvst,vst_fitandvst_transformwith both trends, blind and with the design, against a step-by-step transform and DESeq2 1.50.2, and new samples.Line coverage
How it was measured:
--cov=pydeseq2 --cov-context=testand the repository's[tool.coverage]settings, on Python 3.12.13 with pandas 3.0.6, numpy 2.5.4 and scipy 1.18.1, and-n 4.src/pydeseq2that ran at least once; branch coverage is off.n_cpus1 in both runs: the existing tests leaven_cpusat its default of one worker per core, so they ran with the CPU count Python reports set to 1, and the new tests withPYDESEQ2_TEST_CPUS=1. The per-gene solvers then run in the pytest process, where coverage.py credits them to a test.tests/test_pydeseq2.py,tests/test_edge_cases.pyandtests/test_stats.py) ran in a checkout of 2ca3b74; "this suite" is the new modules alone, on this branch.inference.py, whose covered lines are its class and method definitions, shows none.What the suite does not cover
Behaviour that contradicts the documentation or DESeq2 is left untested, so that fixing it will not break the suite.
tests/README_NEW_TESTS.mddescribes every such case under "Known issues in the current code (not tested)", together with one case the tests do check: with three or fewer residual degrees of freedom,fit_dispersion_priorwarns and keeps the trigamma formula where DESeq2 1.50.2 simulates. Docstrings or comments in the tests describe 13 of them where a test avoids them; the other four (zero counts indeseq2_norm_transform, the trend's first fit,min_betainirls_solver, and the circular import) are described only in the README. There are 17 such behaviours ofmain. #491 reports 13 of them with reproductions, and describes one more, gene-wise dispersions held atmin_dispwhen the starting value sits there, as a limitation DESeq2 shares. It cites the other three, which are already reported:lfc_nullandalt_hypothesisapplied per coefficient (#483), the prior curvature added to every row of the shrinkage Hessian (#470), and the MAP dispersion fallback dropping the prior (#471).Public names no test file names (3 of 74):
DeseqDataSet.plot_dispersions,make_MA_plotandmake_scatter.Lines no test reaches:
pydeseq2/dds.py(29 lines not run, 94.2% covered),pydeseq2/plotting.py(22, 47.6% covered),pydeseq2/ds.py(11, 94.5% covered),pydeseq2/datasets.py(3, 88.9% covered),pydeseq2/misc.py(3, 90.0% covered) andpydeseq2/default_inference.py(2, 97.4% covered).Platforms: Linux on x86_64 here, and on GitHub Actions in a fork with
ubuntu-latest,windows-latestandmacos-latest(arm64) in all three hatch test environments. With pandas below 3.1 every test passed on all three, and under pandas 3.1.0 the only failures, on every OS and in every environment, were the 49 existing tests that fail and the 2 that error onmain(see Checked locally). Other architectures (Linux on aarch64, macOS on Intel) were not tried. Expected values are compared at the precision of each solver rather than bitwise, because other numpy and scipy builds round differently; the suite passes with the lowest scipy the package allows (table below).Checked locally
The suite was run on Ubuntu 24.04.5 LTS (x86_64, AMD EPYC 9575F 64-Core Processor, 256 cores), each configuration limited to eight cores with one BLAS and OpenMP thread per process. The CI configurations use the hatch test environments CI builds (
uvx hatch env create hatch-test.<env>, resolved on 2026-10-28: numpy 2.5.4, scipy 1.18.1, anndata 0.13.4, pytest 9.2.0), and the others copies of them with the versions their names give:main(2ca3b74): 49 fail and 2 error. The repository'sfilterwarnings = errorturns twoPandas4Warnings into errors: anndata 0.13.4 reads the deprecatedfuture.infer_stringoption whenever anAnnDatais built, andDeseqDataSet._fit_parametric_dispersion_trendcallsSeries.drop(..., inplace=True). TheTestworkflow will therefore fail on this pull request, in the existing tests, untilmainhandles these two warnings. The configurations that run the wholetests/directory in the stable environments use pandas 3.0.6, under which the existing tests pass; the new tests pass under pandas 3.1.0 in all three CI environments, and with the two messages ignored on the command line the whole directory passes there too.tests/directory, the CPU count Python reports was capped at 4, because the existing tests leaven_cpusat its default of one worker per core and the machine has 256 cores.uns["trend_coeffs"]anduns["vst_trend_coeffs"]as a dict instead of a pandas Series; the tests read the two coefficients by key, throughhelpers.trend_coeffs, which works with either. With Store dispersion trend coefficients as a dict so the dataset can be written to h5ad #477 merged, the wholetests/directory also passes with Python 3.12 and pandas 3.0.6 (CPU count capped at 4, as in the other runs of the whole directory): the 300 new tests and 66 existing tests, the same 66 that pass onmainmerged with Store dispersion trend coefficients as a dict so the dataset can be written to h5ad #477 alone. Eleven other open pull requests that change PyDESeq2's code (feat: add likelihood ratio test (LRT) to DeseqStats #452, add cupy backend #447, Feature/gpu inference #445, jax deseq2 #441, Normalization factors and DESeqDataSetFromTximport equivalent #412, fix: improve the numerical stability of lfc_shrink #370, Add Relative Log Expression (RLE) Plots #348, ENH volcano plots #317, ENH implement local fit for trend curve #234, Adding an example with real data #180 and FEAT: Implement Likelihood-Ratio test #178) conflict withmainand were not tried; Chore(deps): bump the actions-deps group across 1 directory with 2 updates #473 and [pre-commit.ci] pre-commit autoupdate #449 change no Python code and no test settings.Check Buildworkflow'suv buildandtwine check --strictpass.🤖 Generated with Claude Code