Repository navigation
Single-target regression model evaluation - #3
Merged
Merged
Conversation
jeremy-wayland
force-pushed
the
feature/regression
branch
from
October 9, 2026 11:22
7985f6f to
d66834e
Compare
jeremy-wayland
marked this pull request as ready for review
October 9, 2026 11:23
Adds TaskType::Regression. A regression model returns one predicted value per patient, and y holds one continuous target column. - baselines are rmse, mae and r2 on the clean cohort; metric_based is keyed by "rmse", with resiliency = clean RMSE / blackout RMSE. - Jitter variance is divided by the target's variance before the existing squash, so the score does not depend on the target's unit. - Flipper pairs a patient below a clinical cutoff with one at or above it (FlipperSimulator::generate_regression) and finds where the prediction crosses the cutoff, reusing the binary flip finder. A grid whose pairs do not straddle the cutoff is rejected. - A new optional `regression` section reports the same numbers in target units. It is absent for classifiers, so their JSON is unchanged. Constant or missing targets are errors rather than silent NaNs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `task_type: "regression"` in metadata.json. The default output is `variable` (what sklearn-onnx regressors emit), then `predictions`. - `output.classes` is optional; its one entry names the prediction column, which is otherwise `prediction`. - Checked at load: a static output width other than 1, `output.thresholds`, and --positive-class-index are all rejected. - The output is read as a `[n]` or `[n, 1]` float tensor with no range or sum check; NaN predictions are refused as for classifiers. Fixtures: LinearRegression and RandomForestRegressor fit on random data, added to generate.py. The generator now takes an output directory; regenerating with it reproduces every existing fixture's expected values exactly, and only the new files are committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `--task regression` scores a single-target regression model. Its true values come from `--targets <parquet> --target-col <name>`, joined to the cohort by ID as text (`--targets-id-col`, default `node_id`), so non-integer IDs work. - A cohort row with no target, a duplicate ID, or a missing or non-finite value stops the run with counts and example IDs. - `--labels` with regression, and `--targets` with a classifier, are errors rather than ignored. - `simulate --task regression --flip-threshold <cutoff>` writes a flipper grid of pairs across the clinical cutoff; without a cutoff the grid is skipped. `evaluate`/`compare` need the same cutoff to score it. `--flip-threshold` keeps its 0.5 default for classifiers. - `compare --predictions-out` writes `target` (null on synthetic flipper rows) and one prediction column per model; `all_agree` is the side of the cutoff, or null without one. Classifier flags, JSON and predictions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- data-contracts: the --targets table (IDs matched as text, every row needs one finite target) and the regression metadata.json. - metrics: RMSE/MAE/R² baselines, RMSE-ratio resiliency, variance- scaled jitter, and the clinical-cutoff flipper with its caveats. - cli, outputs, cli/README: the new flags, the `regression` JSON section and the regression predictions columns. - CHANGELOG: an Unreleased entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clean RMSE over blackout RMSE rewarded a model for having little accuracy to lose: once a feature is gone and the model falls back to the mean, that ratio is sqrt(1 - R²). On synthetic HbA1c targets with glucose blacked out for every patient, a 1-nearest-neighbour model fell from R² 0.26 to -0.09, worse than predicting the mean, yet had the best RMSE ratio (0.82) of the models that used glucose; ridge, boosting and an MLP kept about 40% of their R² and scored 0.67-0.69. The R² ratio ranks the 1-NN model last (-0.35). metric_based is now keyed `r2`, and the regression section gains `blackout_r2`. A model with no clean skill (R² <= 0) reports the neutral 1.0, as classifier labels at chance do. Docs also note how often regression flipper pairs never cross a cutoff in the tail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeremy-wayland
force-pushed
the
feature/regression
branch
from
October 9, 2026 11:48
d66834e to
5ed3bc8
Compare
jeremy-wayland
added a commit
that referenced
this pull request
Oct 9, 2026
- AGENTS.md: state the patient-data upload rule that README.md and cli/README.md point to; add --locked to match CI and CONTRIBUTING.md. - pasteur-rs skill: cover --task (multiclass, multilabel, regression), --targets and the regression cutoff; drop the link to AGENTS.md workflows that never existed there. - cli/README.md: describe multi-output predictions columns and what all_agree compares per task. - README.md: point to AGENTS.md for rules, skills for the workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jeremy-wayland
added a commit
that referenced
this pull request
Oct 9, 2026
* Simplify AGENTS.md for agent-facing docs. Keep policy and non-obvious dev commands; drop redundant tool-boundary detail and doc links. Co-authored-by: Cursor <cursoragent@cursor.com> * Bring agent docs up to date with #2 and #3. - AGENTS.md: state the patient-data upload rule that README.md and cli/README.md point to; add --locked to match CI and CONTRIBUTING.md. - pasteur-rs skill: cover --task (multiclass, multilabel, regression), --targets and the regression cutoff; drop the link to AGENTS.md workflows that never existed there. - cli/README.md: describe multi-output predictions columns and what all_agree compares per task. - README.md: point to AGENTS.md for rules, skills for the workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
Rebased onto
mainafter #2 squash-merged (2114f9d). That merge's tree is identical to #2's last tip, so the branch's final tree did not change in the rebase.Adds single-target regression models to Pasteur's simulation and evaluation. Binary, multiclass and multilabel output is unchanged: in a full before/after run, all 36 output files are identical (see Verification).
What changes
Model contract (
metadata.json)task_type: "regression".output.namedefaults tovariable, which is what sklearn-onnx regressors emit, thenpredictions.output.classesis optional. Its one entry names the prediction column; without it the column is calledprediction.output.thresholds;--positive-class-index.Targets
--targets <parquet> --target-col <name>, joined to the cohort by ID as text (--targets-id-col, defaultnode_id).Metrics
baselinesrmse,mae,r2on the clean cohortmetric_based.r2.resiliencyjitter_stability1/(1+100v)squash, withvdivided by the target's variance so the score doesn't depend on the unitflipper_stability--flip-thresholdand one at or above it. Scoring reuses the binary flip finder.simulateand toevaluate. A grid whose pairs don't straddle the cutoff is rejected. Without a cutoff,simulateskips flipper.regressionsection reports the same behaviour in target units:target_sd,blackout_rmse,blackout_r2,jitter_prediction_sd,flip_thresholdandflipper_never_flipped. It is absent for classifiers.CLI
--task regression, with--targets,--target-coland--targets-id-col.--labelswith regression is an error, and so is--targetswith a classifier.evaluate/compare--labelsis optional at parse time and checked per task.--flip-thresholdkeeps its 0.5 default for classifiers. Regression has no default.compare --predictions-outwrites:target, which is null on synthetic flipper rows;all_agree: whether every model puts the patient on the same side of the cutoff (null without a cutoff).Commits
core: the regression task, metrics, flipper simulation and scoring.model: the contract and output reading, with two new fixtures.cli:--task regressionand--targets.docs: data contracts, metrics, CLI, outputs and CHANGELOG.core: resiliency becomes the R² ratio, after sandbox validation (see the end).Adding a
TaskTypevariant breaks the exhaustivematches downstream until their layer handles it. So, as in #2:pasteur-corebuilds and passes its tests at commit 1 (47 tests);pasteur-modelbuilds from commit 2 (81 tests);Every new
matchnames each variant; none uses a wildcard.Breaking (library):
TaskTypegainsRegression.EvaluationResultgainsregression: Option<RegressionScores>.EvaluateArgs.labels/CompareArgs.labelsandflip_thresholdbecomeOptions.Verification
cargo fmt --check,cargo clippy --workspace -D warnings(with and without--all-targets) andcargo test --workspace --exclude pypasteur-bindingsall pass at the final commit: 110 tests, 32 of them new. A strict (-W) Sphinx build ofdocs/passes too.dbb8669) and through this branch, for binary, multiclass and multilabel. Each task went throughsimulate,evaluate(blackout, jitter, flipper) andcompare --predictions-out. All 36 outputs match:created_at;mean_squared_error/mean_absolute_error/r2_scoreon onnxruntime's predictions to within 2.2e-16.all_agreeunit tests, plus 5 integration tests (end-to-end, cutoff required and checked, cohort labels and classifier models refused, targets missing for part of the cohort).generate.pygainsLinearRegressionand a 5-treeRandomForestRegressor, fit on random data. It now takes an output directory. Regenerating every fixture reproduces each existing fixture's expected values exactly, and only the two new.onnxfiles and the newexpected.jsonentries are committed.Known limits
simulateandevaluateis caught only when some pair doesn't straddle the new cutoff.--task binaryisn't detected. The binary path is deliberately untouched.multimachinery.generalizabilityis still a placeholder.All fixtures and test data are synthetic. No patient data.
Sandbox validation (real models, synthetic HbA1c targets)
Seven regressors were trained: ridge, gradient boosting, random forest, MLP, 1-NN, ridge without glucose, and intercept-only.
Oracle agreement:
(c − f(a)) / (f(b) − f(a)).Unit invariance: R², resiliency, jitter and flipper are identical in % and mmol/mol (cutoffs 6.5% and 47.54 mmol/mol).
Validity:
Changed after validation: resiliency is blackout R² ÷ clean R²
Clean RMSE ÷ blackout RMSE rewarded models for having little accuracy to lose. When a model loses a feature entirely and falls back to the mean, that ratio is
sqrt(1 − R²). With glucose blacked out for every patient:Under the RMSE ratio, 1-NN looked the most resilient after falling below a mean predictor. Under the R² ratio, it ranks last.
metric_basedis now keyedr2, and theregressionsection gainsblackout_r2.Docs: flipper never-flipped share
Regression models shrink towards the mean. With the cutoff in the tail (8% at or above 6.5%), 57–68% of pairs never cross for well-fitted models, and those pairs dominate
flipper_stability.metrics.rstnow says so.After rebasing onto
main(5ed3bc8), these all pass at commit 5: fmt, clippy (--all-targets -D warnings), the workspace tests, the strict Sphinx build, and all 78 sandbox checks.🤖 Generated with Claude Code