| |
| """Comprehensive benchmark: DeepPTR vs analytical scPTR on synthetic + real data. |
| |
| Runs: |
| 1. Synthetic recovery: gamma correlation, CI coverage, latent CCA |
| 2. Real datasets (pancreas, dentate gyrus): analytical vs DeepPTR |
| - Half-life correlation (mouse + human references) |
| - ARE/NMD enrichment |
| - Subsampling robustness |
| - Analytical vs DeepPTR gamma agreement |
| 3. sci-fate metabolic labeling: ground-truth validation for both methods |
| |
| All results saved to output/deep_benchmark/. |
| """ |
|
|
| from __future__ import annotations |
|
|
| |
| import os |
| os.environ["OMP_NUM_THREADS"] = "4" |
| os.environ["MKL_NUM_THREADS"] = "4" |
| os.environ["OPENBLAS_NUM_THREADS"] = "4" |
| os.environ["NUMEXPR_NUM_THREADS"] = "4" |
|
|
| import json |
| import sys |
| import time |
| from pathlib import Path |
|
|
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| from scipy import stats |
|
|
| import torch |
| torch.set_num_threads(4) |
|
|
| sys.path.insert(0, str(Path(__file__).parent)) |
| from _common import set_figure_style |
|
|
| import scptr |
|
|
| OUTPUT_DIR = Path(__file__).parent.parent / "output" / "deep_benchmark" |
|
|
|
|
| def save_fig(fig, name, subdir="figures"): |
| if fig is None: |
| print(f" [WARNING] {name}: plot returned None, skipping save") |
| return |
| out_dir = OUTPUT_DIR / subdir |
| out_dir.mkdir(parents=True, exist_ok=True) |
| path = out_dir / f"{name}.png" |
| fig.savefig(path, dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| print(f" Saved: {path}") |
|
|
|
|
| def ensure_dirs(): |
| for sub in ("figures", "results"): |
| (OUTPUT_DIR / sub).mkdir(parents=True, exist_ok=True) |
|
|
|
|
| |
| |
| |
|
|
| def run_synthetic_benchmark(): |
| """End-to-end DeepPTR on synthetic kinetic data with known ground truth.""" |
| from scptr.deep.synthetic import ( |
| generate_kinetic_data, |
| gamma_recovery, |
| ci_coverage, |
| latent_recovery, |
| ) |
|
|
| print("=" * 60) |
| print("1. SYNTHETIC RECOVERY BENCHMARK") |
| print("=" * 60) |
|
|
| adata, truth = generate_kinetic_data( |
| n_cells=1500, n_genes=100, n_cell_types=5, |
| dispersion=10.0, sparsity=0.3, seed=0, |
| ) |
| print(f" Generated: {adata.shape}, {truth['gamma'].shape}") |
|
|
| |
| torch.set_num_threads(4) |
| t0 = time.time() |
| model, history = scptr.deep.fit_deepptr( |
| adata, |
| d_T=8, d_PT=8, d_hidden=48, n_enc_layers=2, |
| batch_size=256, max_epochs=150, kl_warmup_epochs=20, |
| patience=15, n_posterior_samples=20, |
| device="cpu", seed=0, verbose=True, |
| ) |
| elapsed = time.time() - t0 |
| print(f" Training: {len(history.train_loss)} epochs in {elapsed:.1f}s") |
|
|
| |
| gamma_r = gamma_recovery(truth["gamma"], adata.layers["gamma"], per_gene=True) |
| gamma_r_global = gamma_recovery(truth["gamma"], adata.layers["gamma"], per_gene=False) |
| ci_cov = ci_coverage(truth["gamma"], adata.layers["gamma"], adata.layers["gamma_var"]) |
| z_T_r = latent_recovery(truth["z_T"], adata.obsm["X_z_T"]) |
| z_PT_r = latent_recovery(truth["z_PT"], adata.obsm["X_z_PT"]) |
|
|
| results = { |
| "gamma_recovery_per_gene": gamma_r, |
| "gamma_recovery_global": gamma_r_global, |
| "ci_coverage_95": ci_cov, |
| "latent_recovery_T": z_T_r, |
| "latent_recovery_PT": z_PT_r, |
| "n_epochs": len(history.train_loss), |
| "final_train_loss": history.train_loss[-1], |
| "final_val_loss": history.val_loss[-1], |
| "training_time_s": elapsed, |
| } |
|
|
| print(f"\n Gamma recovery (per-gene median Spearman r): {gamma_r:.4f}") |
| print(f" Gamma recovery (global Spearman r): {gamma_r_global:.4f}") |
| print(f" 95% CI coverage: {ci_cov:.4f}") |
| print(f" Latent recovery z_T (mean CCA): {z_T_r:.4f}") |
| print(f" Latent recovery z_PT (mean CCA): {z_PT_r:.4f}") |
|
|
| with open(OUTPUT_DIR / "results" / "synthetic_recovery.json", "w") as f: |
| json.dump(results, f, indent=2) |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4)) |
| epochs = range(1, len(history.train_loss) + 1) |
| axes[0].plot(epochs, history.train_loss, label="train") |
| axes[0].plot(epochs, history.val_loss, label="val") |
| axes[0].set_xlabel("Epoch") |
| axes[0].set_ylabel("Loss") |
| axes[0].set_title("Total Loss") |
| axes[0].legend() |
|
|
| axes[1].plot(epochs, history.train_recon, label="train") |
| axes[1].plot(epochs, history.val_recon, label="val") |
| axes[1].set_xlabel("Epoch") |
| axes[1].set_ylabel("Reconstruction Loss") |
| axes[1].set_title("Reconstruction") |
| axes[1].legend() |
|
|
| axes[2].plot(epochs, history.kl_weight, "k-") |
| axes[2].set_xlabel("Epoch") |
| axes[2].set_ylabel("KL Weight") |
| axes[2].set_title("KL Annealing") |
|
|
| fig.suptitle(f"DeepPTR Training (synthetic, gamma r={gamma_r:.3f})", y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "synthetic_training_curves") |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def preprocess_for_analytical(adata, cluster_key="clusters"): |
| """Standard scPTR preprocessing + analytical gamma.""" |
| scptr.pp.filter_genes(adata) |
| scptr.pp.normalize_layers(adata) |
| scptr.pp.neighbors(adata, n_neighbors=30) |
| scptr.pp.smooth_layers(adata) |
| scptr.tl.estimate_beta(adata) |
| scptr.tl.estimate_gamma(adata) |
| return adata |
|
|
|
|
| def select_top_genes(adata, n_top=500): |
| """Select top genes by unspliced signal for DeepPTR (reduces dim for CPU speed). |
| |
| Uses total unspliced counts × fraction of cells expressing as the ranking. |
| Returns a view of adata with only the selected genes. |
| """ |
| from scipy.sparse import issparse |
|
|
| u = adata.layers["unspliced"] |
| if issparse(u): |
| u = np.asarray(u.todense()) |
| u = np.asarray(u, dtype=np.float32) |
|
|
| |
| total_counts = u.sum(axis=0) |
| frac_nonzero = (u > 0).mean(axis=0) |
| score = total_counts * frac_nonzero |
|
|
| top_idx = np.argsort(score)[::-1][:n_top] |
| top_idx = np.sort(top_idx) |
|
|
| gene_names = adata.var_names[top_idx] |
| print(f" Selected top {len(gene_names)} genes for DeepPTR (from {adata.n_vars})") |
| adata_sub = adata[:, gene_names].copy() |
|
|
| |
| from scipy.sparse import issparse as _issparse |
| for key in ("spliced", "unspliced"): |
| if key in adata_sub.layers and _issparse(adata_sub.layers[key]): |
| adata_sub.layers[key] = np.asarray(adata_sub.layers[key].todense()) |
| return adata_sub |
|
|
|
|
| def run_halflife_comparison(adata, adata_deep, dataset_name): |
| """Compare half-life correlations: analytical vs DeepPTR.""" |
| hl_mouse = scptr.datasets.herzog2017_halflives() |
| hl_human = scptr.datasets.schofield2018_halflives() |
|
|
| results = {} |
| for ref_name, hl_df in [("mouse_herzog", hl_mouse), ("human_schofield", hl_human)]: |
| |
| corr_an = scptr.benchmark.correlate_with_halflives(adata, hl_df) |
| |
| corr_dp = scptr.benchmark.correlate_with_halflives(adata_deep, hl_df) |
|
|
| results[ref_name] = { |
| "analytical": { |
| "spearman_r": corr_an["spearman_r"], |
| "pearson_r": corr_an["pearson_r"], |
| "n_genes": corr_an["n_genes"], |
| }, |
| "deepptr": { |
| "spearman_r": corr_dp["spearman_r"], |
| "pearson_r": corr_dp["pearson_r"], |
| "n_genes": corr_dp["n_genes"], |
| }, |
| } |
| print(f" {ref_name}:") |
| print(f" Analytical: Spearman r = {corr_an['spearman_r']:.4f} (n={corr_an['n_genes']})") |
| print(f" DeepPTR: Spearman r = {corr_dp['spearman_r']:.4f} (n={corr_dp['n_genes']})") |
|
|
| return results |
|
|
|
|
| def run_enrichment_comparison(adata, adata_deep, dataset_name): |
| """Compare ARE/NMD enrichment: analytical vs DeepPTR.""" |
| results = {} |
| for test_name, test_fn in [("ARE", scptr.benchmark.are_enrichment), |
| ("NMD", scptr.benchmark.nmd_enrichment)]: |
| res_an = test_fn(adata) |
| res_dp = test_fn(adata_deep) |
|
|
| results[test_name] = { |
| "analytical": { |
| "U_statistic": float(res_an.get("U_statistic", np.nan)), |
| "p_value": float(res_an.get("p_value", np.nan)), |
| "n_genes_in_set": int(res_an.get("n_genes_in_set", 0)), |
| }, |
| "deepptr": { |
| "U_statistic": float(res_dp.get("U_statistic", np.nan)), |
| "p_value": float(res_dp.get("p_value", np.nan)), |
| "n_genes_in_set": int(res_dp.get("n_genes_in_set", 0)), |
| }, |
| } |
| p_an = res_an.get("p_value", np.nan) |
| p_dp = res_dp.get("p_value", np.nan) |
| print(f" {test_name}: analytical p={p_an:.2e}, DeepPTR p={p_dp:.2e}") |
|
|
| return results |
|
|
|
|
| def run_gamma_agreement(adata, adata_deep, dataset_name): |
| """Correlate per-gene median gamma: analytical vs DeepPTR on shared genes.""" |
| gamma_an_s = pd.Series( |
| np.median(adata.layers["gamma"], axis=0), index=adata.var_names |
| ) |
| gamma_dp_s = pd.Series( |
| np.median(adata_deep.layers["gamma"], axis=0), index=adata_deep.var_names |
| ) |
|
|
| |
| shared = gamma_an_s.index.intersection(gamma_dp_s.index) |
| g_an = gamma_an_s[shared].values.astype(float) |
| g_dp = gamma_dp_s[shared].values.astype(float) |
|
|
| mask = (g_an > 0) & (g_dp > 0) & np.isfinite(g_an) & np.isfinite(g_dp) |
| g_an = g_an[mask] |
| g_dp = g_dp[mask] |
|
|
| if len(g_an) < 3: |
| print(f" Analytical vs DeepPTR gamma: too few shared genes ({len(g_an)})") |
| return {"spearman_r": np.nan, "pearson_r": np.nan, "n_genes": 0} |
|
|
| sp_r, sp_p = stats.spearmanr(g_an, g_dp) |
| pe_r, pe_p = stats.pearsonr(np.log1p(g_an), np.log1p(g_dp)) |
|
|
| result = { |
| "spearman_r": float(sp_r), |
| "spearman_p": float(sp_p), |
| "pearson_r": float(pe_r), |
| "pearson_p": float(pe_p), |
| "n_genes": int(mask.sum()), |
| } |
| print(f" Analytical vs DeepPTR gamma: Spearman r = {sp_r:.4f} (n={mask.sum()})") |
|
|
| |
| fig, ax = plt.subplots(figsize=(6, 5)) |
| ax.scatter(g_an, g_dp, alpha=0.15, s=8, c="steelblue") |
| ax.set_xscale("log") |
| ax.set_yscale("log") |
| ax.set_xlabel("Analytical median gamma") |
| ax.set_ylabel("DeepPTR median gamma") |
| ax.set_title(f"{dataset_name}: Analytical vs DeepPTR (r={sp_r:.3f}, n={mask.sum()})") |
| lims = [min(g_an.min(), g_dp.min()), max(g_an.max(), g_dp.max())] |
| ax.plot(lims, lims, "k--", alpha=0.3, lw=1) |
| save_fig(fig, f"{dataset_name}_analytical_vs_deepptr") |
|
|
| return result |
|
|
|
|
| def run_real_dataset(name, adata_loader, cluster_key="clusters"): |
| """Full benchmark for one real dataset.""" |
| print(f"\n{'=' * 60}") |
| print(f"2. REAL DATA: {name.upper()}") |
| print("=" * 60) |
|
|
| |
| print(f"\n--- Loading {name} ---") |
| adata = adata_loader() |
| print(f" Shape: {adata.shape}") |
|
|
| print(f"\n--- Preprocessing (analytical) ---") |
| preprocess_for_analytical(adata, cluster_key=cluster_key) |
| gamma_an = adata.layers["gamma"] |
| gamma_med_an = np.median(gamma_an, axis=0) |
| print(f" Analytical gamma: median of medians = {np.median(gamma_med_an):.4f}") |
|
|
| |
| print(f"\n--- Running DeepPTR ---") |
| adata_deep = adata_loader() |
| scptr.pp.filter_genes(adata_deep) |
| scptr.pp.normalize_layers(adata_deep) |
| scptr.pp.neighbors(adata_deep, n_neighbors=30) |
| scptr.pp.smooth_layers(adata_deep) |
| scptr.tl.estimate_beta(adata_deep) |
| |
| adata_deep = select_top_genes(adata_deep, n_top=300) |
|
|
| torch.set_num_threads(4) |
| t0 = time.time() |
| model, history = scptr.deep.fit_deepptr( |
| adata_deep, |
| d_T=8, d_PT=8, d_hidden=48, n_enc_layers=2, |
| batch_size=512, max_epochs=100, kl_warmup_epochs=20, |
| patience=15, n_posterior_samples=15, |
| device="cpu", seed=0, verbose=True, |
| ) |
| elapsed = time.time() - t0 |
| n_epochs = len(history.train_loss) |
| print(f" DeepPTR: {n_epochs} epochs in {elapsed:.1f}s") |
|
|
| gamma_dp = adata_deep.layers["gamma"] |
| gamma_med_dp = np.median(gamma_dp, axis=0) |
| print(f" DeepPTR gamma: median of medians = {np.median(gamma_med_dp):.4f}") |
|
|
| |
| all_results = { |
| "dataset": name, |
| "n_cells": adata.n_obs, |
| "n_genes": adata.n_vars, |
| "deepptr_epochs": n_epochs, |
| "deepptr_time_s": elapsed, |
| "deepptr_final_val_loss": history.val_loss[-1], |
| } |
|
|
| |
| print(f"\n--- Half-life correlations ---") |
| hl_results = run_halflife_comparison(adata, adata_deep, name) |
| all_results["halflife"] = hl_results |
|
|
| |
| print(f"\n--- ARE/NMD enrichment ---") |
| try: |
| enrich_results = run_enrichment_comparison(adata, adata_deep, name) |
| all_results["enrichment"] = enrich_results |
| except Exception as e: |
| print(f" Enrichment failed: {e}") |
| all_results["enrichment"] = {"error": str(e)} |
|
|
| |
| print(f"\n--- Analytical vs DeepPTR agreement ---") |
| agree = run_gamma_agreement(adata, adata_deep, name) |
| all_results["gamma_agreement"] = agree |
|
|
| |
| print(f"\n--- Subsampling robustness (analytical) ---") |
| try: |
| rob_an = scptr.benchmark.subsampling_robustness( |
| adata, fractions=[0.5, 0.8], n_repeats=2 |
| ) |
| print(f" Analytical: median r @ 30% = {rob_an[rob_an['fraction']==0.3]['spearman_r'].median():.4f}") |
| all_results["robustness_analytical"] = rob_an.to_dict(orient="records") |
| except Exception as e: |
| print(f" Robustness failed: {e}") |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(12, 4)) |
| epochs = range(1, n_epochs + 1) |
| axes[0].plot(epochs, history.train_loss, label="train") |
| axes[0].plot(epochs, history.val_loss, label="val") |
| axes[0].set_xlabel("Epoch") |
| axes[0].set_ylabel("Loss") |
| axes[0].set_title(f"{name}: Training Loss") |
| axes[0].legend() |
|
|
| axes[1].plot(epochs, history.train_recon, label="train recon") |
| axes[1].plot(epochs, history.train_kl, label="train KL") |
| axes[1].set_xlabel("Epoch") |
| axes[1].set_ylabel("Loss Component") |
| axes[1].set_title(f"{name}: Loss Components") |
| axes[1].legend() |
| fig.tight_layout() |
| save_fig(fig, f"{name}_training_curves") |
|
|
| |
| gamma_var = adata_deep.layers["gamma_var"] |
| mean_var = np.mean(gamma_var, axis=0) |
| fig, ax = plt.subplots(figsize=(6, 5)) |
| ax.scatter(gamma_med_dp, mean_var, alpha=0.2, s=8, c="steelblue") |
| ax.set_xscale("log") |
| ax.set_yscale("log") |
| ax.set_xlabel("Posterior mean gamma (median over cells)") |
| ax.set_ylabel("Posterior variance (mean over cells)") |
| ax.set_title(f"{name}: DeepPTR Uncertainty") |
| save_fig(fig, f"{name}_uncertainty") |
|
|
| |
| with open(OUTPUT_DIR / "results" / f"{name}_benchmark.json", "w") as f: |
| json.dump(all_results, f, indent=2, default=str) |
|
|
| return all_results |
|
|
|
|
| |
| |
| |
|
|
| def run_scifate_benchmark(): |
| """Compare analytical vs DeepPTR on sci-fate metabolic labeling data.""" |
| import gzip |
| from scipy.io import mmread |
| from scipy.sparse import csc_matrix |
|
|
| print(f"\n{'=' * 60}") |
| print("3. SCI-FATE METABOLIC LABELING VALIDATION") |
| print("=" * 60) |
|
|
| CACHE_DIR = Path.home() / ".cache" / "scptr" / "scifate" |
| if not CACHE_DIR.exists(): |
| print(" [SKIP] sci-fate data not cached. Run analyses/run_scifate.py first.") |
| return None |
|
|
| |
| print(" Loading sci-fate data...") |
| cell_ann = pd.read_csv(CACHE_DIR / "GSM3770930_A549_cell_annotate.txt.gz", compression="gzip") |
| gene_ann = pd.read_csv(CACHE_DIR / "GSM3770930_A549_gene_annotate.txt.gz", compression="gzip") |
|
|
| with gzip.open(CACHE_DIR / "GSM3770930_A549_gene_count.txt.gz", "rb") as f: |
| total_mat = csc_matrix(mmread(f)).T |
| with gzip.open(CACHE_DIR / "GSM3770930_A549_gene_count_newly_synthesised.txt.gz", "rb") as f: |
| new_mat = csc_matrix(mmread(f)).T |
|
|
| import anndata as ad |
| adata_raw = ad.AnnData( |
| X=total_mat, |
| obs=cell_ann.set_index("sample"), |
| var=gene_ann.set_index("gene_id"), |
| ) |
| adata_raw.layers["new"] = new_mat |
| adata_raw.var_names_make_unique() |
| adata_raw.var["gene_id_full"] = adata_raw.var_names.tolist() |
| adata_raw.var_names = adata_raw.var["gene_short_name"].values |
| adata_raw.var_names_make_unique() |
| print(f" Shape: {adata_raw.shape}") |
|
|
| |
| total = np.asarray(adata_raw.X.toarray() if hasattr(adata_raw.X, "toarray") else adata_raw.X) |
| new = np.asarray(adata_raw.layers["new"].toarray() if hasattr(adata_raw.layers["new"], "toarray") else adata_raw.layers["new"]) |
| old = total - new |
| mean_new = new.mean(axis=0) |
| mean_old = old.mean(axis=0) |
| mean_total = total.mean(axis=0) |
| reliable = (mean_total >= 0.5) & (mean_old > 0.1) |
| gt_ratio = np.full(adata_raw.n_vars, np.nan) |
| gt_ratio[reliable] = mean_new[reliable] / mean_old[reliable] |
| print(f" Ground truth: {reliable.sum()} reliable genes") |
|
|
| |
| keep = mean_total >= 0.5 |
| if "gene_type" in adata_raw.var.columns: |
| is_pc = adata_raw.var["gene_type"] == "protein_coding" |
| keep = keep & is_pc.values |
|
|
| def make_scptr_adata(): |
| a = ad.AnnData( |
| X=total[:, keep].astype(np.float32), |
| obs=adata_raw.obs.copy(), |
| var=adata_raw.var.iloc[keep].copy(), |
| ) |
| a.layers["unspliced"] = new[:, keep].astype(np.float32) |
| a.layers["spliced"] = old[:, keep].astype(np.float32) |
| return a |
|
|
| |
| print("\n--- Analytical pipeline ---") |
| adata_an = make_scptr_adata() |
| scptr.pp.filter_genes(adata_an, min_unspliced_counts=1, min_unspliced_cells=1) |
| scptr.pp.normalize_layers(adata_an) |
| scptr.pp.neighbors(adata_an, n_neighbors=30) |
| scptr.pp.smooth_layers(adata_an) |
| scptr.tl.estimate_beta(adata_an) |
| scptr.tl.estimate_gamma(adata_an) |
| gamma_med_an = np.median(adata_an.layers["gamma"], axis=0) |
| print(f" Analytical: {adata_an.shape}, median gamma = {np.median(gamma_med_an):.4f}") |
|
|
| |
| print("\n--- DeepPTR ---") |
| adata_dp = make_scptr_adata() |
| scptr.pp.filter_genes(adata_dp, min_unspliced_counts=1, min_unspliced_cells=1) |
| scptr.pp.normalize_layers(adata_dp) |
| scptr.pp.neighbors(adata_dp, n_neighbors=30) |
| scptr.pp.smooth_layers(adata_dp) |
| scptr.tl.estimate_beta(adata_dp) |
| adata_dp = select_top_genes(adata_dp, n_top=500) |
|
|
| t0 = time.time() |
| model, history = scptr.deep.fit_deepptr( |
| adata_dp, |
| d_T=8, d_PT=8, d_hidden=48, n_enc_layers=2, |
| batch_size=512, max_epochs=100, kl_warmup_epochs=20, |
| patience=15, n_posterior_samples=15, |
| device="cpu", seed=0, verbose=True, |
| ) |
| elapsed = time.time() - t0 |
| gamma_med_dp = np.median(adata_dp.layers["gamma"], axis=0) |
| print(f" DeepPTR: {len(history.train_loss)} epochs in {elapsed:.1f}s") |
|
|
| |
| gt_s_an = pd.Series(gt_ratio, index=adata_raw.var_names) |
| gamma_s_an = pd.Series(gamma_med_an, index=adata_an.var_names) |
| gamma_s_dp = pd.Series(gamma_med_dp, index=adata_dp.var_names) |
|
|
| shared_an = gamma_s_an.index.intersection(gt_s_an.dropna().index) |
| shared_dp = gamma_s_dp.index.intersection(gt_s_an.dropna().index) |
|
|
| def correlate(gamma_s, gt_s, shared): |
| g = gamma_s[shared].values.astype(float) |
| t = gt_s[shared].values.astype(float) |
| mask = np.isfinite(g) & np.isfinite(t) & (g > 0) & (t > 0) |
| if mask.sum() < 3: |
| return {"spearman_r": np.nan, "n_genes": 0} |
| sp_r, sp_p = stats.spearmanr(g[mask], t[mask]) |
| return {"spearman_r": float(sp_r), "spearman_p": float(sp_p), "n_genes": int(mask.sum())} |
|
|
| corr_an = correlate(gamma_s_an, gt_s_an, shared_an) |
| corr_dp = correlate(gamma_s_dp, gt_s_an, shared_dp) |
|
|
| print(f"\n--- Ground truth correlation (new/old ratio) ---") |
| print(f" Analytical: Spearman r = {corr_an['spearman_r']:.4f} (n={corr_an['n_genes']})") |
| print(f" DeepPTR: Spearman r = {corr_dp['spearman_r']:.4f} (n={corr_dp['n_genes']})") |
|
|
| |
| print(f"\n--- Half-life correlations ---") |
| hl_human = scptr.datasets.schofield2018_halflives() |
| corr_hl_an = scptr.benchmark.correlate_with_halflives(adata_an, hl_human) |
| corr_hl_dp = scptr.benchmark.correlate_with_halflives(adata_dp, hl_human) |
| print(f" Analytical: Spearman r = {corr_hl_an['spearman_r']:.4f} (n={corr_hl_an['n_genes']})") |
| print(f" DeepPTR: Spearman r = {corr_hl_dp['spearman_r']:.4f} (n={corr_hl_dp['n_genes']})") |
|
|
| |
| shared_both = gamma_s_an.index.intersection(gamma_s_dp.index) |
| g_an = gamma_s_an[shared_both].values |
| g_dp = gamma_s_dp[shared_both].values |
| mask_both = (g_an > 0) & (g_dp > 0) & np.isfinite(g_an) & np.isfinite(g_dp) |
| if mask_both.sum() >= 3: |
| agree_r, _ = stats.spearmanr(g_an[mask_both], g_dp[mask_both]) |
| print(f"\n Analytical vs DeepPTR: Spearman r = {agree_r:.4f} (n={mask_both.sum()})") |
| else: |
| agree_r = np.nan |
|
|
| results = { |
| "dataset": "scifate", |
| "n_cells": int(adata_an.n_obs), |
| "n_genes_analytical": int(adata_an.n_vars), |
| "n_genes_deep": int(adata_dp.n_vars), |
| "ground_truth_corr": { |
| "analytical": corr_an, |
| "deepptr": corr_dp, |
| }, |
| "halflife_human": { |
| "analytical": {"spearman_r": corr_hl_an["spearman_r"], "n_genes": corr_hl_an["n_genes"]}, |
| "deepptr": {"spearman_r": corr_hl_dp["spearman_r"], "n_genes": corr_hl_dp["n_genes"]}, |
| }, |
| "gamma_agreement": {"spearman_r": float(agree_r), "n_genes": int(mask_both.sum())}, |
| "deepptr_epochs": len(history.train_loss), |
| "deepptr_time_s": elapsed, |
| } |
|
|
| with open(OUTPUT_DIR / "results" / "scifate_benchmark.json", "w") as f: |
| json.dump(results, f, indent=2, default=str) |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(16, 4.5)) |
|
|
| |
| g = gamma_s_an[shared_an].values.astype(float) |
| t = gt_s_an[shared_an].values.astype(float) |
| m = np.isfinite(g) & np.isfinite(t) & (g > 0) & (t > 0) |
| axes[0].scatter(t[m], g[m], alpha=0.1, s=5, c="steelblue") |
| axes[0].set_xscale("log") |
| axes[0].set_yscale("log") |
| axes[0].set_xlabel("Ground truth (new/old ratio)") |
| axes[0].set_ylabel("Analytical gamma") |
| axes[0].set_title(f"Analytical (r={corr_an['spearman_r']:.3f})") |
|
|
| |
| g = gamma_s_dp[shared_dp].values.astype(float) |
| t = gt_s_an[shared_dp].values.astype(float) |
| m = np.isfinite(g) & np.isfinite(t) & (g > 0) & (t > 0) |
| axes[1].scatter(t[m], g[m], alpha=0.1, s=5, c="darkorange") |
| axes[1].set_xscale("log") |
| axes[1].set_yscale("log") |
| axes[1].set_xlabel("Ground truth (new/old ratio)") |
| axes[1].set_ylabel("DeepPTR gamma") |
| axes[1].set_title(f"DeepPTR (r={corr_dp['spearman_r']:.3f})") |
|
|
| |
| if mask_both.sum() >= 3: |
| axes[2].scatter(g_an[mask_both], g_dp[mask_both], alpha=0.1, s=5, c="seagreen") |
| axes[2].set_xscale("log") |
| axes[2].set_yscale("log") |
| lims = [min(g_an[mask_both].min(), g_dp[mask_both].min()), |
| max(g_an[mask_both].max(), g_dp[mask_both].max())] |
| axes[2].plot(lims, lims, "k--", alpha=0.3, lw=1) |
| axes[2].set_xlabel("Analytical gamma") |
| axes[2].set_ylabel("DeepPTR gamma") |
| axes[2].set_title(f"Agreement (r={agree_r:.3f})") |
|
|
| fig.suptitle("sci-fate: Analytical vs DeepPTR", y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "scifate_comparison") |
|
|
| return results |
|
|
|
|
| |
| |
| |
|
|
| def print_summary(synth, pancreas, dg, scifate): |
| """Print final comparison table.""" |
| print(f"\n{'=' * 70}") |
| print("SUMMARY: Analytical vs DeepPTR") |
| print("=" * 70) |
|
|
| |
| print(f"\n{'Metric':<40} {'Analytical':>12} {'DeepPTR':>12}") |
| print("-" * 65) |
|
|
| if synth: |
| print(f"\n SYNTHETIC RECOVERY") |
| print(f" {'Gamma recovery (per-gene r)':<38} {'N/A':>12} {synth['gamma_recovery_per_gene']:>12.4f}") |
| print(f" {'95% CI coverage':<38} {'N/A':>12} {synth['ci_coverage_95']:>12.4f}") |
| print(f" {'Latent recovery z_T':<38} {'N/A':>12} {synth['latent_recovery_T']:>12.4f}") |
| print(f" {'Latent recovery z_PT':<38} {'N/A':>12} {synth['latent_recovery_PT']:>12.4f}") |
|
|
| for name, res in [("PANCREAS", pancreas), ("DENTATE GYRUS", dg)]: |
| if res is None: |
| continue |
| print(f"\n {name}") |
| for ref in ("mouse_herzog", "human_schofield"): |
| if ref in res.get("halflife", {}): |
| hl = res["halflife"][ref] |
| an_r = hl["analytical"]["spearman_r"] |
| dp_r = hl["deepptr"]["spearman_r"] |
| print(f" {'Half-life ' + ref:<38} {an_r:>12.4f} {dp_r:>12.4f}") |
| if "gamma_agreement" in res: |
| print(f" {'Gamma agreement (Spearman r)':<38} {'---':>12} {res['gamma_agreement']['spearman_r']:>12.4f}") |
|
|
| if scifate: |
| print(f"\n SCI-FATE") |
| gt = scifate.get("ground_truth_corr", {}) |
| if "analytical" in gt and "deepptr" in gt: |
| an_r = gt["analytical"]["spearman_r"] |
| dp_r = gt["deepptr"]["spearman_r"] |
| print(f" {'Ground truth (new/old ratio)':<38} {an_r:>12.4f} {dp_r:>12.4f}") |
| hl = scifate.get("halflife_human", {}) |
| if "analytical" in hl and "deepptr" in hl: |
| an_r = hl["analytical"]["spearman_r"] |
| dp_r = hl["deepptr"]["spearman_r"] |
| print(f" {'Half-life (human Schofield)':<38} {an_r:>12.4f} {dp_r:>12.4f}") |
|
|
| print() |
|
|
|
|
| def main(): |
| set_figure_style() |
| ensure_dirs() |
|
|
| |
| synth_results = run_synthetic_benchmark() |
|
|
| |
| pancreas_results = run_real_dataset( |
| "pancreas", scptr.datasets.pancreas, cluster_key="clusters" |
| ) |
|
|
| |
| dg_results = run_real_dataset( |
| "dentate_gyrus", scptr.datasets.dentate_gyrus, cluster_key="clusters" |
| ) |
|
|
| |
| scifate_results = run_scifate_benchmark() |
|
|
| |
| print_summary(synth_results, pancreas_results, dg_results, scifate_results) |
|
|
| |
| combined = { |
| "synthetic": synth_results, |
| "pancreas": pancreas_results, |
| "dentate_gyrus": dg_results, |
| "scifate": scifate_results, |
| } |
| with open(OUTPUT_DIR / "results" / "combined_benchmark.json", "w") as f: |
| json.dump(combined, f, indent=2, default=str) |
|
|
| print(f"\nAll results saved to: {OUTPUT_DIR}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|