| |
| """Half-life ceiling analysis: is r=-0.40 near the theoretical maximum? |
| |
| Compares published half-life datasets against EACH OTHER to establish |
| inter-study agreement. If Herzog and Schofield agree at r=0.5-0.6, |
| then r=0.40 is 67-80% of the ceiling β near-optimal. |
| """ |
| from _common import * |
|
|
| OUT = output_dir("18_halflife_ceiling") |
|
|
|
|
| def main(): |
| set_figure_style() |
|
|
| hl_mouse, hl_human = load_halflife_refs() |
|
|
| |
| print("=" * 60) |
| print("INTER-STUDY HALF-LIFE AGREEMENT") |
| print("=" * 60) |
|
|
| hl_m = hl_mouse.set_index("gene_symbol")["half_life_hours"] |
| hl_h = hl_human.set_index("gene_symbol")["half_life_hours"] |
|
|
| |
| m_upper = {g.upper(): g for g in hl_m.index if isinstance(g, str)} |
| h_upper = {g.upper(): g for g in hl_h.index if isinstance(g, str)} |
| shared = set(m_upper.keys()) & set(h_upper.keys()) |
|
|
| m_vals = np.array([hl_m[m_upper[u]] for u in shared], dtype=float) |
| h_vals = np.array([hl_h[h_upper[u]] for u in shared], dtype=float) |
| valid = np.isfinite(m_vals) & np.isfinite(h_vals) & (m_vals > 0) & (h_vals > 0) |
| m_vals, h_vals = m_vals[valid], h_vals[valid] |
|
|
| r_inter, p_inter = stats.spearmanr(m_vals, h_vals) |
| r_inter_log, p_inter_log = stats.pearsonr(np.log(m_vals), np.log(h_vals)) |
|
|
| print(f"\n Herzog (mouse) vs Schofield (human):") |
| print(f" Shared genes: {len(m_vals)}") |
| print(f" Spearman r = {r_inter:.4f} (p={p_inter:.2e})") |
| print(f" Pearson r (log) = {r_inter_log:.4f}") |
|
|
| |
| print(f"\n Ceiling analysis:") |
| print(f" Inter-study agreement (ceiling): |r| = {abs(r_inter):.4f}") |
|
|
| methods = { |
| "scPTR analytical (pancreas, human)": -0.4021, |
| "scPTR analytical (DG, human)": -0.3812, |
| "scVelo SS (pancreas, human)": -0.3730, |
| "scVelo SS (DG, human)": -0.3675, |
| "velVI (pancreas, human)": -0.2783, |
| "velVI (DG, human)": -0.3522, |
| "DeepPTR (pancreas, human)": -0.2767, |
| "DeepPTR (DG, human)": -0.3577, |
| } |
|
|
| print(f"\n {'Method':<45} {'|r|':>6} {'% ceiling':>10}") |
| print(" " + "-" * 65) |
| for name, r in sorted(methods.items(), key=lambda x: abs(x[1]), reverse=True): |
| pct = abs(r) / abs(r_inter) * 100 |
| print(f" {name:<45} {abs(r):.4f} {pct:>9.1f}%") |
|
|
| |
| rng = np.random.RandomState(42) |
| boot_rs = [] |
| for _ in range(1000): |
| idx = rng.choice(len(m_vals), len(m_vals), replace=True) |
| boot_rs.append(stats.spearmanr(m_vals[idx], h_vals[idx]).statistic) |
| ci_lo, ci_hi = np.percentile(boot_rs, [2.5, 97.5]) |
|
|
| print(f"\n Inter-study 95% CI: [{ci_lo:.4f}, {ci_hi:.4f}]") |
|
|
| |
| print(f"\n--- Split-half reliability of scPTR gamma ---") |
| for ds_name, loader, ck in DATASETS: |
| adata = run_analytical(loader) |
| gamma = adata.layers["gamma"] |
|
|
| rng = np.random.RandomState(0) |
| n = adata.n_obs |
| perm = rng.permutation(n) |
| half1 = perm[:n // 2] |
| half2 = perm[n // 2:] |
|
|
| med1 = np.median(gamma[half1], axis=0) |
| med2 = np.median(gamma[half2], axis=0) |
|
|
| valid = (med1 > 0) & (med2 > 0) & np.isfinite(med1) & np.isfinite(med2) |
| r_split, _ = stats.spearmanr(med1[valid], med2[valid]) |
|
|
| |
| r_full = 2 * r_split / (1 + r_split) |
|
|
| print(f" {ds_name}: split-half r={r_split:.4f}, Spearman-Brown corrected={r_full:.4f}") |
|
|
| results = { |
| "inter_study_r": float(r_inter), |
| "inter_study_r_log": float(r_inter_log), |
| "inter_study_n": int(len(m_vals)), |
| "inter_study_ci": [float(ci_lo), float(ci_hi)], |
| "method_ceiling_pct": {k: abs(v) / abs(r_inter) * 100 for k, v in methods.items()}, |
| } |
| save_json(results, "halflife_ceiling", OUT) |
|
|
| |
| fig, axes = plt.subplots(1, 2, figsize=(12, 5)) |
|
|
| |
| axes[0].scatter(np.log10(m_vals), np.log10(h_vals), alpha=0.1, s=5, c="gray") |
| axes[0].set_xlabel("log10(Herzog mouse half-life)") |
| axes[0].set_ylabel("log10(Schofield human half-life)") |
| axes[0].set_title(f"Inter-study agreement (r={r_inter:.3f}, n={len(m_vals)})") |
|
|
| |
| names_short = [n.split("(")[0].strip() for n in methods] |
| pcts = [abs(v) / abs(r_inter) * 100 for v in methods.values()] |
| colors = ["darkorange" if "scPTR" in n else "steelblue" if "scVelo" in n |
| else "seagreen" if "velVI" in n else "gray" for n in methods] |
| axes[1].barh(range(len(methods)), pcts, color=colors, alpha=0.7) |
| axes[1].set_yticks(range(len(methods))) |
| axes[1].set_yticklabels(list(methods.keys()), fontsize=7) |
| axes[1].set_xlabel("% of inter-study ceiling") |
| axes[1].set_title("Method performance relative to ceiling") |
| axes[1].axvline(100, color="red", ls="--", alpha=0.3, label="Ceiling") |
| axes[1].legend() |
|
|
| fig.tight_layout() |
| save_fig(fig, "halflife_ceiling", OUT) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|