| """cluster the top-500 attributed genes by cross-class attribution correlation."""
|
| from __future__ import annotations
|
| from pathlib import Path
|
| import warnings, json, pickle, sys, numpy as np, pandas as pd
|
| warnings.filterwarnings("ignore")
|
| from scipy.cluster.hierarchy import linkage, fcluster
|
| from scipy.spatial.distance import squareform
|
|
|
| import os as _os
|
| from pathlib import Path as _Path
|
| PANDA_ROOT = _Path(_os.environ.get("PANDA_ROOT", str(_Path(__file__).resolve().parents[2])))
|
| CORP_ROOT = Path(str(PANDA_ROOT / "data/corpus"))
|
| OUT = Path(str(PANDA_ROOT / "discovery")); OUT.mkdir(exist_ok=True)
|
| CKPT_ROOT = Path(str(PANDA_ROOT / "checkpoints"))
|
|
|
| TOP_GENES = 500
|
| N_MODULES = 15
|
|
|
|
|
| def main(sys):
|
| print(f"\n===== {sys} =====", flush=True)
|
| stats = np.load(CORP_ROOT / sys / "harmonized/corpus_stats.npz", allow_pickle=True)
|
| hvgs = [str(g) for g in stats["shared_hvgs"]]
|
| import torch
|
| ck = torch.load(CKPT_ROOT / sys / "marker" / "panda_final.pt", map_location="cpu", weights_only=False)
|
| classes = ck["classes"]
|
|
|
| A = np.load(OUT / f"80_{sys}_gene_attribution_full.npy")
|
| print(f"[load] A shape {A.shape}, classes={classes}", flush=True)
|
|
|
| gene_score = np.abs(A).sum(axis=0)
|
| top_idx = np.argsort(-gene_score)[:TOP_GENES]
|
| A_top = A[:, top_idx]
|
| top_genes = [hvgs[i] for i in top_idx]
|
|
|
| Xn = A_top - A_top.mean(axis=0, keepdims=True)
|
| Xn = Xn / (Xn.std(axis=0, keepdims=True) + 1e-8)
|
| C = np.corrcoef(Xn.T)
|
| print(f"[corr] gene-gene C shape {C.shape}, diag mean={np.diag(C).mean():.3f}", flush=True)
|
|
|
| D = 1 - C
|
| np.fill_diagonal(D, 0)
|
| D = np.clip(D, 0, 2)
|
| Z = linkage(squareform(D, checks=False), method="average")
|
| labels = fcluster(Z, t=N_MODULES, criterion="maxclust")
|
|
|
| rows = []
|
| for mod in sorted(set(labels)):
|
| members = np.where(labels == mod)[0]
|
| if len(members) < 3: continue
|
| member_genes = [top_genes[i] for i in members]
|
| mod_att = A_top[:, members].mean(axis=1)
|
| dom_ci = int(np.argmax(mod_att))
|
| rows.append({
|
| "module_id": int(mod),
|
| "size": int(len(members)),
|
| "dominant_class": classes[dom_ci],
|
| "dom_class_mean_att": float(mod_att[dom_ci]),
|
| "member_genes": ",".join(member_genes[:30]),
|
| "n_shown": min(30, len(member_genes)),
|
| })
|
|
|
| df = pd.DataFrame(rows).sort_values(["dominant_class", "dom_class_mean_att"], ascending=[True, False])
|
| df.to_csv(OUT / f"82_{sys}_coatt_modules.csv", index=False)
|
| print(f"[wrote] {len(rows)} modules to {OUT}/82_{sys}_coatt_modules.csv", flush=True)
|
| print(df.head(15).to_string(index=False)[:2000], flush=True)
|
|
|
|
|
| for sys in ["pan_skin", "hematopoiesis", "pancreas"]:
|
| try:
|
| main(sys)
|
| except Exception as e:
|
| import traceback; traceback.print_exc()
|
| print(f"[!] {sys}: {e}", flush=True)
|
| print("\n=== DONE ===", flush=True)
|
|
|