| |
| """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) |
|
|
| |
| 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)})") |
|
|
| |
| _, hl_human = load_halflife_refs() |
| g_hl, h_hl, names = match_halflife(adata, hl_human) |
|
|
| |
| 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) |
|
|
| |
| |
| 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) |
|
|
| |
| 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}") |
|
|
| |
| 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) |
|
|
| |
| 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})") |
|
|
| |
| 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() |
|
|