60 分钟真实数据快照已执行的网页阅读版

检验响应候选基因的通路富集

定义实际测量的背景集合,用超几何模型检验真实 Reactome 通路,并对完整检验族校正。

生物信息学 · 第 04 课🔋 离线核心分析⏱ 60 分钟内嵌真实数据📦 numpy · pandas · scipy · matplotlib · networkx · IPython

目标与运行方法

本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。

学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。

环境与参数

In [1] · Python
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from scipy.stats import hypergeom
import networkx as nx
RECIPE_ID='04-reactome-enrichment'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
                      "axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6
Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads
Core data mode: embedded snapshot; no network requests

读取内嵌快照

In [2] · Python
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
    """Read embedded data from this notebook, including a renamed copy."""
    for path in sorted(Path.cwd().glob("*.ipynb")):
        try:
            notebook = json.loads(path.read_text(encoding="utf-8"))
            metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
            blob = metadata.get("snapshots", {}).get(snapshot_key)
            if metadata.get("recipe_id") != recipe_id or not blob:
                continue
            if blob.get("sha256") != expected_sha256:
                continue
            raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
            if hashlib.sha256(raw).hexdigest() != expected_sha256:
                raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
            return json.loads(raw)
        except (OSError, json.JSONDecodeError):
            continue
    raise FileNotFoundError(
        "Save/open the complete .ipynb in the active working folder. "
        "The embedded snapshot could not be located for " + recipe_id)

