| """Cell-line titration: vary the number of non-holdout training cell lines. |
| |
| In-memory subsetting — no predefined split files are needed. For each holdout, |
| we permute the non-holdout cells with a fixed seed and take nested prefixes of |
| increasing size. Holdout-cell training rows (the fewshot rows) are always kept |
| so test-time cell lookup still resolves. |
| |
| Usage: |
| uv run python scripts/run_titrations_cells.py [--wandb] |
| """ |
| import json |
| import os |
| import sys |
|
|
|
|
| import numpy as np |
| from rhaister import train |
| from rhaister.prepare_combined import prepare_all |
|
|
| HOLDOUTS = [5, 6, 7, 8, 9] |
| CELL_LEVELS = [1, 5, 10, 20, 45] |
| SEED = 42 |
| METRICS = [ |
| "pdex_static/pearson_delta_mean", |
| "pdex_static/auprc_p05", |
| "state/pearson_delta_mean", |
| "state/de_overlap_mean", |
| "state/de_spearman_sig", |
| "state/pr_auc_mean", |
| "state/spearman_lfc_sig_mean", |
| "state/discrimination_mean", |
| ] |
| TRAIN_ARRAYS = ( |
| "Y_train", "P_train", "D_train", "F_train", "R_train", |
| "train_cells", "train_treatments", |
| ) |
| OUTPUT = "titration_cells_results.json" |
|
|
|
|
| def _subsample_cells(data, selected_cells): |
| """Restrict training rows to selected_cells ∪ holdout_cells, adding whole |
| cells if needed to cover every test treatment. |
| |
| Two invariants to respect: |
| (a) local_treat_map in train.py:45 is built from training treatments, so |
| every test treatment must appear in training → avoids KeyError. |
| (b) _compute_regression intersects treatment sets across non-holdout cells |
| (train.py:307-311); the intersection must be non-empty → each |
| non-holdout cell must contribute its full treatment inventory. |
| |
| We enforce (a) by greedily adding non-selected cells (in full) whose |
| treatment inventory contains missing test treatments. (b) then holds |
| because every non-holdout cell in the final subset retains all its rows. |
| Added cells count toward the effective training-cell total but not toward |
| the requested level L. |
| """ |
| train_cells = np.asarray(data["train_cells"]) |
| train_tr = np.asarray(data["train_treatments"]) |
| holdout_set = set(data["test_cells"]) |
| test_tr_set = set(np.asarray(data["test_treatments"])) |
|
|
| keep_cells = set(selected_cells) | holdout_set |
| base_mask = np.array([c in keep_cells for c in train_cells]) |
| covered = set(train_tr[base_mask]) |
| missing = test_tr_set - covered |
|
|
| candidates = sorted(set(train_cells) - holdout_set - set(selected_cells)) |
| extra_cells = [] |
| for t in sorted(missing): |
| if t in covered: |
| continue |
| for c in candidates: |
| if c in extra_cells: |
| continue |
| c_trs = set(train_tr[train_cells == c]) |
| if t in c_trs: |
| extra_cells.append(c) |
| covered |= c_trs |
| break |
|
|
| if extra_cells: |
| print(f" coverage augmentation: +{len(extra_cells)} whole cells for {len(missing)} missing test treatments") |
|
|
| final_keep = keep_cells | set(extra_cells) |
| final_mask = np.array([c in final_keep for c in train_cells]) |
|
|
| out = dict(data) |
| for k in TRAIN_ARRAYS: |
| out[k] = np.asarray(data[k])[final_mask] |
| return out |
|
|
|
|
| def main(): |
| use_wandb = "--wandb" in sys.argv |
| if use_wandb: |
| import wandb |
|
|
| results = {m: {str(h): {} for h in HOLDOUTS} for m in METRICS} |
| actual_levels = {str(h): {} for h in HOLDOUTS} |
|
|
| for h in HOLDOUTS: |
| split_name = f"tahoe_{h}_holdout" |
| print(f"\n### Holdout {h}: {split_name} ###") |
| |
| probe = prepare_all(split_name) |
| holdout_cells = set(probe["test_cells"]) |
| non_holdout = sorted(set(probe["train_cells"]) - holdout_cells) |
| n_total = len(non_holdout) |
| shuffled = list(np.random.RandomState(SEED).permutation(non_holdout)) |
| del probe |
| print(f" {n_total} non-holdout cells available (+{len(holdout_cells)} holdout kept)") |
|
|
| for L in CELL_LEVELS: |
| L_actual = min(L, n_total) |
| selected = shuffled[:L_actual] |
| |
| |
| data = prepare_all(split_name) |
| sub_data = _subsample_cells(data, selected) |
| n_train = len(sub_data["train_cells"]) |
| print(f"\n{'='*60}\nh={h} L={L} (actual={L_actual}, train_rows={n_train})\n{'='*60}") |
|
|
| metrics = train.train_and_evaluate( |
| data=sub_data, log=False, compute_discrimination=True, |
| ) |
|
|
| row = {m: float(metrics[m]) for m in METRICS} |
| for m, v in row.items(): |
| results[m][str(h)][str(L)] = v |
| actual_levels[str(h)][str(L)] = L_actual |
| print(f" h={h} L={L:>3} " + " ".join(f"{m}={row[m]:.4f}" for m in METRICS)) |
|
|
| if use_wandb: |
| wandb.init( |
| project="perturbation-eval", |
| job_type="rhaister_titration_cells", |
| name=f"titration_cells_h{h}_L{L}", |
| config={"holdout": h, "level": L, "level_actual": L_actual, "axis": "cells", "seed": SEED}, |
| reinit=True, |
| ) |
| wandb.log(row) |
| wandb.finish() |
|
|
| out = { |
| "experiment": "cell_titration", |
| "description": ( |
| "Cell-line titration by in-memory subsetting of non-holdout " |
| "training cells (seed 42, nested prefixes). Holdout cells' " |
| "training rows are always kept. 'level' is the requested number " |
| "of non-holdout cells; 'actual_levels' records what was used " |
| "after clamping to the split's available count." |
| ), |
| "seed": SEED, |
| "holdouts": HOLDOUTS, |
| "levels": CELL_LEVELS, |
| "actual_levels": actual_levels, |
| "metrics": results, |
| } |
| with open(OUTPUT, "w") as f: |
| json.dump(out, f, indent=2) |
| print(f"\nwrote {OUTPUT}") |
|
|
| print(f"\n{'='*60}\nSUMMARY\n{'='*60}") |
| for m in METRICS: |
| print(f"\n{m}") |
| header = " h \\ L " + " ".join(f"{L:>8}" for L in CELL_LEVELS) |
| print(header) |
| for h in HOLDOUTS: |
| vals = " ".join(f"{results[m][str(h)].get(str(L), float('nan')):>8.4f}" for L in CELL_LEVELS) |
| print(f" {h} {vals}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|