PANDA / scripts /analysis /107_dingwall_class_deg_count.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
5.12 kB
"""rank dingwall cell types by En1-cKO vs WT DEG count (|log2FC|>1, padj<0.05)."""
from __future__ import annotations
import json
import warnings
from pathlib import Path
import anndata as ad
import numpy as np
import pandas as pd
import scanpy as sc
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))
OUT = ROOT / "discovery/pan_skin/marker"
OUT.mkdir(parents=True, exist_ok=True)
RAW = ROOT / "data/raw/GSE220977_combined.h5ad"
PRED = ROOT / "discovery/pan_skin/marker/dingwall_predictions.csv"
CKO_GSMS = {"GSM6833482", "GSM6833483"}
WT_GSMS = {"GSM6833478", "GSM6833479", "GSM6833480", "GSM6833481"}
MIN_PER_GROUP = 15
LFC_THRESH = 1.0
PADJ_THRESH = 0.05
def main():
print("[load]", flush=True)
a = ad.read_h5ad(RAW)
pred = pd.read_csv(PRED)
pred_map = dict(zip(pred["cell_id"].astype(str), pred["pred_label"]))
a.obs["pred_label"] = pd.Categorical(
[pred_map.get(c, "unknown") for c in a.obs_names])
samp = a.obs["sample"].astype(str)
a.obs["group"] = np.where(samp.isin(list(CKO_GSMS)), "En1-cKO",
np.where(samp.isin(list(WT_GSMS)), "WT", "other"))
a = a[a.obs["group"].isin(["En1-cKO", "WT"])].copy()
print(f"[load] {a.n_obs} cells, {a.n_vars} genes", flush=True)
sc.pp.normalize_total(a, target_sum=1e4)
sc.pp.log1p(a)
rows = []
for cls in sorted(a.obs["pred_label"].astype(str).unique()):
mask = (a.obs["pred_label"].astype(str) == cls)
n_cko = int((mask & (a.obs["group"] == "En1-cKO")).sum())
n_wt = int((mask & (a.obs["group"] == "WT")).sum())
if n_cko < MIN_PER_GROUP or n_wt < MIN_PER_GROUP:
print(f"[skip] {cls}: n_cKO={n_cko} n_WT={n_wt}", flush=True)
continue
sub = a[mask].copy()
sub.obs["group"] = pd.Categorical(sub.obs["group"].values,
categories=["En1-cKO", "WT"])
# reference=WT so positive LFC means up in cKO
try:
sc.tl.rank_genes_groups(sub, "group", reference="WT",
groups=["En1-cKO"], method="wilcoxon",
n_genes=sub.n_vars, use_raw=False,
pts=True)
rgg = sub.uns["rank_genes_groups"]
df = pd.DataFrame({
"gene": [x[0] for x in rgg["names"]],
"lfc": [x[0] for x in rgg["logfoldchanges"]],
"padj": [x[0] for x in rgg["pvals_adj"]],
})
up = int(((df["lfc"] > LFC_THRESH) & (df["padj"] < PADJ_THRESH)).sum())
down = int(((df["lfc"] < -LFC_THRESH) & (df["padj"] < PADJ_THRESH)).sum())
total = up + down
# persist the COMPLETE significant sets. An earlier .head(10) meant
# n_DEG could exceed the stored gene list (HF-placode: 12 vs 10),
# so two of the reported DEGs were never written to any artefact.
top_up = df[(df["lfc"] > LFC_THRESH) & (df["padj"] < PADJ_THRESH)] \
.sort_values("lfc", ascending=False)["gene"].tolist()
top_down = df[(df["lfc"] < -LFC_THRESH) & (df["padj"] < PADJ_THRESH)] \
.sort_values("lfc", ascending=True)["gene"].tolist()
except Exception as e:
print(f"[fail] {cls}: {e}", flush=True)
continue
rows.append({
"class": cls,
"n_cKO": n_cko,
"n_WT": n_wt,
"n_DEG": total,
"n_up": up,
"n_down": down,
"top_up": ";".join(top_up),
"top_down": ";".join(top_down),
})
print(f"[ok] {cls}: n_cKO={n_cko} n_WT={n_wt} DEG={total} "
f"(up={up}, down={down})", flush=True)
df_out = pd.DataFrame(rows).sort_values("n_DEG", ascending=False)
df_out.to_csv(OUT / "107_dingwall_class_deg_count.csv", index=False)
print("\n[rank] classes ordered by DEG count (|LFC|>1 padj<0.05):")
print(df_out[["class", "n_cKO", "n_WT", "n_DEG", "n_up", "n_down"]]
.to_string(index=False))
if not df_out.empty:
winner = df_out.iloc[0]
summary = {
"lfc_threshold": LFC_THRESH, "padj_threshold": PADJ_THRESH,
"n_classes_tested": int(len(df_out)),
"top_class": str(winner["class"]),
"top_n_DEG": int(winner["n_DEG"]),
"top_up": winner["top_up"],
"top_down": winner["top_down"],
"ranking": df_out[["class", "n_DEG"]].to_dict("records"),
}
else:
summary = {"error": "no eligible classes"}
with open(OUT / "107_dingwall_class_deg_count.json", "w") as f:
json.dump(summary, f, indent=2)
print("[done]", flush=True)
if __name__ == "__main__":
main()