File size: 7,869 Bytes
141bacd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""primary + secondary EDEN on Dingwall via score_genes against dingwall's own Derm0-11 markers (Data S1C top-30); argmax identity + Fisher cKO depletion."""
from pathlib import Path
import warnings, json, sys, numpy as np, pandas as pd, anndata as ad, scanpy as sc, scipy.sparse as sp
from scipy.stats import fisher_exact
warnings.filterwarnings("ignore"); sc.settings.verbosity = 0

import os as _os
from pathlib import Path as _Path
PANDA_ROOT = _Path(_os.environ.get("PANDA_ROOT", str(_Path(__file__).resolve().parents[2])))
ROOT = Path(str(PANDA_ROOT))
CKO_GSMS = {"GSM6833482", "GSM6833483"}  # CORRECTED: 480/481 are rttaControl (WT), not cKO
WT_GSMS  = {"GSM6833478", "GSM6833479", "GSM6833480", "GSM6833481"}  # CORRECTED: 4 Cre-neg controls per GEO metadata
DERM_MARKERS = ROOT / "data/external_labels/dingwall_supp/biorxiv_media-3.xlsx"
TOP_N = 30  # top N markers per Derm cluster for scoring

# EDEN identity map based on Data S1C + Data S2 CellChat
EDEN_IDENTITY = {
    10: "Secondary_EDEN",
    2: "Primary_EDEN_candidate_1",
    9: "Primary_EDEN_candidate_2",
    3: "EDEN_signalling",
    6: "EDEN_signalling",
}


def load_derm_marker_panels():
    df = pd.read_excel(DERM_MARKERS)
    df = df.sort_values(["cluster", "avg_log2FC"], ascending=[True, False])
    panels = {}
    for cl in sorted(df["cluster"].unique()):
        genes = df[df["cluster"] == cl].head(TOP_N)["gene"].tolist()
        panels[int(cl)] = genes
    return panels


def load_dingwall_v3_dermal():
    raw = ad.read_h5ad(ROOT / "data/raw/GSE220977_combined.h5ad")
    pred = pd.read_csv(ROOT / "discovery/pan_skin/marker/dingwall_predictions.csv")
    pred_map = dict(zip(pred["cell_id"].astype(str), pred["pred_label"]))
    raw.obs["pred_label"] = np.array([pred_map.get(c, "unknown") for c in raw.obs_names.astype(str)])
    raw.obs["genotype"] = np.where(raw.obs["sample"].astype(str).isin(list(CKO_GSMS)), "En1-cKO",
                          np.where(raw.obs["sample"].astype(str).isin(list(WT_GSMS)), "WT", "other"))
    labeled = raw.obs["genotype"].isin(["WT", "En1-cKO"]).values
    raw = raw[labeled].copy()
    dermal_mask = np.isin(raw.obs["pred_label"], ["fibroblast-papillary", "fibroblast-reticular"])
    dermal = raw[dermal_mask].copy()
    return dermal


def score_derm_identities(a, panels):
    for cl, genes in panels.items():
        present = [g for g in genes if g in a.var_names]
        if not present:
            a.obs[f"derm{cl}_score"] = 0.0
            continue
        sc.tl.score_genes(a, gene_list=present, score_name=f"derm{cl}_score",
                          random_state=0, use_raw=False)
    return a


