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()
|