Spaces:
Sleeping
Prompt β Iterative unsupervised discovery ("peel off axes")
Add a "find the next axis" capability to the UNSUPERVISED objective: after a run, residualise the discovered split out of the data and re-run, so the engine surfaces a ranked list of independent axes (Axis 1 = KRT5, Axis 2 = β¦, each with its own held-out, p, revealed genes, and post-hoc MSI/TMB alignment). Read the named code first; don't assume. Airgap stays absolute (opaque IDs, no labels in the search). Only applies to the unsupervised objective (target "none").
Why this design
To residualise against a prior axis we need that axis's per-patient score over the FULL cohort. Rather than re-parse a program_repr string back into a Node (no such parser exists), we STORE the winner's full-cohort score in the run result and reuse it. So:
PART A β engine_v2/pipeline.py
- Store full-cohort winner scores. The pipeline already re-executes the winner on ctx_test for held-out scores (
run_v2_pipeline_streaming, ~lines 417β431). ALSO execute the winner on a FULL-cohort context (all patients, train+test; labels empty for unsup) and store, in the result's"winning"block (the existing nested dict at line ~462, NOT a new "winner" key),full_scores(per-patient, finite-guarded) andfull_sample_ids(str ids aligned to them) β alongside the existing test-onlyholdout_scores/holdout_sample_ids. This is what the next run residualises against. - Residualisation hook. Add an optional param to
run_v2_pipeline_streaming(and run_v2_pipeline), e.g.residualize_scores: pd.DataFrame | None(index = sample id, columns = prior axis scores). When provided, IMMEDIATELY after loading M and BEFORE the train/test split, regress every gene column of M on[intercept, *prior scores](vectorised OLS:resid = M - P @ np.linalg.lstsq(P, M, rcond=None)[0], aligning P to M.index, guarding rank/NaN) and REPLACE M with the residuals (same opaque columns + index). Residualising before the split keeps train and test consistent. (No effect for MSI/TMB β only the unsupervised worker passes it.)
PART B β api/app.py
RunRequest(~line 294) gains an optionalresidualize_against: list[str] | None= prior run_ids in this discovery chain.- In
_worker(~line 417), for an unsupervised run withresidualize_againstset: look up each prior run_id in the in-memory run store, pull its storedrun.result["winning"]["full_scores"]+["full_sample_ids"], assemble a DataFrame aligned by sample id, and pass it to the pipeline asresidualize_scores. Note_json_finitemay have turned non-finite scores intonullβ drop those patients (or treat as NaN) when aligning. Validate the run_ids exist and were unsupervised; ignore the field for MSI/TMB. (In-memory store β a chain is valid within a server session; note this.) - Keep all payloads opaque-ID-only; extend the airgap test to the residualisation path (scores are opaque-derived numbers β no labels, no gene names leak).
PART C β Frontend (web/app/Lab.tsx)
- After an unsupervised run completes, show a "Find next axis β" button. Clicking it POSTs a new run with the SAME params + unsupervised objective and
residualize_against= the run_ids of ALL axes discovered so far in this chain. - Accumulate results into a "Discovered axes" stack rendered below the Result: Axis 1, Axis 2, β¦ Each axis is a compact card reusing the existing verdict + post-hoc rendering β held-out score, permutation p, revealed gene(s), and "aligns with MSI at AUROC β¦ / TMB β¦". So the user reads a ranked list of independent axes the engine found blind, each named by its post-hoc alignment.
- The chain resets when the user starts a fresh run (not "next axis").
Honesty / scope
- This is a discovery HEURISTIC: linear residualisation removes the linear component of each prior axis; deeper non-linear structure may remain. State that in the "?" for the axes stack.
- It does not guarantee MSI ever appears β the value is the ranked, blind, honestly-labelled list of axes (and the chance that a deeper axis turns out to be MSI).
Checkpoint
Run unsupervised β Axis 1 should be KRT5. Hit "Find next axis" β Axis 2 should be a DIFFERENT gene/axis (orthogonal to KRT5), with its own held-out/p and a post-hoc MSI/TMB read. Confirm airgap tests stay green and MSI/TMB runs are unaffected.