SNAPSHOT_HASHES={'airway': '5440d5e8dc3a3da15735ad2592cf908bcfc7f4bdadc89b1404aa48d728bdeaab', 'reactome': '7ac8938695d294fb8c8b56072fca97d7420fafb2f436c6437a8ef72b7c3bfbe9'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['airway', 'reactome']

方法函数

In [3] · Python
def validate_counts(counts, metadata):
    if not counts.index.is_unique or not counts.columns.is_unique:
        raise ValueError("Gene and sample identifiers must be unique")
    if not metadata.index.is_unique or list(counts.columns) != list(metadata.index):
        raise ValueError("Sample columns must match metadata order exactly")
    values = counts.to_numpy()
    if not np.isfinite(values).all() or (values < 0).any() or not np.equal(values,np.floor(values)).all():
        raise ValueError("Counts must be finite nonnegative integers")
    if (values.sum(axis=0) == 0).any():
        raise ValueError("A sample has no reads")
    if set(metadata.condition) != {"control","treated"}:
        raise ValueError("Expected control and treated conditions")
    for donor, rows in metadata.groupby("donor", sort=True):
        if len(rows) != 2 or set(rows.condition) != {"control","treated"}:
            raise ValueError("Each donor needs one control and one treated sample: " + str(donor))

print("Method ready: validate_counts")
Method ready: validate_counts
In [4] · Python
def normalize_counts(counts, min_count=10, min_samples=4):
    values = counts.to_numpy(dtype=float)
    keep = (values >= min_count).sum(axis=1) >= min_samples
    filtered = counts.loc[keep].astype(float)
    positive = (filtered.to_numpy() > 0).all(axis=1)
    if positive.sum() == 0:
        raise ValueError("Median-ratio method needs genes positive in every sample")
    reference_values = filtered.to_numpy()[positive]
    geometric_means = np.exp(np.log(reference_values).mean(axis=1))
    factors = np.median(reference_values / geometric_means[:,None], axis=0)
    if not np.isfinite(factors).all() or (factors <= 0).any():
        raise ValueError("Invalid size factors")
    factors /= np.exp(np.log(factors).mean())
    normalized = filtered / factors
    return filtered, pd.Series(factors,index=counts.columns,name="size_factor"), normalized

print("Method ready: normalize_counts")
Method ready: normalize_counts
In [5] · Python
def paired_response(log_counts, metadata, min_effect=1.0, min_consistent=3):
    differences = {}
    for donor, rows in metadata.groupby("donor",sort=True):
        control = rows.index[rows.condition == "control"][0]
        treated = rows.index[rows.condition == "treated"][0]
        differences[donor] = log_counts[treated] - log_counts[control]
    differences = pd.DataFrame(differences)
    response = pd.DataFrame(dict(mean_log2_change=differences.mean(axis=1),
                                 positive_pairs=(differences>0).sum(axis=1),
                                 negative_pairs=(differences<0).sum(axis=1)))
    response["direction"] = "other"
    response.loc[(response.mean_log2_change>=min_effect)&(response.positive_pairs>=min_consistent),"direction"]="up"
    response.loc[(response.mean_log2_change<=-min_effect)&(response.negative_pairs>=min_consistent),"direction"]="down"
    return differences, response

print("Method ready: paired_response")
Method ready: paired_response
In [6] · Python
def bh_adjust(pvalues):
    p = np.asarray(pvalues,dtype=float)
    if not len(p):
        return p.copy()
    if not np.isfinite(p).all() or (p<0).any() or (p>1).any():
        raise ValueError("p-values must be finite and in [0,1]")
    order = np.argsort(p,kind="stable")
    adjusted = np.minimum.accumulate((p[order]*len(p)/np.arange(1,len(p)+1))[::-1])[::-1]
    result = np.empty_like(p); result[order] = np.minimum(adjusted,1)
    return result

print("Method ready: bh_adjust")
Method ready: bh_adjust
In [7] · Python
def overrepresentation(up, down, expressed, pathways, min_size=10, max_size=500):
    annotated = set().union(*(set(row["genes"]) for row in pathways)) if pathways else set()
    background = set(expressed) & annotated
    columns = ["direction","pathway","name","background_size","query_size","term_size","overlap","pvalue","genes","qvalue"]
    rows = []
    for direction, supplied in (("up",up),("down",down)):
        query = set(supplied) & background
        for term in pathways:
            members = set(term["genes"]) & background
            if not min_size <= len(members) <= max_size:
                continue
            hits = sorted(query & members)
            p = float(hypergeom.sf(len(hits)-1,len(background),len(members),len(query))) if query else 1.0
            rows.append(dict(direction=direction,pathway=term["id"],name=term["name"],
                background_size=len(background),query_size=len(query),term_size=len(members),
                overlap=len(hits),pvalue=p,genes=";".join(hits)))
    result = pd.DataFrame(rows)
    if len(result):
        result["qvalue"] = bh_adjust(result.pvalue.to_numpy())
        result = result.sort_values(["qvalue","pvalue","pathway","direction"],kind="stable")
    else:
        result = pd.DataFrame(columns=columns)
    return background,result

print("Method ready: overrepresentation")
Method ready: overrepresentation

分步分析

1. 独立重建候选基因分析

本 notebook 包含完整 airway 矩阵,无需先运行第 03 课或读取其输出。实验来源与供体配对关系保持一致,先核对 8 个样本标识符。只有基因列表、没有筛选规则及实际测量背景,不能支持可靠的富集分析。

In [8] · Python
airway = snapshot("airway")
counts = pd.DataFrame(airway["counts"], index=airway["genes"], columns=airway["samples"])
metadata = pd.DataFrame(airway["metadata"]).set_index("sample").loc[counts.columns]
validate_counts(counts, metadata)
print("Complete count matrix:", counts.shape)
def display_scroll_table(frame):
    # Keep identifiers and numeric values intact on narrow device screens.
    table = frame.to_html(border=0, classes="vibeit-scroll-table")
    style = """<style>
    table.vibeit-scroll-table {table-layout:auto !important;width:max-content !important;max-width:none !important;}
    table.vibeit-scroll-table th, table.vibeit-scroll-table td {
      white-space:nowrap !important;word-break:normal !important;padding:6px 8px;}
    </style>"""
    display(HTML(style+'<div style="max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch">'+table+'</div>'))
display_scroll_table(metadata)
display_scroll_table(counts.iloc[:6])
Complete count matrix: (63677, 8)
condition donor geo
SRR1039508 control N61311 GSM1275862
SRR1039509 treated N61311 GSM1275863
SRR1039512 control N052611 GSM1275866
SRR1039513 treated N052611 GSM1275867
SRR1039516 control N080611 GSM1275870
SRR1039517 treated N080611 GSM1275871
SRR1039520 control N061011 GSM1275874
SRR1039521 treated N061011 GSM1275875
SRR1039508 SRR1039509 SRR1039512 SRR1039513 SRR1039516 SRR1039517 SRR1039520 SRR1039521
ENSG00000000003 679 448 873 408 1138 1047 770 572
ENSG00000000005 0 0 0 0 0 0 0 0
ENSG00000000419 467 515 621 365 587 799 417 508
ENSG00000000457 260 211 263 164 245 331 233 229
ENSG00000000460 60 55 40 35 78 63 76 60
ENSG00000000938 0 0 2 0 1 0 0 0

2. 筛选候选前进行归一化

采用第 03 课相同的过滤及 median-ratio 方法。候选应从测量结果中筛选,而不是看过通路结果之后再选择。先挑出某通路的基因,再检验该通路是否富集,会产生循环论证。

In [9] · Python
MIN_COUNT, MIN_SAMPLES = 10, 4
filtered, size_factors, normalized = normalize_counts(counts, MIN_COUNT, MIN_SAMPLES)
log_counts = np.log2(normalized + 1)
print("Retained genes:", len(filtered), "of", len(counts))
print("Genes positive in every sample:", int((filtered > 0).all(axis=1).sum()))
display(size_factors.to_frame())
Retained genes: 16139 of 63677
Genes positive in every sample: 16072
size_factor
SRR1039508 1.019000
SRR1039509 0.893462
SRR1039512 1.170560
SRR1039513 0.666422
SRR1039516 1.171945
SRR1039517 1.395186
SRR1039520 0.913847
SRR1039521 0.942312

3. 明确写出筛选规则

上调候选要求平均配对 log2 变化 ≥1,且至少 3 个供体差值为正;下调候选要求平均值 ≤−1,且至少 3 个供体差值为负。这是按效应筛选的响应候选,不是经过统计检验的 DEG。散点图同时展示选中与未选中的基因,使阈值保持可见。

In [10] · Python
paired_changes,response=paired_response(log_counts,metadata,min_effect=1,min_consistent=3)
up=set(response.index[response.direction=="up"])
down=set(response.index[response.direction=="down"])
fig,ax=plt.subplots(figsize=(7,4))
for direction,color in [("other","#bdc5c0"),("up","#bc6541"),("down","#416aa6")]:
    selected=response.direction==direction
    ax.scatter(normalized.mean(axis=1)[selected],response.mean_log2_change[selected],s=8,alpha=.45,color=color,label=direction)
ax.set_xscale("log");ax.axhline(1,color="#888",ls="--",lw=.7);ax.axhline(-1,color="#888",ls="--",lw=.7)
ax.set(xlabel="Mean normalized count",ylabel="Mean paired log2 change",title="Effect screening before pathway testing")
ax.legend();plt.tight_layout();plt.show()
print("Up / down / retained:",len(up),len(down),len(filtered))
Computed figure from this code cell
Up / down / retained: 527 529 16139

4. 用实际测量定义背景集合

冻结的 Reactome 映射包含人类 Ensembl 基因与底层通路归属。背景定义为通过表达过滤且被这些注释覆盖的基因,未映射候选需要报告。不能将所有人类基因作为已过滤实验的背景。通路大小规则为在该背景内有 10–500 个成员,该规则在查看结果前确定。

In [11] · Python
reactome=snapshot("reactome")
pathways=reactome["pathways"]
annotated=set().union(*(set(term["genes"]) for term in pathways))
background=set(filtered.index)&annotated
print("Reactome version:",reactome["version"])
audit=pd.DataFrame([dict(stage="Measured",genes=len(counts)),dict(stage="Expression filter",genes=len(filtered)),
 dict(stage="Annotated background",genes=len(background)),dict(stage="Up candidates mapped",genes=len(up&background)),
 dict(stage="Down candidates mapped",genes=len(down&background))])
display(audit)
print("Unmapped up / down candidates:",len(up-background),len(down-background))
Reactome version: 97
stage genes
0 Measured 63677
1 Expression filter 16139
2 Annotated background 7980
3 Up candidates mapped 288
4 Down candidates mapped 272
Unmapped up / down candidates: 239 257

5. 检验全部合格通路并校正完整检验族

若背景有 N 个基因、通路有 K 个成员、候选有 n 个基因、重叠有 k 个基因,则单侧 p 值为 P(X≥k),使用 hypergeom.sf(k−1,N,K,n)。零重叠通路也纳入检验(p=1),再对两个方向的全部检验共同进行 BH 校正。q 值不是通路被激活的概率;表达筛选与通路之间的相关性也限制了结果解释。

In [12] · Python
background,enrichment=overrepresentation(up,down,filtered.index,pathways,min_size=10,max_size=500)
print("Complete test family:",len(enrichment))
print("Terms with q < 0.05:",int((enrichment.qvalue<.05).sum()) if len(enrichment) else 0)
display(enrichment.drop(columns="genes").head(12))
Complete test family: 2086
Terms with q < 0.05: 5
direction pathway name background_size query_size term_size overlap pvalue qvalue
1436 down R-HSA-418594 G alpha (i) signalling events 7980 272 85 13 0.000005 0.009649
661 up R-HSA-71384 Ethanol oxidation 7980 288 10 5 0.000013 0.009649
1427 down R-HSA-416476 G alpha (q) signalling events 7980 272 67 11 0.000014 0.009649
1641 down R-HSA-6785807 Interleukin-4 and Interleukin-13 signaling 7980 272 74 11 0.000037 0.019049
1437 down R-HSA-418597 G alpha (z) signalling events 7980 272 33 7 0.000099 0.041178
828 up R-HSA-9615017 FOXO-mediated transcription of oxidative stres... 7980 288 19 5 0.000453 0.123393
348 up R-HSA-381426 Regulation of Insulin-like Growth Factor (IGF)... 7980 288 79 10 0.000509 0.123393
1594 down R-HSA-5669034 TNFs bind their physiological receptors 7980 272 12 4 0.000526 0.123393
1376 down R-HSA-375276 Peptide ligand-binding receptors 7980 272 31 6 0.000532 0.123393
292 up R-HSA-3000157 Laminin interactions 7980 288 30 6 0.000599 0.124920
744 up R-HSA-8948216 Collagen chain trimerization 7980 288 31 6 0.000720 0.136611
255 up R-HSA-2214320 Anchoring fibril formation 7980 288 13 4 0.000918 0.143979

6. 阅读富集气泡图

最多展示 12 个非零重叠通路,按 q 值及 p 值排序。气泡面积表示重叠基因数,横轴为 overlap/query size,颜色为 −log10(q)。即使没有 q 值达到 0.05,也保留描述性标签。排序图总会有第一行,排名靠前本身不等同于显著。

In [13] · Python
plot_terms=enrichment.loc[enrichment.overlap>0].head(12).copy() if len(enrichment) else enrichment.copy()
fig,ax=plt.subplots(figsize=(9,5))
if len(plot_terms):
    colors=-np.log10(plot_terms.qvalue.clip(lower=1e-300))
    dots=ax.scatter(plot_terms.overlap/plot_terms.query_size,np.arange(len(plot_terms)),
        s=30+18*plot_terms.overlap,c=colors,cmap="YlGnBu")
    names=[r.direction+": "+r.name[:62] for r in plot_terms.itertuples()]
    ax.set(yticks=range(len(names)),yticklabels=names);ax.invert_yaxis()
    fig.colorbar(dots,ax=ax,label="−log10(BH q)")
else:
    ax.text(.5,.5,"No eligible terms with overlapping candidates",ha="center",va="center",transform=ax.transAxes)
ax.set(xlabel="Overlap / mapped candidate count",title="Reactome overrepresentation: effect-screened candidates")
plt.tight_layout();plt.show()
Computed figure from this code cell

7. 通过共享基因观察通路冗余

相关通路可能共享大部分证据。对展示的 12 个通路计算候选重叠集合的 Jaccard 相似度,即交集/并集。这幅网络描述结果的冗余,不代表蛋白互作。Jaccard ≥0.2 才连边,节点颜色表示候选集合方向。

In [14] · Python
term_graph=nx.Graph()
sets={}
for r in plot_terms.itertuples():
    key=r.direction+":"+r.pathway;sets[key]=set(r.genes.split(";"))
    term_graph.add_node(key,label=r.name[:28],direction=r.direction)
keys=list(sets)
for i,x in enumerate(keys):
    for y in keys[i+1:]:
        similarity=len(sets[x]&sets[y])/len(sets[x]|sets[y]) if sets[x]|sets[y] else 0
        if similarity>=.2:term_graph.add_edge(x,y,jaccard=similarity)
fig,ax=plt.subplots(figsize=(8,5))
if term_graph:
    pos=nx.spring_layout(term_graph,seed=42)
    nx.draw_networkx(term_graph,pos,ax=ax,labels=nx.get_node_attributes(term_graph,"label"),
        node_color=["#bde3d1" if term_graph.nodes[k]["direction"]=="up" else "#c5d6ee" for k in term_graph],
        node_size=650,font_size=8,edge_color="#a4b4ac")
else:ax.text(.5,.5,"No terms to compare",ha="center",transform=ax.transAxes)
ax.set_title("Shared candidate genes between displayed pathways");ax.axis("off")
plt.tight_layout();plt.show()
Computed figure from this code cell

8. 导出全部检验,包括不显著结果

导出保留两个方向的所有合格通路,不仅保留展示或显著的行。同时导出背景标识符及候选筛选的测量值。候选为空时,检验 p 值均为 1;空背景或缺失注释是需要报告的分析限制,不能为了得到预期结果而反复修改背景。

In [15] · Python
if len(enrichment):
    assert enrichment.qvalue.between(0,1).all()
    assert (enrichment.qvalue+1e-15>=enrichment.pvalue).all()
    assert (enrichment.overlap<=enrichment.term_size).all()
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
enrichment.to_csv(OUTPUT_DIR/"reactome-all-tests.csv",index=False)
response.to_csv(OUTPUT_DIR/"response-screen.csv")
pd.Series(sorted(background),name="ensembl_gene").to_csv(OUTPUT_DIR/"background.csv",index=False)
audit.to_csv(OUTPUT_DIR/"universe-audit.csv",index=False)
print("Checks passed; full background and test family exported")
Checks passed; full background and test family exported

自主练习与参考答案

  1. 第一行结果中的 N、K、n、k 各表示什么?为什么 BH 应包含所有检验通路?
  2. 将 min_effect 改为 2,重新运行完整检验族,保留零重叠通路。

参考解释。 N 是有注释的实测背景,K 是该背景内通路大小,n 是该方向已映射候选数,k 是重叠数。只校正有趣的通路会低估多重检验。更严格筛选改变的不只是候选数,也改变研究问题。

先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。

可选的 AI 编程练习

提示词 1

在显示表中添加 N/K/n/k,用概率质量函数求和独立核验一个超几何尾概率,保持实测背景与双方向 BH 检验族。

提示词 2

比较所有实测且有注释基因与通过表达过滤且有注释基因作为背景的敏感性;明确说明它们是不同背景,p 值回答不同问题。

人工验收。 检查候选均属于背景;零重叠 p=1;空候选 p=q=1;q 位于 [0,1];所有合格检验保留在导出表。

检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。

可选联网扩展

In [16] · Python
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://reactome.org/ContentService/data/database/version',timeout=20)
        response.raise_for_status()
        print("Source:",response.url)
        print(response.text[:700])
    except requests.RequestException as error:
        print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
    print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged

来源、快照与下一步

  • airway read counts — LGPL (airway package); original experiment: Himes et al. 2014; retrieved 2026-10-09. SHA-256 98d05e82867ae4bed92295eb8863ab6c1aa694876f2b29b455c30a559f801cd0. rdata 1.1.0: assays.data.listData['counts'], rowRanges.partitioning.NAMES, colData; no scaling or gene subsetting

  • Reactome human Ensembl pathway mapping — CC0 1.0; retrieved 2026-10-09. SHA-256 34852dc2ac258a851bad914708c0045a636161dbbeca604f04343174cca8d7fb. Lowest-level pathways; Homo sapiens; retain genes present in the complete airway matrix; deduplicate memberships

下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。