| """per-class-residual PCA on the 128d projections; skin only (only cached)."""
|
| from pathlib import Path
|
| import json, warnings, numpy as np, pandas as pd, anndata as ad
|
| warnings.filterwarnings("ignore")
|
| from sklearn.decomposition import PCA
|
|
|
| import os as _os
|
| from pathlib import Path as _Path
|
| PANDA_ROOT = _Path(_os.environ.get("PANDA_ROOT", str(_Path(__file__).resolve().parents[2])))
|
| OUT = Path(str(PANDA_ROOT / "discovery")); OUT.mkdir(exist_ok=True)
|
|
|
| P = ad.read_h5ad(str(PANDA_ROOT / "discovery/pan_skin/marker/50_aldrich_projections.h5ad"))
|
| Z = np.asarray(P.obsm["Z_projection"])
|
| pred = (P.obs["pred_bbse_label"] if "pred_bbse_label" in P.obs
|
| else P.obs["pred_label"]).astype(str).values
|
| print(f"[skin] Z shape {Z.shape}, n_pred_classes={len(np.unique(pred))}", flush=True)
|
|
|
| Zres = np.zeros_like(Z)
|
| for cls in np.unique(pred):
|
| m = pred == cls
|
| if m.sum() < 2: continue
|
| Zres[m] = Z[m] - Z[m].mean(0, keepdims=True)
|
|
|
| pca = PCA(n_components=20, random_state=0).fit(Zres)
|
| ev = pca.explained_variance_ratio_
|
| print(f"[skin] top-10 residual PC EV: {[f'{e:.4f}' for e in ev[:10]]}", flush=True)
|
| print(f"[skin] cumulative top-10: {ev[:10].cumsum()[-1]:.3f}", flush=True)
|
| print(f"[skin] cumulative top-20: {ev.cumsum()[-1]:.3f}", flush=True)
|
|
|
| scores = pca.transform(Zres)
|
| aux_cols = {}
|
| if "log10_counts" in P.obs:
|
| aux_cols["log10_counts"] = P.obs["log10_counts"].astype(float).values
|
| elif "n_counts" in P.obs:
|
| aux_cols["log10_counts"] = np.log10(P.obs["n_counts"].astype(float).values + 1)
|
| if "missing_hvg_frac" in P.obs:
|
| aux_cols["missing_hvg_frac"] = P.obs["missing_hvg_frac"].astype(float).values
|
| if "max_cos" in P.obs:
|
| aux_cols["max_cos"] = P.obs["max_cos"].astype(float).values
|
| else:
|
| import torch
|
| ck = torch.load(str(PANDA_ROOT / "checkpoints/pan_skin/panda_final.pt"),
|
| map_location="cpu", weights_only=False)
|
| protos = ck["prototypes"]
|
| protos = protos / (np.linalg.norm(protos, axis=1, keepdims=True) + 1e-8)
|
| Zn = Z / (np.linalg.norm(Z, axis=1, keepdims=True) + 1e-8)
|
| aux_cols["max_cos"] = (Zn @ protos.T).max(axis=1)
|
|
|
| if "genotype" in P.obs:
|
| genotype_bin = (P.obs["genotype"] == "En1-cKO").astype(int).values
|
| aux_cols["genotype_cKO"] = genotype_bin.astype(float)
|
|
|
| print(f"[skin] auxiliaries: {list(aux_cols.keys())}", flush=True)
|
|
|
| rows = []
|
| for pc in range(10):
|
| row = {"PC": f"PC{pc+1}", "EV": float(ev[pc])}
|
| for aux_name, aux_vals in aux_cols.items():
|
| r = float(np.corrcoef(scores[:, pc], aux_vals)[0, 1])
|
| row[f"corr_{aux_name}"] = r
|
| rows.append(row)
|
| axes_df = pd.DataFrame(rows)
|
| axes_df.to_csv(OUT / "72_emergent_axes_skin.csv", index=False)
|
| print(axes_df.to_string(index=False), flush=True)
|
|
|
| loadings = pca.components_[:5]
|
| np.save(OUT / "72_emergent_axes_skin_pc_loadings.npy", loadings)
|
|
|
| summary = {
|
| "system": "pan_skin",
|
| "total_ev_top10": float(ev[:10].sum()),
|
| "total_ev_top20": float(ev.sum()),
|
| "top10_ev": [float(e) for e in ev[:10]],
|
| "auxiliaries": list(aux_cols.keys()),
|
| }
|
| json.dump(summary, open(OUT / "72_emergent_axes_summary.json", "w"), indent=2)
|
| print(f"\nwrote {OUT}/72_emergent_axes_summary.json", flush=True)
|
|
|