Skip to content

Add linear mixed model to the differential expression interface - #1147

Open
Sizerta wants to merge 1 commit into
scverse:mainfrom
Sizerta:linear-mixed-model
Open

Sizerta wants to merge 1 commit into
scverse:mainfrom
Sizerta:linear-mixed-model

Conversation

@Sizerta

@Sizerta Sizerta commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

PR Checklist

  • Referenced issue is linked
  • If you've fixed a bug or added code that should be tested, add tests!
  • Documentation in docs is updated

Description of changes

Closes #614
This adds pt.tl.LinearMixedModel to the DE interface.
It wraps statsmodels' MixedLM and fits one mixed model per variable with a random intercept. This means you can use all your cells while accounting for the donor they came from, or use random effects in pseudobulk-style designs.
The random-intercept syntax is the same (1 | variable) syntax already used by Milo, thanks to reusing parse_random_effects:

model = pt.tl.LinearMixedModel(adata, design="~condition + (1 | donor)")
model.fit()
res = model.test_contrasts(
    model.contrast(column="condition", baseline="A", group_to_compare="B")
)

# or let paired_by add the random intercept for you
pt.tl.LinearMixedModel.compare_groups(
    adata,
    column="condition",
    baseline="A",
    groups_to_compare="B",
    paired_by="donor",
)

Technical details

  • A bit more honest about p-values: statsmodels uses a z-test for fixed effects, which is too optimistic when there are few donors. We use a between-within degrees-of-freedom correction instead. In null simulations with the condition assigned per donor (3 vs 3 donors), the z-test flagged 12% of genes at α = 0.05, the corrected test 5–6%, and Statsmodels without the random effect 57%. A calibration test is included.
  • Less shouting about convergence: statsmodels can report non-convergence even when the optimizer has already reached the optimum (refitting changed the estimates by less than 1e-13). Those fits are retried with derivative-free optimizers unless method is explicitly provided. Any genuine remaining failures are reported once via a warning and the converged column.
  • Small refactor: LinearModelBase.compare_groups now builds its formula through _comparison_design, which lets LinearMixedModel turn paired_by into a random intercept without changing the existing behavior of other methods.
  • Adds 15 tests; the DE test suite passes (268 passed, 2 skipped).
  • The scope is intentionally small for now: one random intercept. Random slopes, crossed random effects, and the MAST and DREAM options from the issue can come later.
  • No new dependencies.
  • Fitting takes roughly 20–30 ms per variable on 180 cells (single core) and supports n_jobs parallelization just like Statsmodels.

@codecov-commenter

codecov-commenter commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.27273% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.96%. Comparing base (54b9990) to head (393767b).

Files with missing lines Patch % Lines
...ifferential_gene_expression/_linear_mixed_model.py 97.05% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1147      +/-   ##
==========================================
+ Coverage   75.69%   75.96%   +0.26%     
==========================================
  Files          55       56       +1     
  Lines        8431     8537     +106     
==========================================
+ Hits         6382     6485     +103     
- Misses       2049     2052       +3     
Files with missing lines Coverage Δ
src/pertpy/tools/__init__.py 91.42% <100.00%> (ø)
...py/tools/_differential_gene_expression/__init__.py 96.15% <ø> (ø)
...ertpy/tools/_differential_gene_expression/_base.py 96.06% <100.00%> (+0.12%) ⬆️
...ifferential_gene_expression/_linear_mixed_model.py 97.05% <97.05%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Sizerta
Sizerta force-pushed the linear-mixed-model branch from 93ecbb2 to 393767b Compare October 2, 2026 10:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add linear mixed effects model to DE interface

2 participants