PANDA / scripts /analysis /72_emergent_axes.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
3.26 kB
"""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) # (N, 20)
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] # (5, 128)
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)