Skip to content

Reject design matrices the MLE model cannot fit - #25

Merged
davidliwei merged 2 commits into
mainfrom
fix/validate-design-matrix
Aug 4, 2026
Merged

Reject design matrices the MLE model cannot fit#25
davidliwei merged 2 commits into
mainfrom
fix/validate-design-matrix

Conversation

@davidliwei

Copy link
Copy Markdown
Owner

Follow-up to the design-matrix questions in #23.

The problem

DesignMatCache.save_record fits the model from orig_design_mat[1:,1:] (mledesignmat.py): the first column is consumed as the baseline term and the first row as the reference sample, so that row's condition entries are never read. Nothing downstream checked either assumption, so a matrix that violated them ran to completion and produced a silently wrong answer — whatever sample happened to be listed first became the reference, and the conditions assigned to it were discarded.

Concretely, taking a correct 4-genotype matrix and moving the baseline row from the top to the bottom shifted a common-essential beta from -1.37 to -0.93, with no error and no warning. Nothing in the output distinguishes that run from a good one.

--day0-label builds its matrix baseline-first by construction (sample_order = args.day0_label + nonday0sample) and was never affected. This only concerns user-supplied -d matrices.

The change

A shared validate_designmat(desmat, samples), called from both mle postargs paths before any read counts are loaded, so a bad matrix fails in under a second with nothing half-computed. It rejects:

  • a baseline column that is not 1 for every sample,
  • a matrix with no baseline-only sample (nothing to measure betas against),
  • a first row that is not one of the baseline samples.

Errors name the offending sample rather than describing the rule abstractly:

ERROR: The first row of the design matrix must be a baseline sample, but 'WT_r1'
has non-zero entries in condition column(s). Baseline sample(s) found: 'T0'.
Move a baseline row to the top of the design matrix.

Multiple baseline-only samples remain legal — they are pooled into a single shared reference, which is what #23 was originally asking about — and are now reported as such at INFO level rather than being silent.

It errors rather than auto-reordering. Permuting the baseline row to the top would be safe (rows and labels move together, and read_gene_from_file matches count-table columns by name), but silently rewriting a user's matrix hides the case where they meant something the model can't express. If the convenience is wanted later, it belongs behind an opt-in flag.

Tests

Four tests in tests/test_smoke.py, covering each rejection and — importantly — that the two-baseline demo matrix still runs, so the validation can't over-reject the pooled-baseline design.

Local run: 14 passed. The 4 mageckGSEA failures in my environment are a stale local binary and reproduce identically on unmodified main.

Also

Fixes a Python 2 era implicit relative import (from mledesignmat import parse_designmat) in mlemageck_bayes.py, which would have raised ModuleNotFoundError on the same code path. That module is currently disabled at mlemageck.py:43, so this is latent rather than user-visible, but the new call site sits next to it.

Not addressed here: the docs side. TUTORIAL.md states "at least one initial/reference sample should have 1 only in the baseline column" without mentioning ordering — worth folding into davidliwei/mageck2-doc#8, which is already open against that same rules list.

🤖 Generated with Claude Code

DesignMatCache.save_record() fits from orig_design_mat[1:,1:]: the first column
is consumed as the baseline term and the first row as the reference sample, so
that row's condition entries are never read. Nothing checked either assumption,
so a matrix that violated them ran to completion and produced a silently wrong
answer -- whatever sample was listed first became the reference and the
conditions assigned to it were discarded.

Moving the baseline row of an otherwise-correct matrix to the bottom shifted a
common-essential beta from -1.37 to -0.93 with no error and no warning.

Add validate_designmat(), called from both mle postargs paths before any read
counts are loaded, rejecting: a baseline column that is not 1 for every sample,
a matrix with no baseline-only sample, and a first row that is not one. Errors
name the offending sample. Multiple baseline-only samples stay legal -- they are
pooled into one shared reference (see #23) -- and are now
reported as such.

The --day0-label path builds its matrix baseline-first by construction and was
never affected; this only concerns user-supplied -d matrices.

Also fix a Python 2 era implicit relative import in mlemageck_bayes.py that
would have raised ModuleNotFoundError on the same code path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d4e3d9ba0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mageck2/mledesignmat.py
Comment on lines +122 to +123
if 0 not in basesample:
logging.error('The first row of the design matrix must be a baseline sample, but '+label(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the now-invalid design-matrix example

When users follow mageck2 mle --help, mageck2/argsParser.py:171 still recommends 1,1;1,0, but this new check rejects that exact matrix because its first row is not baseline-only. The same stale example remains in mleargparse.py:127 and mlemageck_bayes.py:100. Update these examples to a baseline-first matrix such as 1,0;1,1 so the documented simplest invocation remains usable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — confirmed and fixed in b47dfb2.

Reproduced it first: mageck2 mle -d "1,1;1,0" ... now fails with

ERROR: The first row of the design matrix must be a baseline sample, but 'T0'
has non-zero entries in condition column(s).

so following the help text verbatim errored out. Swapped all three parsers (argsParser.py:171, mleargparse.py:127, mlemageck_bayes.py:100) to "1,0;1,1" and made the requirement explicit rather than implied by the example: "where the first row is the baseline sample".

Also added a regression test, since the underlying problem is that the help text and the validation rules can drift independently. It runs mle --help, extracts whichever example is currently advertised, and pushes it through parse_designmat + validate_designmat. I checked it actually bites by reverting argsParser.py to the old string — the test fails with SystemExit: -1 and the validator's own error message.

One note on scope: only argsParser.py feeds the user-visible mageck2 mle --help, so that's the one the test covers. The other two were updated for consistency, but mlemageck_bayes.py is unreachable while the bayes module is disabled at mlemageck.py:43.

davidliwei added a commit to davidliwei/mageck2-doc that referenced this pull request Aug 4, 2026
Three gaps in the design-matrix rules, all surfacing in davidliwei/mageck2#23:

- Multiple baseline-only samples (row sum 1) are pooled into one averaged
  reference, which is exactly the two-cell-line demo (HL60.initial +
  KBM7.initial). A user reasonably asked whether MAGeCK distinguishes them by
  cell line. It does not. Note that this assumes a shared starting
  representation, and that genuinely different references should be run
  separately.

- A baseline sample must be the *first row*. The model consumes row 0 as the
  reference and never reads the conditions assigned to it, so a treated sample
  listed first silently measures every beta against the wrong reference.
  davidliwei/mageck2#25 makes mle reject such a matrix; document the rule.

- Beta scores are natural-log, not log2 fold changes. The text said only "behaves
  like a log fold change", which invites reading a 4-fold dropout as -2 rather
  than -1.39.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`mle --help` advertised "1,1;1,0" as the simplest invocation, but that matrix
leads with a condition sample, so validate_designmat rejects it -- following the
documentation verbatim now errors out. Swap it for "1,0;1,1" and say explicitly
that the first row is the baseline sample. Same stale example appeared in all
three parsers (argsParser, mleargparse, mlemageck_bayes).

Add a test that pulls whichever example `mle --help` currently advertises and
runs it through parse_designmat + validate_designmat, so the help text and the
validation rules cannot drift apart again. Verified it fails against the old
"1,1;1,0" example.

Reported by Codex review on #25.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidliwei
davidliwei merged commit 1dc31e4 into main Aug 4, 2026
6 checks passed
@davidliwei
davidliwei deleted the fix/validate-design-matrix branch August 4, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant