File size: 11,430 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""primary EDEN discovery on Dingwall: PANDA-v3 dermal-fibro subset, Leiden res=1.5, wilcoxon markers + Fisher cKO depletion + module scoring."""
from pathlib import Path
import warnings, json, sys, pickle, numpy as np, pandas as pd, anndata as ad, scanpy as sc, scipy.sparse as sp, torch, torch.nn.functional as F
from scipy.stats import fisher_exact, mannwhitneyu
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])))
sys.path.insert(0, str(PANDA_ROOT))
from panda import PANDAEncoder

ROOT = Path(str(PANDA_ROOT))
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

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

# secondary EDEN definition per Dingwall 2024
SECONDARY_EDEN_PANEL = ["S100a4", "Tnc", "Pdgfra"]

# En1-responsive eccrine program (from restored 57_pathway_analysis.py, En1 removed)
SWEAT_GLAND_PANEL_ENSMINUSEN1 = ["Foxi3", "Foxa1", "Krt8", "Krt18", "Krt19", "Muc5b", "Aqp5"]
# Eda pathway
EDA_PATHWAY_PANEL = ["Eda", "Edar", "Edaradd", "Nfkb1", "Nfkb2", "Rela"]


def load_dingwall_with_v3_predictions():
    print("[eden] loading Dingwall raw + v3 predictions", flush=True)
    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()
    print(f"[eden] {dermal.n_obs} dermal-fibroblast cells for sub-clustering", flush=True)
    return dermal


def subcluster_dermal(dermal, resolution=1.5):
    print(f"[eden] preprocessing + PCA (Leiden resolution={resolution})", flush=True)
    sc.pp.normalize_total(dermal, target_sum=1e4); sc.pp.log1p(dermal)
    sc.pp.highly_variable_genes(dermal, n_top_genes=3000, flavor="seurat_v3",
                                inplace=True, batch_key=None)
    dermal_hvg = dermal[:, dermal.var["highly_variable"]].copy() if "highly_variable" in dermal.var else dermal
    sc.pp.scale(dermal_hvg, max_value=10)
    sc.tl.pca(dermal_hvg, n_comps=30, random_state=0)
    sc.pp.neighbors(dermal_hvg, n_neighbors=20, use_rep="X_pca")
    sc.tl.leiden(dermal_hvg, resolution=resolution, random_state=0)
    dermal.obs["leiden"] = dermal_hvg.obs["leiden"].astype(str)
    print(f"[eden] {dermal.obs['leiden'].nunique()} sub-clusters found", flush=True)
    return dermal


def score_modules(dermal):
    for name, genes in [("secondary_eden", SECONDARY_EDEN_PANEL),
                         ("sweat_gland", SWEAT_GLAND_PANEL_ENSMINUSEN1),
                         ("eda_pathway", EDA_PATHWAY_PANEL)]:
        present = [g for g in genes if g in dermal.var_names]
        if not present:
            dermal.obs[f"score_{name}"] = 0.0
            continue
        sc.tl.score_genes(dermal, gene_list=present, score_name=f"score_{name}",
                          random_state=0, use_raw=False)
    return dermal


def per_subcluster_analysis(dermal):
    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 frac = {baseline_cko_frac:.3f})", flush=True)

    sc.tl.rank_genes_groups(dermal, "leiden", method="wilcoxon", n_genes=30, use_raw=False)

    rows = []
    for cls in sorted(dermal.obs["leiden"].unique(), key=int):
        sub = dermal[dermal.obs["leiden"] == cls]
        n_wt = int((sub.obs["genotype"] == "WT").sum())
        n_cko = int((sub.obs["genotype"] == "En1-cKO").sum())
        if n_wt + n_cko < 20:
            continue
        cko_frac = n_cko / (n_wt + n_cko)

        # fisher 2x2: (n_wt_in, n_wt_out) vs (n_cko_in, n_cko_out) — cluster depletion in cKO
        n_wt_elsewhere = n_wt_tot - n_wt
        n_cko_elsewhere = n_cko_tot - n_cko
        odds, p_fisher = fisher_exact([[n_wt, n_wt_elsewhere], [n_cko, n_cko_elsewhere]],
                                      alternative="two-sided")
        depletion_direction = "cKO-depleted" if cko_frac < baseline_cko_frac else "cKO-enriched"

        s2eden_mean_wt = float(sub[sub.obs["genotype"] == "WT"].obs["score_secondary_eden"].mean()) if n_wt > 0 else 0.0
        s2eden_mean_cko = float(sub[sub.obs["genotype"] == "En1-cKO"].obs["score_secondary_eden"].mean()) if n_cko > 0 else 0.0
        sg_mean_wt = float(sub[sub.obs["genotype"] == "WT"].obs["score_sweat_gland"].mean()) if n_wt > 0 else 0.0
        sg_mean_cko = float(sub[sub.obs["genotype"] == "En1-cKO"].obs["score_sweat_gland"].mean()) if n_cko > 0 else 0.0
        eda_mean_wt = float(sub[sub.obs["genotype"] == "WT"].obs["score_eda_pathway"].mean()) if n_wt > 0 else 0.0
        eda_mean_cko = float(sub[sub.obs["genotype"] == "En1-cKO"].obs["score_eda_pathway"].mean()) if n_cko > 0 else 0.0

        genes_list = list(dermal.uns["rank_genes_groups"]["names"][cls][:10])
        lfc_list = list(dermal.uns["rank_genes_groups"]["logfoldchanges"][cls][:10])
        top_markers = ", ".join([f"{g}({lfc:+.1f})" for g, lfc in zip(genes_list, lfc_list)])

        rows.append({
            "leiden_cluster": cls,
            "n_cells": n_wt + n_cko,
            "n_WT": n_wt, "n_cKO": n_cko,
            "cko_frac": cko_frac,
            "baseline_cko_frac": baseline_cko_frac,
            "depletion_direction": depletion_direction,
            "fisher_p_two_sided": p_fisher,
            "odds_ratio": odds,
            "score_secondary_eden_WT_mean": s2eden_mean_wt,
            "score_secondary_eden_cKO_mean": s2eden_mean_cko,
            "score_sweat_gland_WT_mean": sg_mean_wt,
            "score_sweat_gland_cKO_mean": sg_mean_cko,
            "score_eda_pathway_WT_mean": eda_mean_wt,
            "score_eda_pathway_cKO_mean": eda_mean_cko,
            "top_wilcoxon_markers": top_markers,
        })
    return pd.DataFrame(rows), baseline_cko_frac


def call_primary_and_secondary(df, baseline_cko_frac):
    # secondary EDEN: highest score_secondary_eden_WT_mean AND cKO-depleted (Fisher p<0.05)
    df_wt_ordered = df.sort_values("score_secondary_eden_WT_mean", ascending=False)
    secondary_candidates = df_wt_ordered[
        (df_wt_ordered["depletion_direction"] == "cKO-depleted") &
        (df_wt_ordered["fisher_p_two_sided"] < 0.05)
    ]
    secondary = secondary_candidates.iloc[0]["leiden_cluster"] if len(secondary_candidates) > 0 else None

    # primary EDEN: cKO-depleted + LOW secondary_eden (S100a4-neg) + HIGH Eda_pathway (En1-responsive)
    df_ranked = df.copy()
    df_ranked["depletion_score"] = -np.log10(df_ranked["fisher_p_two_sided"].clip(lower=1e-300)) * \
                                   (df_ranked["cko_frac"] < baseline_cko_frac).astype(int)
    primary_score = df_ranked["depletion_score"] * \
                    (1.0 / (df_ranked["score_secondary_eden_WT_mean"].abs() + 0.01)) * \
                    (df_ranked["score_eda_pathway_WT_mean"] + 0.1)
    df_ranked["primary_eden_composite_score"] = primary_score
    df_ranked = df_ranked.sort_values("primary_eden_composite_score", ascending=False)
    primary_candidates = df_ranked[
        (df_ranked["depletion_direction"] == "cKO-depleted") &
        (df_ranked["fisher_p_two_sided"] < 0.05) &
        (df_ranked["leiden_cluster"] != secondary)
    ].head(3)
    return secondary, primary_candidates, df_ranked


def main():
    dermal = load_dingwall_with_v3_predictions()
    dermal = subcluster_dermal(dermal, resolution=1.5)
    dermal = score_modules(dermal)
    df, baseline_cko = per_subcluster_analysis(dermal)
    secondary, primary_cands, df_ranked = call_primary_and_secondary(df, baseline_cko)

    out = ROOT / "discovery/pan_skin/marker"
    out.mkdir(parents=True, exist_ok=True)
    df_ranked.to_csv(out / "100_primary_eden_discovery.csv", index=False)

    summary = {
        "target": "Dingwall_GSE220977",
        "hypothesis": "Primary EDEN precedes Secondary EDEN (S100a4+/Tnc+ cluster 20/Derm10) in dermal lineage",
        "method": "PANDA-v3 predicts dermal-fibroblast compartment; Leiden sub-clustering "
                  "(resolution=1.5) resolves substructure; Wilcoxon markers + Fisher-exact "
                  "cKO enrichment + module scoring (Secondary_EDEN, Sweat_gland, Eda_pathway) "
                  "rank sub-clusters for Primary EDEN candidacy",
        "baseline_cko_frac": float(baseline_cko),
        "n_subclusters": int(len(df)),
        "secondary_eden_call": {
            "leiden_cluster": str(secondary),
            "criteria": "highest S100a4+Tnc+Pdgfra score AND Fisher cKO-depleted p<0.05",
            "row": df[df["leiden_cluster"] == secondary].iloc[0].to_dict() if secondary else None,
        },
        "primary_eden_candidates_top3": primary_cands[[
            "leiden_cluster", "n_cells", "n_WT", "n_cKO", "cko_frac",
            "fisher_p_two_sided", "score_secondary_eden_WT_mean",
            "score_sweat_gland_WT_mean", "score_eda_pathway_WT_mean",
            "top_wilcoxon_markers", "primary_eden_composite_score",
        ]].to_dict("records") if len(primary_cands) > 0 else [],
    }
    (out / "100_primary_eden_summary.json").write_text(json.dumps(summary, indent=2, default=str))

    print(f"\n[eden] wrote {out}/100_primary_eden_*", flush=True)
    print(f"\n=== SECONDARY EDEN CALL ===", flush=True)
    print(f"  leiden cluster: {secondary}", flush=True)
    if secondary:
        row = df[df["leiden_cluster"] == secondary].iloc[0]
        print(f"  n={row['n_cells']} (WT {row['n_WT']} / cKO {row['n_cKO']}), "
              f"cko_frac={row['cko_frac']:.3f} vs baseline {baseline_cko:.3f}", flush=True)
        print(f"  Fisher p={row['fisher_p_two_sided']:.2e}, "
              f"score_secondary_eden WT={row['score_secondary_eden_WT_mean']:.3f}", flush=True)

    print(f"\n=== PRIMARY EDEN CANDIDATES (top 3) ===", flush=True)
    for _, row in primary_cands.iterrows():
        print(f"  leiden {row['leiden_cluster']} n={row['n_cells']} (WT {row['n_WT']} / cKO {row['n_cKO']}), "
              f"cko_frac={row['cko_frac']:.3f}, Fisher p={row['fisher_p_two_sided']:.2e}", flush=True)
        print(f"    S2EDEN_WT={row['score_secondary_eden_WT_mean']:.3f}, "
              f"Sweat_WT={row['score_sweat_gland_WT_mean']:.3f}, "
              f"Eda_WT={row['score_eda_pathway_WT_mean']:.3f}", flush=True)
        print(f"    top markers: {row['top_wilcoxon_markers']}", flush=True)


if __name__ == "__main__":
    main()