File size: 6,561 Bytes
07fcdfe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """Preprocess SciPlex3 data for training.
Memory-efficient: uses backed mode for initial load, filters metadata first,
keeps sparse matrix through HVG selection, only extracts dense for final 2000 HVGs.
Usage:
python scripts/preprocess_sciplex3.py \
--h5ad /data/boom/Protein/regulatory_field/data/raw/scPerturb/rna_protein/SrivatsanTrapnell2020_sciplex3.h5ad \
--output data/processed/sciplex3_k562_24h.pt \
--cell_lines K562 \
--doses 10 100 1000 10000 \
--times 24 \
--n_hvg 2000
"""
import argparse
import gc
import os
import sys
import numpy as np
import torch
def preprocess(args):
"""Preprocess SciPlex3 and save compact representation."""
try:
import anndata as ad
import scipy.sparse as sp
except ImportError:
print("Need anndata and scipy")
sys.exit(1)
import scanpy as sc
print(f"Loading {args.h5ad}...")
# Use backed mode to avoid loading full matrix into memory
# We only need obs (metadata) for filtering first
try:
adata = ad.read_h5ad(args.h5ad, backed='r')
print(f" Loaded in backed mode: {adata.shape}")
# Materialize to memory (backed mode → in-memory)
adata = adata.to_memory()
print(f" Materialized to memory")
except Exception as e:
print(f" Backed mode failed ({e}), loading directly")
adata = ad.read_h5ad(args.h5ad)
print(f" Raw shape: {adata.shape}")
# Filter by metadata BEFORE any dense operations
obs = adata.obs.copy()
if args.cell_lines:
obs = obs[obs['cell_line'].isin(args.cell_lines)]
if args.doses:
obs = obs[obs['dose_value'].isin(args.doses)]
elif not args.include_vehicle:
obs = obs[obs['dose_value'] > 0]
if args.times:
obs = obs[obs['time'].isin(args.times)]
keep_idx = obs.index.values
n_keep = len(keep_idx)
print(f" After filtering: {n_keep} cells (from {adata.shape[0]})")
# Check if X is sparse before subsetting
is_sparse = sp.issparse(adata.X)
if is_sparse:
print(f" Source matrix is sparse ({adata.X.nnz / 1e6:.1f}M non-zeros)")
# Subset to filtered cells
adata = adata[keep_idx].copy()
print(f" Subset shape: {adata.shape}")
# Check memory usage after subset
if sp.issparse(adata.X):
print(f" Sparse matrix: {adata.X.nnz / 1e6:.1f}M non-zeros, "
f"{adata.X.data.nbytes / 1e6:.1f}MB data")
else:
print(f" Dense matrix: {adata.X.nbytes / 1e9:.2f} GB — WARNING: this will use lots of RAM!")
# Normalize + log1p (operates on sparse matrix)
print(" Normalizing...")
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
# HVG selection on SPARSE matrix — memory efficient
print(f" Selecting {args.n_hvg} HVGs (sparse mode)...")
try:
# Try scanpy's HVG (works on sparse, uses seurat_v3)
sc.pp.highly_variable_genes(adata, n_top_genes=args.n_hvg, flavor="seurat_v3")
hvg_mask = adata.var['highly_variable'].values
hvg_idx = np.where(hvg_mask)[0]
print(f" Scanpy HVG selected: {len(hvg_idx)} genes")
except Exception as e:
print(f" Scanpy HVG failed ({e}), using variance-based sampling")
# For variance-based: sample cells to estimate variance
n_sample = min(10000, adata.shape[0])
sample_idx = np.random.choice(adata.shape[0], n_sample, replace=False)
if sp.issparse(adata.X):
X_sample = adata.X[sample_idx].toarray()
else:
X_sample = adata.X[sample_idx]
gene_var = np.var(X_sample, axis=0)
del X_sample
gc.collect()
top_idx = np.argsort(gene_var)[-args.n_hvg:]
hvg_idx = np.sort(top_idx)
print(f" Variance-based HVG: {len(hvg_idx)} genes")
# Extract ONLY the HVG columns as dense (2000 genes × N cells)
print(f" Extracting HVG expression matrix...")
n_cells = adata.shape[0]
n_hvg = len(hvg_idx)
print(f" Extracting {n_hvg} genes × {n_cells} cells...")
if sp.issparse(adata.X):
X_hvg = adata.X[:, hvg_idx].toarray().astype(np.float32)
else:
X_hvg = np.array(adata.X[:, hvg_idx], dtype=np.float32)
gene_names = list(adata.var.index[hvg_idx])
# Fix: use ensembl_id if var.index is non-standard (e.g. "nan:X")
if hasattr(adata.var, 'columns') and 'ensembl_id' in adata.var.columns:
ensembl_ids = adata.var['ensembl_id'].values.copy()
# Skip header row if present (CSV parsing artifact)
if len(ensembl_ids) > 0 and str(ensembl_ids[0]) == 'id gene_short_name':
ensembl_ids = ensembl_ids[1:]
# Pad to original length if header was removed
if len(ensembl_ids) < len(adata.var):
ensembl_ids = np.array(list(ensembl_ids) + ['unknown'] * (len(adata.var) - len(ensembl_ids)))
gene_names = [str(ensembl_ids[i]) for i in hvg_idx]
print(f" HVG matrix: {X_hvg.shape}, {X_hvg.nbytes / 1e9:.2f} GB")
del adata # free memory
gc.collect()
# Extract metadata (obs was already filtered)
obs['_cell_idx'] = np.arange(len(obs))
# Save
os.makedirs(os.path.dirname(args.output) or '.', exist_ok=True)
save_dict = {
'X': X_hvg,
'gene_names': gene_names,
'obs': obs,
'cell_lines': args.cell_lines,
'doses': args.doses,
'times': args.times,
}
torch.save(save_dict, args.output)
print(f" Saved to {args.output}")
file_size_gb = os.path.getsize(args.output) / 1e9
print(f" File size: {file_size_gb:.2f} GB")
# Summary
print(f"\nSummary:")
print(f" Cells: {len(obs)}")
print(f" Genes: {len(gene_names)}")
print(f" Cell lines: {obs['cell_line'].value_counts().to_dict()}")
print(f" Doses: {sorted(obs['dose_value'].unique())}")
print(f" Drugs: {obs['perturbation'].nunique()}")
print(f" Perturbations: {obs['perturbation'].value_counts().head(10).to_dict()}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--h5ad", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--cell_lines", nargs="*")
parser.add_argument("--doses", nargs="*", type=float)
parser.add_argument("--times", nargs="*", type=float)
parser.add_argument("--n_hvg", type=int, default=2000)
parser.add_argument("--include_vehicle", action="store_true")
args = parser.parse_args()
preprocess(args)
if __name__ == "__main__":
main()
|