File size: 4,331 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
#!/usr/bin/env python
"""Expression level vs degradation rate: are they independent?

Tests the degree to which gamma is decorrelated from expression level.
High decorrelation validates that gamma captures post-transcriptional
regulation beyond what's explained by expression abundance.
"""
from _common import *

OUT = output_dir("11_expression_vs_gamma")


def run(name, loader, cluster_key):
    print(f"\n{'=' * 60}\n{name.upper()}: Expression vs gamma\n{'=' * 60}")

    adata = run_analytical(loader)

    from scipy.sparse import issparse
    X = adata.X
    if issparse(X):
        X = np.asarray(X.todense())
    X = np.asarray(X, dtype=np.float32)

    gamma = adata.layers["gamma"]
    gamma_med = np.median(gamma, axis=0)
    expr_mean = X.mean(axis=0)

    # Filter to expressed genes with gamma > 0
    valid = (gamma_med > 0) & (expr_mean > 0) & np.isfinite(gamma_med) & np.isfinite(expr_mean)
    g, e = gamma_med[valid], expr_mean[valid]

    r_expr_gamma, p = stats.spearmanr(np.log1p(e), np.log1p(g))
    print(f"  Expression vs gamma: r={r_expr_gamma:.4f} (p={p:.2e}, n={len(g)})")

    # Does controlling for expression improve half-life correlation?
    _, hl_human = load_halflife_refs()
    g_hl, h_hl, names = match_halflife(adata, hl_human)

    # Get expression for matched genes
    name_to_idx = {n: i for i, n in enumerate(adata.var_names)}
    expr_matched = np.array([expr_mean[name_to_idx[n]] for n in names])

    r_raw, _ = stats.spearmanr(g_hl, h_hl)

    # Partial correlation: gamma vs half-life, controlling for expression
    # Residualize gamma and halflife on expression
    from numpy.polynomial.polynomial import polyfit, polyval
    log_e = np.log1p(expr_matched)
    log_g = np.log1p(g_hl)
    log_h = np.log1p(h_hl)

    # Residualize
    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)
    print(f"  Raw gamma-halflife: r={r_raw:.4f}")
    print(f"  Partial (ctrl expression): r={r_partial:.4f} (p={p_partial:.2e})")
    print(f"  Expression confound: Δr={abs(r_raw) - abs(r_partial):.4f}")

    # Variance decomposition: how much gamma variance is explained by expression?
    from sklearn.linear_model import LinearRegression
    lr = LinearRegression().fit(np.log1p(e).reshape(-1, 1), np.log1p(g))
    r2_expr = lr.score(np.log1p(e).reshape(-1, 1), np.log1p(g))
    print(f"  R² of gamma ~ expression: {r2_expr:.4f} ({r2_expr*100:.1f}% explained)")
    print(f"  Residual gamma variance:  {1 - r2_expr:.4f} ({(1-r2_expr)*100:.1f}% independent)")

    results = {
        "r_expr_gamma": float(r_expr_gamma),
        "r_raw_halflife": float(r_raw),
        "r_partial_halflife": float(r_partial),
        "expression_confound": float(abs(r_raw) - abs(r_partial)),
        "r2_gamma_from_expression": float(r2_expr),
        "n_genes": int(len(g)),
    }
    save_json(results, f"{name}_expr_vs_gamma", OUT)

    # Figure
    fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))

    axes[0].scatter(np.log1p(e), np.log1p(g), alpha=0.05, s=3, c="steelblue")
    axes[0].set_xlabel("log(1 + mean expression)")
    axes[0].set_ylabel("log(1 + median gamma)")
    axes[0].set_title(f"Expression vs gamma (r={r_expr_gamma:.3f})")

    bars = axes[1].bar(["Raw", "Partial\n(ctrl expr)"],
                       [abs(r_raw), abs(r_partial)],
                       color=["steelblue", "darkorange"], alpha=0.7)
    axes[1].set_ylabel("|Spearman r| with half-life")
    axes[1].set_title(f"Expression confound (Δr={abs(r_raw)-abs(r_partial):.3f})")

    # Pie: variance decomposition
    axes[2].pie([r2_expr, 1 - r2_expr],
                labels=[f"Expression\n({r2_expr*100:.0f}%)",
                        f"Independent\n({(1-r2_expr)*100:.0f}%)"],
                colors=["lightcoral", "lightsteelblue"],
                autopct="%1.0f%%", startangle=90)
    axes[2].set_title("Gamma variance decomposition")

    fig.suptitle(f"{name}: Expression vs degradation", y=1.02)
    fig.tight_layout()
    save_fig(fig, f"{name}_expr_vs_gamma", OUT)

    return results


def main():
    set_figure_style()
    for name, loader, ck in DATASETS:
        run(name, loader, ck)


if __name__ == "__main__":
    main()