| |
| """Cross-platform benchmarking: compare scPTR gamma estimates across |
| different sequencing platforms and datasets. |
| |
| Compares gamma estimates between: |
| 1. 10x Chromium datasets (pancreas, dentate gyrus) |
| 2. sci (combinatorial indexing) dataset (sci-fate A549) |
| 3. Assesses whether gene-level gamma rankings are consistent across platforms |
| |
| This addresses the cross-platform benchmarking component of the research plan. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sys |
| 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 |
|
|
| sys.path.insert(0, str(Path(__file__).parent)) |
| from _common import set_figure_style |
|
|
| import scptr |
|
|
| OUTPUT_DIR = Path(__file__).parent.parent / "output" / "cross_platform" |
|
|
|
|
| def save_fig(fig, name, subdir="figures"): |
| 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 run_pipeline(adata, name): |
| """Run full scPTR pipeline and return per-gene median gamma.""" |
| import copy |
| adata = copy.deepcopy(adata) |
| 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) |
|
|
| gamma = np.median(adata.layers["gamma"], axis=0) |
| expr = np.mean(adata.layers["spliced"], axis=0) if "spliced" in adata.layers else np.mean(adata.X, axis=0) |
| if hasattr(expr, 'A1'): |
| expr = np.asarray(expr).flatten() |
|
|
| return pd.DataFrame({ |
| "gene": adata.var_names, |
| "gamma": gamma, |
| "expression": expr, |
| "nonzero_frac": (adata.layers["gamma"] > 0).mean(axis=0), |
| }).set_index("gene"), adata |
|
|
|
|
| def compare_datasets(df_a, df_b, name_a, name_b): |
| """Compare gamma estimates between two datasets.""" |
| print(f"\n {name_a} vs {name_b}:") |
|
|
| |
| genes_a = {g.upper(): g for g in df_a.index} |
| genes_b = {g.upper(): g for g in df_b.index} |
| shared = set(genes_a.keys()) & set(genes_b.keys()) |
| print(f" Shared genes: {len(shared)}") |
|
|
| if len(shared) < 50: |
| print(f" Too few shared genes for comparison.") |
| return None |
|
|
| gamma_a = np.array([df_a.loc[genes_a[g], "gamma"] for g in shared]) |
| gamma_b = np.array([df_b.loc[genes_b[g], "gamma"] for g in shared]) |
| expr_a = np.array([df_a.loc[genes_a[g], "expression"] for g in shared]) |
| expr_b = np.array([df_b.loc[genes_b[g], "expression"] for g in shared]) |
| nonzero_a = np.array([df_a.loc[genes_a[g], "nonzero_frac"] for g in shared]) |
| nonzero_b = np.array([df_b.loc[genes_b[g], "nonzero_frac"] for g in shared]) |
|
|
| |
| valid = (gamma_a > 0) & (gamma_b > 0) |
| if valid.sum() < 20: |
| print(f" Too few valid genes (both gamma>0): {valid.sum()}") |
| return None |
|
|
| r_gamma, p_gamma = stats.spearmanr(gamma_a[valid], gamma_b[valid]) |
| r_expr, p_expr = stats.spearmanr(expr_a[valid], expr_b[valid]) |
|
|
| print(f" Gamma Spearman r = {r_gamma:.4f} (n={valid.sum()})") |
| print(f" Expression Spearman r = {r_expr:.4f}") |
|
|
| |
| expr_combined = expr_a + expr_b |
| quartiles = np.percentile(expr_combined[valid], [25, 50, 75]) |
| labels = ["Q1 (low)", "Q2", "Q3", "Q4 (high)"] |
| bounds = [(-np.inf, quartiles[0]), (quartiles[0], quartiles[1]), |
| (quartiles[1], quartiles[2]), (quartiles[2], np.inf)] |
|
|
| print(f"\n Stratified by expression level:") |
| stratified = [] |
| for label, (lo, hi) in zip(labels, bounds): |
| mask = valid & (expr_combined >= lo) & (expr_combined < hi) |
| if mask.sum() < 10: |
| continue |
| r_q, p_q = stats.spearmanr(gamma_a[mask], gamma_b[mask]) |
| r_e, _ = stats.spearmanr(expr_a[mask], expr_b[mask]) |
| print(f" {label}: gamma r={r_q:.3f}, expr r={r_e:.3f} (n={mask.sum()})") |
| stratified.append({ |
| "quartile": label, |
| "gamma_r": float(r_q), |
| "expr_r": float(r_e), |
| "n_genes": int(mask.sum()), |
| }) |
|
|
| |
| informative = valid & (nonzero_a >= 0.1) & (nonzero_b >= 0.1) |
| if informative.sum() >= 20: |
| r_inf, _ = stats.spearmanr(gamma_a[informative], gamma_b[informative]) |
| print(f"\n Informative genes only (>10% nonzero both): " |
| f"r={r_inf:.4f} (n={informative.sum()})") |
|
|
| return { |
| "dataset_a": name_a, |
| "dataset_b": name_b, |
| "shared_genes": len(shared), |
| "valid_genes": int(valid.sum()), |
| "gamma_r": float(r_gamma), |
| "expr_r": float(r_expr), |
| "informative_gamma_r": float(r_inf) if informative.sum() >= 20 else None, |
| "stratified": stratified, |
| } |
|
|
|
|
| def main(): |
| set_figure_style() |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| |
| print("=" * 60) |
| print("CROSS-PLATFORM BENCHMARKING") |
| print("=" * 60) |
|
|
| datasets = {} |
|
|
| |
| print("\nLoading pancreas (10x Chromium)...") |
| df_pan, adata_pan = run_pipeline(scptr.datasets.pancreas(), "pancreas") |
| datasets["pancreas_10x"] = df_pan |
| print(f" {len(df_pan)} genes, {(df_pan['gamma'] > 0).sum()} with gamma>0") |
|
|
| print("\nLoading dentate gyrus (10x Chromium)...") |
| df_dg, adata_dg = run_pipeline(scptr.datasets.dentate_gyrus(), "dentate_gyrus") |
| datasets["dg_10x"] = df_dg |
| print(f" {len(df_dg)} genes, {(df_dg['gamma'] > 0).sum()} with gamma>0") |
|
|
| |
| try: |
| print("\nLoading sci-fate (sci)...") |
| df_sci, adata_sci = run_pipeline(scptr.datasets.sci_fate(), "sci_fate") |
| datasets["scifate_sci"] = df_sci |
| print(f" {len(df_sci)} genes, {(df_sci['gamma'] > 0).sum()} with gamma>0") |
| except Exception as e: |
| print(f" sci-fate not available: {e}") |
|
|
| |
| print("\n" + "=" * 60) |
| print("PAIRWISE COMPARISONS") |
| print("=" * 60) |
|
|
| pairs = [] |
| dataset_names = list(datasets.keys()) |
| all_comparisons = [] |
|
|
| for i in range(len(dataset_names)): |
| for j in range(i + 1, len(dataset_names)): |
| name_a, name_b = dataset_names[i], dataset_names[j] |
| result = compare_datasets(datasets[name_a], datasets[name_b], |
| name_a, name_b) |
| if result: |
| all_comparisons.append(result) |
|
|
| |
| print(f"\n{'='*60}") |
| print("PLATFORM COMPARISON SUMMARY") |
| print(f"{'='*60}") |
|
|
| |
| same_platform = [] |
| cross_platform = [] |
| for comp in all_comparisons: |
| a, b = comp["dataset_a"], comp["dataset_b"] |
| a_platform = "10x" if "10x" in a else "sci" if "sci" in a else "other" |
| b_platform = "10x" if "10x" in b else "sci" if "sci" in b else "other" |
|
|
| if a_platform == b_platform: |
| same_platform.append(comp) |
| else: |
| cross_platform.append(comp) |
|
|
| print(f"\n Same platform comparisons:") |
| for comp in same_platform: |
| print(f" {comp['dataset_a']} vs {comp['dataset_b']}: " |
| f"gamma r={comp['gamma_r']:.3f}") |
|
|
| print(f"\n Cross-platform comparisons:") |
| for comp in cross_platform: |
| print(f" {comp['dataset_a']} vs {comp['dataset_b']}: " |
| f"gamma r={comp['gamma_r']:.3f}") |
|
|
| |
| print(f"\n Half-life validation per platform:") |
| halflife_dir = Path(__file__).parent.parent / "src" / "scptr" / "datasets" / "data" |
| for hl_file, hl_name in [("schofield2018_halflives.csv", "Schofield 2018")]: |
| hl_path = halflife_dir / hl_file |
| if not hl_path.exists(): |
| continue |
|
|
| hl = pd.read_csv(hl_path) |
| for ds_name, df in datasets.items(): |
| gene_map = {g.upper(): g for g in df.index} |
| gamma_vals, hl_vals = [], [] |
| for _, row in hl.iterrows(): |
| g = str(row.iloc[0]).upper() |
| if g in gene_map and df.loc[gene_map[g], "gamma"] > 0: |
| gamma_vals.append(df.loc[gene_map[g], "gamma"]) |
| hl_vals.append(float(row.iloc[1])) |
| if len(gamma_vals) >= 20: |
| r, p = stats.spearmanr(gamma_vals, hl_vals) |
| print(f" {ds_name}: r={r:.4f}, n={len(gamma_vals)} ({hl_name})") |
|
|
| |
| res_dir = OUTPUT_DIR / "results" |
| res_dir.mkdir(parents=True, exist_ok=True) |
| with open(res_dir / "cross_platform_results.json", "w") as f: |
| json.dump(all_comparisons, f, indent=2) |
|
|
| |
| n_comps = len(all_comparisons) |
| fig, axes = plt.subplots(1, max(n_comps, 1), figsize=(6 * max(n_comps, 1), 5)) |
| if n_comps == 1: |
| axes = [axes] |
|
|
| for idx, comp in enumerate(all_comparisons): |
| name_a, name_b = comp["dataset_a"], comp["dataset_b"] |
| df_a, df_b = datasets[name_a], datasets[name_b] |
|
|
| genes_a = {g.upper(): g for g in df_a.index} |
| genes_b = {g.upper(): g for g in df_b.index} |
| shared = set(genes_a.keys()) & set(genes_b.keys()) |
|
|
| ga = np.array([df_a.loc[genes_a[g], "gamma"] for g in shared]) |
| gb = np.array([df_b.loc[genes_b[g], "gamma"] for g in shared]) |
| valid = (ga > 0) & (gb > 0) |
|
|
| axes[idx].scatter(ga[valid], gb[valid], s=2, alpha=0.3, color="steelblue") |
| axes[idx].set_xlabel(f"Gamma ({name_a})") |
| axes[idx].set_ylabel(f"Gamma ({name_b})") |
| axes[idx].set_title(f"r={comp['gamma_r']:.3f} (n={comp['valid_genes']})") |
| lim = max(ga[valid].max(), gb[valid].max()) * 1.1 |
| axes[idx].plot([0, lim], [0, lim], "r--", alpha=0.5) |
|
|
| fig.suptitle("Cross-Platform Gamma Comparison", fontsize=13, y=1.02) |
| fig.tight_layout() |
| save_fig(fig, "cross_platform_gamma") |
|
|
| print(f"\nResults saved to: {OUTPUT_DIR.resolve()}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|