PANDA / scripts /analysis /101_primary_eden_derm_scoring.py
bryan7264's picture
Correction pass: gate-matched Dahlin, retracted unsupported claims, complete HF-placode DEG set, restyled figures
141bacd verified
Raw
History Blame Contribute Delete
7.87 kB
"""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()