Files
RGC-ADOA/script/10_rgc2_signature.py
rain 092f39a662 chore: 初始化 RGC-ADOA 分析仓库
纳入 script/doc/ref/output 及配置;忽略 data/(26G 原始/中间数据)。
git 身份:rain <wjs_Rain@126.com>。
2026-09-18 18:21:19 +08:00

139 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
10_rgc2_signature.py — P1-② RGC-2 重定义:作者 DEG 签名打分法(替代"WT ETC top30%"循环定义)
签名构建(数据文件 S2,仅用 WT 检出率,不用疾病 DE 信息):
RGC-2 身份基因 = pct.WT(RGC-2) >= 0.5 且 pct.WT(RGC-2) - pct.WT(RGC-1) >= 0.25,按差值取 top 100。
RGC-1 签名对称构建。分类器 = score(RGC2签名) - score(RGC1签名)>0 判为 RGC-2-like。
⚠️ 已知局限:RGC-2 的身份基因(微管/ETC/核糖体)在突变 RGC-2 中被强烈下调,
无法剔除疾病 DE 基因(剔了签名只剩 2 个基因)——突变细胞的分类因此偏向"保留身份"的较健康细胞,
会低估效应量,解读时注意。
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scanpy as sc
OUT = Path("output/10_rgc2_signature")
OUT.mkdir(parents=True, exist_ok=True)
plt.rcParams.update({"figure.dpi": 300, "savefig.dpi": 300, "font.size": 8})
# ---- 1. 从作者 S2 构建身份签名(pct.WT only,不用 DE 过滤) ----
r1 = pd.read_excel("ref/Data files/S2. snRNAseq-DEG_All_Celltype.xlsx", sheet_name="RGC-1")
r2 = pd.read_excel("ref/Data files/S2. snRNAseq-DEG_All_Celltype.xlsx", sheet_name="RGC-2")
for df in (r1, r2):
df.columns = [c.lower() for c in df.columns]
m = r1.merge(r2, on="gene", suffixes=("_r1", "_r2"))
m["pct_diff_r2"] = m["pct.wt_r2"] - m["pct.wt_r1"]
sig2_df = m[(m["pct.wt_r2"] >= 0.5) & (m["pct_diff_r2"] >= 0.25)].nlargest(100, "pct_diff_r2")
sig1_df = m[(m["pct.wt_r1"] >= 0.5) & (m["pct_diff_r2"] <= -0.25)].nsmallest(100, "pct_diff_r2")
adata = sc.read_h5ad("data/04_scored.h5ad")
sig2 = [g for g in sig2_df["gene"] if g in adata.raw.var_names]
sig1 = [g for g in sig1_df["gene"] if g in adata.raw.var_names]
print(f"RGC-2 签名: {len(sig2_df)} -> 数据中 {len(sig2)} 基因")
print(f"RGC-1 签名: {len(sig1_df)} -> 数据中 {len(sig1)} 基因")
sig2_df.to_csv(OUT / "authors_RG2_signature.csv", index=False)
sig1_df.to_csv(OUT / "authors_RG1_signature.csv", index=False)
# ---- 2. 在全部 RGC 上打分 ----
raw = adata.raw.to_adata()
raw.obs = adata.obs.copy()
rgc_mask = adata.obs["cell_type"] == "RGC"
rgc = raw[rgc_mask].copy()
sc.tl.score_genes(rgc, gene_list=sig2, score_name="sig2", use_raw=False, random_state=0)
sc.tl.score_genes(rgc, gene_list=sig1, score_name="sig1", use_raw=False, random_state=0)
rgc.obs["sig_diff"] = rgc.obs["sig2"] - rgc.obs["sig1"]
rgc.obs["rgc2_sig_group"] = np.where(rgc.obs["sig_diff"] > 0, "RGC2-like", "RGC1-like")
adata.obs["score_RG2_signature"] = np.nan
adata.obs["score_RG1_signature"] = np.nan
adata.obs["rgc2_sig_group"] = "non-RGC"
adata.obs.loc[rgc.obs_names, "score_RG2_signature"] = rgc.obs["sig2"]
adata.obs.loc[rgc.obs_names, "score_RG1_signature"] = rgc.obs["sig1"]
adata.obs.loc[rgc.obs_names, "rgc2_sig_group"] = rgc.obs["rgc2_sig_group"].values
print("\n签名分组核数:")
print(rgc.obs.groupby(["rgc2_sig_group", "genotype"], observed=True).size().unstack(fill_value=0))
print("\n与 P0 ETC 分组重叠:")
print(pd.crosstab(rgc.obs["rgc2_sig_group"], adata.obs.loc[rgc.obs_names, "rgc_group"]))
# ---- 3. 新分组下的模块分比较(复用 04_scored 中的 score_ 列) ----
mods = ["score_ETC_WP295", "score_CI_biogenesis_RE", "score_Glycolysis_WP157",
"score_Ribosomal_WP163", "score_Mitophagy_Autophagy"]
rows = []
for grp in ["RGC2-like", "RGC1-like"]:
gm = adata.obs["rgc2_sig_group"] == grp
for col in mods:
wt = adata.obs.loc[gm & (adata.obs["genotype"] == "WT"), col]
per_s = adata.obs.loc[gm].groupby("sample")[col].median()
s1, s2 = per_s.get("Opa1V291D_S1", np.nan), per_s.get("Opa1V291D_S2", np.nan)
rows.append({"group": grp, "module": col.replace("score_", ""),
"n_WT": int((gm & (adata.obs["genotype"] == "WT")).sum()),
"median_WT": round(wt.median(), 4),
"delta_S1": round(s1 - wt.median(), 4), "delta_S2": round(s2 - wt.median(), 4)})
mod_res = pd.DataFrame(rows)
mod_res.to_csv(OUT / "module_scores_newgrouping.csv", index=False)
print("\n", mod_res.to_string(index=False))
# ---- 4. 基因级一致率(作者 RGC-2 显著下调基因 vs 我们 RGC2-like ----
s2_sig = r2[(r2["p_val"] < 0.001) & (r2["avg_log2fc"] < -0.5)]
rawX = raw.X
vidx = {g: i for i, g in enumerate(raw.var_names)}
gm = adata.obs["rgc2_sig_group"] == "RGC2-like"
wt_m = (gm & (adata.obs["genotype"] == "WT")).values
mu_m = (gm & (adata.obs["genotype"] == "V291D")).values
print(f"\nRGC2-like: WT n={wt_m.sum()}, V291D n={mu_m.sum()}")
gene_rows = []
for g in s2_sig["gene"]:
if g not in vidx:
continue
i = vidx[g]
c_wt = rawX[wt_m, i].mean()
c_mu = rawX[mu_m, i].mean()
gene_rows.append({"gene": g, "author_log2fc": r2.loc[r2["gene"] == g, "avg_log2fc"].iloc[0],
"our_log2fc_approx": np.log2((c_mu + 1e-9) / (c_wt + 1e-9))})
gd = pd.DataFrame(gene_rows)
if len(gd):
concord = (np.sign(gd["author_log2fc"]) == np.sign(gd["our_log2fc_approx"])).mean()
gd.to_csv(OUT / "genelevel_concordance_RGC2like.csv", index=False)
print(f"基因级方向一致率(作者 RGC-2 下调基因 vs 我们 RGC2-like: {concord*100:.1f}% (n={len(gd)})")
# ---- 5. 图 ----
fig, axes = plt.subplots(1, 3, figsize=(12, 3.6))
# (a) sig_diff 分布按基因型
for geno, c in [("WT", "black"), ("V291D", "firebrick")]:
v = rgc.obs.loc[rgc.obs["genotype"] == geno, "sig_diff"]
axes[0].hist(v, bins=50, histtype="step", density=True, color=c, lw=1.4, label=f"{geno} (n={len(v)})")
axes[0].axvline(0, color="grey", ls="--", lw=0.8)
axes[0].set_xlabel("score(RGC-2 sig) - score(RGC-1 sig)")
axes[0].set_ylabel("density"); axes[0].legend(fontsize=7, frameon=False)
axes[0].set_title("Signature score distribution in our RGCs")
# (b) 新旧分组重叠(百分比堆叠)
ct = pd.crosstab(rgc.obs["rgc2_sig_group"], adata.obs.loc[rgc.obs_names, "rgc_group"], normalize="index")
ct.plot(kind="bar", stacked=True, ax=axes[1], color=["#d62728", "#ff9896", "lightgrey"])
axes[1].set_ylabel("fraction"); axes[1].set_title("New signature group vs P0 ETC group")
axes[1].legend(fontsize=6, frameon=False); axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation=0)
# (c) 新分组模块分 delta
x = np.arange(len(mods)); w = 0.35
for off, grp, c in [(-w/2, "RGC2-like", "#d62728"), (w/2, "RGC1-like", "#ff9896")]:
sub = mod_res[mod_res["group"] == grp].set_index("module")
d1 = sub["delta_S1"].reindex([m.replace("score_", "") for m in mods])
d2 = sub["delta_S2"].reindex([m.replace("score_", "") for m in mods])
axes[2].bar(x + off - w/4, d1, width=w/2, color=c, alpha=0.6, label=f"{grp} S1")
axes[2].bar(x + off + w/4, d2, width=w/2, color=c, alpha=1.0, label=f"{grp} S2")
axes[2].set_xticks(x, [m.replace("score_", "").replace("_", "\n") for m in mods], fontsize=6)
axes[2].axhline(0, color="grey", lw=0.6); axes[2].set_ylabel("delta median score vs WT")
axes[2].legend(fontsize=5, frameon=False); axes[2].set_title("Metabolism modules in new groups")
fig.tight_layout()
fig.savefig(OUT / "rgc2_signature_redefinition.png", bbox_inches="tight")
adata.write_h5ad("data/05_rgc2sig.h5ad")
print("Done ->", OUT, "+ data/05_rgc2sig.h5ad")