feat: add drug_mean and drug_tissue_mean response transformations (conditional-mean residualization) - #465
Conversation
The shipped response transformations ("standard", "minmax", "robust")
are global monotone rescalings of the response. Since the reported
metrics are rank-based (Spearman) and partly mean-centered per drug and
per cell line, such rescalings cannot change any score.
GroupMeanCenterer subtracts a conditional mean instead: the per-drug
mean, estimated on the training fold only, so the model spends its
capacity on the residual drug x cell line structure rather than on the
drug main effect. inverse_transform adds the mean back, so predictions
stay on the original response scale. Drugs unseen during fit fall back
to the global training mean, which keeps the transformation safe for LDO
and cross-study prediction.
DrugResponseDataset supplies the drug ids as groups when the
transformation advertises requires_groups, so plain sklearn scalers keep
being called exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add the new drevalpy.response_transformation module to the API page and list drug_mean in the --response_transformation option and in the "Available Response Transformations" section of the usage docs. Also make the DrugResponseDataset cross-reference in the module docstring fully qualified so sphinx can resolve it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## development #465 +/- ##
===============================================
+ Coverage 80.34% 83.72% +3.38%
===============================================
Files 101 133 +32
Lines 8171 10885 +2714
===============================================
+ Hits 6565 9114 +2549
- Misses 1606 1771 +165 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
GroupMeanCenterer takes the fields the group key is built from as group_fields, and estimates a mean for every prefix of it. The lookup walks the levels from specific to coarse, so an unseen (drug, tissue) combination falls back to the mean of its drug and only an unknown drug to the global training mean. Without that nesting the transformation would be useless in LTO, where every test tissue is unseen by construction. DrugResponseDataset supplies the requested columns instead of always the drug ids, and raises if it does not have one of them, so that a dataset without tissues does not silently behave like drug_mean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Nice! Thanks! I actually think I once started something similar, but didn't push on because I got distracted. Did you try it out yet? The only thing I am wondering is if NaiveMeanEffectsPredictor should be exempt? Do you know if it makes any difference on its results? |
|
Yes, we tried the Linear and weak models gained a lot: ElasticNet +0.13 … +0.15 nPCC (though from near zero, And yes, I think I'll push another commit for that, plus a regression test that NME predictions are independent of |
The NaiveMeanEffectsPredictor is force-added to every run because it is the reference of all "Normalized *" metrics. So far it went through response_transformation like any other model. For the globally affine scalers (standard, minmax, robust) that is a no-op, but the conditional means added in this PR (drug_mean, drug_tissue_mean) do change its predictions: on CTRPv2/LCO/5 folds they move by up to 0.107 (sd 0.059), and a fixed RandomForest arm scores nPCC 0.3083 instead of 0.3106. The ruler moved with the models measured against it, so two runs with different response_transformation were silently no longer on the same scale. The model loop now passes None instead of response_transformation for NORMALIZATION_BASELINE; every other model is unaffected. tests/test_normalization_baseline_transformation.py runs the experiment twice on TOYv1 and asserts that the NaiveMeanEffectsPredictor predictions are identical with and without transformation (drug_mean and standard), while NaivePredictor still reacts to drug_mean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…an-response-transformation
PR Checklist for all PRs
docsis updated — new module added todocs/API.rst, new options documented indocs/usage.rstChanges
New features
--response_transformationcurrently offersstandard,minmaxandrobust. All three are global monotonerescalings of the response. That means they cannot change any of the metrics DrEval reports: Spearman and Kendall
are rank-based and therefore invariant under a monotone map, and the
Normalized *metrics subtract theNaiveMeanEffectsPredictorbaseline, which rescales along with everything else. Pearson/R² are invariant under anaffine map. So today the flag exists, but for the reported scores it is close to a no-op — the only effect is on the
optimizer's conditioning.
This adds two transformations that actually change what the model has to learn:
drug_meananddrug_tissue_mean.drevalpy/response_transformation.py(new) containsGroupMeanCenterer, a transformer that subtracts aconditional mean — estimated on the training fold only — instead of a global constant:
The model is then trained on the residuals rather than on the main effect, which is where the
Normalized *metricslook anyway.
inverse_transformadds the group mean back, so predictions are written out on the original responsescale and every downstream evaluation is unaffected.
group_fieldsnames the columns ofDrugResponseDatasetthe group key is built from:drug_mean→("drug_ids",): removes the drug main effect, i.e. how sensitive cell lines are to this drug onaverage.
drug_tissue_mean→("drug_ids", "tissue"): also removes the tissue-specific sensitivity of a drug, so themodel is left with the within-tissue drug × cell line structure.
The means are nested, not flat.
fitestimates a mean for every prefix ofgroup_fields, and the lookup walksthem from specific to coarse: a (drug, tissue) combination that did not occur in the training fold falls back to the
mean of its drug, an unknown drug to the global training mean. Without that,
drug_tissue_meanwould be useless inLTO, where every test tissue is unseen by construction and a flat fallback would push every test row onto the global
mean; with it, LTO simply degenerates to
drug_mean.Because the transformer needs to know which row belongs to which group,
DrugResponseDatasetsupplies the columns —and only for transformations that ask for them:
transform,fit_transformandinverse_transformforward these kwargs. ForStandardScaler,MinMaxScalerandRobustScalerthe dict is empty, so those calls are byte-for-byte the ones that ran before — no existingbehaviour changes, and
--response_transformationkeeps its current default.Details that matter for the eval modes DrEval cares about:
fitonly ever sees the training fold, so no test-fold information leaks into the offsets.with the ASCII unit separator and uses
searchsortedper level, so this stays one vectorized pass per levelinstead of a per-row dict lookup.
tissueis optional onDrugResponseDataset. If a dataset has none,drug_tissue_meanraises with a messagenaming the missing field rather than silently behaving like
drug_mean.Wiring:
get_response_transformationreturns the transformer for both names,check_argumentsaccepts them, and thetyper help text plus
docs/usage.rstlist them.Tests:
tests/test_response_transformation.py(22 tests) coversrequires_groupsis set onGroupMeanCentererand absent on plain sklearn scalersfitrecords the global mean and the per-group means, and both nesting levels for two group fieldstransformcenters every group on zero, andinverse_transformround-trips to the original values(n,)and(n, 1)input, since the dataset calls it with column vectorssearchsortedclipping)
("A", "B_C")vs("A_B", "C"))groupsit degenerates to plain mean-centering, and an empty training fold does not raiseDrugResponseDataset.fit_transform/inverse_transformhand over drug ids and tissues, and transform thepredictions with the same groups
drug_tissue_meangroupsargumentget_response_transformationandcheck_argumentsaccept both names and still reject nonsenseBug fixes
Maintenance