Spaces:
Sleeping
Sleeping
KevinIsInCoding
feat(landscape): bring experimental ALS therapy landscape to main (#19)
2e5367f unverified | #!/usr/bin/env python3 | |
| """ | |
| Evaluate the mechanism classifier against the hand-labeled gold set. | |
| Reports multi-label precision / recall / F1 (micro), exact-set-match rate, and abstention rate, | |
| plus a per-therapy breakdown so misclassifications are visible. Physician's rule — misleading is | |
| worse than missing — so we optimize for PRECISION: a wrong label counts against us; an abstention | |
| (no label) does not count as a precision error, only against recall. | |
| Usage: | |
| uv run python scripts/eval_landscape.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from rich.console import Console | |
| from config import LANDSCAPE_PATH, THERAPY_GOLD_PATH | |
| console = Console() | |
| def _predicted_index(landscape: dict) -> dict[str, set]: | |
| """name/alias (lower) -> set of predicted mechanism classes. Abstained therapies map to set().""" | |
| idx: dict[str, set] = {} | |
| seen = {} | |
| def add(th: dict, mech_classes: set): | |
| for nm in [th["name"], *th.get("aliases", [])]: | |
| key = nm.lower() | |
| # a therapy appears in multiple class buckets; union its mechanism set once | |
| seen.setdefault(key, set()).update(mech_classes) | |
| idx[key] = seen[key] | |
| for c in landscape.get("classifications", []): | |
| for th in c["therapies"]: | |
| add(th, {m["class"] for m in th.get("mechanisms", [])}) | |
| for th in landscape.get("unclassified", []): | |
| add(th, set()) | |
| return idx | |
| def main() -> None: | |
| if not LANDSCAPE_PATH.exists(): | |
| console.print("[red]No landscape.json — run scripts/build_landscape.py first[/red]"); sys.exit(1) | |
| landscape = json.loads(LANDSCAPE_PATH.read_text()) | |
| gold = json.loads(THERAPY_GOLD_PATH.read_text())["gold"] | |
| idx = _predicted_index(landscape) | |
| tp = fp = fn = 0 | |
| exact = matched = abstained = missing = 0 | |
| rows = [] | |
| for g in gold: | |
| names = [g["therapy"], *g.get("aliases", [])] | |
| pred = None | |
| for nm in names: | |
| if nm.lower() in idx: | |
| pred = idx[nm.lower()]; break | |
| gset = set(g["mechanisms"]) | |
| if pred is None: | |
| missing += 1 | |
| rows.append(("?", g["therapy"], "NOT IN LANDSCAPE", gset, set())) | |
| continue | |
| matched += 1 | |
| if not pred: | |
| abstained += 1 | |
| inter = pred & gset | |
| tp += len(inter); fp += len(pred - gset); fn += len(gset - pred) | |
| if pred == gset: | |
| exact += 1 | |
| mark = "✓" if pred == gset else ("~" if inter else "✗") | |
| rows.append((mark, g["therapy"], "", gset, pred)) | |
| prec = tp / (tp + fp) if (tp + fp) else 0.0 | |
| rec = tp / (tp + fn) if (tp + fn) else 0.0 | |
| f1 = 2 * prec * rec / (prec + rec) if (prec + rec) else 0.0 | |
| console.print("\n[bold]Per-therapy (✓ exact, ~ partial, ✗ wrong):[/bold]") | |
| for mark, name, note, gset, pred in sorted(rows, key=lambda r: r[0]): | |
| color = {"✓": "green", "~": "yellow", "✗": "red", "?": "dim"}.get(mark, "white") | |
| console.print(f" [{color}]{mark}[/{color}] {name:22s} gold={sorted(gset)} pred={sorted(pred)} {note}") | |
| console.print(f"\n[bold]Matched {matched}/{len(gold)} gold therapies[/bold] " | |
| f"({missing} not in landscape, {abstained} abstained)") | |
| console.print(f" Micro precision: [bold]{prec:.2f}[/bold] recall: {rec:.2f} F1: {f1:.2f}") | |
| console.print(f" Exact-set match: {exact}/{matched}") | |
| # hard assertions for the flagged cases | |
| def pred_of(name): | |
| return idx.get(name.lower()) | |
| console.print("\n[bold]Flagged-case checks:[/bold]") | |
| cnm = pred_of("CNM-Au8") or set() | |
| ok_cnm = "Neuroinflammation" not in cnm and ({"Mitochondrial dysfunction", "Oxidative stress"} & cnm) | |
| console.print(f" CNM-Au8 not Neuroinflammation & has bioenergetic/oxidative: " | |
| f"[{'green' if ok_cnm else 'red'}]{bool(ok_cnm)}[/] (pred={sorted(cnm)})") | |
| if __name__ == "__main__": | |
| main() | |