File size: 7,246 Bytes
4e2940e | 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 | #!/usr/bin/env python
"""CRITICAL: scVelo gamma semantics and honest method comparison.
Findings:
1. scPTR gamma β scVelo velocity_gamma (r=0.96) β nearly identical math
2. scVelo dynamical fit_gamma has DIFFERENT semantics (r=-0.37 with SS)
3. fit_gamma/fit_beta correlates with half-life (r=-0.35)
4. The "scVelo dynamical failure" was a comparison error, not a model failure
This script documents these findings honestly and provides corrected comparisons.
"""
from _common import *
import scvelo as scv
OUT = output_dir("28_gamma_semantics")
def main():
set_figure_style()
hl_mouse, hl_human = load_halflife_refs()
all_results = {}
for ds_name, loader, ck in DATASETS:
print(f"\n{'=' * 60}\n{ds_name.upper()}: Gamma Semantics\n{'=' * 60}")
adata_raw = loader()
# ββ scVelo SS ββββββββββββββββββββββββββββββββββββββββββββββββ
adata_sv = adata_raw.copy()
scv.pp.filter_and_normalize(adata_sv, min_shared_counts=20, n_top_genes=2000)
scv.pp.moments(adata_sv, n_pcs=30, n_neighbors=30)
scv.tl.velocity(adata_sv, mode="steady_state")
vg = adata_sv.var["velocity_gamma"].values.astype(float)
# ββ scVelo dynamical βββββββββββββββββββββββββββββββββββββββββ
adata_dyn = adata_raw.copy()
scv.pp.filter_and_normalize(adata_dyn, min_shared_counts=20, n_top_genes=2000)
scv.pp.moments(adata_dyn, n_pcs=30, n_neighbors=30)
scv.tl.recover_dynamics(adata_dyn, n_jobs=4)
scv.tl.velocity(adata_dyn, mode="dynamical")
fg = adata_dyn.var["fit_gamma"].values.astype(float)
fb = adata_dyn.var["fit_beta"].values.astype(float)
# ββ scPTR ββββββββββββββββββββββββββββββββββββββββββββββββββββ
adata_sp = run_analytical(loader)
sp_gamma = np.median(adata_sp.layers["gamma"], axis=0)
sp_gs = pd.Series(sp_gamma, index=adata_sp.var_names)
# ββ Correlations between methods βββββββββββββββββββββββββββββ
print("\n--- Method-to-method gamma correlation ---")
# scPTR vs scVelo SS
sv_gs = pd.Series(vg, index=adata_sv.var_names)
shared_ss = sp_gs.index.intersection(sv_gs.index)
g1, g2 = sp_gs[shared_ss].values.astype(float), sv_gs[shared_ss].values.astype(float)
v = np.isfinite(g1) & np.isfinite(g2) & (g1 > 0) & (g2 > 0)
r_sp_sv, _ = stats.spearmanr(g1[v], g2[v])
print(f" scPTR vs scVelo SS: r={r_sp_sv:.4f} (n={v.sum()})")
# scVelo SS vs dynamical fit_gamma
shared_dyn = adata_sv.var_names.intersection(adata_dyn.var_names)
vg_sh = pd.Series(vg, index=adata_sv.var_names)[shared_dyn].values.astype(float)
fg_sh = pd.Series(fg, index=adata_dyn.var_names)[shared_dyn].values.astype(float)
v2 = np.isfinite(vg_sh) & np.isfinite(fg_sh) & (vg_sh > 0) & (fg_sh > 0)
r_ss_dyn, _ = stats.spearmanr(vg_sh[v2], fg_sh[v2])
print(f" scVelo SS vs dyn fit_gamma: r={r_ss_dyn:.4f} (n={v2.sum()})")
# ββ Corrected half-life comparison ββββββββββββββββββββββββββββ
print("\n--- Half-life correlation (corrected) ---")
methods = {}
# scPTR gamma
r_sp, n_sp = halflife_spearman(adata_sp, hl_human)
methods["scPTR gamma"] = (r_sp, n_sp)
# scVelo SS velocity_gamma
adata_sv_tmp = adata_sv.copy()
adata_sv_tmp.layers["gamma"] = np.tile(vg, (adata_sv.n_obs, 1))
r_ss, n_ss = halflife_spearman(adata_sv_tmp, hl_human)
methods["scVelo SS velocity_gamma"] = (r_ss, n_ss)
# scVelo dyn fit_gamma (raw β the "failed" metric)
adata_dyn_tmp = adata_dyn.copy()
adata_dyn_tmp.layers["gamma"] = np.tile(fg, (adata_dyn.n_obs, 1))
r_dyn_raw, n_dyn_raw = halflife_spearman(adata_dyn_tmp, hl_human)
methods["scVelo dyn fit_gamma (raw)"] = (r_dyn_raw, n_dyn_raw)
# scVelo dyn fit_gamma/fit_beta (CORRECTED)
ratio = fg / (fb + 1e-8)
adata_dyn_tmp.layers["gamma"] = np.tile(ratio, (adata_dyn.n_obs, 1))
r_dyn_corr, n_dyn_corr = halflife_spearman(adata_dyn_tmp, hl_human)
methods["scVelo dyn fit_gamma/fit_beta"] = (r_dyn_corr, n_dyn_corr)
for mname, (r, n) in methods.items():
print(f" {mname:<35} r={r:.4f} (n={n})")
ds_results = {
"scptr_vs_scvelo_ss": {"r": float(r_sp_sv), "n": int(v.sum())},
"scvelo_ss_vs_dyn": {"r": float(r_ss_dyn), "n": int(v2.sum())},
"halflife": {m: {"r": float(r), "n": n} for m, (r, n) in methods.items()},
}
# ββ Honest assessment βββββββββββββββββββββββββββββββββββββββββ
print(f"\n--- Honest assessment ---")
print(f" scPTR gamma β scVelo SS gamma (r={r_sp_sv:.3f})")
print(f" scVelo dynamical fit_gamma has different semantics")
print(f" Corrected (fit_gamma/fit_beta): r={r_dyn_corr:.3f} β comparable to SS")
print(f" scPTR's methodological contribution over scVelo SS:")
print(f" 1. Per-cell gamma (not just per-gene)")
print(f" 2. Beta estimation + multiplication")
print(f" 3. Two-stage clipping (per-gene + global)")
print(f" 4. Downstream: PT states, PT velocity, networks")
print(f" 5. DeepPTR: uncertainty + disentanglement")
all_results[ds_name] = ds_results
# Figure
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))
# Panel 1: scPTR vs scVelo SS scatter
axes[0].scatter(g2[v], g1[v], alpha=0.1, s=3, c="steelblue")
axes[0].set_xlabel("scVelo SS velocity_gamma")
axes[0].set_ylabel("scPTR median gamma")
axes[0].set_title(f"scPTR β scVelo SS (r={r_sp_sv:.3f})")
axes[0].set_xscale("log"); axes[0].set_yscale("log")
# Panel 2: Half-life comparison bar
mnames = list(methods.keys())
rs = [abs(methods[m][0]) for m in mnames]
colors = ["darkorange", "steelblue", "lightcoral", "seagreen"]
axes[1].barh(mnames, rs, color=colors[:len(mnames)], alpha=0.7)
axes[1].set_xlabel("|Spearman r| with half-life")
axes[1].set_title(f"{ds_name}: Corrected comparison")
# Panel 3: SS vs dyn scatter
axes[2].scatter(vg_sh[v2], fg_sh[v2], alpha=0.1, s=3, c="gray")
axes[2].set_xlabel("scVelo SS velocity_gamma")
axes[2].set_ylabel("scVelo dyn fit_gamma")
axes[2].set_title(f"SS vs dyn gamma (r={r_ss_dyn:.3f})")
axes[2].set_xscale("log"); axes[2].set_yscale("log")
fig.suptitle(f"{ds_name}: Gamma semantics", y=1.02)
fig.tight_layout()
save_fig(fig, f"{ds_name}_gamma_semantics", OUT)
save_json(all_results, "gamma_semantics", OUT)
if __name__ == "__main__":
main()
|