"""en1-cKO vs WT contrast on aldrich predictions.""" from __future__ import annotations from pathlib import Path import warnings warnings.filterwarnings("ignore") import numpy as np import pandas as pd import anndata as ad import scanpy as sc from scipy import stats import os as _os from pathlib import Path as _Path PANDA_ROOT = _Path(_os.environ.get("PANDA_ROOT", str(_Path(__file__).resolve().parents[2]))) TARGET = Path(str(PANDA_ROOT / "data/processed/skin/adata_processed.h5ad")) PROJ = Path(str(PANDA_ROOT / "discovery/pan_skin/marker/50_aldrich_projections.h5ad")) NOVEL = Path(str(PANDA_ROOT / "discovery/pan_skin/marker/51_aldrich_novel_annotation.csv")) OUT = Path(str(PANDA_ROOT / "discovery/pan_skin/marker")) def main(): a = ad.read_h5ad(TARGET) p = ad.read_h5ad(PROJ) for c in ["pred_label", "pred_conf", "abstain", "pred_bbse_label"]: if c in p.obs.columns: a.obs[c] = p.obs[c].values if NOVEL.exists(): nov = pd.read_csv(NOVEL, index_col=0) a.obs["novel_cluster"] = nov.reindex(a.obs_names)["novel_cluster"].values print(f"[cko] target: {a.shape}, genotype: {a.obs['genotype'].value_counts().to_dict()}", flush=True) rows = [] for cls in a.obs["pred_bbse_label"].unique(): mask = a.obs["pred_bbse_label"] == cls n_cko = int((mask & (a.obs["genotype"] == "En1-cKO")).sum()) n_wt = int((mask & (a.obs["genotype"] == "WT")).sum()) total_cko = (a.obs["genotype"] == "En1-cKO").sum() total_wt = (a.obs["genotype"] == "WT").sum() contingency = np.array([[n_cko, total_cko - n_cko], [n_wt, total_wt - n_wt]]) odds, p_val = stats.fisher_exact(contingency) f_cko = (n_cko + 1) / (total_cko + 2) f_wt = (n_wt + 1) / (total_wt + 2) log2_fc = np.log2(f_cko / f_wt) rows.append({ "class": cls, "n_En1cKO": n_cko, "n_WT": n_wt, "pct_En1cKO": round(100 * n_cko / total_cko, 2), "pct_WT": round(100 * n_wt / total_wt, 2), "log2_fold_enrich_cKO_vs_WT": round(log2_fc, 3), "fisher_pvalue": p_val, }) df = pd.DataFrame(rows).sort_values("log2_fold_enrich_cKO_vs_WT") print("\n[cko] class enrichment (cKO vs WT):") print(df.to_string(index=False)) df.to_csv(OUT / "53_en1_cko_class_enrichment.csv", index=False) de_rows = [] for cls in sorted(a.obs["pred_bbse_label"].unique()): cls_mask = a.obs["pred_bbse_label"] == cls if cls_mask.sum() < 50: continue sub = a[cls_mask].copy() vc = sub.obs["genotype"].value_counts() if not {"En1-cKO", "WT"}.issubset(vc.index) or vc.min() < 15: continue try: sc.tl.rank_genes_groups(sub, "genotype", method="wilcoxon", n_genes=40, use_raw=False) for grp in ["En1-cKO", "WT"]: if grp not in sub.uns["rank_genes_groups"]["names"].dtype.names: continue names = list(sub.uns["rank_genes_groups"]["names"][grp][:15]) lfcs = list(sub.uns["rank_genes_groups"]["logfoldchanges"][grp][:15]) for g, lf in zip(names, lfcs): de_rows.append({ "class": cls, "up_in": grp, "gene": g, "logfc": round(float(lf), 3), }) except Exception as exc: print(f"[cko] DE failed for {cls}: {exc}") continue de_df = pd.DataFrame(de_rows) de_df.to_csv(OUT / "53_en1_cko_wilcoxon_within_class.csv", index=False) md = ["# Aldrich En1-cKO vs WT contrast on PANDA-MLP predictions\n", f"Total cells: {a.n_obs:,} ({int((a.obs['genotype']=='En1-cKO').sum())} En1-cKO, " f"{int((a.obs['genotype']=='WT').sum())} WT).\n", "## Class-level cKO/WT enrichment (BBSE-corrected predictions)\n", df.to_markdown(index=False), "", "The direction of `log2_fold_enrich_cKO_vs_WT` indicates whether a class is over-represented", "in En1-cKO (positive) or WT (negative). Fisher exact p-value tests significance vs the", "background genotype ratio (~40% cKO / 60% WT).\n", "## Per-class Wilcoxon DE (En1-cKO vs WT within each class)\n", "Top genes differentially expressed BETWEEN genotypes WITHIN a predicted class. Genes up in", "cKO reveal En1-loss-responsive programs specific to that cell type; genes up in WT are the", "opposite.\n", ] if len(de_df): for cls in sorted(de_df["class"].unique()): md.append(f"\n### {cls}\n") for grp in ["En1-cKO", "WT"]: sub = de_df[(de_df["class"] == cls) & (de_df["up_in"] == grp)] if not len(sub): continue md.append(f"**Up in {grp}**: " + ", ".join(sub["gene"].tolist())) md += ["", "## Novel population x genotype cross-tab\n"] if "novel_cluster" in a.obs.columns: xt = pd.crosstab(a.obs["novel_cluster"], a.obs["genotype"]) md.append(xt.to_markdown()) (OUT / "53_en1_cko_contrast.md").write_text("\n".join(md)) print(f"[cko] wrote {OUT}/53_en1_cko_contrast.md") if __name__ == "__main__": main()