File size: 3,710 Bytes
0fff343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""H1 verification β€” known-answer check, NOT blind discovery.

Composes the DSL operators over the NAMED expression matrix (real gene symbols)
with hand-picked MMR and immune gene panels, and reports the four numbers the
H1 program should produce on the usable MSI-H-vs-MSS CRC cohort:

1. medians of `mmr_score` and `immune_score` split by MSI status β€” MSI-H should
   sit LOW on the MMR score and HIGH on the immune score;
2. `Effect(mmr_score, immune_score, adjust={stage, age})` β€” the adjusted partial
   correlation vs the unadjusted pearson on the same rows, and the n used;
3. `Fit(state=(mmr_score, immune_score)) β†’ MSI-H` β€” held-out AUROC and
   balanced accuracy.

This module is biology-aware on purpose. The blind-discovery path in
[`engine`](../engine) will instead consume the airgapped view and is forbidden
from importing these gene lists.
"""

from __future__ import annotations

from dataclasses import dataclass

import pandas as pd

from dsl import Cohort, Effect, EffectResult, Fit, FitResult, Load, Reduce, Select

MMR_GENES: list[str] = ["MLH1", "MSH2", "MSH6", "PMS2"]
IMMUNE_GENES: list[str] = ["CD8A", "GZMA", "PRF1"]

LABEL_COL = "msi_status"
POSITIVE_LABEL = "MSI-H"
NEGATIVE_LABEL = "MSS"


def usable_msi_cohort(cohort: Cohort) -> Cohort:
    """Restrict to MSI-H vs MSS samples with non-missing stage and age."""
    lab = cohort.labels
    clin = cohort.clinical
    mask = (
        lab[LABEL_COL].isin([POSITIVE_LABEL, NEGATIVE_LABEL])
        & (clin["stage"] != "NA")
        & clin["stage"].notna()
        & clin["age"].notna()
    )
    return cohort.restrict(mask)


@dataclass
class H1Result:
    n_used: int
    n_msi_h: int
    n_mss: int

    mmr_score: pd.Series
    immune_score: pd.Series
    msi_status: pd.Series

    mmr_median_msi_h: float
    mmr_median_mss: float
    immune_median_msi_h: float
    immune_median_mss: float

    effect: EffectResult
    fit: FitResult

    @property
    def mmr_separates_correct_direction(self) -> bool:
        return self.mmr_median_msi_h < self.mmr_median_mss

    @property
    def immune_separates_correct_direction(self) -> bool:
        return self.immune_median_msi_h > self.immune_median_mss


def run_h1(cohort: Cohort | None = None) -> H1Result:
    """Run the full H1 program and return the verification numbers."""
    if cohort is None:
        cohort = Load("processed")
    cohort = usable_msi_cohort(cohort)

    M = cohort.expression
    mmr_score = Reduce(Select(M, MMR_GENES), agg="mean")
    immune_score = Reduce(Select(M, IMMUNE_GENES), agg="mean")

    status = cohort.labels[LABEL_COL]
    is_msi_h = status == POSITIVE_LABEL

    mmr_medians = mmr_score.groupby(status).median()
    immune_medians = immune_score.groupby(status).median()

    adjust = pd.DataFrame({
        "stage": cohort.clinical["stage"].astype("category"),
        "age": cohort.clinical["age"].astype(float),
    })
    effect = Effect(mmr_score, immune_score, adjust)

    state = pd.DataFrame({"mmr_score": mmr_score, "immune_score": immune_score})
    fit = Fit(state, is_msi_h.astype(int).values)

    return H1Result(
        n_used=len(cohort.sample_ids),
        n_msi_h=int(is_msi_h.sum()),
        n_mss=int((status == NEGATIVE_LABEL).sum()),
        mmr_score=mmr_score,
        immune_score=immune_score,
        msi_status=status,
        mmr_median_msi_h=float(mmr_medians.get(POSITIVE_LABEL, float("nan"))),
        mmr_median_mss=float(mmr_medians.get(NEGATIVE_LABEL, float("nan"))),
        immune_median_msi_h=float(immune_medians.get(POSITIVE_LABEL, float("nan"))),
        immune_median_mss=float(immune_medians.get(NEGATIVE_LABEL, float("nan"))),
        effect=effect,
        fit=fit,
    )