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