Release 4.0.0: repository is the replication package (protocol, records, audit, conventions, fixed toolkit)
b890fab verified Download scripts/validate_depmap_f1.py from sgao-academics/causalscale: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/sgao-academics/causalscale/resolve/main/scripts/validate_depmap_f1.py
- Command line
-
hf download hf://sgao-academics/causalscale/scripts/validate_depmap_f1.py
-
curl -L -o validate_depmap_f1.py https://huggingface.co/sgao-academics/causalscale/resolve/main/scripts/validate_depmap_f1.py
10.3 kB
| """ | |
| DepMap F1 Validation: causalscale LowRankGNN vs STRING/TRRUST Gold Standard | |
| ============================================================================ | |
| Verifies the paper claim: "LowRankGNN identifies 28 causal edges (F1>0.97) on DepMap" | |
| using the actual causalscale package (not standalone prototype). | |
| Strategy: | |
| 1. Load DepMap CRISPR CERES scores (1208 cell lines x 18531 genes) | |
| 2. Run causalscale CausalDiscovery with method="lowrank" | |
| 3. Get top edges | |
| 4. Compare against STRING+TRRUST curated gold standard | |
| 5. GO enrichment analysis | |
| """ | |
| import sys | |
| import os, os, time, json, gzip | |
| import numpy as np | |
| import pandas as pd | |
| from collections import defaultdict | |
| # Add causalscale to path | |
| # causalscale installed via pip install -e . | |
| import torch | |
| print(f"CUDA: {torch.cuda.is_available()}") | |
| print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. LOAD DEPMAP DATA | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n[1] Loading DepMap CRISPR data...") | |
| t0 = time.time() | |
| # DATA: Path to DepMap CRISPR data. Download from https://depmap.org/ and set DATA accordingly. | |
| DATA = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "depmap") | |
| # Use CRISPRGeneEffect (CERES scores) - the gold standard for gene dependency | |
| df = pd.read_csv(f"{DATA}/CRISPRGeneEffect.csv", index_col=0) | |
| print(f" Raw shape: {df.shape}") | |
| # Parse gene names from column headers (format: "GENE (ID)") | |
| gene_names = [] | |
| for col in df.columns: | |
| gene = col.split(" (")[0] if " (" in col else col | |
| gene_names.append(gene) | |
| # Drop genes with >50% NaN | |
| nan_frac = df.isna().mean() | |
| keep_genes = nan_frac[nan_frac < 0.5].index | |
| df = df[keep_genes] | |
| gene_names_filtered = [gene_names[df.columns.get_loc(c)] for c in df.columns] | |
| print(f" After NaN filter: {df.shape}") | |
| # Fill remaining NaN with 0 | |
| X_raw = df.fillna(0).values.astype(np.float32) | |
| print(f" Final matrix: {X_raw.shape[0]} cell lines x {X_raw.shape[1]} genes") | |
| # Standardize | |
| from sklearn.preprocessing import StandardScaler | |
| X = StandardScaler().fit_transform(X_raw).astype(np.float32) | |
| d = X.shape[1] | |
| print(f" d={d}, n={X.shape[0]}, memory={X.nbytes/1e6:.1f} MB") | |
| print(f" Load time: {time.time()-t0:.1f}s") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. RUN CAUSALSCALE LOWRANKGNN | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print(f"\n[2] Running causalscale LowRankGNN (d={d}, rank=64)...") | |
| t1 = time.time() | |
| from causalscale.core.lowrank import LowRankGNN, train_lowrank_gnn | |
| result = train_lowrank_gnn( | |
| X, | |
| rank=64, | |
| epochs=500, # more epochs for genome-scale data | |
| threshold=0.3, | |
| lr=0.005, | |
| device="cuda", | |
| verbose=True | |
| ) | |
| train_time = time.time() - t1 | |
| W = result["adjacency"] | |
| n_edges = result["gnn_edges"] | |
| print(f"\n Training complete: {train_time/60:.1f} min") | |
| print(f" Edges found (|W|>0.3): {n_edges}") | |
| print(f" Correlation-reconstruction F1: {result['f1']:.4f}") | |
| # Get top edges | |
| edges = [] | |
| for i in range(d): | |
| for j in range(d): | |
| if i != j and abs(W[i, j]) > 0.3: | |
| edges.append((i, j, float(W[i, j]))) | |
| edges.sort(key=lambda x: -abs(x[2])) | |
| print(f" Top edges: {len(edges)}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. LOAD STRING + TRRUST GOLD STANDARD | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n[3] Loading STRING/TRRUST gold standard...") | |
| # Build gene name -> index mapping | |
| gene_to_idx = {g: i for i, g in enumerate(gene_names_filtered)} | |
| # STRING PPI | |
| # STRING_PATH: Download from https://string-db.org/ and set accordingly. | |
| STRING_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "validation", "string_ppi_full.txt.gz") | |
| string_pairs = set() | |
| try: | |
| with gzip.open(STRING_PATH, "rt", encoding="utf-8", errors="ignore") as f: | |
| for line in f: | |
| parts = line.strip().split() | |
| if len(parts) >= 2: | |
| string_pairs.add((parts[0], parts[1])) | |
| print(f" STRING pairs: {len(string_pairs)}") | |
| except Exception as e: | |
| print(f" STRING load error: {e}") | |
| string_pairs = set() | |
| # TRRUST | |
| # TRRUST_PATH: Download from https://www.grnpedia.org/trrust/ and set accordingly. | |
| TRRUST_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "validation", "trrust_human.tsv") | |
| trrust_pairs = set() | |
| try: | |
| with open(TRRUST_PATH, encoding="utf-8") as f: | |
| for line in f: | |
| parts = line.strip().split("\t") | |
| if len(parts) >= 2: | |
| trrust_pairs.add((parts[0].upper(), parts[1].upper())) | |
| print(f" TRRUST pairs: {len(trrust_pairs)}") | |
| except Exception as e: | |
| print(f" TRRUST load error: {e}") | |
| trrust_pairs = set() | |
| # Merge gold standard | |
| gold_pairs = string_pairs | trrust_pairs | |
| print(f" Gold standard (UNION): {len(gold_pairs)} pairs") | |
| # Map to indices | |
| gold_idx_pairs = set() | |
| for src, tgt in gold_pairs: | |
| if src in gene_to_idx and tgt in gene_to_idx: | |
| gold_idx_pairs.add((gene_to_idx[src], gene_to_idx[tgt])) | |
| print(f" Gold pairs mapped to causalscale indices: {len(gold_idx_pairs)}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. COMPUTE F1 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n[4] Computing F1 vs gold standard...") | |
| # Predicted edges (top N and various thresholds) | |
| def compute_f1(pred_edges, gold, top_n=None): | |
| """Compute F1 for top-N predicted edges vs gold standard.""" | |
| if top_n: | |
| pred_set = set((i, j) for i, j, _ in pred_edges[:top_n]) | |
| else: | |
| pred_set = set((i, j) for i, j, _ in pred_edges) | |
| tp = len(pred_set & gold) | |
| fp = len(pred_set - gold) | |
| fn = len(gold - pred_set) | |
| prec = tp / max(tp + fp, 1) | |
| rec = tp / max(tp + fn, 1) | |
| f1 = 2 * prec * rec / (prec + rec) if (prec + rec) > 0 else 0 | |
| return {"tp": tp, "fp": fp, "fn": fn, "prec": prec, "rec": rec, "f1": f1} | |
| # Try multiple thresholds | |
| for thresh in [0.1, 0.2, 0.3, 0.4, 0.5]: | |
| top_edges = [(i, j, float(W[i, j])) for i in range(d) for j in range(d) | |
| if i != j and abs(W[i, j]) > thresh] | |
| top_edges.sort(key=lambda x: -abs(x[2])) | |
| if len(top_edges) > 0: | |
| stats = compute_f1(top_edges, gold_idx_pairs) | |
| print(f" threshold={thresh}: edges={len(top_edges)}, TP={stats['tp']}, " | |
| f"FP={stats['fp']}, FN={stats['fn']}, F1={stats['f1']:.4f}") | |
| # Also try top-28 (matching paper claim) | |
| top_28 = edges[:28] | |
| stats_28 = compute_f1(top_28, gold_idx_pairs) | |
| print(f"\n Top-28 edges (paper claim):") | |
| print(f" TP={stats_28['tp']}, FP={stats_28['fp']}, FN={stats_28['fn']}") | |
| print(f" Precision={stats_28['prec']:.4f}, Recall={stats_28['rec']:.4f}, F1={stats_28['f1']:.4f}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 5. GO ENRICHMENT (simplified) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n[5] Edge gene pairs (for GO enrichment)...") | |
| top_edge_genes = [] | |
| for i, j, w in edges[:50]: | |
| gn_i = gene_names_filtered[i] | |
| gn_j = gene_names_filtered[j] | |
| top_edge_genes.append((gn_i, gn_j, w)) | |
| if len(top_edge_genes) <= 20: | |
| print(f" {gn_i} -> {gn_j}: {w:.4f}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SAVE RESULTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| result_json = { | |
| "engine": "causalscale v3.0.0 LowRankGNN", | |
| "data": "DepMap 24Q2 CRISPR CERES", | |
| "shape": [int(X.shape[0]), int(X.shape[1])], | |
| "rank": 64, | |
| "epochs": 500, | |
| "train_time_s": round(train_time, 1), | |
| "total_edges_found": n_edges, | |
| "top28_f1": round(stats_28["f1"], 4), | |
| "top28_precision": round(stats_28["prec"], 4), | |
| "top28_recall": round(stats_28["rec"], 4), | |
| "top28_tp": stats_28["tp"], | |
| "top28_fp": stats_28["fp"], | |
| "top28_fn": stats_28["fn"], | |
| "gold_standard_size": len(gold_idx_pairs), | |
| "top_edges": [(gene_names_filtered[i], gene_names_filtered[j], round(w, 4)) | |
| for i, j, w in edges[:100]] | |
| } | |
| OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "results") | |
| with open(f"{OUT}/exp13_depmap_causalscale_f1.json", "w") as f: | |
| json.dump(result_json, f, indent=2) | |
| print(f"\nResults saved to exp13_depmap_causalscale_f1.json") | |
| print(f"\nTotal wall time: {(time.time()-t0)/60:.1f} min") | |