def main():
    print("[eden] loading Data S1C marker panels", flush=True)
    panels = load_derm_marker_panels()
    print(f"[eden] loaded {len(panels)} Derm panels", flush=True)

    print("[eden] loading Dingwall + PANDA-v3 dermal predictions", flush=True)
    dermal = load_dingwall_v3_dermal()
    print(f"[eden] {dermal.n_obs} dermal-fibroblast cells", flush=True)

    print("[eden] normalise + log1p", flush=True)
    sc.pp.normalize_total(dermal, target_sum=1e4); sc.pp.log1p(dermal)

    print("[eden] scoring cells on all 12 Derm identity panels", flush=True)
    dermal = score_derm_identities(dermal, panels)

    score_cols = [f"derm{cl}_score" for cl in sorted(panels.keys())]
    score_mat = dermal.obs[score_cols].values
    argmax = np.argmax(score_mat, axis=1)
    derm_ids = [int(score_cols[i].replace("derm", "").replace("_score", "")) for i in argmax]
    dermal.obs["derm_identity"] = derm_ids
    dermal.obs["max_derm_score"] = score_mat.max(axis=1)

    n_wt_tot = int((dermal.obs["genotype"] == "WT").sum())
    n_cko_tot = int((dermal.obs["genotype"] == "En1-cKO").sum())
    baseline_cko_frac = n_cko_tot / (n_wt_tot + n_cko_tot)
    print(f"[eden] baseline WT={n_wt_tot} cKO={n_cko_tot} (baseline cKO={baseline_cko_frac:.3f})", flush=True)

    rows = []
    for cl in sorted(panels.keys()):
        sub = dermal[dermal.obs["derm_identity"] == cl]
        n_wt = int((sub.obs["genotype"] == "WT").sum())
        n_cko = int((sub.obs["genotype"] == "En1-cKO").sum())
        if n_wt + n_cko == 0:
            continue
        cko_frac = n_cko / (n_wt + n_cko) if (n_wt + n_cko) > 0 else 0
        n_wt_else = n_wt_tot - n_wt
        n_cko_else = n_cko_tot - n_cko
        try:
            odds, p_fisher = fisher_exact([[n_wt, n_wt_else], [n_cko, n_cko_else]],
                                          alternative="two-sided")
        except ValueError:
            odds, p_fisher = 1.0, 1.0
        rows.append({
            "derm_id": cl,
            "identity": EDEN_IDENTITY.get(cl, "other"),
            "n_cells": n_wt + n_cko,
            "n_WT": n_wt, "n_cKO": n_cko,
            "cko_frac": cko_frac,
            "baseline_cko_frac": baseline_cko_frac,
            "cko_delta": cko_frac - baseline_cko_frac,
            "fisher_p": float(p_fisher),
            "odds_ratio": float(odds) if not np.isnan(odds) else None,
            "depletion_direction": "cKO-depleted" if cko_frac < baseline_cko_frac
                                   else "cKO-enriched",
            "top10_markers_dingwall": ", ".join(panels[cl][:10]),
        })
    df = pd.DataFrame(rows).sort_values("cko_delta")

    out = ROOT / "discovery/pan_skin/marker"
    out.mkdir(parents=True, exist_ok=True)
    df.to_csv(out / "101_derm_subcluster_scores.csv", index=False)

    secondary_row = df[df["derm_id"] == 10].iloc[0].to_dict() if 10 in df["derm_id"].values else None
    primary_1_row = df[df["derm_id"] == 2].iloc[0].to_dict() if 2 in df["derm_id"].values else None
    primary_2_row = df[df["derm_id"] == 9].iloc[0].to_dict() if 9 in df["derm_id"].values else None

    summary = {
        "target": "Dingwall_GSE220977",
        "method": "Score PANDA-v3 dermal-fibroblast predictions on Dingwall's own Derm0-11 "
                  "marker panels (Data S1C top-30 genes each); argmax identity per cell; "
                  "Fisher-exact cKO depletion per Derm identity",
        "baseline_cko_frac": baseline_cko_frac,
        "n_dermal_cells_total": int(dermal.n_obs),
        "secondary_eden_Derm10": secondary_row,
        "primary_eden_Derm2": primary_1_row,
        "primary_eden_Derm9": primary_2_row,
        "all_derm_summary": df.to_dict("records"),
    }
    (out / "101_derm_identity_summary.json").write_text(json.dumps(summary, indent=2, default=str))

    print(f"\n[eden] wrote {out}/101_derm_*", flush=True)
    print(f"\n=== SECONDARY EDEN (Derm10) ===", flush=True)
    if secondary_row:
        print(f"  n={secondary_row['n_cells']} (WT {secondary_row['n_WT']} / cKO {secondary_row['n_cKO']}), "
              f"cko_frac={secondary_row['cko_frac']:.3f} vs baseline {baseline_cko_frac:.3f}", flush=True)
        print(f"  {secondary_row['depletion_direction']}, Fisher p={secondary_row['fisher_p']:.2e}", flush=True)

    print(f"\n=== PRIMARY EDEN candidate 1 (Derm2) ===", flush=True)
    if primary_1_row:
        print(f"  n={primary_1_row['n_cells']} (WT {primary_1_row['n_WT']} / cKO {primary_1_row['n_cKO']}), "
              f"cko_frac={primary_1_row['cko_frac']:.3f}", flush=True)
        print(f"  {primary_1_row['depletion_direction']}, Fisher p={primary_1_row['fisher_p']:.2e}", flush=True)

    print(f"\n=== PRIMARY EDEN candidate 2 (Derm9) ===", flush=True)
    if primary_2_row:
        print(f"  n={primary_2_row['n_cells']} (WT {primary_2_row['n_WT']} / cKO {primary_2_row['n_cKO']}), "
              f"cko_frac={primary_2_row['cko_frac']:.3f}", flush=True)
        print(f"  {primary_2_row['depletion_direction']}, Fisher p={primary_2_row['fisher_p']:.2e}", flush=True)


if __name__ == "__main__":
    main()