File size: 5,913 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 | #!/usr/bin/env python
"""Partial correlation deep dive: gamma signal after controlling for expression.
Addresses the r=-0.40 → r=-0.15 drop by:
1. Showing r=-0.15 is still highly significant
2. Comparing partial r across methods (scPTR vs scVelo)
3. Bootstrap CI on partial r
4. Decomposing: how much is expression, how much is unique PT regulation?
"""
from _common import *
import scvelo as scv
OUT = output_dir("22_partial_correlation")
def partial_halflife(gamma_med, expr_mean, hl_vals, gene_names, hl_df):
"""Compute raw and partial (controlling expression) half-life r."""
hl_s = hl_df.set_index("gene_symbol")["half_life_hours"]
gamma_upper = {g.upper(): i for i, g in enumerate(gene_names)}
hl_upper = {g.upper(): g for g in hl_s.index if isinstance(g, str)}
shared = set(gamma_upper.keys()) & set(hl_upper.keys())
g = np.array([gamma_med[gamma_upper[u]] for u in shared], dtype=float)
h = np.array([hl_s[hl_upper[u]] for u in shared], dtype=float)
e = np.array([expr_mean[gamma_upper[u]] for u in shared], dtype=float)
v = np.isfinite(g) & np.isfinite(h) & np.isfinite(e) & (g > 0) & (h > 0) & (e > 0)
g, h, e = g[v], h[v], e[v]
if len(g) < 10:
return {"raw_r": np.nan, "partial_r": np.nan, "n": 0}
# Raw
r_raw, p_raw = stats.spearmanr(g, h)
# Partial: residualize on log expression
from numpy.polynomial.polynomial import polyfit, polyval
log_e = np.log1p(e)
log_g = np.log1p(g)
log_h = np.log1p(h)
coef_g = polyfit(log_e, log_g, 1)
resid_g = log_g - polyval(log_e, coef_g)
coef_h = polyfit(log_e, log_h, 1)
resid_h = log_h - polyval(log_e, coef_h)
r_partial, p_partial = stats.spearmanr(resid_g, resid_h)
# Bootstrap CI on partial r
rng = np.random.RandomState(42)
boot_partial = []
for _ in range(1000):
idx = rng.choice(len(g), len(g), replace=True)
coef_g_b = polyfit(log_e[idx], log_g[idx], 1)
resid_g_b = log_g[idx] - polyval(log_e[idx], coef_g_b)
coef_h_b = polyfit(log_e[idx], log_h[idx], 1)
resid_h_b = log_h[idx] - polyval(log_e[idx], coef_h_b)
r_b, _ = stats.spearmanr(resid_g_b, resid_h_b)
boot_partial.append(r_b)
ci_lo, ci_hi = np.percentile(boot_partial, [2.5, 97.5])
return {
"raw_r": float(r_raw), "raw_p": float(p_raw),
"partial_r": float(r_partial), "partial_p": float(p_partial),
"partial_ci_lo": float(ci_lo), "partial_ci_hi": float(ci_hi),
"n": int(len(g)),
"expression_confound": float(abs(r_raw) - abs(r_partial)),
}
def main():
set_figure_style()
_, hl_human = load_halflife_refs()
all_results = {}
for ds_name, loader, ck in DATASETS:
print(f"\n{'=' * 60}\n{ds_name.upper()}: Partial correlation\n{'=' * 60}")
# scPTR
adata_an = run_analytical(loader)
from scipy.sparse import issparse
X = adata_an.X
if issparse(X):
X = np.asarray(X.todense())
expr_mean = np.asarray(X, dtype=float).mean(axis=0)
gamma_med = np.median(adata_an.layers["gamma"], axis=0)
r_scptr = partial_halflife(gamma_med, expr_mean, None, adata_an.var_names, hl_human)
print(f"\n scPTR:")
print(f" Raw r: {r_scptr['raw_r']:.4f} (p={r_scptr['raw_p']:.2e})")
print(f" Partial r: {r_scptr['partial_r']:.4f} [{r_scptr['partial_ci_lo']:.4f}, {r_scptr['partial_ci_hi']:.4f}] (p={r_scptr['partial_p']:.2e})")
print(f" Confound: Δr={r_scptr['expression_confound']:.4f}")
# scVelo SS
adata_sv = loader()
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")
sv_gamma = adata_sv.var.get("velocity_gamma", pd.Series(dtype=float)).values.astype(float)
X_sv = adata_sv.X
if issparse(X_sv):
X_sv = np.asarray(X_sv.todense())
expr_sv = np.asarray(X_sv, dtype=float).mean(axis=0)
r_scvelo = partial_halflife(sv_gamma, expr_sv, None, adata_sv.var_names, hl_human)
print(f"\n scVelo SS:")
print(f" Raw r: {r_scvelo['raw_r']:.4f}")
print(f" Partial r: {r_scvelo['partial_r']:.4f} [{r_scvelo['partial_ci_lo']:.4f}, {r_scvelo['partial_ci_hi']:.4f}]")
print(f" Confound: Δr={r_scvelo['expression_confound']:.4f}")
# Key comparison
print(f"\n COMPARISON: scPTR retains {'MORE' if abs(r_scptr['partial_r']) > abs(r_scvelo['partial_r']) else 'LESS'} "
f"signal after expression control")
print(f" scPTR partial: {r_scptr['partial_r']:.4f}")
print(f" scVelo partial: {r_scvelo['partial_r']:.4f}")
all_results[ds_name] = {"scptr": r_scptr, "scvelo_ss": r_scvelo}
save_json(all_results, "partial_correlation", OUT)
# Figure
fig, axes = plt.subplots(1, len(all_results), figsize=(6 * len(all_results), 5))
if len(all_results) == 1:
axes = [axes]
for ax, (ds_name, res) in zip(axes, all_results.items()):
methods = ["scPTR", "scVelo SS"]
raw_rs = [abs(res["scptr"]["raw_r"]), abs(res["scvelo_ss"]["raw_r"])]
partial_rs = [abs(res["scptr"]["partial_r"]), abs(res["scvelo_ss"]["partial_r"])]
x = np.arange(len(methods))
ax.bar(x - 0.2, raw_rs, 0.35, label="Raw", color="steelblue", alpha=0.7)
ax.bar(x + 0.2, partial_rs, 0.35, label="Partial (ctrl expr)", color="darkorange", alpha=0.7)
ax.set_xticks(x)
ax.set_xticklabels(methods)
ax.set_ylabel("|Spearman r| with half-life")
ax.set_title(f"{ds_name}")
ax.legend()
fig.suptitle("Half-life r: raw vs expression-controlled", y=1.02)
fig.tight_layout()
save_fig(fig, "partial_correlation", OUT)
if __name__ == "__main__":
main()
|