Download experiments/21_hest_prep.py from Pathstudy/fourmodal-pathology-bind: direct link, hf CLI and curl.
- Browser
- Download file 6.03 kB
-
https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/experiments/21_hest_prep.py
- Command line
-
hf download hf://Pathstudy/fourmodal-pathology-bind/experiments/21_hest_prep.py
-
curl -L -o 21_hest_prep.py https://huggingface.co/Pathstudy/fourmodal-pathology-bind/resolve/main/experiments/21_hest_prep.py
6.03 kB
| """Gene-expression modality: HEST-1k human Visium spots (H&E patch + whole-transcriptome spot expression). | |
| Input ../HEST/visium_humandisease_adata.pickle list of 421 AnnData (1% spot subsample per sample) | |
| ../HEST/UNI/visium_hest_univ2_imagefeature.pickle list of 421 (n_spot, 1536) UNI2-h spot features (aligned) | |
| ../HEST/prov-gigapath-conchinside/visium_hest_gigapath_imagefeature.pickle (GigaPath, aligned) | |
| ../HEST/HEST_v1_1_0.csv sample metadata; list order = Visium/Homo sapiens rows | |
| Kept samples whose panel covers the common whole-transcriptome genes (targeted ~1k-gene panels dropped). | |
| Groups for CV: study + patient (patient missing -> whole study), i.e. no patient leaks across folds. | |
| Outputs cache/hest/ | |
| meta.csv one row per spot (sample id, organ, oncotree, group, total counts) | |
| counts.npz raw counts (n_spot, n_common_genes) sparse + gene names | |
| hvg.npy log1p(CP10k) of the top-2000 HVGs (n_spot, 2000) float32 -> MLP gene encoder input | |
| he_uni2.npy, he_gigapath.npy (n_spot, 1, 1536) float16 -> frozen H&E anchor | |
| """ | |
| import os | |
| import pickle | |
| import weakref | |
| import numpy as np | |
| import pandas as pd | |
| import scipy.sparse as sp | |
| import torch | |
| from anndata._core import file_backing | |
| from common import CACHE, ROOT | |
| HEST = os.path.join(ROOT, "..", "HEST") | |
| OUT = os.path.join(CACHE, "hest") | |
| os.makedirs(OUT, exist_ok=True) | |
| def _ss(self, state): # old anndata pickles store '_adata' instead of '_adata_ref' | |
| self.__dict__ = state.copy() | |
| ref = state.get("_adata_ref", state.get("_adata")) | |
| self.__dict__["_adata_ref"] = weakref.ref(ref) if ref is not None else (lambda: None) | |
| file_backing.AnnDataFileManager.__setstate__ = _ss | |
| A = pickle.load(open(os.path.join(HEST, "visium_humandisease_adata.pickle"), "rb")) | |
| FU = pickle.load(open(os.path.join(HEST, "UNI", "visium_hest_univ2_imagefeature.pickle"), "rb")) | |
| FG = pickle.load(open(os.path.join(HEST, "prov-gigapath-conchinside", "visium_hest_gigapath_imagefeature.pickle"), "rb")) | |
| H = pd.read_csv(os.path.join(HEST, "HEST_v1_1_0.csv")) | |
| H = H[(H.st_technology == "Visium") & (H.species == "Homo sapiens")].reset_index(drop=True) | |
| assert len(H) == len(A) == len(FU) == len(FG) | |
| # --- check the list order against the metadata (full-res size from thumbnail / scalefactor, oncotree code) | |
| ok = [] | |
| for a, (_, h) in zip(A, H.iterrows()): | |
| st = a.uns["spatial"]["ST"] | |
| sh = np.array(st["images"]["downscaled_fullres"].shape[:2]) / st["scalefactors"]["tissue_downscaled_fullres_scalef"] | |
| size_ok = np.allclose(sh, [h.fullres_px_height, h.fullres_px_width], rtol=0.02) | |
| onc = a.obs.oncotree_code.iloc[0] | |
| onc_ok = (pd.isna(onc) and pd.isna(h.oncotree_code)) or onc == h.oncotree_code | |
| ok.append(size_ok and onc_ok) | |
| print(f"metadata order check: {sum(ok)}/{len(ok)} samples match (image size + oncotree)") | |
| assert np.mean(ok) > 0.95 | |
| # --- harmonise gene names to HGNC symbols (samples use symbols, Ensembl ids + SYMBOL column, or 'GRCh38__' prefixes) | |
| e2s = {} | |
| for a in A: | |
| if "gene_ids" in a.var and not str(a.var_names[0]).startswith(("ENSG", "GRCh")): | |
| e2s.update(dict(zip(a.var.gene_ids.astype(str), a.var_names))) | |
| def symbols(a): | |
| if "SYMBOL" in a.var: | |
| return a.var.SYMBOL.astype(str).tolist() | |
| out = [] | |
| for x in map(str, a.var_names): | |
| x = x.split("__")[-1] if x.startswith("GRCh") else x | |
| out.append(e2s.get(x.split(".")[0], x) if x.startswith("ENSG") else x) | |
| return out | |
| for a in A: | |
| a.var_names = symbols(a) | |
| # --- common gene set over whole-transcriptome samples | |
| wt = [i for i, a in enumerate(A) if a.n_vars >= 17000] | |
| common = set(A[wt[0]].var_names) | |
| for i in wt[1:]: | |
| common &= set(A[i].var_names) | |
| common = sorted(g for g in common if not g.startswith(("MT-", "RPL", "RPS"))) | |
| print(f"whole-transcriptome samples: {len(wt)}/{len(A)}; common genes (no MT/ribo): {len(common)}") | |
| rows, Xs, fu, fg = [], [], [], [] | |
| for i in wt: | |
| a, h = A[i], H.iloc[i] | |
| a = a[:, ~a.var_names.duplicated()] | |
| X = a[:, common].X | |
| Xs.append(sp.csr_matrix(X, dtype=np.float32)) | |
| fu.append(FU[i].numpy()); fg.append(FG[i].numpy()) | |
| study = str(h.dataset_title) | |
| grp = f"{study}|{h.patient}" if pd.notna(h.patient) else study | |
| for b in a.obs_names: | |
| rows.append({"sample": h.id, "spot": b, "organ": h.organ, "oncotree": h.oncotree_code, | |
| "disease": h.disease_state, "study": study, "patient": h.patient, "group": grp}) | |
| M = pd.DataFrame(rows) | |
| X = sp.vstack(Xs).tocsr() | |
| M["total_counts"] = np.asarray(X.sum(1)).ravel() | |
| keep = (M.total_counts >= 200).values | |
| M, X = M[keep].reset_index(drop=True), X[keep] | |
| fu, fg = np.concatenate(fu)[keep], np.concatenate(fg)[keep] | |
| M["n_genes"] = np.asarray((X > 0).sum(1)).ravel() | |
| # log1p CP10k, HVG = top-2000 by variance of the log-normalised values (genes expressed in >=1% spots) | |
| L = sp.diags(1e4 / M.total_counts.values) @ X | |
| L.data = np.log1p(L.data) | |
| frac = np.asarray((X > 0).mean(0)).ravel() | |
| mu = np.asarray(L.mean(0)).ravel() | |
| var = np.asarray(L.multiply(L).mean(0)).ravel() - mu ** 2 | |
| var[frac < 0.01] = -1 | |
| hvg = np.sort(np.argsort(-var)[:2000]) | |
| np.save(os.path.join(OUT, "hvg.npy"), L[:, hvg].toarray().astype(np.float32)) | |
| sp.save_npz(os.path.join(OUT, "counts.npz"), X) | |
| pd.Series(common).to_csv(os.path.join(OUT, "genes.csv"), index=False, header=["gene"]) | |
| pd.Series(np.array(common)[hvg]).to_csv(os.path.join(OUT, "hvg_genes.csv"), index=False, header=["gene"]) | |
| np.save(os.path.join(OUT, "he_uni2.npy"), fu.astype(np.float16)[:, None]) | |
| np.save(os.path.join(OUT, "he_gigapath.npy"), fg.astype(np.float16)[:, None]) | |
| M.to_csv(os.path.join(OUT, "meta.csv"), index_label="idx") | |
| print(f"spots kept: {len(M)} (dropped {int((~keep).sum())} with <200 counts); samples {M['sample'].nunique()}, " | |
| f"groups {M.group.nunique()}, studies {M.study.nunique()}") | |
| print("organs:", M.organ.value_counts().to_dict()) | |
| print("median counts / genes per spot:", M.total_counts.median(), M.n_genes.median()) | |