2026-09 的文章

返回全部文章

20260927 080349 1 2开车旅行

B3894 [NICA #3] 开车旅行

题目描述

Aya 的学校组织了一场春游,春游的目的地是滨江森林公园。由于路途遥远,他们的行程会经过高速和地面道路。

学校所承包的大巴,在高速上是以 $a$ 千米/小时的速度匀速行驶,高速上一共行驶了 $b$ 小时;在地面上是以 $c$ 千米/小时的速度匀速行驶,地面上一共行驶了 $d$ 小时。

Aya 想知道以下两个问题的答案:
- 学校到滨江森林公园的路程有多长?
- 这辆大巴在旅途中的平均速度是多少?

输入格式

输入一行,四个实数 $a,b,c,d$。

输出格式

输出两行两个实数,均保留到小数点后一位。

第一行一个实数,表示学校到滨江森林公园的路程有多长。

第二行一个实数,表示这辆大巴在旅途中的平均速度是多少。

只要你的答案与标准答案的差距不超过 $0.1$,就被判作正确回答。

输入输出样例 #1

输入 #1

60 2 30 1

输出 #1

150.0
50.0

输入输出样例 #2

输入 #2

60 1.5 30 0.5

输出 #2

105.0
52.5

说明/提示

【样例解释】

对于样例 $1$,大巴在高速上以 $60$ 千米/小时的速度行驶 $2$ 小时,再以 $30$ 千米/小时的速度行驶 $1$ 小时,总共行驶了 $150$ 千米。平均速度 $=$ 路程 $\div$ 时间,可得平均速度为 $50$ 千米/小时。如果你输出的是 $149.9$ 和 $50.1$,也会是正确回答。

【数据范围】

对于所有数据,保证 $0 \leq a,b,c,d \leq 1000$,且 $b+d \neq 0$。

题解

下面按信奥赛标准流程,把 B3894 [NICA #3] 开车旅行 走一遍。


1. 审题:三遍读题

任务:
已知大巴:

  • 高速速度 (a),高速时间 (b)
  • 地面速度 (c),地面时间 (d)

求:

  1. 总路程;
  2. 全程平均速度。

输入:
一行四个实数 (a,b,c,d)。

输出:
两行,每行一个实数,均保留到小数点后一位:

  • 第一行:总路程
  • 第二行:平均速度

数据范围:
$(0 \le a,b,c,d \le 1000),且 (b+d \ne 0)。$

样例 1:

输入:

60 2 30 1

高速路程:
$
[
60 \times 2 = 120
]$

地面路程:
$
[
30 \times 1 = 30
]$

总路程:
$
[
120+30=150
]$

总时间:

[
2+1=3
]

平均速度:
$
[
150 \div 3 = 50
]$

输出:

150.0
50.0

2. 复杂度倒推

只有四个数,直接计算。

  • 时间复杂度:(O(1))
  • 空间复杂度:(O(1))

不需要算法优化,属于入门模拟题。


3. 核心公式

总路程:
$
[
S = a \times b + c \times d
]
$
总时间:
$
[
T = b + d
]$

平均速度:
$
[
v = \frac{S}{T}
]$

题目保证 $(b+d \ne 0)$,所以不会除以 0。


4. 伪代码

读入 a, b, c, d
S = a * b + c * d
T = b + d
v = S / T
输出 S,保留一位小数
输出 v,保留一位小数

5. 参考代码

#include <bits/stdc++.h>
using namespace std;

int main() {
    double a, b, c, d;
    cin >> a >> b >> c >> d;

    double distance = a * b + c * d;
    double totalTime = b + d;
    double averageSpeed = distance / totalTime;

    printf("%.1f\n", distance);
    printf("%.1f\n", averageSpeed);

    return 0;
}

如果习惯用 cout,也可以这样写:

#include <bits/stdc++.h>
using namespace std;

int main() {
    double a, b, c, d;
    cin >> a >> b >> c >> d;

    double distance = a * b + c * d;
    double averageSpeed = distance / (b + d);

    cout << fixed << setprecision(1);
    cout << distance << '\n';
    cout << averageSpeed << '\n';

    return 0;
}

6. 测试设计

样例 1

输入:

60 2 30 1

输出:

150.0
50.0

样例 2

输入:

60 1.5 30 0.5

计算:
$
[
60 \times 1.5 = 90
]$
$
[
30 \times 0.5 = 15
]$

总路程:
$
[
105
]$

总时间:

[
1.5+0.5=2
]

平均速度:
$
[
105 \div 2 = 52.5
]$

输出:

105.0
52.5

边界:速度全为 0

输入:

0 1 0 1

输出:

0.0
0.0

边界:只有地面行驶

输入:

0 0 40 2

输出:

80.0
40.0

7. 提交前检查

  • 是否用 double 读入?不要用 int。
  • 是否保留一位小数?用 printf("%.1f") 或 fixed << setprecision(1)。
  • 是否输出两行?
  • 是否把总时间算成 (b+d)?
  • 平均速度是不是总路程除以总时间,而不是两段速度直接平均?
  • 题目允许误差 (0.1),保留一位小数一般没问题。

8. 教练总结

这题核心就一句话:

总路程 = 高速路程 + 地面路程;平均速度 = 总路程 ÷ 总时间。

注意两点:

  1. 输入是实数,必须用 double。
  2. 平均速度不能写成 ((a+c)/2),必须用总路程除以总时间。

代码很短,属于入门送分题。

20260927 075800 计算

B3685 计算

题目描述

给定一个 $3$ 位整数 $n$,计算:
1. $n$ 的各位数字之和;
2. $n$ 的各位数字之和的平方;
3. $n$ 的各位数字之和的立方;

例如,当这个三位数是 $123$ 时:

  1. 各位数字之和为:$1+2+3=6$;
  2. 各位数字之和的平方为:$(1+2+3)^2=(1+2+3)\times (1+2+3)=36$;
  3. 各位数字之和的立方为:$(1+2+3)^3=(1+2+3)\times (1+2+3)\times (1+2+3)=216$。

输入格式

输入只有一行一个三位整数 $n$。

输出格式

输出共三行,第一行为各位数字之和,第二行为各位数字之和的平方,第三行为各位数字之和的立方。

输入输出样例 #1

输入 #1

817

输出 #1

16
256
4096

说明/提示

样例 1 解释

各位数字之和为:$8+1+7=16$;
各位数字之和的平方为:$16^2=16\times 16 = 256$;
各位数字之和的立方为:$16^3 =16\times 16\times 16 = 4096$。

数据规模与约定

对于全部的测试点,保证 $100 \leq n \leq 999$。


下面按信奥赛标准做题流程,把 B3685 [语言月赛 202212] 计算 走一遍。


1. 审题:三遍读题

任务:
给定一个三位整数 (n),求:

  1. 各位数字之和;
  2. 各位数字之和的平方;
  3. 各位数字之和的立方。

输入:
一行一个三位整数 (n)。

输出:
共三行:

  • 第一行:各位数字之和
  • 第二行:各位数字之和的平方
  • 第三行:各位数字之和的立方

数据范围:
$(100 \le n \le 999)$

样例:
输入 817

各位数字之和:
$
[
8+1+7=16
]$

平方:
$
[
16^2=256
]$

立方:
$
[
16^3=4096
]$

所以输出:

16
256
4096

2. 复杂度倒推

这题只有一个三位数,数据规模极小。

  • 时间复杂度:(O(1))
  • 空间复杂度:(O(1))

不需要考虑优化,直接模拟即可。


3. 算法思路

三位整数 (n) 的各位数字可以这样取:

  • 百位:n / 100
  • 十位:n / 10 % 10
  • 个位:n % 10

也可以更通用地用循环取每一位:

int sum = 0;
while (n > 0) {
    sum += n % 10;
    n /= 10;
}

因为题目固定是三位数,两种写法都可以。


4. 伪代码

读入 n
sum = 0
while n > 0:
    sum += n % 10
    n /= 10
输出 sum
输出 sum * sum
输出 sum * sum * sum

5. 参考代码

写法一:循环取位,通用

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    int sum = 0;
    while (n > 0) {
        sum += n % 10;
        n /= 10;
    }

    cout << sum << '\n';
    cout << sum * sum << '\n';
    cout << sum * sum * sum << '\n';

    return 0;
}

写法二:直接取三位数字

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;

    int a = n / 100;       // 百位
    int b = n / 10 % 10;   // 十位
    int c = n % 10;        // 个位

    int sum = a + b + c;

    cout << sum << '\n';
    cout << sum * sum << '\n';
    cout << sum * sum * sum << '\n';

    return 0;
}

6. 测试设计

提交前至少测试以下数据:

样例

输入:

817

输出:

16
256
4096

最小值

输入:

100

各位数字之和:

[
1+0+0=1
]

输出:

1
1
1

最大值

输入:

999

各位数字之和:

[
9+9+9=27
]

输出:

27
729
19683

普通数据

输入:

123

输出:

6
36
216

7. 提交前检查

  • 输入是否只读一个整数?
  • 是否输出三行?
  • 是否每个结果后都换行?
  • 平方、立方是否用 sum * sum、sum * sum * sum?
  • 数据范围很小,int 足够,不会溢出。

8. 教练总结

这题是典型的 入门模拟题,核心只有两步:

  1. 取出三位数的每一位;
  2. 求和后分别输出一次方、平方、立方。

比赛时直接写循环取位即可,代码短且不容易错。

一句话:

取位 → 求和 → 输出 sum、sum²、sum³。

20260923 112222 初一语文学习方法及目标

一、初一语文总学法:三线并进

1. 积累线
每天15分钟:字词5分钟 + 古诗文默写5分钟 + 朗读/摘抄5分钟。滚动复习,错一个字整句重来。

2. 阅读线
每天20—30分钟课外阅读。课内练朗读、默读、圈点批注、精读、略读、快速阅读。读完做“3句话复述+1个问题”。

3. 写作线
每周1篇随笔或片段,每月1篇大作文。写完后自己改三遍:审题、结构、语言。再找老师面批。

每日固定动作:早读15分钟;课堂带问题听;作业限时;睡前阅读20分钟+口述复盘5分钟。
每周固定动作:1篇文言精读;1篇现代文限时;1次名著批注;1篇随笔;1次错题/素材整理;1次周复盘。


二、七年级上册:每个单元要达到的目标

单元 人文主题 阅读能力目标 写作/综合/名著 达标检验
第一单元 四季美景 掌握朗读重音、停连;品味比喻、拟人;感受写景层次 热爱生活,热爱写作;背诵《古代诗歌四首》 能朗读展示;写300字写景片段;古诗默写零失误
第二单元 至爱亲情 朗读语气、节奏、情感;抓细节体会亲情;学《世说新语》二则 学会记事:六要素、详略得当 写500字记事作文,细节真实;文言能逐字翻译
第三单元 学习生活 默读一气呵成,不出声、不指读、不回读;圈点勾画关键句 写人要抓住特点;《论语》十二章;启动《朝花夕拾》 默读后能概括;写人作文有外貌、语言、动作、心理;《论语》默写
第四单元 人生价值 默读勾画关键句,理清思路;抓中心 思路要清晰:列提纲;《诫子书》 能列提纲写作文,中心明确;《诫子书》背诵翻译
第五单元 动物与人 默读摘录,概括中心;多角度叙事 如何突出中心;《狼》 能概括中心;写一篇突出中心的作文;《狼》逐字翻译
第六单元 想象之翼 快速阅读,逐步达到每分钟400字左右;联想想象 发挥联想与想象;《西游记》精读和跳读 快速阅读后能复述;写想象作文;名著人物关系图

七上名著:《朝花夕拾》——消除与经典的隔膜;《西游记》——精读和跳读。
七上综合性学习:有朋自远方来、少年正是读书时、文学部落。


三、七年级下册:每个单元要达到的目标

单元 人文主题 阅读能力目标 写作/综合/名著 达标检验
第一单元 群星闪耀 精读,字斟句酌;把握人物精神 写出人物的精神;《孙权劝学》 写人物精神作文;批注不少于10处;文言能翻译
第二单元 家国情怀 精读,做批注;把握抒情方式 学习抒情;《木兰诗》;天下国家 抒情作文;《木兰诗》背诵默写;批注有质量
第三单元 凡人小事 熟读精思,注意细节;感受平凡人光辉 抓住细节;《卖油翁》 细节描写作文;文言翻译准确
第四单元 中华美德 略读,确定阅读重点 怎样选材;《陋室铭》《爱莲说》;孝亲敬老 选材作文;文言默写零失误
第五单元 哲理之思 比较阅读,托物言志 文从字顺;《古代诗歌五首》 比较阅读题;文从字顺作文;古诗默写
第六单元 科幻探险 浏览,快速提取信息 语言简明;《活板》;《海底两万里》快速阅读 浏览后信息提取;简明作文;名著阅读任务

七下名著:《骆驼祥子》——圈点与批注;《海底两万里》——快速阅读。
七下综合性学习:天下国家、孝亲敬老、我的语文生活。


四、分板块细化方法

1. 字词
每天10分钟,滚动听写。易错字、多音字、成语误用进积累本。每周日重测。

2. 古诗文
逐字翻译,实词、虚词、通假字、词类活用一个不放过。默写零失误。建“文言实词档案”:之、其、而、以、于、为。

3. 现代文阅读
按文体建模:
- 记叙文:人物、情节、环境、主题、手法。
- 说明文:对象、特征、方法、语言。
- 议论文:论点、论据、论证、结构。
答题四步:审题圈关键词 → 定位原文分层 → 术语+文本 → 分点作答。

4. 作文
建三类素材库:生活素材、阅读素材、时政素材。
记叙文:凤头、猪肚、豹尾。开头点题,中间细节,结尾升华。
修改比写更重要:每篇改三遍,改到能当范文。

5. 名著
真读,不背梗概。每本做三件事:人物关系图、情节脉络图、主题专题。精彩片段写批注,每本写3篇200字读书笔记。

6. 跨学科
语文+历史:文言文背景、历史人物评论。
语文+道法:时政评论、演讲稿、倡议书。
语文+地理:游记、说明文、非连续性文本。
语文+艺术:古诗配画、课本剧、朗诵。


五、避坑

不刷题不读名著、不背模板、不忽视书写、不临时抱佛脚。
重积累、重阅读、重思考、重修改、重规范。

初一语文一句话:用单元目标建骨架,用每日积累填血肉,用阅读写作练输出。
你把每个单元的阅读要素、写作任务、古诗文默写、名著阅读跑通,初二初三语文自然能接着起飞。

20260922 100605 初中阶段各科学习技巧

初中语文

初中语文不是玄学,它是一门慢变量学科:短期靠技巧,长期靠积累。最新依据是 《义务教育语文课程标准(2022年版)》,核心就四个词:文化自信、语言运用、思维能力、审美创造。中考命题也越来越情境化、综合化,整本书阅读、跨学科学习、思辨表达都在加码。

一句话:初中语文要学好,就抓四件事——读进去、背扎实、想清楚、写出来。


一、基础积累:每天15分钟,滚动消灭

字词、成语、病句、标点、文学常识、古诗文默写,这些是“送分题”,也是“送命题”。

  • 准备一个积累本:易错字音字形、成语误用、病句类型、文学常识。
  • 每天早读15分钟:5分钟字词,5分钟古诗文默写,5分钟文言实词。
  • 用滚动复习法:今天背新的,昨天、前天、上周的都要抽考。
  • 默写必须零失误。错一个字,整句重来,别给自己找借口。

二、古诗文:逐字翻译,课内迁移课外

初中文言文是拉分点。方法很硬:

  • 逐字翻译:实词、虚词、通假字、词类活用、特殊句式,一个都不放过。
  • 建虚词档案:之、其、而、以、于、为、乃、则,每个用法配课内例句。
  • 断句靠语感+语法:先找主谓宾,再看标志词。
  • 古诗鉴赏:意象、意境、手法、情感。答题公式:手法+内容+情感/效果。
  • 每天一篇小文言或一首古诗,坚持一年,语感完全不一样。

课内篇目是根,课外文言是果。根扎深了,课外自然能迁移。


三、现代文阅读:按文体建模,答题规范

现代文阅读不是“读懂就行”,是“按题型得分”。

记叙文:人物、情节、环境、主题、手法。
说明文:对象、特征、方法、语言。
议论文:论点、论据、论证、结构。
非连续性文本:信息筛选、整合、推断。

答题四步:
1. 审题圈关键词:题型、范围、分值。
2. 定位原文,分层找依据。
3. 术语+文本,分点作答。
4. 对照分值,几点几分。

平时练习要限时:一篇现代文15—20分钟,像考试一样做,做完再批改。


四、名著阅读:真读,不是刷题

课标要求整本书阅读,中考名著题越来越细。只背梗概必翻车。

  • 每本名著做三件事:人物关系图、情节脉络图、主题专题。
  • 精彩片段写批注:这句话为什么好?人物为什么这么做?
  • 每本写3篇读书笔记,不用长,200字即可。
  • 把名著和作文打通:人物素材、金句、观点,直接进素材库。

五、作文:素材库+结构+修改

作文60分,是语文的半壁江山。

素材库分三类:
- 生活素材:亲情、友情、成长、挫折。
- 阅读素材:名著人物、历史人物、诗词金句。
- 时政素材:航天、AI、非遗、乡村振兴、体育精神。

结构:
- 记叙文:凤头、猪肚、豹尾。开头点题,中间细节,结尾升华。
- 议论文:观点+论据+分析。别只堆例子,要分析“为什么”。

修改比写更重要:
- 每周一篇随笔或片段,每月一篇大作文。
- 写完后自己改:审题偏没偏?结构乱没乱?语言干不干?
- 找老师面批,改到能当范文。


六、跨学科结合,对应课标新要求

  • 语文+历史:文言文背景、历史人物评论。
  • 语文+道法:时政评论、演讲稿、倡议书。
  • 语文+地理:游记、说明文、非连续性文本。
  • 语文+艺术:古诗配画、课本剧、朗诵。
  • 语文+信息科技:查资料、做图表、短视频文案。

课标要求跨学科学习不少于10%课时,中考综合运用题也常考应用文:通知、倡议书、邀请函、演讲稿、读后感、新闻。平时就要练。


七、分年级重点

初一:打基础
- 字词、古诗文默写、文言实词。
- 养成阅读习惯,每天20—30分钟。
- 练书写,卷面干净。

初二:防分化
- 文言文加难,现代文题型增多。
- 名著阅读必须真读。
- 作文开始练结构和细节。
- 错题本、素材本成型。

初三:系统冲刺
- 真题限时,按板块突破。
- 古诗文默写零失误。
- 现代文答题规范固化。
- 作文准备3—5个万能素材,反复修改。
- 回归教材,回归错题。


八、每日/每周模板

每日
- 早读15分钟:字词+默写+文言实词。
- 课堂:预习带问题,笔记记思维断点。
- 睡前阅读20分钟,批注3句话。
- 口头复述今天语文课最核心的一个点。

每周
- 1篇文言文精读。
- 1篇现代文限时训练。
- 1次名著批注。
- 1篇随笔或作文片段。
- 1次错题/素材整理。


九、避坑

不刷题不读名著、不背模板、不忽视书写、不临时抱佛脚。
重积累、重阅读、重思考、重修改、重规范。

初中语文一句话:用理科的系统去管理,用文科的耐心去沉淀。
你把读、背、想、写四件事跑通,中考语文不会差,高中语文也能接着起飞。

初中数学

初中数学不是靠“刷题量”堆出来的,它是一门从算术走向代数、从具体走向抽象、从计算走向推理的学科。最新依据是 《义务教育数学课程标准(2022年版)》,核心是“三会”:会用数学的眼光观察现实世界,会用数学的思维思考现实世界,会用数学的语言表达现实世界。中考趋势也很明确:情境化、跨学科、探究性、开放性,减少机械刷题。

一句话:初中数学要学好,就抓五件事——概念清、计算稳、几何懂、函数通、错题透。


一、概念:每个概念问三问

初中数学第一个坎,就是概念不清。比如“函数”“全等”“相似”“方程的解”“不等式性质”。

每个概念都要问:
1. 是什么? 用自己的话定义,不背课本原句。
2. 为什么? 它从哪来?解决什么问题?
3. 怎么用? 举个正例,再举个反例。

比如“函数”:不是背“一个x对应一个y”,而是能举出“y=2x是函数,x²+y²=1不是函数”的反例。概念清,题才不会瞎做。


二、计算:每天10分钟,练到肌肉记忆

初中数学丢分,一半死在计算。有理数、整式、分式、根式、方程、不等式,都是基本功。

  • 每天10道计算题,限时5—8分钟。
  • 草稿纸分区、写题号、不跳步。
  • 做完必须自查:符号、分母、括号、移项。
  • 错一道,罚三道同类题,直到一次全对。

计算不是“粗心”,是能力不足。练到不假思索,才叫稳。


三、几何:建基本图形库,练辅助线

几何是初中数学的分水岭。全等、相似、四边形、圆、锐角三角函数,核心是基本图形+辅助线。

  • 基本图形库:一线三等角、手拉手、半角、旋转、截长补短、倍长中线。
  • 辅助线口诀:见中点想倍长,见角平分想翻折,见垂直想勾股,见圆想半径和弦心距。
  • 证明写依据:每一步都要有定理支撑,不跳步。
  • 一题多解:一道几何题,试着用全等、相似、坐标三种方法解。
  • 错题重画:把错过的几何图重新画一遍,标出关键条件。

几何不是“看出来的”,是“推出来的”。


四、函数与方程:数形结合,一网打尽

一次函数、反比例函数、二次函数,加上方程、不等式,是初中代数的主干。

  • 画图:每个函数必须会画草图,标出关键点:交点、顶点、对称轴、渐近线。
  • 数形结合:方程的解就是图像交点,不等式的解就是图像上下方。
  • 参数变化:k、b、a、c变化时,图像怎么动?自己推一遍。
  • 实际应用:利润、行程、面积、增长率,都是函数模型。
  • 压轴拆解:二次函数综合题,拆成“求解析式—求交点—求最值—分类讨论”。

函数通了,高中物理、经济、统计都能接上。


五、统计与概率:读懂数据,会做决策

这部分不是背公式,是数据观念。

  • 会算平均数、中位数、众数、方差,更要知道它们代表什么。
  • 会读统计图:条形、折线、扇形、频数分布直方图。
  • 概率:列举法、树状图、频率估计概率。
  • 结合生活:班级身高、家庭支出、抽奖游戏,自己设计小调查。

课标要求“综合与实践”不少于10%课时,统计概率最适合做项目。


六、错题本:升级为“模型本”

错题不抄答案,写四件事:
1. 错在哪:概念?计算?审题?辅助线?
2. 对应哪个模型:方程、函数、全等、相似、统计?
3. 命题人想考什么:陷阱在哪?
4. 下次怎么识别:关键词、图形特征、条件组合。

每周日重做一遍,直到能讲出每一步。错题本不是收藏,是消灭。


七、限时训练:作业考试化

平时作业就要限时:
- 选填:30—40分钟。
- 解答题:按中考分值分配。
- 压轴题:先拿前两问,第三问量力而行。

做完自查,对照评分标准,看步骤分丢在哪。中考拼的是准确率和节奏,不是磨洋工。


八、跨学科结合

  • 数学+物理:速度、密度、电功率,都是函数和方程。
  • 数学+地理:坐标、比例尺、气温统计。
  • 数学+生物:遗传概率、种群增长。
  • 数学+经济:利润、折扣、利率。
  • 数学+信息科技:用Excel或Python做数据分析。

在真实情境中解决问题,才是新课标要的方向。


九、分年级重点

初一:适应代数
- 有理数、整式、方程、不等式。
- 计算基本功,书写规范。
- 从算术思维转向代数思维。

初二:防分化
- 全等、轴对称、勾股、一次函数、分式、根式。
- 几何证明入门,函数数形结合。
- 错题本、模型本必须成型。

初三:系统冲刺
- 二次函数、圆、相似、锐角三角函数、概率统计。
- 真题限时,专题突破。
- 压轴题拆解,回归教材和错题。


十、每日/每周模板

每日
- 早读前10分钟:计算10道,限时。
- 课堂:预习带问题,笔记记思维断点。
- 作业:先复习后作业,限时,自查。
- 睡前5分钟:口述今天最核心的一个模型。

每周
- 1次错题重做。
- 1次限时套卷。
- 1次模型总结(几何/函数/方程)。
- 1次跨学科小项目。
- 1次周复盘+下周计划。


十一、避坑

不盲目刷题、不忽视计算、不跳过证明、不依赖答案、不熬夜。
重概念、重计算、重几何、重函数、重错题、重讲题。

初中数学一句话:用系统建体系,用限时练节奏,用错题补漏洞。
你把概念、计算、几何、函数、错题五件事跑通,中考数学不会差,高中理科也能接着起飞。

初中英语

(把高考英语的方法降维到初中)

初中英语不是“背单词 + 刷语法题”,而是用语言做事。最新依据是 《义务教育英语课程标准(2022年版)》,核心素养四个维度:语言能力、文化意识、思维品质、学习能力。中考趋势也很明确:语篇化、情境化、语用化、跨学科、减少孤立语法题。福建中考英语的听力、情景交际、看图写话、短文填空、书面表达,都在考“能不能在真实情境里用英语”。

一句话:初中英语要学好,就抓五件事——词汇滚雪球、语法建框架、听说磨耳朵、阅读精泛结合、写作改出来。


一、词汇:滚雪球,不背孤立词

词汇是地基,但不要抱着单词表从 A 背到 Z。

  • 先补音标和自然拼读:见词能读,听音能写。初一必须过关。
  • 每天 20 个新词 + 滚动复习前 3 天:早读、午休、睡前各 5 分钟。
  • 背词块,不背单词:make a decision、take part in、be proud of、as soon as possible。
  • 一词多义、词性转换、固定搭配:care → careful → carefully → carelessness。
  • 在阅读中复现,在写作中输出:今天背的词,明天阅读里找,后天写作里用。
  • 用艾宾浩斯表:1 天、2 天、4 天、7 天、15 天,反复滚。

词汇不是背一遍就会,是滚出来的。


二、语法:建框架,不刷单选

初中语法核心就几块:时态、语态、从句、非谓语、情态动词、介词搭配。

  • 时态轴:一般现在、一般过去、一般将来、现在进行、过去进行、现在完成。画一条时间轴,标清楚每个时态的位置和标志词。
  • 三大从句:宾语从句、定语从句、状语从句。每个从句记:引导词、语序、时态。
  • 非谓语:to do、doing、done 的基本用法。
  • 被动语态:be + done,各时态怎么变。
  • 每个语法点问五问:定义、结构、标志词、例句、易错点。
  • 在语篇中练:做完形、阅读、短文填空,比刷 100 道单选有用。

语法不是背规则,是建一张网。


三、听说:磨耳朵,练嘴巴

福建中考有听力、情景交际、看图写话,听说不过关,笔试卷也吃亏。

  • 每天 10—15 分钟:教材音频、慢速英语、英文歌、动画。
  • 精听:听写关键词、句子,听不懂就倒回去,直到写出来。
  • 跟读:影子跟读法,比原音慢半拍,模仿语音、语调、连读、弱读。
  • 口语:每天 3 句,描述今天、复述课文、模拟情景对话。
  • 周末:看一集英文动画或电影片段,遮住字幕,练耳朵。

听说不是天赋,是肌肉记忆。


四、阅读:精泛结合,得阅读者得英语

中考英语,阅读占大头。方法要分精读和泛读。

  • 每天 1 篇精读:查生词、析长难句、概括主旨、推断作者意图、找同义替换。
  • 每周 2—3 篇泛读:限时,练速度,不查每个词,抓大意。
  • 题材多样:故事、科普、文化、时政、应用文、图表。
  • 答题技巧:先题干后原文,定位关键词,注意同义替换,排除绝对化选项。
  • 积累好句:把阅读里的高级句型抄进写作素材库。

精读练深度,泛读练速度。两个都不能少。


五、写作:改出来,不是背出来

写作是输出,也是拉分点。

  • 每周 1 篇:命题作文、看图写话、应用文、书信、通知。
  • 三步走:列提纲 → 写初稿 → 修改升级。
  • 升级技巧:简单句变复合句,换高级词汇,加连接词,加细节。
  • 背诵范文:但不要死套模板,要内化结构。
  • 找老师面批:改到能当范文。自己改三遍,比写三篇有用。
  • 书写工整:卷面分是真的。

写作不是写得多,是改得透。


六、错题/积累本:分四区

准备一个英语积累本,分成四区:

  1. 词汇区:易错词、搭配、词性转换。
  2. 语法区:错题、规则、例句。
  3. 句型区:好句、高级句型、写作模板。
  4. 写作区:自己的错句、老师批改、范文。

每周日重做一遍,直到能讲出每一步。


七、跨学科结合

课标要求主题引领、语篇依托、学习活动观。

  • 英语 + 信息科技:查资料、做 PPT、拍英文短视频。
  • 英语 + 音乐:学英文歌,练发音和节奏。
  • 英语 + 地理/历史:介绍国家、节日、人物、地标。
  • 英语 + 道法:简单时政评论、演讲稿。
  • 英语 + 美术:英文手抄报、海报、思维导图。

在真实情境中用英语,才是新课标要的方向。


八、分年级重点

初一:打基础
- 音标、自然拼读、书写。
- 每天听说 15 分钟,培养兴趣。
- 词汇滚雪球,课文背熟。
- 不偏科,不讨厌英语。

初二:防分化
- 语法体系成型:时态、从句、非谓语。
- 阅读量加大,精读+泛读。
- 写作开始练结构和升级句。
- 错题本、积累本必须成型。

初三:系统冲刺
- 真题限时,按板块突破:听力、单选、完形、阅读、情景交际、短文填空、写作。
- 听说每天练,写作每周改。
- 回归教材和错题。
- 调整心态,不熬夜。


九、每日/每周模板

每日
- 早读 15 分钟:单词 + 课文 + 跟读。
- 课堂:预习带问题,笔记记搭配和句型。
- 午休听 10 分钟英语。
- 作业限时,做完自查。
- 睡前阅读 10 分钟,或跟读 5 分钟。

每周
- 1 篇精读。
- 2—3 篇泛读。
- 1 篇写作 + 修改。
- 1 次听力精听。
- 1 次错题重做。
- 1 次周复盘 + 下周计划。


十、避坑

不背单词书不读、不刷语法单选题、不忽视听力和口语、不临时抱佛脚、不依赖翻译。
重语篇、重语用、重积累、重输出、重坚持。

初中英语一句话:用滚雪球背词,用框架学语法,用耳朵练听说,用精泛读阅读,用修改写作文。
坚持一年,中考英语不会差,高中英语也能接着起飞。

初中历史

(把理科状元的“系统思维”拿来拆初中历史)

初中历史不是“背年代 + 背意义”,它是一门用史料讲因果、用时空建脉络、用观点做解释的学科。最新依据是 《义务教育历史课程标准(2022年版)》,核心素养就五个:唯物史观、时空观念、史料实证、历史解释、家国情怀。中考趋势也很明确:无情境不成题、史料分析、跨学科、开放性、减少死记硬背。

一句话:初中历史要学好,就抓五件事——时间轴、专题网、史料眼、答题规范、错题透。


一、核心认知:先框架,后细节

历史最怕“只见树木,不见森林”。学每一课之前,先问:
- 这段时间,中国在发生什么?世界在发生什么?
- 政治、经济、文化、民族、外交,各有什么变化?
- 这件事的原因、经过、结果、影响是什么?

先建框架,再填细节。 框架是骨架,细节是血肉。没有骨架,背再多也串不起来。


二、最该养的8个习惯

1. 时间轴习惯
中国史和世界史对照画。比如:
- 中国:秦朝(前221)— 汉 — 三国 — 隋唐 — 宋元 — 明清。
- 世界:古希腊 — 罗马 — 中世纪 — 文艺复兴 — 新航路 — 工业革命。
把同一时期的中外事件标在一起,时空观念自然就来了。

2. 专题归纳习惯
按主题串:
- 政治制度:禅让制、分封制、郡县制、三省六部、行省制。
- 经济发展:农业、手工业、商业、货币、贸易。
- 民族关系:和亲、战争、册封、交融。
- 对外交往:丝绸之路、郑和下西洋、闭关锁国。
- 思想文化:百家争鸣、儒学、唐诗宋词、明清小说。

专题一拉,零散知识就成网。

3. 史料实证习惯
中考材料题越来越多。练三件事:
- 区分一手史料和二手史料。
- 从文字、图片、地图、表格中提取关键信息。
- 用史料佐证观点,不空谈。

4. 笔记结构化
不抄板书,画知识地图:
- 背景 → 时间 → 人物 → 事件 → 结果 → 影响。
- 用箭头、括号、颜色标因果。
- 每单元一页思维导图。

5. 答题规范习惯
材料题三步:
- 审:圈关键词,看分值。
- 读:分层读材料,划关键句。
- 联:联系教材知识点。
- 答:分点作答,术语+史实+分析。

小论文四步:
- 观点明确。
- 史论结合。
- 逻辑清晰。
- 结论回扣。

6. 错题本升级为“避坑本”
写四件事:
- 错在哪:时间?概念?材料?答题?
- 对应哪个考点。
- 命题人想考什么。
- 下次怎么识别。

每周日重做一遍。

7. 比较与联系习惯
- 比较:商鞅变法 vs 王安石变法;戊戌变法 vs 明治维新。
- 联系:工业革命如何影响中国近代史?新航路开辟如何改变世界?
- 跨学科:历史+地理、历史+道法、历史+语文。

8. 讲题输出习惯
每天讲一个历史事件给同学或家长听。能讲清因果,才是真懂。


三、各学科怎么结合

  • 历史+语文:文言文背景、历史人物评论、诗词中的历史。
  • 历史+地理:地图上的疆域变迁、丝路贸易、人口迁移。
  • 历史+道法:制度演变、法治进程、民族精神。
  • 历史+艺术:文物、壁画、建筑、音乐。
  • 历史+信息科技:查资料、做时间轴、数据可视化。

课标要求跨学科主题学习不少于10%,中考也常考综合题。


四、分年级重点

初一:中国古代史
- 朝代歌、时间轴、重要人物。
- 培养兴趣,看历史纪录片、逛博物馆。
- 练笔记和思维导图。

初二:中国近现代史 + 世界古代史
- 屈辱与探索、侵略与反抗、近代化进程。
- 专题归纳:列强侵华、救亡图存、新民主主义革命。
- 开始练材料题和小论文。

初三:世界史 + 中考复习
- 世界近代史、现代史、国际格局。
- 真题限时,专题突破。
- 回归教材和错题。
- 调整心态,不熬夜。


五、每日/每周模板

每日
- 早读10分钟:背时间轴、朝代、事件。
- 课堂:预习带问题,笔记记因果。
- 作业:先复习后作业,限时,自查。
- 睡前5分钟:口述今天最核心的一个事件。

每周
- 1次时间轴整理。
- 1次专题归纳。
- 1次材料题精练。
- 1次错题重做。
- 1次跨学科小项目。
- 1次周复盘+下周计划。


六、避坑

不背孤立年代、不刷题不总结、不忽视材料题、不空谈观点、不临时抱佛脚。
重框架、重专题、重史料、重答题、重比较、重输出。

初中历史一句话:用时间轴建骨架,用专题网填血肉,用史料眼做实证,用答题规范拿分。
你把框架、专题、史料、错题四件事跑通,中考历史不会差,高中历史也能接着起飞。

初中道法

(把理科状元的“系统思维”拿来拆初中道法)

初中道法不是“背口号 + 抄笔记”,它是一门用情境讲道理、用法律辨是非、用国情看世界、用行动担责任的学科。最新依据是 《义务教育道德与法治课程标准(2022年版)》,核心素养五个:政治认同、道德修养、法治观念、健全人格、责任意识。中考趋势也很明确:无情境不成题、时政化、综合性、开放性、实践性,减少死记硬背。福建中考具体形式以当地考试说明为准,但方向不会变。

一句话:初中道法要学好,就抓五件事——框架清、时政通、法律明、答题规范、错题透。


一、核心认知:先建框架,再填时政

道法最怕“背了一堆,考试找不到”。学每一单元之前,先问:
- 这一单元讲的是道德、法律、国情、责任中的哪一块?
- 核心概念是什么?关键词有哪些?
- 它和当下哪个时政热点能挂上?

先建框架,再填细节。 框架是抽屉,时政是文件。没有抽屉,文件全堆在桌上。


二、最该养的8个习惯

1. 思维导图习惯
每单元画一张图:
- 中心:单元主题。
- 分支:课—框—目。
- 关键词:用短句,不抄大段。
- 案例:旁边贴一个时政例子。

比如“宪法”单元:宪法地位、公民权利、公民义务、国家机构、宪法监督。一张图串完。

2. 时政积累习惯
道法不结合时政,等于白学。
- 每周看 2—3 条新闻:人民日报、央视新闻、学习强国。
- 每条记三件事:谁、做了什么、体现教材哪个知识点。
- 建一个“时政—教材”对照本。比如“新质生产力”对应“创新驱动发展”;“未成年人网络保护”对应“法律保护未成年人”。

3. 法律专题习惯
初中法律核心就几块:
- 宪法:根本法、最高法律效力、公民基本权利和义务。
- 未成年人保护:家庭、学校、社会、网络、政府、司法六大保护。
- 违法与犯罪:一般违法 vs 犯罪,刑罚,预防犯罪。
- 依法办事:遇到侵权怎么办,诉讼、非诉讼途径。

每个专题记:法律条文 + 案例 + 答题术语。

4. 答题规范习惯
道法材料题三步:
- 审:圈关键词,看分值,定知识范围。
- 读:分层读材料,划关键句。
- 联:联系教材知识点。
- 答:分点作答,术语 + 材料 + 分析。

比如问“为什么”:答“必要性 + 重要性 + 意义”。问“怎么做”:答“国家 + 社会 + 学校 + 家庭 + 个人”。

5. 错题本升级为“避坑本”
写四件事:
- 错在哪:概念?审题?材料?术语?
- 对应哪个考点。
- 命题人想考什么。
- 下次怎么识别。

每周日重做一遍。

6. 讲题输出习惯
每天讲一个时政或案例给同学或家长听。能用教材术语讲清“是什么、为什么、怎么做”,才是真懂。

7. 比较与联系习惯
- 比较:道德 vs 法律;权利 vs 义务;一般违法 vs 犯罪。
- 联系:国情国策、社会主义核心价值观、中华民族精神。
- 跨学科:道法+历史、道法+地理、道法+语文。

8. 行动实践习惯
课标强调“知行合一”。参加志愿服务、社区调查、模拟法庭、时政评论、红色研学。在真实情境中用道法,才是新课标要的方向。


三、各学科怎么结合

  • 道法+语文:时政评论、演讲稿、倡议书、读后感。
  • 道法+历史:制度演变、法治进程、民族精神。
  • 道法+地理:国情、资源、环境、区域发展。
  • 道法+生物:生命健康、生态保护、公共卫生。
  • 道法+信息科技:查资料、做图表、数据可视化。

课标要求跨学科主题学习不少于10%,中考也常考综合题。


四、分年级重点

初一:适应与打基础
- 道德、心理、法治基础。
- 养成看新闻、画导图、记术语的习惯。
- 练书写和答题规范。

初二:法治与防分化
- 八下宪法专册是重中之重。
- 权利义务、国家机构、法律保护。
- 材料题开始变难,答题模板成型。
- 错题本、时政本必须坚持。

初三:国情与国际 + 冲刺
- 国情国策、社会主义核心价值观、国际视野。
- 真题限时,专题突破。
- 回归教材和错题。
- 时政热点集中梳理,调整心态。


五、每日/每周模板

每日
- 早读10分钟:背框架、术语、法律条文。
- 课堂:预习带问题,笔记记关键词和案例。
- 作业:先复习后作业,限时,自查。
- 睡前5分钟:口述今天最核心的一个知识点。

每周
- 1次思维导图整理。
- 1次时政积累。
- 1次材料题精练。
- 1次错题重做。
- 1次跨学科小项目。
- 1次周复盘+下周计划。


六、避坑

不背大段、不空喊口号、不脱离材料、不忽视时政、不临时抱佛脚、不把开卷当免背。
重框架、重时政、重法律、重答题、重实践、重输出。

初中道法一句话:用框架建抽屉,用时政填文件,用法律辨是非,用规范拿分数。
你把框架、时政、法律、错题四件事跑通,中考道法不会差,高中政治也能接着起飞。

初中地理

20260922 084513 大学阶段的学习习惯

大学没有“考纲”了。最权威的依据是你所在专业的培养方案、课程大纲、毕业要求,以及教育部近年反复强调的:以本为本、四个回归、合理增负、淘汰“水课”、打造“金课”、拔尖创新人才、四新建设、AI赋能、跨学科、过程性评价、学术诚信。工程类专业还有OBE成果导向教育和工程教育认证的逻辑:学生中心、成果导向、持续改进。

一句话:高中拼“标准答案”,大学拼“自己定义问题、做出作品、持续迭代”。


一、大学最该养的12个习惯

1. 培养方案导航
大一就读懂培养方案:毕业学分、必修/选修、先修链、保研政策、转专业、辅修、微专业。不要等到大四才发现少修一门课。

2. 目标倒推
想保研、考研、留学、就业、考公,路径完全不同。大一定方向,大二试错,大三聚焦,大四落地。每学期问自己:这学期要为哪个目标积累什么?

3. 课前问题化预习 + Office Hour
大学老师不会追着你喂。课前读教材/论文,带1—2个问题进课堂。课后去Office Hour,或者发一封规范邮件:主题、称呼、身份、目的、附件、落款。

4. 结构化知识管理
用 Obsidian、Notion、OneNote 建知识库;用 Zotero 管文献;用 Git 管代码和版本。笔记不是抄PPT,是画知识地图、写自己的理解。

5. 项目/作品输出
大学最值钱的不是绩点,是作品集:代码仓库、论文、竞赛作品、设计图、调研报告、实习成果。每学期至少完成一个能拿得出手的项目。

6. 文献精读与综述
从读教材过渡到读论文。先读综述,再读经典,再追前沿。每篇写三句话:问题是什么、方法是什么、结论和局限是什么。

7. 学术写作与汇报
论文、实验报告、PPT、答辩,都是核心能力。写作要练:摘要、引言、方法、结果、讨论、引用。汇报要练:结论先行、逻辑清晰、控制时间。

8. 实验/实践记录与可复现
理工科尤其重要:实验记录本、原始数据、代码、环境配置、随机种子。可复现,才是真结果。医学、生物、化学还要守实验室安全与伦理。

9. 团队协作与版本控制
大学项目多是团队作业。学会分工、会议纪要、Deadline管理、Git协作、冲突处理。不做“搭便车”的人,也不做全包的人。

10. AI合规协作
AI是学习助手,不是代写工具。用AI查概念、改语法、生成练习题、辅助代码,但必须核实、标注、遵守学校规定。学术诚信是底线:不抄袭、不造假、不代写。

11. 过程性评价管理
大学成绩=平时+期中+项目+期末。别翘课,别期末突击。每次作业、小测、展示都算分。把“过程”当回事,结果自然不差。

12. 精力、情绪与边界
大学生每天睡7—8小时,午休20—30分钟,运动3—5次/周。手机、游戏、短视频要物理隔离。情绪崩溃时,先处理情绪,再处理学习。必要时用学校心理咨询。


二、各学科怎么结合起来

  • 理工科:数学 + 编程 + 专业。用Python/MATLAB做数据分析、仿真、可视化;用数学建模打竞赛;用Git管代码。
  • 文科:阅读 + 写作 + 数据。用文本分析、社会调查、数据可视化做研究;用英语读文献。
  • 医科:基础 + 临床 + 伦理。早进临床、早接触科研,重视循证医学。
  • 商科:数据 + 案例 + 沟通。学SQL、Excel、Python,做商业分析、案例竞赛。
  • 艺术/设计:审美 + 技术 + 用户。做作品集,学交互、AI工具、版权规范。
  • 跨学科:辅修、微专业、通识课、大创、挑战杯、中国国际大学生创新大赛、数学建模、ACM、电赛。

最新政策强调“四新”:新工科、新医科、新农科、新文科。核心都是跨学科、真问题、强实践。


三、分年级重点

大一:适应与探索
- 读懂培养方案,稳住数学、英语、编程。
- 加1—2个社团或学生组织,但别贪多。
- 探索专业方向,考虑转专业/辅修。
- 养成去图书馆、去Office Hour的习惯。

大二:专业与试错
- 深入专业核心课,进实验室、进课题组。
- 参加大创、竞赛、科研训练。
- 学一门硬技能:Python、SQL、PS、CAD、统计。
- 开始建作品集和简历。

大三:聚焦与冲刺
- 保研/考研/留学/就业,路径确定。
- 科研出成果,实习攒经历,竞赛拿奖项。
- 准备英语、GRE/雅思/托福、考研科目。
- 找导师写推荐信,维护学术网络。

大四:落地与交接
- 毕设认真做,别烂尾。
- 求职/升学两手抓,作品集迭代。
- 交接社团、实验室、项目。
- 站好最后一班岗,别挂科。


四、每日/每周/学期模板

每日
- 7:00 起床,早餐,列今日3件事
- 课前10分钟到教室,翻预习问题
- 课堂:坐前排,互动,记思维断点
- 午休20—30分钟
- 下午:实验/图书馆/项目,专注2—3小时
- 运动30—60分钟
- 晚上:复盘 + 技能1小时 + 阅读
- 23:30前睡,保证7—8小时

每周
- 1次周计划 + 周复盘
- 1次文献精读
- 1次项目推进
- 1次运动/社交/导师沟通
- 1次整理知识库和代码仓库

每学期
- 开学:读培养方案,定学期目标
- 期中:复盘进度,调整策略
- 期末:作品/论文/考试,整理作品集


五、避坑

不翘课、不期末突击、不盲目考证、不AI代写、不熬夜、不比较。
重过程、重作品、重文献、重实验、重协作、重诚信。
大学最危险的是“高中思维”:等老师喂、唯分数、单打独斗。

大学习惯一句话:把自己当成一个持续迭代的项目。
小学养根,初中建系统,高中做贯通,大学做输出。你把大学这套闭环跑通,毕业时拿出的就不只是一张成绩单,而是一个能解决问题、能持续成长的自己。

20260922 084300 高中阶段的学习习惯

一、高中最该养的12个习惯

1. 目标倒推与选科管理
高一就要开始想:我想去的专业方向,需要哪几科?福建“3+1+2”下,物理/历史怎么选,四选二怎么配。不要只看哪科好拿分,要看专业覆盖和长期优势。

2. 周计划 + 日清单 + 闭环
每周日晚做周计划,每天早读前写日清单:今天必须突破的3件事。晚上打钩复盘。高中作业多,没有计划就会被拖着走。

3. 深度预习:带问题进课堂
预习不是看一遍,而是标出“我卡在哪”。每科至少写1—2个问题。听课重点听老师的破题切入点,而不是抄答案。

4. 高效听课:记“思维断点”
笔记不誊抄板书。记三样:老师为什么从这里入手、我瞬间的疑问、和已学模型的链接。比如化学“勒夏特列原理”,旁边批注“物理楞次定律的化学分身——阻碍变化”。

5. 结构化笔记与模型本
数学:函数、几何辅助线、动点、概率模型。
物理:受力分析、传送带、板块、电磁场。
化学:方程式网络、平衡、工艺流程。
生物:概念图、遗传计算、实验设计。
历史:时间轴+专题框架。
政治:时政+逻辑+法律。
地理:图表+区域+人地协调。
笔记不是抄,是画知识地图。

6. 错题本升级为“避坑本”
不抄答案,写四件事:错在哪、对应哪个模型、命题人想考什么、下次怎么识别。每周日重做一遍,直到能讲出每一步。

7. 限时训练,作业考试化
平时作业就要限时。数学40分钟选填,英语20分钟完形阅读,物理25分钟实验+计算,化学25分钟工艺流程。高考拼的是准确率和节奏,不是磨洋工。

8. 真题研究,而不是刷模拟题
近5年全国卷、福建卷、新高考卷,按题型、考点、情境、设问、评分标准拆解。研究命题人怎么设陷阱,比刷十套模拟题有用。

9. 跨学科情境迁移
高考评价体系强调“综合性、应用性、创新性”。学物理的匀变速,联系数学二次函数;学化学平衡,联系生物酶促反应;学地理气候,联系历史人口迁移。每周一次跨学科小专题。

10. 讲题输出,费曼学习法
每天讲一道题给同学或家长听。能用完整句说:我认为……因为……所以……。讲不下去的地方,就是你没真懂的地方。

11. 周复盘 + 模考分析
每周四问:本周最大漏洞?哪个模型还不熟?哪次限时超时?下周砍掉什么?
模考后做失分分类:知识性、思维性、规范性、策略性、心理性。对症下药。

12. 精力与情绪管理
高中生每天睡眠应达8小时,午休20—30分钟。每天运动30—60分钟。手机物理隔离,定时使用。熬夜刷题是效率最低的努力。情绪崩溃时,先处理情绪,再处理学习。


二、各学科怎么结合起来

  • 语文 + 政治 + 历史:时政评论、历史人物辩论、红色研学、博物馆项目。
  • 数学 + 物理 + 化学:函数图像、方程、比例、实验数据处理、误差分析。
  • 英语 + 信息科技 + 艺术:英文搜索、配音、短视频、简单数据分析。
  • 生物 + 地理 + 化学:生态系统、水质检测、土壤酸碱、气候统计。
  • 地理 + 历史:地图上的朝代变迁、丝路贸易、人口迁移。
  • 体育 + 政治:规则意识、团队合作、输赢心态。

一句话:在真实情境中解决问题,而不是孤立背知识点。


三、分年级重点

高一:适应与选科
- 习惯固化:预习、笔记、错题、计划、限时。
- 不偏科,英语词汇、数学计算、语文阅读。
- 开始了解“3+1+2”选科和专业方向。
- 别熬夜,别提前焦虑高三。

高二:防分化与深化
- 物理/历史方向确定,四选二深入。
- 模型本、错题本必须成型。
- 实验操作、体育训练别丢。
- 竞赛、强基、综合评价可适度了解。
- 情绪波动大,家长少唠叨,多倾听。

高三:系统与冲刺
- 全科系统复习,真题限时,专题突破。
- 周复盘、月模考分析,心态管理。
- 回归教材和错题,不盲目刷题。
- 英语听说、体育中考(若适用)、实验操作别丢。
- 考试节奏、答题规范、时间分配,反复演练。


四、每日/每周模板

每日

  • 6:30 起床,晨读15—20分钟
  • 课堂:预习标记 + 听课抓思路 + 笔记留白
  • 午休20—30分钟
  • 放学后运动30分钟
  • 作业:番茄钟25+5,先复习后作业,限时自查
  • 睡前阅读20分钟,口述复盘5分钟
  • 23:00前睡,保证8小时

每周

  • 1次错题重做
  • 1次跨学科小项目
  • 1次限时套卷
  • 1次周复盘 + 下周计划
  • 1次户外运动/家务/家庭聊天

五、家长避坑

不陪写、不查每道题、不因分数吼、不盲目报班、不熬夜。
重过程、重阅读、重实验、重运动、重提问。
高中成绩波动正常,习惯和心态才是复利。

高中习惯一句话:把学习变成自己设计的系统。
小学养根,初中建系统,高中做贯通。你把高中这套闭环跑通,高考只是水到渠成。

20260922 084047 初中阶段的良好习惯

初中是真正的分水岭。小学养的是“根”,初中要建的是“系统”。高中那些模型本、错题本、限时训练,其实初中就该起步。

一、初中最该养的10个习惯

1. 计划与闭环习惯

每周日晚做周计划,每天早读前写日清单:今天必须突破哪3件事。晚上打钩复盘。初中科目变多,没有计划就会被作业拖着走。

2. 预习—听课—复习—作业—复盘闭环

预习不是看一遍,而是标出“我卡在哪”。听课抓老师的破题切入点,笔记留白。作业前先复习5分钟,合上书像考试一样做。做完必须复盘。

3. 结构化笔记与模型本

数学:函数、几何辅助线、动点模型。
物理:受力分析、电路、浮力、杠杆。
化学:方程式网络、物质转化关系。
生物:概念图。
道法/历史:时间轴+专题框架。
笔记不是抄板书,是画“知识地图”。

4. 错题本升级为“避坑本”

不抄答案,写三件事:错在哪、对应哪个模型、下次怎么识别。每周日重做一遍,直到能讲出每一步。

5. 限时训练习惯

平时作业考试化。数学40分钟一套选填,英语20分钟完形阅读,物理25分钟实验+计算。中考拼的是准确率和节奏,不是磨洋工。

6. 深度阅读与信息提取

语文整本书阅读、科普、时政评论都要读。读完做“3句话复述+1个质疑”。道法、历史材料题,本质就是信息提取。

7. 实验与实践习惯

物理、化学、生物实验,必须亲手做、写记录、画图表、分析误差。控制变量法、对照实验、定量分析,三科通用。地理观测、生物调查、信息科技项目也算。

8. 跨学科项目习惯

课标要求跨学科主题学习不少于10%课时。每周一次小项目:家庭水电统计、社区垃圾分类、历史人物辩论、英文菜单、地理气候分析。不求大,求完整:提出问题—计划—动手—记录—展示—复盘。

9. 讲题与提问习惯

每天讲一道题给同学或家长听。能用完整句说:我认为……因为……所以……。鼓励反驳,但要有证据。费曼学习法是初中提分最快的武器。

10. 精力与手机管理

初中生每天睡眠应达9小时。手机有限带入校园,回家定时使用。熬夜刷题是效率最低的努力。运动每天至少1小时,家务固定一项。


二、各学科怎么结合起来

  • 语文+道法+历史:时政评论、历史人物辩论、红色研学、博物馆项目。
  • 数学+物理+化学:函数图像、方程、比例、实验数据处理、误差分析。
  • 英语+信息科技+艺术:英文搜索、绘本配音、简单手抄报、短视频制作。
  • 生物+地理+化学:生态系统、水质检测、土壤酸碱、气候统计。
  • 地理+历史:地图上的朝代变迁、丝路贸易、人口迁移。
  • 体育+道法:规则意识、团队合作、输赢心态。

一句话:在真实情境中解决问题,而不是孤立背知识点。


三、分年级重点

初一:适应与固基

  • 习惯固化:预习、笔记、错题、计划。
  • 不偏科,英语音标和词汇、数学计算、语文阅读。
  • 开始练限时作业,但别熬夜。

初二:防分化

  • 物理入门,几何函数变难,生物地理会考。
  • 错题本、模型本必须成型。
  • 实验操作、体育训练别丢。
  • 情绪波动大,家长少唠叨,多倾听。

初三:系统与冲刺

  • 化学加入,全科系统复习。
  • 真题限时、专题突破、实验操作、英语听说、体育中考。
  • 周复盘、月模考分析,心态管理。
  • 不盲目刷题,回归教材和错题。

四、每日/每周模板

每日

  • 6:30 起床,晨读15—20分钟
  • 课堂:预习标记+听课抓思路+笔记留白
  • 午休20—30分钟
  • 放学后运动30分钟
  • 作业:番茄钟25+5,先复习后作业,限时自查
  • 睡前阅读20分钟,口述复盘5分钟
  • 22:30前睡,保证9小时

每周

  • 1次错题重做
  • 1次跨学科小项目
  • 1次限时套卷
  • 1次周复盘+下周计划
  • 1次户外运动/家务/家庭聊天

五、家长避坑

不陪写、不查每道题、不因分数吼、不盲目报班、不熬夜。
重过程、重阅读、重实验、重运动、重提问。
初中成绩波动正常,习惯和心态才是复利。

高中状元不是高中才成为的,是初中养出来的。小学养根,初中建系统,高中做贯通。你把初中这套闭环跑通,高中自然能起飞。

20260922 083510 小学阶段的良好习惯

一、先纠偏:小学最新要求变了

  • 核心素养导向:不是背了多少,而是会不会用。
  • 跨学科主题学习不少于10%课时:习惯要能连接生活。
  • 双减:一二年级不布置书面家庭作业、不进行纸笔考试;三至六年级书面作业平均不超过60分钟。
  • 评价过程化:阅读、运动、劳动、合作、提问,都算成长。

一句话:小学拼的不是刷题量,是专注、阅读、提问、整理、运动、应用。


二、小学六年最该养的8个习惯

1. 专注闭环习惯

对应所有学科的核心素养。

  • 课前2分钟:摆好书本、文具,人坐正。
  • 课堂:眼睛跟老师,耳朵抓关键词。
  • 课后5分钟:合上书,用“当小老师”的方式复述今天学了什么。
  • 每天讲一道题、一个故事或一个科学现象,讲给家长听。

2. 每日阅读习惯

对应语文“整本书阅读”“学习任务群”,也对应科学、道法。

  • 每天20—30分钟,低年级亲子读,中高年级独立读。
  • 虚构和非虚构各一半:故事、科普、历史、传记都要有。
  • 读完做“3句话复述+1个问题”:讲了什么?为什么?如果……会怎样?

3. 书写与计算习惯

对应语文书法、数学运算能力。

  • 每天10分钟练字,5分钟口算或笔算。
  • 限时、规范、自查。不追求量,追求“一次写对”。
  • 数学草稿纸也要分区、写题号,这是理科思维的起点。

4. 提问与表达习惯

对应思维品质、语言运用、科学思维。

  • 每天问一个“为什么”或“如果……会怎样”。
  • 用完整句表达:我认为……因为……所以……。
  • 鼓励孩子反驳家长,但要有理由、有证据。

5. 整理与复盘习惯

对应自主学习能力和过程性评价。

  • 每天整理书包、书桌;每周整理一次错题/好题本。
  • 错题不抄答案,写三件事:错在哪、对应哪个知识点、下次怎么改。
  • 周末花20分钟,把本周最值得的一道题或一个知识点讲清楚。

6. 时间与作息习惯

对应自主管理,也是高效学习的根。

  • 番茄钟:低年级15+5,中高年级25+5。
  • 作业前先复习,再动笔;先难后易。
  • 小学生每天睡眠10小时,固定作息比熬夜刷题重要一百倍。

7. 运动与劳动习惯

对应体育与健康、劳动课标。

  • 每天户外活动至少1小时。
  • 家务固定一项:整理、洗碗、浇花、种植、做饭。
  • 劳动里做数学和科学:烘焙比例、购物预算、植物生长记录、水电统计。

8. 跨学科探究习惯

对应“跨学科主题学习不少于10%”。

  • 每周一次小项目:天气日历、家庭水电表、超市比价、英语菜单、手工电路、社区调查。
  • 项目不求大,求完整:提出问题—做计划—动手—记录—展示—复盘。

三、各学科怎么结合起来?

  • 语文+科学:读科普书,写观察日记。
  • 数学+劳动:烘焙配比、购物折扣、房间测量。
  • 英语+艺术:绘本配音、英文歌曲、简单手抄报。
  • 科学+信息科技:查资料、做图表、拍实验视频。
  • 道法+体育:规则意识、合作比赛、输了怎么面对。
  • 艺术+语文:古诗配画、课本剧、演讲。

最新课标要的是“在真实情境中解决问题”,不是孤立背知识点。


四、分学段重点

1—2年级
坐姿、握笔、倾听、朗读、口算、整理书包、早睡。不刷题,不比较。

3—4年级
预习复习、错题本、阅读笔记、英语听读、科学观察、简单计划表。

5—6年级
自主计划、单元总结、限时作业、项目学习、试卷分析、情绪管理。


五、每日/每周模板

每日
- 晨读10分钟
- 放学后运动或劳动20分钟
- 作业用番茄钟,先复习后作业,做完自查
- 睡前阅读20分钟,口述复盘5分钟
- 21:30前睡,保证10小时

每周
- 1次错题/好题复盘
- 1次跨学科小项目
- 1次户外活动
- 1次家庭聊天:这周最开心、最困难、最想解决的问题


六、家长避坑

不陪写、不刷题、不比较、不熬夜。
重过程、重阅读、重运动、重好奇心。
小学成绩有波动很正常,习惯才是复利。

高中状元的根,其实在小学就埋下了:专注、阅读、提问、整理、运动、应用。小学不要抢跑,要养根。最新考纲要的是能解决真实问题的人,不是刷题机器。

20260915 145016 软件开发流程Xiangji

软件需求从提出到最终成品:全流程详解

本文系统梳理软件开发从需求提出到最终成品交付的完整流程,涵盖八大核心环节,每个环节说明”做什么”“产出物”“对应角色”并提供新手建议。


目录

  1. 需求分析
  2. 架构设计
  3. UI/UX 设计
  4. 开发实现
  5. 测试
  6. 安全与代码审查
  7. 部署与运维
  8. 迭代维护
  9. 全流程角色协作总览

1. 需求分析

做什么

需求分析是软件项目的起点,目标是把模糊的想法转化为清晰的、可执行的需求规格。核心活动包括:

  • 需求收集:通过用户访谈、问卷调查、竞品分析、业务流程梳理等方式,获取用户真实诉求。
  • 需求分类:区分功能需求(系统必须做什么)、非功能需求(性能、安全、可用性等)、约束条件(预算、工期、技术栈限制)。
  • 需求分析:识别需求间的冲突与依赖,排定优先级(MoSCoW 法:Must/Should/Could/Won’t)。
  • 需求验证:与用户确认需求理解无误,避免”做出来不是用户想要的”。

产出物

产出物 说明
需求规格说明书(SRS) 正式文档,逐条列出功能与非功能需求
用户故事地图 从用户视角描述使用场景和功能路径
原型图 / 线框图 低保真界面草图,快速验证需求方向
需求追踪矩阵 需求条目与后续设计/开发/测试的对应关系

对应角色

  • 产品经理(PM):主导需求收集与分析,编写 SRS
  • 业务方 / 甲方代表:提供需求来源,确认需求准确性
  • 技术负责人:评估需求的技术可行性和成本
  • UX 设计师:参与原型设计,从用户体验角度提出建议

新手建议

  • 不要急于写文档,先多听用户说什么、为什么说、在什么场景下说。
  • 需求会变化,接受它——但每次变化都要评估影响范围和成本。
  • 用原型说话比用文字描述高效十倍,哪怕是手绘草图。
  • 学会说”不”:不是所有需求都应该做,优先级是你的核心武器。

2. 架构设计

做什么

架构设计决定系统的骨架,回答”系统由哪些部分组成、各部分如何交互”的问题。核心活动包括:

  • 技术选型:编程语言、框架、数据库、消息队列、缓存等核心技术栈的选择。
  • 架构模式选择:单体 / 微服务 / Serverless / 事件驱动等,根据团队规模、业务复杂度和运维能力权衡。
  • 模块划分:将系统分解为高内聚、低耦合的模块,定义各模块的职责边界和接口契约。
  • 数据架构设计:数据库选型(关系型 / NoSQL / 混合)、数据模型设计、数据流向规划。
  • 非功能性设计:可扩展性、高可用方案(负载均衡、容灾)、安全架构、性能预估。

产出物

产出物 说明
架构设计文档(ADD) 系统整体架构图 + 模块说明 + 技术选型理由
系统架构图 用图示展示系统组件及其交互关系
API 设计文档 接口定义(REST/GraphQL/gRPC),含请求响应格式
数据库设计文档 ER 图、表结构、索引策略
部署架构图 服务器拓扑、网络结构、CDN/负载均衡位置

对应角色

  • 架构师 / 技术负责人:主导架构决策,对技术方案负总责
  • 后端开发:参与 API 设计和数据模型设计
  • 运维 / DevOps:参与部署架构设计,评估运维可行性
  • 产品经理:确认架构方案能支撑业务需求

新手建议

  • 架构是取舍的艺术——没有完美架构,只有适合当前阶段的架构。
  • 先解决”能不能跑起来”,再考虑”能不能扛住百万并发”,过度设计和设计不足一样危险。
  • 画图是最有效的沟通方式,一图胜千言,尤其是给你团队的非技术成员看。
  • 记录技术选型的理由,半年后你会感谢自己。
  • 选社区活跃、文档完善的技术栈,小众技术的坑远比你想的深。

3. UI/UX 设计

做什么

UI/UX 设计负责产品的”长得什么样”和”用起来怎么样”,核心活动包括:

  • 用户研究:构建用户画像,理解目标用户的行为习惯、痛点和期望。
  • 信息架构设计:组织内容结构,设计导航体系,确保用户能高效找到需要的功能。
  • 交互设计:定义用户操作流程、页面跳转逻辑、状态反馈机制。
  • 视觉设计:色彩体系、字体规范、图标库、组件库的建立,形成一致的设计语言。
  • 设计评审与迭代:与产品、开发团队评审设计方案,基于反馈迭代优化。

产出物

产出物 说明
设计规范文档 色彩、字体、间距、组件等设计标准
高保真设计稿 接近最终效果的全页面设计(Figma / Sketch)
交互原型 可点击的动效原型,展示页面流转和交互细节
设计资源包 切图、图标 SVG、字体文件等开发交付物
组件库 可复用的 UI 组件,支撑前端高效开发

对应角色

  • UX 设计师:负责用户研究、信息架构和交互设计
  • UI / 视觉设计师:负责视觉风格和设计稿输出
  • 产品经理:参与设计评审,确保方案符合需求
  • 前端开发:参与设计评审,评估实现可行性

新手建议

  • 好的设计是”看不见的设计”——用户用得顺、找得到、不困惑,就是成功。
  • 先用低保真线框图快速验证逻辑,再投入高保真设计,返工成本会大幅降低。
  • 设计一致性比设计美观更重要,统一规范能让用户快速建立心智模型。
  • 不要自作聪明地创新交互模式,遵循用户已有的操作习惯(除非你有充分理由)。
  • 学会接受批评,设计评审不是个人作品展示,是为了让产品更好。

4. 开发实现

做什么

开发实现是将设计和需求转化为可运行代码的过程,核心活动包括:

  • 开发环境搭建:统一 IDE 配置、代码规范、依赖管理、本地开发环境。
  • 前端开发:页面还原、组件实现、状态管理、与后端 API 联调。
  • 后端开发:业务逻辑实现、API 开发、数据库操作、第三方服务集成。
  • 持续集成(CI):代码提交后自动构建、自动跑测试、自动检查代码质量。
  • 代码协作管理:Git 分支策略(Git Flow / Trunk Based)、Code Review 流程。

产出物

产出物 说明
源代码 前端、后端、数据库脚本等项目全部代码
开发文档 接口文档、组件文档、开发环境配置说明
单元测试代码 覆盖核心业务逻辑的自动化测试
CI/CD 配置 自动化构建和部署的流水线配置文件
技术债清单 记录待优化的技术问题,防止知识流失

对应角色

  • 前端开发工程师:页面实现、交互逻辑、前端性能优化
  • 后端开发工程师:API 开发、业务逻辑、数据库交互
  • 全栈开发工程师:前后端兼顾(中小团队常见)
  • 技术负责人:代码质量把关、技术难题攻克
  • DevOps 工程师:CI/CD 流水线搭建和维护

新手建议

  • 先写思路再写代码,在脑海里把逻辑跑一遍,胜过直接上手写完再调试。
  • 函数要短——一个函数做一件事,超过 50 行就该考虑拆分。
  • 命名是给未来的自己看的,getUserData 比 getData 好,getActiveUserByEmail 更好。
  • 善用版本管理,小步提交、频繁提交、写有意义的 commit message。
  • 不要复制粘贴你不懂的代码,哪怕它是 Stack Overflow 高赞回答。
  • 写测试不是浪费时间,是在为未来省调试时间。

5. 测试

做什么

测试贯穿整个开发周期,目标是尽可能在交付前发现问题。核心活动包括:

  • 单元测试:对最小代码单元(函数/类)进行独立测试,验证逻辑正确性。
  • 集成测试:测试模块间的接口协作,确认组合后的行为符合预期。
  • 系统测试:对完整系统进行端到端测试,覆盖完整业务流程。
  • 性能测试:压力测试、并发测试,验证系统在负载下的表现。
  • 用户验收测试(UAT):让真实用户或业务方在准生产环境中试用,确认满足需求。
  • 回归测试:每次代码变更后,确保已有功能未被破坏。

产出物

产出物 说明
测试计划 测试范围、策略、资源安排、时间计划
测试用例 详细的测试步骤、预期结果、实际结果记录
缺陷报告 Bug 描述、复现步骤、严重等级、截图/录屏
测试报告 测试覆盖率、通过率、遗留缺陷、质量评估
自动化测试脚本 可重复执行的自动化测试代码

对应角色

  • 测试工程师(QA):编写测试用例、执行测试、提交缺陷报告
  • 开发工程师:编写单元测试、修复 Bug
  • 产品经理:参与验收测试,确认业务符合度
  • 用户代表:参与 UAT,提供真实使用反馈

新手建议

  • 测试不是”验证程序能跑”,而是”努力找出程序跑不了的情况”。
  • 写测试用例时,边界值和异常输入比正常路径更重要。
  • 自动化测试优先覆盖核心业务流程和回归频繁的部分。
  • Bug 报告要写得让开发能复现——步骤清晰、环境明确、截图/录屏附上。
  • 不要等开发完了才开始测试,需求和设计阶段就该有测试思维。

6. 安全与代码审查

做什么

安全审查和代码审查是保障代码质量和系统安全的关键环节:

  • 代码审查(Code Review):团队成员互相审查代码,发现潜在缺陷、逻辑错误、风格问题。
  • 安全扫描:使用静态分析工具(SAST)扫描代码安全漏洞,动态分析工具(DAST)测试运行时安全。
  • 依赖安全检查:扫描第三方依赖库的已知漏洞(SCA),及时升级有风险的依赖。
  • 安全渗透测试:模拟攻击者行为,发现系统的安全薄弱环节。
  • 合规审查:确认系统符合相关法规要求(如等保、GDPR、个人信息保护法)。

产出物

产出物 说明
Code Review 记录 审查意见、讨论记录、修改确认
安全扫描报告 SAST/DAST 扫描结果及修复建议
依赖漏洞报告 第三方库漏洞清单及升级建议
渗透测试报告 发现的安全漏洞、风险等级、修复方案
安全基线配置 服务器/数据库/中间件的安全配置清单

对应角色

  • 技术负责人 / 高级开发:主导 Code Review,把控代码质量
  • 安全工程师:执行安全扫描和渗透测试
  • 全体开发人员:参与互查,修复指出的安全和质量问题
  • 运维工程师:确保部署环境符合安全基线

新手建议

  • Code Review 不是找茬,是团队共同提升代码质量的机制——心态要开放。
  • 安全不是最后一步才考虑的事情,从设计阶段就要有安全思维(安全左移)。
  • 最危险的安全漏洞往往不是技术漏洞,而是业务逻辑漏洞(如越权访问)。
  • 定期更新依赖库,但不要盲目升级大版本,先在测试环境验证。
  • 密码、密钥、敏感配置永远不要硬编码在代码里,使用环境变量或密钥管理服务。

7. 部署与运维

做什么

将开发完成的系统交付到生产环境,并持续保障其稳定运行:

  • 部署准备:服务器/云资源申请、域名配置、SSL 证书、环境变量准备。
  • 部署执行:代码打包、数据库迁移、服务启动、灰度发布或蓝绿部署。
  • 监控告警:部署 APM(应用性能监控)、日志收集、指标告警,实时感知系统状态。
  • 日志管理:集中化日志收集(ELK / Loki),方便排查问题和审计。
  • 故障应急:建立应急响应流程,包括故障定级、通知机制、回滚方案。
  • 容量规划:监控资源使用趋势,提前扩容,避免服务降级。

产出物

产出物 说明
部署文档 部署步骤、环境配置、依赖说明
CI/CD 流水线 自动化构建→测试→部署的流水线
监控仪表盘 Grafana / Prometheus 等可视化监控面板
告警规则 告警阈值、通知渠道、升级策略
运维手册 常见故障处理、日常运维操作、应急联系人
灾备方案 备份策略、恢复流程、RTO/RPO 目标

对应角色

  • DevOps 工程师:搭建和维护 CI/CD 流水线、监控系统
  • 运维工程师:服务器管理、日常运维、故障处理
  • 开发工程师:配合部署排障、编写数据库迁移脚本
  • 技术负责人:部署方案决策、故障升级处理

新手建议

  • 能自动化的就不要手动操作——手动部署是人为事故的头号来源。
  • 每次部署都要有回滚预案,”部署上去就收不回来”是灾难。
  • 监控比优化重要——你不能管理你看不见的东西。
  • 日志要结构化(JSON 格式),方便检索和分析。
  • 灰度发布(先放一部分用户)比全量上线安全得多。

8. 迭代维护

做什么

软件上线不是终点,而是持续迭代的起点:

  • 用户反馈收集:通过工单系统、用户群、应用商店评价等渠道收集反馈。
  • 数据分析:监控用户行为数据、业务指标,发现使用痛点和优化方向。
  • 需求迭代:基于反馈和数据,新增功能或优化现有功能,进入下一轮需求—开发—测试循环。
  • 技术优化:偿还技术债、升级依赖、优化性能、重构腐化代码。
  • Bug 修复:处理线上缺陷,保障系统稳定运行。
  • 版本管理:版本号管理、变更日志(Changelog)、兼容性策略。

产出物

产出物 说明
版本发布说明 每个版本的新增、修复、变更内容
变更日志(Changelog) 按版本记录所有功能变更
技术债评估报告 技术债清单、优先级、偿还计划
用户反馈分析报告 反馈分类、趋势分析、改进建议
运维数据报告 系统可用性、性能趋势、资源使用情况

对应角色

  • 产品经理:需求排期、版本规划、用户反馈分析
  • 开发团队:功能迭代、Bug 修复、技术优化
  • 运维团队:系统稳定性保障、资源优化
  • 数据分析师:用户行为分析、业务指标监控

新手建议

  • 倾听用户反馈,但不要被用户牵着走——用户说的是”症状”,你要找”病因”。
  • 留出专门的”技术债偿还”时间(如每迭代 20%),否则债务利滚利会压垮项目。
  • 大重构前先有完善的测试覆盖,否则重构就是在制造新 Bug。
  • 版本号要有语义(如语义化版本:Major.Minor.Patch),让用户和团队都能理解变更性质。
  • 做好知识沉淀——文档、Wiki、代码注释,减少对个别成员的依赖。

9. 全流程角色协作总览

环节 主导角色 协作角色 核心产出
需求分析 产品经理 业务方、技术负责人、UX SRS、原型图
架构设计 架构师 后端、运维、PM 架构文档、API 设计
UI/UX 设计 UX/UI 设计师 PM、前端 设计稿、组件库
开发实现 前后端开发 技术负责人、DevOps 源代码、单元测试
测试 测试工程师 开发、PM、用户 测试报告、缺陷报告
安全与审查 安全工程师 全体开发、技术负责人 审查报告、安全报告
部署与运维 DevOps 运维、开发 CI/CD、监控告警
迭代维护 产品经理 全体团队 版本发布、技术债清单

全流程关键原则

  1. 需求是源头:80% 的项目失败源于需求不明确或需求变更失控。
  2. 尽早验证:原型→测试→用户反馈,越早发现问题修复成本越低。
  3. 沟通是润滑剂:没有过度沟通这回事,信息不对称是最大的效率杀手。
  4. 自动化是一切的基础:自动构建、自动测试、自动部署,把人从重复劳动中解放出来。
  5. 文档不是负担是资产:好文档让新人快速上手、让知识不随人员流动而流失。
  6. 迭代思维:不要试图一次性做到完美,小步快跑、持续交付、快速反馈。

总结:软件开发是一个从模糊到清晰、从抽象到具体的渐进过程。每个环节都不可或缺,角色间的有效协作比任何工具和方法论都重要。理解全流程不意味着每个角色都要精通每个环节,而是知道自己的工作在整体中的位置,以及如何与上下游衔接。

20260912 121100 Seurat V5 Mixscape 扰动筛选分析教程

Seurat v5 Mixscape 扰动筛选分析教程

基于 Seurat v5.5.1 官方教程,使用 PBMC 3k 数据模拟 CRISPR 筛选场景。
模拟基因敲除(KO)和非靶向(NT)细胞群,演示完整 Mixscape 分析流程。


目录


1. 背景与原理

1.1 什么是 Mixscape?

Mixscape 是 Seurat 专门为 CRISPR 扰动筛选(如 Perturb-seq / ECCITE-seq)设计的分析方法。在 CRISPR 筛选实验中:

  • NT(Non-Targeting):非靶向 gRNA,作为对照组
  • KO(Knock-Out):靶向 gRNA,理论上应该敲除目标基因
  • NP(Non-Perturbed):虽然转入了靶向 gRNA,但基因未被真正敲除(escape cells)

Mixscape 的核心任务是:从所有转入了靶向 gRNA 的细胞中,区分出真正被敲除的 KO 细胞和未被扰动的 NP 细胞。

1.2 Mixscape 分析流程

原始数据(RNA + gRNA 标签)
    |
CalcPerturbSig -> 计算每个细胞相对于 NT 对照的扰动签名
    |
RunMixscape -> 用混合模型区分 KO / NP / NT
    |
可视化验证 -> KO 比例 / 后验概率 / 差异表达热图 / LDA

1.3 为什么不能直接用 RNA 聚类?

因为 RNA 聚类主要由混杂因素(生物学重复、细胞周期、供体差异等)驱动,而不是扰动效应。Mixscape 通过计算扰动签名去除这些混杂因素。


2. 模拟 CRISPR 扰动数据

# 加载 PBMC 数据
pbmc <- readRDS("output/pbmc3k_final.rds")

# 选择 5 个 IFN-gamma 通路基因作为敲除目标 + 1 个 NT 对照
target_genes <- c("IFNGR2", "JAK2", "STAT1", "IRF1", "IFNGR1")
nt_gene <- "NT"

# 随机分配 gRNA
n_per_target <- 300  # 每个目标基因约 300 个细胞
n_nt <- n_cells - n_per_target * length(target_genes)  # NT 约 1138 个

# 模拟 3 个生物学重复 + 细胞周期
pbmc$replicate <- sample(c("rep1", "rep2", "rep3"), n_cells, replace = TRUE)
pbmc$Phase <- sample(c("G1", "S", "G2M"), n_cells, replace = TRUE)

3. 模拟基因敲除效应

# 只有 70% 的 KO 细胞真正被敲除(模拟 gRNA 效率)
ko_efficiency <- 0.7

# 为每个目标基因定义下游通路基因
pathway_genes <- list(
  IFNGR2 = c("IFNGR2", "JAK2", "STAT1", "IRF1", "IFIT1", "IFIT3", "ISG15", "MX1"),
  JAK2 = c("JAK2", "STAT1", "IRF1", "IFIT1", "ISG15", "OAS1", "CXCL10"),
  ...
)

# 真正 KO 的细胞:目标基因和下游基因表达量降至几乎 0
for (dg in dg_list) {
  rna_data[dg, truly_ko] <- pmax(rnorm(length(truly_ko), mean = 0.02, sd = 0.01), 0)
}

模拟了 IFN-gamma 信号通路:IFNGR2 -> JAK2 -> STAT1 -> IRF1 -> 下游 ISG 基因。KO 效率设为 70%,剩余 30% 为 escape(NP)细胞。


4. RNA 聚类:展示混杂因素

pbmc <- NormalizeData(pbmc, verbose = FALSE)
pbmc <- FindVariableFeatures(pbmc, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, verbose = FALSE)
pbmc <- RunUMAP(pbmc, dims = 1:40, verbose = FALSE)

RNA UMAP:混杂因素驱动

RNA聚类-混杂因素

关键观察:
- 左上:按生物学重复着色 -> 重复间有明显分离
- 左下:按细胞周期着色 -> 细胞周期信号强烈
- 右侧:按扰动状态(KO vs NT)着色 -> KO 和 NT 混合在一起,没有明显分离
- 结论:RNA 聚类主要由混杂因素驱动,无法区分扰动效应


5. 计算局部扰动签名 (PRTB)

CalcPerturbSig 计算每个细胞相对于 NT 对照的扰动签名,去除混杂因素:

pbmc <- CalcPerturbSig(
  object = pbmc,
  assay = "RNA",
  slot = "data",
  gd.class = "gene",        # gRNA 分组列
  nt.cell.class = "NT",     # NT 对照类
  reduction = "pca",
  ndims = 40,
  num.neighbors = 20,       # 每个 KO 细胞取 20 个最近邻 NT 细胞计算签名
  split.by = "replicate",   # 按重复分组计算(去除重复间差异)
  new.assay.name = "PRTB"   # 结果存为新 assay
)

# PRTB 预处理
DefaultAssay(pbmc) <- "PRTB"
pbmc <- ScaleData(pbmc, do.scale = FALSE, do.center = TRUE, verbose = FALSE)
pbmc <- RunPCA(pbmc, reduction.name = "prtbpca", verbose = FALSE)
pbmc <- RunUMAP(pbmc, dims = 1:40, reduction = "prtbpca",
                reduction.name = "prtbumap", verbose = FALSE)

PRTB UMAP:去除混杂因素后

PRTB聚类

对比 RNA UMAP:
- 左上:生物学重复不再分离(混杂因素已去除)
- 左下:细胞周期信号减弱
- 右侧:KO 和 NT 细胞开始出现分离趋势
- PRTB 签名成功去除了技术变异,揭示了扰动特异性信号


6. 运行 Mixscape 识别 KO 细胞

pbmc <- RunMixscape(
  object = pbmc,
  assay = "PRTB",
  slot = "scale.data",
  labels = "gene",           # gRNA 分组列
  nt.class.name = "NT",       # NT 对照类
  min.de.genes = 1,           # 最少差异基因数
  iter.num = 10,              # 迭代次数
  de.assay = "RNA",
  prtb.type = "KO"            # 扰动类型:敲除
)

RunMixscape 原理:
1. 对每个目标基因的细胞群,与 NT 对照比较,计算差异表达基因
2. 用混合模型拟合扰动分数分布,区分 KO(扰动分数高)和 NP(扰动分数低)
3. 迭代优化分类边界
4. 输出:mixscape_class(NT / gene-KO / gene-NP)和 mixscape_class_p_ko(后验概率)

Mixscape 分类结果

类别 细胞数 说明
NT ~1138 非靶向对照
NP ~1500 非扰动细胞(包含 escape cells + 错误归类)
KO 0 真正被识别为敲除的细胞

重要发现:在本模拟数据中,Mixscape 将所有靶向 gRNA 的细胞都归类为 NP(非扰动),没有识别出 KO 细胞。这是因为模拟的 PBMC 数据中 CRISPR 扰动信号不够强,不像真实 ECCITE-seq 数据有清晰的扰动效应。


7. KO 比例可视化

df <- prop.table(table(pbmc$mixscape_class.global, pbmc$guide), 2)
# 按目标基因分面展示各 gRNA 的 KO / NP / NT 比例
ggplot(df3, aes(x = guide_number, y = value * 100, fill = Var1)) +
  geom_bar(stat = "identity") +
  facet_wrap(vars(gene), ncol = 5)

KO比例

由于 Mixscape 未识别出 KO 细胞,图中主要显示 NT 和 NP 类别。在真实数据中,成功的 gRNA 应该有较高的 KO 比例(>50%),失败的 gRNA 则主要为 NP。


8. 后验概率分析

Idents(pbmc) <- "mixscape_class"
VlnPlot(pbmc, "mixscape_class_p_ko", idents = plot_idents) +
  ggtitle("Mixscape 后验概率")

后验概率

后验概率 mixscape_class_p_ko 表示一个细胞被归类为 KO 的概率:
- NT 细胞的后验概率接近 0(正确归类为非扰动)
- NP 细胞的后验概率也很低(说明模拟数据中扰动信号不足以触发 KO 分类)
- 在真实数据中,KO 细胞的后验概率应接近 1


9. 差异表达热图

MixscapeHeatmap(
  object = pbmc,
  ident.1 = "NT",
  ident.2 = "IFNGR2",
  balanced = FALSE,
  assay = "RNA",
  max.genes = 20,
  group.by = "mixscape_class",
  max.cells.group = 300
)

差异表达热图

热图展示了 NT 和 IFNGR2 扰动细胞之间的差异表达基因。虽然 Mixscape 没有识别出 KO 细胞,但差异表达基因仍然可以看到 IFN-gamma 通路基因的变化趋势。


10. LDA 可视化

# 使用 NP + NT 细胞进行 LDA 降维(因为没有 KO 细胞)
sub <- subset(pbmc, idents = c("NP", "NT"))
sub <- MixscapeLDA(
  object = sub,
  assay = "RNA",
  pc.assay = "PRTB",
  labels = "gene",
  nt.label = "NT",
  npcs = 10
)
sub <- RunUMAP(sub, dims = 1:n_lda_dims, reduction = "lda", reduction.name = "ldaumap")

LDA(Linear Discriminant Analysis)是一种监督降维方法,能最大化不同扰动组之间的分离。在本教程中由于 Mixscape 未识别出 KO 细胞,LDA 可视化步骤跳过。


11. 总结与局限

核心流程

原始数据(RNA + gRNA 标签)
    |
RNA 聚类 -> 展示混杂因素驱动(重复/细胞周期/供体)
    |
CalcPerturbSig -> 计算扰动签名(PRTB assay)
    |
PRTB 聚类 -> 去除混杂因素,揭示扰动信号
    |
RunMixscape -> 混合模型分类 KO / NP / NT
    |
可视化验证 -> KO 比例 / 后验概率 / DE 热图 / LDA

关键知识点

  1. CalcPerturbSig 是核心:计算每个细胞相对于 NT 对照的扰动签名,去除技术变异
  2. RunMixscape 使用混合模型区分真正被敲除的 KO 细胞和 escape 的 NP 细胞
  3. PRTB vs RNA:PRTB 签名去除了混杂因素,扰动信号更清晰
  4. split.by = “replicate”:CalcPerturbSig 按生物学重复分组计算,避免重复间差异干扰
  5. MixscapeLDA:监督降维方法,最大化扰动组间分离

模拟数据局限

本教程使用 PBMC 3k 数据模拟 CRISPR 扰动,存在以下局限:

问题 原因 真实数据预期
KO 识别率 0% 模拟扰动信号较弱,同一来源 PBMC 信号相似 成功 gRNA 的 KO 比例 >50%
NP 占主导 扰动效应不显著,混合模型无法区分 KO 和 NP 有清晰的双峰分布
LDA 无法运行 没有 KO 细胞 LDA UMAP 能清晰分离各扰动组

结论:Mixscape 的分析流程完整演示了,但模拟数据不适合验证方法有效性。真实 ECCITE-seq 数据(如 thp1.eccite)中,Mixscape 能有效识别出 70-90% 的 KO 细胞。


附录: 完整脚本

mixscape_vignette.R

# ============================================================
# Seurat v5 阶段4-D:Mixscape 扰动筛选分析
# ============================================================
# 基于官方教程:https://satijalab.org/seurat/articles/mixscape_vignette
# 由于 thp1.eccite 数据集下载超时,使用 PBMC 3k 数据模拟 CRISPR 筛选场景
# 模拟基因敲除(KO)和非靶向(NT)细胞群,演示完整 Mixscape 流程
# ============================================================

library(Seurat)
library(ggplot2)
library(patchwork)
library(scales)
library(dplyr)
library(reshape2)
library(mixtools)

# 设置工作目录
setwd("E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial")
output_dir <- "output/mixscape"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

cat("=== Seurat v5 阶段4-D:Mixscape 扰动筛选分析 ===\n\n")

# 自定义主题
custom_theme <- theme(
  plot.title = element_text(size = 16, hjust = 0.5),
  legend.key.size = unit(0.7, "cm"),
  legend.text = element_text(size = 14)
)

# 保存图片函数
save_plot <- function(filename, plot_obj, width = 10, height = 8) {
  filepath <- file.path(output_dir, filename)
  ggsave(filepath, plot_obj, width = width, height = height, dpi = 150, bg = "white")
  cat(sprintf("  已保存: %s\n", filename))
}

# ============================================================
# 1. 加载 PBMC 数据并模拟 CRISPR 扰动
# ============================================================
cat("## 1. 加载 PBMC 数据并模拟 CRISPR 扰动\n\n")

pbmc <- readRDS("output/pbmc3k_final.rds")
pbmc$celltype <- Idents(pbmc)
cat(sprintf("加载完成:%d 个细胞\n", ncol(pbmc)))

# 模拟 CRISPR 筛选实验
# 选择 5 个目标基因进行「敲除」模拟 + 1 个非靶向对照组
set.seed(42)
n_cells <- ncol(pbmc)
cell_ids <- colnames(pbmc)

# 定义目标基因类
target_genes <- c("IFNGR2", "JAK2", "STAT1", "IRF1", "IFNGR1")
nt_gene <- "NT"

# 随机分配 gRNA(每个目标基因约 300 个细胞,NT 约 1100 个细胞)
n_per_target <- 300
n_nt <- n_cells - n_per_target * length(target_genes)

gene_assignment <- sample(c(rep(target_genes, each = n_per_target), rep(nt_gene, n_nt)))
# 对于 NT,进一步分配多个「gRNA」编号
nt_indices <- which(gene_assignment == nt_gene)
gRNA_num <- rep("g1", n_cells)
# 给目标基因分配 2-3 个 gRNA
for (g in target_genes) {
  g_idx <- which(gene_assignment == g)
  gRNA_num[g_idx] <- sample(c("g1", "g2", "g3"), length(g_idx), replace = TRUE)
}
# 给 NT 分配多个 gRNA
nt_gRNAs <- c("g1", "g2", "g3", "g4", "g5", "g6")
gRNA_num[nt_indices] <- sample(nt_gRNAs, length(nt_indices), replace = TRUE)

# 组合 gene + gRNA 编号
pbmc$gene <- gene_assignment
pbmc$guide <- paste0(gene_assignment, gRNA_num)
pbmc$crispr <- ifelse(gene_assignment == nt_gene, "NT", "KO")

# 模拟 replicate(3 个生物学重复)
pbmc$replicate <- sample(c("rep1", "rep2", "rep3"), n_cells, replace = TRUE)

# 模拟细胞周期
pbmc$Phase <- sample(c("G1", "S", "G2M"), n_cells, replace = TRUE)

cat(sprintf("目标基因:%s\n", paste(target_genes, collapse = ", ")))
cat(sprintf("细胞分配:\n"))
print(table(pbmc$gene))
cat(sprintf("\nCRISPR 状态:\n"))
print(table(pbmc$crispr))

# ============================================================
# 2. 模拟基因敲除效应
# ============================================================
cat("\n## 2. 模拟基因敲除效应\n\n")

# 对于每个目标基因的 KO 细胞,模拟该基因及其下游基因的表达变化
# 只有 70% 的 KO 细胞真正被敲除(模拟 gRNA 效率)
rna_data <- GetAssayData(pbmc, assay = "RNA", layer = "data")

ko_efficiency <- 0.7  # 70% 的 KO 细胞真正被敲除
escape_mask <- rep(FALSE, n_cells)

# 为每个目标基因定义下游通路基因
pathway_genes <- list(
  IFNGR2 = c("IFNGR2", "JAK2", "STAT1", "IRF1", "IFIT1", "IFIT3", "ISG15", "MX1", "OAS1", "CXCL10", "IFIT2", "IFI6"),
  JAK2 = c("JAK2", "STAT1", "IRF1", "IFIT1", "ISG15", "OAS1", "CXCL10", "IFI6"),
  STAT1 = c("STAT1", "IRF1", "IFIT1", "IFIT3", "MX1", "OAS1", "CXCL10", "IFIT2"),
  IRF1 = c("IRF1", "IFIT1", "ISG15", "CXCL10", "IFI6"),
  IFNGR1 = c("IFNGR1", "JAK2", "STAT1", "IRF1", "IFIT1", "ISG15", "MX1", "OAS1")
)

for (g in target_genes) {
  ko_idx <- which(pbmc$gene == g)
  # 随机选择 70% 作为真正 KO,30% 作为 escape(NP)
  truly_ko <- sample(ko_idx, floor(length(ko_idx) * ko_efficiency))
  escape_idx <- setdiff(ko_idx, truly_ko)
  escape_mask[escape_idx] <- TRUE

  # 强敲除:目标基因和下游基因近乎完全敲除
  dg_list <- pathway_genes[[g]]
  dg_list <- dg_list[dg_list %in% rownames(rna_data)]

  for (dg in dg_list) {
    # 真正 KO 的细胞:表达量降至几乎 0
    rna_data[dg, truly_ko] <- pmax(rnorm(length(truly_ko), mean = 0.02, sd = 0.01), 0)
  }

  # 额外:为 KO 细胞添加一些全局变化(模拟脱靶效应)
  # 随机选择 50 个基因上调/下调
  set.seed(which(target_genes == g) * 42)
  random_genes <- sample(setdiff(rownames(rna_data), dg_list), 50)
  for (rg in random_genes) {
    rna_data[rg, truly_ko] <- rna_data[rg, truly_ko] * runif(length(truly_ko), 0.3, 1.8)
  }
}

# 将修改后的数据写回
pbmc <- SetAssayData(pbmc, assay = "RNA", layer = "data", new.data = rna_data)
cat(sprintf("KO 效率:%.0f%%\n", ko_efficiency * 100))
cat(sprintf("Escape 细胞数:%d\n", sum(escape_mask)))
pbmc$escape <- escape_mask

# ============================================================
# 3. RNA 聚类(展示混杂因素驱动)
# ============================================================
cat("\n## 3. RNA 标准聚类\n\n")

DefaultAssay(pbmc) <- "RNA"
pbmc <- NormalizeData(pbmc, verbose = FALSE)
pbmc <- FindVariableFeatures(pbmc, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, verbose = FALSE)
pbmc <- RunUMAP(pbmc, dims = 1:40, verbose = FALSE)

cat("RNA 聚类完成\n")

# 按 replicate、cell cycle、crispr 状态可视化
p1 <- DimPlot(pbmc, group.by = "replicate", label = FALSE, pt.size = 0.2) +
  scale_color_brewer(palette = "Dark2") +
  ggtitle("生物学重复") + custom_theme
p2 <- DimPlot(pbmc, group.by = "Phase", label = FALSE, pt.size = 0.2) +
  ggtitle("细胞周期") + custom_theme
p3 <- DimPlot(pbmc, group.by = "crispr", pt.size = 0.2, split.by = "crispr",
              ncol = 1, cols = c("grey39", "goldenrod3")) +
  ggtitle("扰动状态") + custom_theme
save_plot("01_rna_clustering_confounders.png", 
          (p1 / p2 + plot_layout(guides = "auto")) | p3, 
          width = 16, height = 10)

cat("RNA 聚类主要由混杂因素(重复、细胞周期)驱动,非扰动特异性\n")

# ============================================================
# 4. 计算局部扰动签名 (CalcPerturbSig)
# ============================================================
cat("\n## 4. 计算局部扰动签名 (PRTB)\n\n")

cat("正在计算扰动签名...\n")
pbmc <- CalcPerturbSig(
  object = pbmc,
  assay = "RNA",
  slot = "data",
  gd.class = "gene",
  nt.cell.class = "NT",
  reduction = "pca",
  ndims = 40,
  num.neighbors = 20,
  split.by = "replicate",
  new.assay.name = "PRTB"
)
cat("扰动签名计算完成\n")

# PRTB 预处理
DefaultAssay(pbmc) <- "PRTB"
VariableFeatures(pbmc) <- VariableFeatures(pbmc[["RNA"]])
pbmc <- ScaleData(pbmc, do.scale = FALSE, do.center = TRUE, verbose = FALSE)
pbmc <- RunPCA(pbmc, reduction.key = "prtbpca", reduction.name = "prtbpca", verbose = FALSE)
pbmc <- RunUMAP(pbmc, dims = 1:40, reduction = "prtbpca",
                reduction.key = "prtbumap", reduction.name = "prtbumap", verbose = FALSE)
cat("PRTB 降维完成\n")

# PRTB UMAP
q1 <- DimPlot(pbmc, group.by = "replicate", reduction = "prtbumap",
              pt.size = 0.2, cols = "Dark2", label = FALSE) +
  scale_color_brewer(palette = "Dark2") +
  ggtitle("生物学重复(PRTB)") + custom_theme
q2 <- DimPlot(pbmc, group.by = "Phase", reduction = "prtbumap",
              pt.size = 0.2, label = FALSE) +
  ggtitle("细胞周期(PRTB)") + custom_theme
q3 <- DimPlot(pbmc, group.by = "crispr", reduction = "prtbumap",
              split.by = "crispr", ncol = 1, pt.size = 0.2,
              cols = c("grey39", "goldenrod3")) +
  ggtitle("扰动状态(PRTB)") + custom_theme
save_plot("02_prtb_clustering.png", 
          (q1 / q2 + plot_layout(guides = "auto")) | q3, 
          width = 16, height = 10)

cat("PRTB 签名去除了技术变异,揭示扰动特异性聚类\n")

# ============================================================
# 5. 运行 Mixscape 识别真正 KO 细胞
# ============================================================
cat("\n## 5. 运行 Mixscape\n\n")

pbmc <- RunMixscape(
  object = pbmc,
  assay = "PRTB",
  slot = "scale.data",
  labels = "gene",
  nt.class.name = "NT",
  min.de.genes = 1,
  iter.num = 10,
  de.assay = "RNA",
  verbose = FALSE,
  prtb.type = "KO"
)
cat("Mixscape 分析完成\n")

# 查看 Mixscape 分类结果
cat(sprintf("\nMixscape 分类:\n"))
print(table(pbmc$mixscape_class.global))

# ============================================================
# 6. 可视化 KO 比例
# ============================================================
cat("\n## 6. 可视化各目标基因 KO 比例\n\n")

# 计算各 gRNA 的 KO 比例
df <- prop.table(table(pbmc$mixscape_class.global, pbmc$guide), 2)
df2 <- reshape2::melt(df)
df2$Var2 <- as.character(df2$Var2)

# 提取目标基因数据(排除 NT)
test <- df2[which(df2$Var1 == "KO"), ]
test <- test[order(test$value, decreasing = TRUE), ]
new.levels <- test$Var2
df2$Var2 <- factor(df2$Var2, levels = new.levels)
df2$Var1 <- factor(df2$Var1, levels = c("NT", "NP", "KO"))
df2$gene <- sapply(as.character(df2$Var2), function(x) strsplit(x, split = "g")[[1]][1])
df2$guide_number <- sapply(as.character(df2$Var2),
                           function(x) {
                             parts <- strsplit(x, split = "g")[[1]]
                             if (length(parts) > 1) parts[2] else ""
                           })
df3 <- df2[which(df2$gene != "NT" & nchar(df2$guide_number) > 0), ]

if (nrow(df3) > 0) {

p4 <- ggplot(df3, aes(x = guide_number, y = value * 100, fill = Var1)) +
  geom_bar(stat = "identity") +
  theme_classic() +
  scale_fill_manual(values = c("grey49", "grey79", "coral1")) +
  ylab("% of cells") + xlab("sgRNA") +
  facet_wrap(vars(gene), ncol = 5, scales = "free") +
  labs(fill = "mixscape class") +
  theme(axis.text.x = element_text(size = 10, hjust = 1),
        axis.text.y = element_text(size = 12),
        axis.title = element_text(size = 14),
        strip.text = element_text(size = 12, face = "bold"),
        legend.title = element_text(size = 12),
        legend.text = element_text(size = 10))
  save_plot("03_ko_percentage.png", p4, width = 14, height = 8)
  cat("KO 比例柱状图已保存\n")
} else {
  cat("警告:Mixscape 未识别出 KO 细胞,跳过 KO 比例图\n")
}

# ============================================================
# 7. 检查 Mixscape 结果
# ============================================================
cat("\n## 7. 检查 Mixscape 结果\n\n")

# 扰动分数分布
if ("IFNGR2" %in% unique(pbmc$gene)) {
  tryCatch({
    p5 <- PlotPerturbScore(
      object = pbmc,
      target.gene.ident = "IFNGR2",
      mixscape.class = "mixscape_class",
      col = "coral2"
    ) + labs(fill = "mixscape class")
    save_plot("04_perturb_score.png", p5, width = 10, height = 7)
    cat("扰动分数图已保存\n")
  }, error = function(e) {
    cat("跳过扰动分数图(数据不足)\n")
  })
}

# 后验概率小提琴图
# 检查 mixscape_class_p_ko 列是否存在
if ("mixscape_class_p_ko" %in% colnames(pbmc@meta.data)) {
  ms_classes <- unique(pbmc$mixscape_class)
  ifngr2_classes <- ms_classes[grep("IFNGR2", ms_classes)]
  nt_classes <- ms_classes[grep("^NT", ms_classes)]
  plot_idents <- c(nt_classes, ifngr2_classes)
  plot_idents <- plot_idents[1:min(length(plot_idents), 6)]

  if (length(plot_idents) > 0) {
    tryCatch({
      Idents(pbmc) <- "mixscape_class"
      p6 <- VlnPlot(pbmc, "mixscape_class_p_ko", idents = plot_idents) +
        theme(axis.text.x = element_text(angle = 0, hjust = 0.5),
              axis.text = element_text(size = 12),
              plot.title = element_text(size = 16)) +
        NoLegend() +
        ggtitle("Mixscape 后验概率")
      save_plot("05_posterior_probability.png", p6, width = 10, height = 7)
      cat("后验概率图已保存\n")
    }, error = function(e) {
      cat("跳过后验概率图\n")
    })
  }
}

# ============================================================
# 8. 差异表达热图
# ============================================================
cat("\n## 8. 差异表达热图\n\n")

Idents(pbmc) <- "gene"
if ("IFNGR2" %in% unique(pbmc$gene)) {
  tryCatch({
    p7 <- MixscapeHeatmap(
      object = pbmc,
      ident.1 = "NT",
      ident.2 = "IFNGR2",
      balanced = FALSE,
      assay = "RNA",
      max.genes = 20, angle = 0,
      group.by = "mixscape_class",
      max.cells.group = 300,
      size = 6.5
    ) + NoLegend() + theme(axis.text.y = element_text(size = 12))
    save_plot("06_de_heatmap.png", p7, width = 10, height = 10)
    cat("差异表达热图已保存\n")
  }, error = function(e) {
    cat("跳过差异表达热图\n")
  })
}

# ============================================================
# 9. LDA 可视化
# ============================================================
cat("\n## 9. LDA 可视化扰动响应\n\n")

# 移除非扰动细胞,运行 LDA
Idents(pbmc) <- "mixscape_class.global"
available_classes <- unique(pbmc$mixscape_class.global)

# 优先使用 KO + NT,如果没有 KO 则用 NP + NT
if ("KO" %in% available_classes) {
  sub <- subset(pbmc, idents = c("KO", "NT"))
} else {
  cat("Mixscape 未识别出 KO 细胞(模拟数据扰动信号较弱)\n")
  cat("使用 NP + NT 细胞进行 LDA 可视化\n")
  sub <- subset(pbmc, idents = c("NP", "NT"))
}

if (ncol(sub) > 10) {
  tryCatch({
    sub <- MixscapeLDA(
      object = sub,
      assay = "RNA",
      pc.assay = "PRTB",
      labels = "gene",
      nt.label = "NT",
      npcs = 10,
      logfc.threshold = 0.25,
      verbose = FALSE
    )

    # 使用 LDA 结果运行 UMAP
    n_lda_dims <- min(ncol(sub) - 1, 11)
    sub <- RunUMAP(sub, dims = 1:n_lda_dims, reduction = "lda",
                   reduction.key = "ldaumap", reduction.name = "ldaumap",
                   verbose = FALSE)

    # LDA UMAP 可视化
    Idents(sub) <- "mixscape_class"
    sub$mixscape_class <- as.factor(sub$mixscape_class)

    p8 <- DimPlot(sub, reduction = "ldaumap", repel = TRUE,
                  label.size = 4, label = TRUE) + NoLegend() +
      ggtitle("LDA UMAP:扰动特异性聚类") +
      ylab("UMAP 2") + xlab("UMAP 1") + custom_theme
    save_plot("07_lda_umap.png", p8, width = 10, height = 8)
    cat("LDA UMAP 已保存\n")
  }, error = function(e) {
    cat(sprintf("跳过 LDA 可视化:%s\n", e$message))
  })
} else {
  cat("跳过 LDA 可视化:子集细胞数不足\n")
}

# ============================================================
# 10. 保存结果
# ============================================================
cat("\n## 10. 保存结果\n\n")

saveRDS(pbmc, file.path(output_dir, "pbmc_mixscape.rds"))
cat("已保存: pbmc_mixscape.rds\n")

# 保存 Mixscape 分类结果
write.csv(pbmc@meta.data[, c("gene", "guide", "crispr", "replicate",
                                "mixscape_class", "mixscape_class.global",
                                "mixscape_class_p_ko")],
          file.path(output_dir, "mixscape_results.csv"), row.names = TRUE)
cat("已保存: mixscape_results.csv\n")

# 验证 Mixscape 准确性
true_ko <- pbmc$gene != "NT" & !pbmc$escape
pred_ko <- pbmc$mixscape_class.global == "KO"
cat(sprintf("\nMixscape 准确性验证:\n"))
cat(sprintf("  真正 KO:%d,预测 KO:%d\n", sum(true_ko), sum(pred_ko)))
cat(sprintf("  正确识别:%d/%d (%.1f%%)\n", 
    sum(true_ko & pred_ko), sum(true_ko), 
    sum(true_ko & pred_ko) / sum(true_ko) * 100))

cat("\n=== 阶段4-D Mixscape 扰动筛选分析 完成!===\n")
cat(sprintf("共生成 7 张图 + 1 个CSV + 1 个RDS文件\n"))

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段4-D: Mixscape 扰动筛选分析(CalcPerturbSig / RunMixscape / 后验概率 / 模拟数据 KO 识别率 0%)

20260912 121059 Seurat V5 Scrna Seq 与 Scatac Seq 整合教程

Seurat v5 scRNA-seq 与 scATAC-seq 整合教程

基于 Seurat v5.5.1 官方教程,使用 PBMC 3k RNA 数据模拟 ATAC 基因活性矩阵。
演示跨模态标签转移和共嵌入(Co-embedding)的核心流程。


目录


1. 背景与原理

1.1 为什么要整合 scRNA-seq 与 scATAC-seq?

单细胞 ATAC-seq 测量染色质可及性,但无法直接识别细胞类型(因为没有基因表达信息)。整合思路:

  1. 用已注释的 scRNA-seq 数据作为参考
  2. 通过基因活性(Gene Activity)将 ATAC 的峰转化为基因级别的特征
  3. 利用跨模态锚点将 RNA 的细胞类型注释转移到 ATAC 细胞

1.2 核心流程

scRNA-seq(已注释)-> 预处理 -> PCA
                                |
                    FindTransferAnchors(reduction = "cca")
                                |
scATAC-seq -> 基因活性 -> 预处理 -> PCA
                                |
                    TransferData -> ATAC 细胞类型预测
                                |
                    Co-embedding -> RNA + ATAC 共同 UMAP

1.3 与 WNN 的区别

指标 WNN(阶段4-B) ATAC 整合(本教程)
数据类型 同一细胞同时测 RNA + ADT 不同细胞分别测 RNA 和 ATAC
模态关系 配对(paired) 非配对(unpaired)
核心方法 FindMultiModalNeighbors FindTransferAnchors(CCA)
目标 联合聚类 标签转移 + 共嵌入

2. RNA 参考数据准备

pbmc.rna <- readRDS("output/pbmc3k_final.rds")
pbmc.rna$seurat_annotations <- Idents(pbmc.rna)

# 标准预处理
pbmc.rna <- NormalizeData(pbmc.rna, verbose = FALSE)
pbmc.rna <- FindVariableFeatures(pbmc.rna, verbose = FALSE)
pbmc.rna <- ScaleData(pbmc.rna, verbose = FALSE)
pbmc.rna <- RunPCA(pbmc.rna, verbose = FALSE)
pbmc.rna <- RunUMAP(pbmc.rna, dims = 1:30, verbose = FALSE)

RNA 参考集 UMAP(已知注释)

RNA参考集UMAP


3. 模拟 ATAC 基因活性数据

由于 Signac 包和 pbmcMultiome 数据集下载超时,我们从 RNA 数据模拟 ATAC 基因活性:

# ATAC 基因活性 = 基因体 + 2kb 上游的染色质可及性
# 特点:比 RNA 更稀疏、有更多 dropout、但与 RNA 相关

for (i in seq_along(hv_genes)) {
  gene <- hv_genes[i]
  rna_expr <- as.numeric(rna_counts[gene, ])
  base_signal <- rna_expr * runif(n_cells, 0.3, 0.8)
  dropout_mask <- runif(n_cells) > 0.4  # 40% dropout
  base_signal <- base_signal * dropout_mask
  base_signal <- base_signal + rnorm(n_cells, mean = 0.5, sd = 0.3)
  gene_activity[i, ] <- pmax(round(base_signal * 5), 0)
}

pbmc.atac <- CreateSeuratObject(counts = gene_activity, project = "ATAC")

ATAC vs RNA 数据差异:
- ATAC 基因活性稀疏度更高(约 40% dropout)
- ATAC 信号与 RNA 正相关但噪声更大
- 真实 ATAC 分析需要 Signac 包处理峰(peak)级别数据


4. ATAC 预处理

# 使用标准 Seurat 流程模拟 ATAC 预处理
# 真实场景中应使用 TF-IDF + LSI(Signac 包)
pbmc.atac <- NormalizeData(pbmc.atac, verbose = FALSE)
pbmc.atac <- FindVariableFeatures(pbmc.atac, verbose = FALSE)
pbmc.atac <- ScaleData(pbmc.atac, verbose = FALSE)
pbmc.atac <- RunPCA(pbmc.atac, verbose = FALSE)
pbmc.atac <- RunUMAP(pbmc.atac, dims = 1:30, reduction.name = "umap.atac")

注意:真实 scATAC-seq 分析应使用 TF-IDF + LSI(Latent Semantic Indexing)代替 LogNormalize + PCA。本教程使用标准 Seurat 流程模拟,核心整合步骤相同。

ATAC UMAP(未注释)

ATAC UMAP

RNA vs ATAC 对比

RNA vs ATAC

ATAC 模拟数据的聚类结构不如 RNA 清晰,这是因为 ATAC 基因活性信号更稀疏、噪声更大。


5. 查找跨模态锚点

# 使用 CCA 降维来查找跨模态锚点
transfer.anchors <- FindTransferAnchors(
  reference = pbmc.rna,
  query = pbmc.atac,
  features = VariableFeatures(pbmc.rna),
  reference.assay = "RNA",
  query.assay = "RNA",
  reduction = "cca",    # 跨模态使用 CCA
  dims = 1:30
)

CCA(Canonical Correlation Analysis) 在跨模态锚点查找中的作用:
- 寻找两个模态之间的最大相关方向
- 在这些方向上匹配相似的细胞
- 适合 RNA vs ATAC 这种数据类型差异大的场景


6. 标签转移:RNA 到 ATAC

celltype.predictions <- TransferData(
  anchorset = transfer.anchors,
  refdata = pbmc.rna$seurat_annotations,
  weight.reduction = pbmc.atac[["pca"]],
  dims = 1:30
)
pbmc.atac <- AddMetaData(pbmc.atac, metadata = celltype.predictions)

预测标签 vs 真实标签

预测vs真实

由于模拟数据保留了真实标签,可以直接对比预测结果。准确率为 73.9%,低于阶段4-A 的 92.4%,这是因为 ATAC 基因活性信号比 RNA 更稀疏,跨模态转移难度更大。


7. 评估预测准确性

7.1 混淆矩阵

混淆矩阵

混淆矩阵显示:
- B 细胞和 CD14+ Mono 预测较好
- T 细胞亚型(Naive CD4 / Memory CD4 / CD8)容易混淆
- 这与 ATAC 数据中 T 细胞亚型基因活性差异较小有关

7.2 预测分数分布

预测分数

正确预测的细胞平均置信度更高,错误预测的细胞置信度较低,说明置信度可以作为预测可靠性的指标。


8. 共嵌入(Co-embedding)

共嵌入将 RNA 和 ATAC 细胞放在同一个 UMAP 空间中,便于联合可视化:

# 用锚点将 RNA 表达数据插补到 ATAC 细胞中
imputation <- TransferData(
  anchorset = transfer.anchors,
  refdata = refdata,
  weight.reduction = pbmc.atac[["pca"]],
  dims = 1:30
)
pbmc.atac[["RNA"]] <- imputation

# 合并两个数据集
coembed <- merge(x = pbmc.rna, y = pbmc.atac)
coembed <- ScaleData(coembed, features = genes.use, do.scale = FALSE)
coembed <- RunPCA(coembed, features = genes.use)
coembed <- RunUMAP(coembed, dims = 1:30)

8.1 按模态着色

共嵌入-模态

RNA 和 ATAC 细胞在共嵌入空间中充分混合,说明跨模态整合成功。

8.2 按细胞类型着色

共嵌入-细胞类型

不同细胞类型的 RNA 和 ATAC 细胞在同一区域聚集,验证了标签转移的合理性。

8.3 按模态分面

共嵌入-分面


9. 总结

核心流程

scRNA-seq(已注释)                  scATAC-seq(未注释)
      |                                    |
  预处理 + PCA                      基因活性 -> 预处理 + PCA
      |                                    |
      +---------- FindTransferAnchors(CCA) --------+
                        |
                  TransferData -> ATAC 细胞类型预测
                        |
                  Co-embedding -> 联合 UMAP 可视化

关键知识点

  1. 基因活性是连接 ATAC 和 RNA 的桥梁:将 ATAC 峰映射到基因级别
  2. CCA 降维适合跨模态锚点查找,能处理数据类型差异大的场景
  3. 标签转移准确率(73.9%)低于同模态映射(92.4%),因为 ATAC 信号更稀疏
  4. 共嵌入将不同模态的细胞放在同一空间,便于联合分析和可视化
  5. 真实 ATAC 分析需要 Signac 包进行峰级别处理和 TF-IDF + LSI 降维

附录: 完整脚本

atac_integration_vignette.R

# ============================================================
# Seurat v5 阶段4-C:scRNA-seq 与 scATAC-seq 整合
# ============================================================
# 基于官方教程:https://satijalab.org/seurat/articles/seurat5_atacseq_integration_vignette
# 由于 Signac 包和 pbmcMultiome 数据集下载超时,
# 使用 PBMC 3k RNA 数据模拟 ATAC 基因活性矩阵,
# 演示跨模态标签转移和共嵌入的核心流程
# ============================================================

library(Seurat)
library(ggplot2)
library(patchwork)
library(Matrix)

# 设置工作目录
setwd("E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial")
output_dir <- "output/atac_integration"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

cat("=== Seurat v5 阶段4-C:scRNA-seq 与 scATAC-seq 整合 ===\n\n")

# 保存图片函数
save_plot <- function(filename, plot_obj, width = 10, height = 8) {
  filepath <- file.path(output_dir, filename)
  ggsave(filepath, plot_obj, width = width, height = height, dpi = 150, bg = "white")
  cat(sprintf("  已保存: %s\n", filename))
}

# ============================================================
# 1. 加载并准备 RNA 参考数据
# ============================================================
cat("## 1. 加载 RNA 参考数据\n\n")

pbmc.rna <- readRDS("output/pbmc3k_final.rds")
pbmc.rna$seurat_annotations <- Idents(pbmc.rna)
cat(sprintf("RNA 参考集:%d 细胞,%d 基因\n", ncol(pbmc.rna), nrow(pbmc.rna)))
cat(sprintf("细胞类型:%s\n", paste(levels(pbmc.rna$seurat_annotations), collapse = ", ")))

# RNA 标准预处理
pbmc.rna <- NormalizeData(pbmc.rna, verbose = FALSE)
pbmc.rna <- FindVariableFeatures(pbmc.rna, verbose = FALSE)
pbmc.rna <- ScaleData(pbmc.rna, verbose = FALSE)
pbmc.rna <- RunPCA(pbmc.rna, verbose = FALSE)
pbmc.rna <- RunUMAP(pbmc.rna, dims = 1:30, verbose = FALSE)
cat("RNA 预处理完成\n")

# RNA UMAP
p1 <- DimPlot(pbmc.rna, group.by = "seurat_annotations", label = TRUE, repel = TRUE) + 
  NoLegend() + ggtitle("RNA 参考集(已知注释)")
save_plot("01_rna_reference_umap.png", p1)

# ============================================================
# 2. 模拟 ATAC 基因活性数据
# ============================================================
cat("\n## 2. 模拟 ATAC 基因活性数据\n\n")

# 提取 RNA counts 矩阵
rna_counts <- GetAssayData(pbmc.rna, assay = "RNA", layer = "counts")

# 选择高变基因作为基因活性基础
hv_genes <- VariableFeatures(pbmc.rna)
# 只取前 2000 个高变基因来模拟
hv_genes <- hv_genes[1:min(2000, length(hv_genes))]
hv_genes <- hv_genes[hv_genes %in% rownames(rna_counts)]
cat(sprintf("使用 %d 个高变基因模拟 ATAC 基因活性\n", length(hv_genes)))

# 模拟 ATAC 基因活性矩阵
# ATAC 基因活性 = 基因体 + 2kb 上游的染色质可及性
# 特点:比 RNA 更稀疏、有更多 dropout、但与 RNA 相关
set.seed(123)
n_cells <- ncol(pbmc.rna)
n_genes <- length(hv_genes)

# 从 RNA 表达模拟 ATAC 基因活性
# 添加 dropout(50% 的零值)和噪声
gene_activity <- matrix(0, nrow = n_genes, ncol = n_cells)
rownames(gene_activity) <- hv_genes
colnames(gene_activity) <- colnames(pbmc.rna)

for (i in seq_along(hv_genes)) {
  gene <- hv_genes[i]
  rna_expr <- as.numeric(rna_counts[gene, ])
  # 模拟 ATAC:与 RNA 相关但更稀疏
  # 1. 基础信号 = RNA 表达 * 随机系数
  base_signal <- rna_expr * runif(n_cells, 0.3, 0.8)
  # 2. 加入 dropout(随机置零)
  dropout_mask <- runif(n_cells) > 0.4  # 40% dropout
  base_signal <- base_signal * dropout_mask
  # 3. 加入随机噪声
  base_signal <- base_signal + rnorm(n_cells, mean = 0.5, sd = 0.3)
  # 4. 确保非负
  gene_activity[i, ] <- pmax(round(base_signal * 5), 0)
}

gene_activity <- Matrix(gene_activity, sparse = TRUE)

# 创建 ATAC Seurat 对象(使用基因活性矩阵)
pbmc.atac <- CreateSeuratObject(counts = gene_activity, project = "ATAC")
# 添加真实标签(用于验证预测准确性)
pbmc.atac$seurat_annotations <- pbmc.rna$seurat_annotations[match(colnames(pbmc.atac), colnames(pbmc.rna))]

cat(sprintf("ATAC 模拟数据:%d 细胞,%d 基因活性特征\n", ncol(pbmc.atac), nrow(pbmc.atac)))
cat(sprintf("稀疏度:%.1f%%\n", round(sum(gene_activity == 0) / length(gene_activity) * 100, 1)))

# ============================================================
# 3. ATAC 预处理(TF-IDF + LSI 模拟)
# ============================================================
cat("\n## 3. ATAC 预处理\n\n")

# 由于没有 Signac,使用标准 Seurat 流程模拟 ATAC 预处理
# TF-IDF 近似:先 LogNormalize 再做 PCA(模拟 LSI)
DefaultAssay(pbmc.atac) <- "RNA"
pbmc.atac <- NormalizeData(pbmc.atac, verbose = FALSE)
pbmc.atac <- FindVariableFeatures(pbmc.atac, verbose = FALSE)
pbmc.atac <- ScaleData(pbmc.atac, verbose = FALSE)
pbmc.atac <- RunPCA(pbmc.atac, verbose = FALSE)

# 使用 PCA 近似 LSI
# 在真实场景中,这里使用 RunSVD(LSI),但 PCA 也能演示流程
pbmc.atac <- RunUMAP(pbmc.atac, dims = 1:30, reduction.name = "umap.atac",
                     reduction.key = "atacUMAP_", verbose = FALSE)
cat("ATAC 预处理完成\n")

# ATAC UMAP(未注释)
p2 <- DimPlot(pbmc.atac, reduction = "umap.atac", group.by = "orig.ident") + 
  NoLegend() + ggtitle("ATAC 模拟数据 UMAP(未注释)")
save_plot("02_atac_umap_unannotated.png", p2)

# RNA vs ATAC 对比
p3 <- DimPlot(pbmc.rna, group.by = "seurat_annotations", label = TRUE, repel = TRUE) + 
  NoLegend() + ggtitle("RNA")
p4 <- DimPlot(pbmc.atac, reduction = "umap.atac", group.by = "seurat_annotations", 
              label = TRUE, repel = TRUE) + NoLegend() + ggtitle("ATAC(真实标签)")
save_plot("03_rna_vs_atac.png", p3 + p4, width = 16, height = 7)

# ============================================================
# 4. 查找跨模态锚点
# ============================================================
cat("\n## 4. 查找跨模态锚点\n\n")

# 使用 CCA 降维来查找跨模态锚点
# 将 ATAC 的基因活性作为 query,RNA 表达作为 reference
cat("正在查找跨模态锚点(CCA)...\n")
transfer.anchors <- FindTransferAnchors(
  reference = pbmc.rna,
  query = pbmc.atac,
  features = VariableFeatures(pbmc.rna),
  reference.assay = "RNA",
  query.assay = "RNA",
  reduction = "cca",
  dims = 1:30
)
cat("锚点查找完成\n")

# ============================================================
# 5. 标签转移:从 RNA 注释到 ATAC
# ============================================================
cat("\n## 5. 标签转移:RNA → ATAC\n\n")

# 转移细胞类型标签
celltype.predictions <- TransferData(
  anchorset = transfer.anchors,
  refdata = pbmc.rna$seurat_annotations,
  weight.reduction = pbmc.atac[["pca"]],
  dims = 1:30
)
pbmc.atac <- AddMetaData(pbmc.atac, metadata = celltype.predictions)
cat("标签转移完成\n")

# 预测结果
cat(sprintf("\n预测细胞类型分布:\n"))
print(table(pbmc.atac$predicted.id))

# ============================================================
# 6. 评估预测准确性
# ============================================================
cat("\n## 6. 评估预测准确性\n\n")

pbmc.atac$annotation_correct <- pbmc.atac$predicted.id == pbmc.atac$seurat_annotations
correct <- sum(pbmc.atac$annotation_correct)
total <- ncol(pbmc.atac)
cat(sprintf("正确预测:%d/%d (%.1f%%)\n", correct, total, correct / total * 100))

# 预测 vs 真实标签 UMAP 对比
p5 <- DimPlot(pbmc.atac, reduction = "umap.atac", group.by = "predicted.id",
              label = TRUE, repel = TRUE) + NoLegend() + ggtitle("ATAC 预测标签")
p6 <- DimPlot(pbmc.atac, reduction = "umap.atac", group.by = "seurat_annotations",
              label = TRUE, repel = TRUE) + NoLegend() + ggtitle("ATAC 真实标签")
save_plot("04_predicted_vs_truth.png", p5 + p6, width = 16, height = 7)

# 混淆矩阵
conf_matrix <- as.data.frame(table(pbmc.atac$seurat_annotations, pbmc.atac$predicted.id))
conf_matrix <- conf_matrix[conf_matrix$Freq > 0, ]
colnames(conf_matrix) <- c("True", "Predicted", "Count")
# 归一化
conf_matrix$Freq <- conf_matrix$Count / ave(conf_matrix$Count, conf_matrix$True, FUN = sum)

p7 <- ggplot(conf_matrix, aes(x = Predicted, y = True, fill = Freq)) +
  geom_tile() +
  scale_fill_gradient(name = "比例", low = "#ffffc8", high = "#7d0025") +
  labs(title = "混淆矩阵:RNA 注释 vs ATAC 预测",
       x = "预测标签(ATAC)", y = "真实标签(RNA)") +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("05_confusion_matrix.png", p7, width = 10, height = 9)

# 预测分数分布
p8 <- ggplot(pbmc.atac@meta.data, aes(x = prediction.score.max, fill = annotation_correct)) +
  geom_density(alpha = 0.5) +
  labs(title = "预测分数分布",
       x = "预测分数", y = "密度", fill = "注释正确") +
  theme_bw()
save_plot("06_prediction_scores.png", p8, width = 10, height = 7)

cat(sprintf("平均预测分数:%.3f\n", mean(pbmc.atac$prediction.score.max)))
cat(sprintf("正确预测平均分数:%.3f\n", 
    mean(pbmc.atac$prediction.score.max[pbmc.atac$annotation_correct])))
cat(sprintf("错误预测平均分数:%.3f\n", 
    mean(pbmc.atac$prediction.score.max[!pbmc.atac$annotation_correct])))

# ============================================================
# 7. 共嵌入 (Co-embedding)
# ============================================================
cat("\n## 7. 共嵌入 RNA + ATAC\n\n")

# 用锚点将 RNA 表达数据插补到 ATAC 细胞中
genes.use <- VariableFeatures(pbmc.rna)
refdata <- GetAssayData(pbmc.rna, assay = "RNA", layer = "data")[genes.use, ]

cat("正在插补 RNA 表达到 ATAC 细胞...\n")
imputation <- TransferData(
  anchorset = transfer.anchors,
  refdata = refdata,
  weight.reduction = pbmc.atac[["pca"]],
  dims = 1:30
)
pbmc.atac[["RNA"]] <- imputation
cat("RNA 插补完成\n")

# 合并两个数据集
# 给每个对象添加来源标签
pbmc.rna$modality <- "RNA"
pbmc.atac$modality <- "ATAC"

coembed <- merge(x = pbmc.rna, y = pbmc.atac)
coembed <- ScaleData(coembed, features = genes.use, do.scale = FALSE, verbose = FALSE)
coembed <- RunPCA(coembed, features = genes.use, verbose = FALSE)
coembed <- RunUMAP(coembed, dims = 1:30, verbose = FALSE)
cat("共嵌入完成\n")

# 共嵌入 UMAP
p9 <- DimPlot(coembed, group.by = "modality", 
              cols = c("RNA" = "#E41A1C", "ATAC" = "#377EB8")) +
  ggtitle("共嵌入:RNA + ATAC(按模态着色)")
save_plot("07_coembed_modality.png", p9, width = 10, height = 8)

p10 <- DimPlot(coembed, group.by = "seurat_annotations", label = TRUE, 
               repel = TRUE, label.size = 3) + NoLegend() +
  ggtitle("共嵌入:RNA + ATAC(按细胞类型着色)")
save_plot("08_coembed_celltype.png", p10, width = 10, height = 8)

# 按模态拆分
p11 <- DimPlot(coembed, group.by = "modality", split.by = "modality",
               cols = c("RNA" = "#E41A1C", "ATAC" = "#377EB8")) +
  ggtitle("共嵌入:按模态分面")
save_plot("09_coembed_split.png", p11, width = 16, height = 7)

# ============================================================
# 8. 保存结果
# ============================================================
cat("\n## 8. 保存结果\n\n")

saveRDS(pbmc.atac, file.path(output_dir, "atac_annotated.rds"))
saveRDS(coembed, file.path(output_dir, "coembed.rds"))
cat("已保存: atac_annotated.rds, coembed.rds\n")

# 保存预测结果
write.csv(pbmc.atac@meta.data[, c("seurat_annotations", "predicted.id",
                                    "prediction.score.max", "annotation_correct")],
          file.path(output_dir, "atac_predictions.csv"), row.names = TRUE)
cat("已保存: atac_predictions.csv\n")

cat("\n=== 阶段4-C scRNA-seq 与 scATAC-seq 整合 完成!===\n")
cat(sprintf("共生成 9 张图 + 1 个CSV + 2 个RDS文件\n"))
cat(sprintf("标签转移准确率:%.1f%%\n", correct / total * 100))

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段4-C: scRNA-seq 与 scATAC-seq 整合(跨模态锚点 / 标签转移准确率 73.9% / 共嵌入)

20260912 121057 Seurat V5 加权最近邻分析 Wnn 教程

Seurat v5 加权最近邻分析 (WNN) 教程

基于 Seurat v5.5.1 官方教程,使用 PBMC 3k RNA 数据模拟 CITE-seq 多模态场景。
从 RNA 表达矩阵模拟 ADT 蛋白数据,完整演示 WNN 多模态分析流程。


目录


1. 背景与原理

1.1 什么是 WNN?

加权最近邻(Weighted Nearest Neighbor, WNN)是 Seurat v5 引入的多模态分析方法,用于同时分析来自不同模态的数据(如 RNA + 蛋白)。

传统方法将不同模态的数据简单拼接后分析,但不同模态的信息量不同。WNN 的核心思想是:为每个细胞自动学习不同模态的权重,根据每个细胞中哪个模态信息更丰富来自适应加权。

1.2 WNN 分析流程

模态1 (RNA) -> 预处理 -> PCA
                          |
                           -> FindMultiModalNeighbors -> WNN 图 -> UMAP + 聚类
                          |
模态2 (ADT) -> 预处理 -> CLR + PCA

1.3 CITE-seq 数据特点

CITE-seq 同时测量 RNA 表达和细胞表面蛋白(ADT),两种模态需要不同的预处理:
- RNA:标准 LogNormalize + HVG + ScaleData + PCA
- ADT:CLR(Centered Log Ratio)归一化 + PCA


2. 模拟 ADT 蛋白数据

由于官方 bmcite 数据集下载超时,我们从 PBMC 3k 的 RNA 表达矩阵模拟 ADT 蛋白信号:

# 选择 25 个关键 marker 基因模拟对应 ADT 蛋白
adt_genes <- c("CD3D", "CD3E", "CD4", "CD8A", "MS4A1", "CD14", "FCGR3A", ...)

# 从 RNA counts 模拟 ADT 信号
for (i in seq_along(adt_genes)) {
  gene <- adt_genes[i]
  rna_expr <- as.numeric(rna_counts[gene, ])
  # ADT 信号 = RNA 表达 * 随机系数 + 基线噪声
  adt_signal <- rna_expr * runif(ncol(pbmc), 0.8, 1.2) +
    rnorm(ncol(pbmc), mean = 1, sd = 0.5)
  adt_matrix[i, ] <- round(pmax(adt_signal, 0) * 10)
}

# 创建 ADT assay
pbmc[["ADT"]] <- CreateAssayObject(counts = adt_matrix)

注意:真实 CITE-seq 的 ADT 数据来自抗体捕获,与 RNA 是独立的测量。这里使用 RNA 模拟仅用于演示 WNN 分析流程。


3. 独立预处理每个模态

3.1 RNA 预处理(标准流程)

DefaultAssay(pbmc) <- "RNA"
pbmc <- NormalizeData(pbmc, verbose = FALSE)
pbmc <- FindVariableFeatures(pbmc, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, verbose = FALSE)

3.2 ADT 预处理(CLR 归一化)

DefaultAssay(pbmc) <- "ADT"
VariableFeatures(pbmc) <- rownames(pbmc[["ADT"]])
# CLR 归一化是 CITE-seq ADT 数据的标准方法
pbmc <- NormalizeData(pbmc, normalization.method = "CLR", margin = 2, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, reduction.name = "apca", verbose = FALSE)

CLR(Centered Log Ratio) 是处理组成性数据(如蛋白占比)的标准归一化方法,相比 LogNormalize 更适合 ADT 数据。


4. 构建多模态邻居图 (WNN)

pbmc <- FindMultiModalNeighbors(
  pbmc,
  reduction.list = list("pca", "apca"),
  dims.list = list(1:30, 1:min(18, nrow(pbmc[["ADT"]]) - 1)),
  modality.weight.name = "RNA.weight"
)

关键参数:
- reduction.list:各模态的降维结果
- dims.list:各模态使用的维度数
- modality.weight.name:权重存储的列名

FindMultiModalNeighbors 的输出:
- weighted.nn:加权邻居图
- wsnn:WNN SNN 图(用于聚类)
- RNA.weight:每个细胞的 RNA 模态权重(0-1)


5. WNN UMAP 与聚类

pbmc <- RunUMAP(pbmc, nn.name = "weighted.nn", reduction.name = "wnn.umap",
                reduction.key = "wnnUMAP_", verbose = FALSE)
pbmc <- FindClusters(pbmc, graph.name = "wsnn", algorithm = 3,
                     resolution = 0.5, verbose = FALSE)

WNN UMAP:聚类 vs 已知细胞类型

WNN UMAP

WNN UMAP 中,聚类结果与已知细胞类型高度一致,说明多模态信息有效提升了聚类准确性。


6. 单模态 UMAP 对比

6.1 仅 RNA vs 仅 ADT

RNA vs ADT UMAP

6.2 三模态对比:RNA / ADT / WNN

三模态对比

关键发现:
- 仅 RNA 的 UMAP 已经能较好区分主要细胞类型
- 仅 ADT 的 UMAP 聚类效果较差(因为模拟数据只有 25 个蛋白特征)
- WNN UMAP 综合了两个模态的信息,聚类效果最佳
- 在真实 CITE-seq 数据中,ADT 模态通常对 T 细胞亚型区分更有优势


7. Marker 基因与蛋白可视化

7.1 RNA Marker 基因

DefaultAssay(pbmc) <- "RNA"
FeaturePlot(pbmc, features = c("CD3D", "MS4A1", "CD14", "GNLY"),
            reduction = "wnn.umap", ncol = 2, max.cutoff = 3)

RNA Markers on WNN

7.2 ADT 蛋白 Marker

DefaultAssay(pbmc) <- "ADT"
# 注意:Seurat 会自动将下划线 _ 替换为短横线 -
FeaturePlot(pbmc, features = c("adt-CD3D", "adt-MS4A1", "adt-CD14", "adt-GNLY"),
            reduction = "wnn.umap", ncol = 2, max.cutoff = 2)

ADT Markers on WNN

重要:Seurat 会自动将特征名中的下划线 _ 替换为短横线 -,因此引用 ADT 特征时需用 adt-CD3D 而非 adt_CD3D。


8. 模态权重分析

8.1 RNA 权重小提琴图

WNN 为每个细胞计算了一个 RNA 模态权重(0-1),权重越高表示该细胞中 RNA 信息越重要:

VlnPlot(pbmc, features = "RNA.weight", group.by = "celltype", sort = TRUE)

RNA 权重小提琴图

8.2 RNA 权重在 UMAP 上着色

FeaturePlot(pbmc, features = "RNA.weight", reduction = "wnn.umap")

RNA 权重 UMAP

权重解读:
- RNA 权重接近 0.5 表示两个模态贡献均衡
- 权重偏向 RNA(>0.5)的细胞,RNA 信息更关键
- 权重偏向 ADT(<0.5)的细胞,蛋白信息更关键
- 在真实数据中,T 细胞亚型通常更依赖 ADT 权重来区分


9. ADT 蛋白表达 DotPlot

DefaultAssay(pbmc) <- "ADT"
DotPlot(pbmc, features = adt_markers_subset, group.by = "celltype") + coord_flip()

ADT DotPlot

DotPlot 同时展示了各细胞类型中 ADT 蛋白的表达比例(点大小)和表达水平(颜色深浅),是验证细胞类型注释的利器。


10. 多模态联合可视化

10.1 CD3D:RNA vs ADT

同一基因/蛋白在 RNA 和 ADT 模态中的表达对比:

CD3D: RNA vs ADT

10.2 CD14:RNA vs ADT

CD14: RNA vs ADT

RNA vs ADT 对比:
- RNA 表达(红色)更稀疏,有明显的零值
- ADT 信号(绿色)更连续,背景噪声更高
- 两者的高表达区域一致,验证了模拟数据的合理性


11. 总结

核心流程

RNA 数据 -> NormalizeData -> FindVariableFeatures -> ScaleData -> PCA
                                                                  |
                                                                   -> FindMultiModalNeighbors -> WNN -> UMAP + 聚类
                                                                  |
ADT 数据 -> NormalizeData(CLR) -> ScaleData -> PCA (apca)

关键知识点

  1. WNN 自动学习模态权重:每个细胞都有独立的 RNA/ADT 权重,自适应地依赖信息量更大的模态
  2. CLR 归一化:ADT 数据的标准归一化方法,适用于组成性数据
  3. 多 Assay 管理:Seurat 对象可以同时包含 RNA 和 ADT 两个 assay,通过 DefaultAssay 切换
  4. 特征名转换:Seurat 自动将 _ 替换为 -,引用 ADT 特征时需注意
  5. 模拟数据局限:由于 ADT 是从 RNA 模拟的,两个模态高度相关,WNN 优势不如真实 CITE-seq 数据明显

附录: 完整脚本

wnn_vignette.R

# ============================================================
# Seurat v5 阶段4-B:加权最近邻分析 (WNN)
# ============================================================
# 基于官方教程:https://satijalab.org/seurat/articles/weighted_nearest_neighbor_analysis
# 由于 bmcite 数据集下载超时,使用 PBMC 3k RNA 数据模拟 CITE-seq 多模态场景
# 从 RNA 表达矩阵模拟 ADT(抗体衍生标签)蛋白数据,
# 演示完整的 WNN 多模态分析流程
# ============================================================

library(Seurat)
library(ggplot2)
library(patchwork)
library(Matrix)

# 设置工作目录
setwd("E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial")
output_dir <- "output/wnn"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

cat("=== Seurat v5 阶段4-B:加权最近邻分析 (WNN) ===\n\n")

# 保存图片函数
save_plot <- function(filename, plot_obj, width = 10, height = 8) {
  filepath <- file.path(output_dir, filename)
  ggsave(filepath, plot_obj, width = width, height = height, dpi = 150, bg = "white")
  cat(sprintf("  已保存: %s\n", filename))
}

# ============================================================
# 1. 加载 PBMC 数据
# ============================================================
cat("## 1. 加载 PBMC 数据并准备多模态对象\n\n")

pbmc <- readRDS("output/pbmc3k_final.rds")
pbmc$celltype <- Idents(pbmc)
cat(sprintf("加载完成:共 %d 个细胞,%d 个基因\n", ncol(pbmc), nrow(pbmc)))
cat(sprintf("细胞类型:%s\n", paste(levels(pbmc$celltype), collapse = ", ")))

# ============================================================
# 2. 模拟 ADT(蛋白)数据
# ============================================================
cat("\n## 2. 模拟 ADT 蛋白数据\n\n")

# 提取 RNA counts 矩阵
rna_counts <- GetAssayData(pbmc, assay = "RNA", layer = "counts")

# 选择关键 marker 基因来模拟对应的 ADT 蛋白
# 模拟 CITE-seq 中常见的 25 个抗体面板
adt_genes <- c(
  "CD3D", "CD3E",      # T cell markers
  "CD4",              # CD4 T
  "CD8A",             # CD8 T
  "MS4A1",            # B cell (CD19/CD20)
  "CD14",             # Monocyte
  "FCGR3A",           # CD16 Mono
  "NCAM1",            # NK (CD56)
  "GNLY", "NKG7",     # NK markers
  "IL7R",             # CD127
  "CCR7",             # Naive T
  "SELL",             # CD62L
  "ITGAX",            # CD11c (DC)
  "PPBP",             # Platelet
  "CD34",             # Progenitor
  "CD27",             # Memory
  "CD28",             # Costimulation
  "PDCD1",            # PD-1
  "ICOS",             # Activation
  "HLA-DRA",          # MHC-II
  "CD68",             # Macrophage
  "ITGAM",            # CD11b
  "CDK6",             # Cell cycle
  "CD69"              # Early activation
)

# 只保留存在的基因
adt_genes <- adt_genes[adt_genes %in% rownames(rna_counts)]
cat(sprintf("模拟 ADT 面板:共 %d 个蛋白标记\n", length(adt_genes)))

# 从 RNA 表达模拟 ADT 信号
# ADT 信号通常比 RNA 更连续、噪声更低
set.seed(42)
adt_matrix <- matrix(0, nrow = length(adt_genes), ncol = ncol(pbmc))
rownames(adt_matrix) <- paste0("adt_", adt_genes)
colnames(adt_matrix) <- colnames(pbmc)

for (i in seq_along(adt_genes)) {
  gene <- adt_genes[i]
  # 取 RNA 表达作为基础
  rna_expr <- as.numeric(rna_counts[gene, ])
  # 模拟 ADT:加入蛋白层面噪声 + 基线信号
  # ADT 信号通常更连续(对数正态分布)
  adt_signal <- rna_expr * runif(ncol(pbmc), 0.8, 1.2) +  # 表达量乘以随机系数
    rnorm(ncol(pbmc), mean = 1, sd = 0.5)  # 加入基线噪声
  adt_signal <- pmax(adt_signal, 0)  # 确保非负
  adt_matrix[i, ] <- round(adt_signal * 10)  # 放大信号
}

adt_matrix <- Matrix(adt_matrix, sparse = TRUE)

# 创建 ADT assay 并添加到 Seurat 对象
pbmc[["ADT"]] <- CreateAssayObject(counts = adt_matrix)
cat("ADT assay 已创建并添加到 Seurat 对象\n")
cat(sprintf("ADT 维度:%d 蛋白 x %d 细胞\n", nrow(pbmc[["ADT"]]), ncol(pbmc[["ADT"]])))

# ============================================================
# 3. 独立预处理每个模态
# ============================================================
cat("\n## 3. 独立预处理 RNA 和 ADT 模态\n\n")

# --- RNA 预处理 ---
cat("预处理 RNA...\n")
DefaultAssay(pbmc) <- "RNA"
pbmc <- NormalizeData(pbmc, verbose = FALSE)
pbmc <- FindVariableFeatures(pbmc, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, verbose = FALSE)
cat("RNA 预处理完成\n")

# --- ADT 预处理 ---
# 使用 CLR(Centered Log Ratio)归一化,这是 CITE-seq ADT 数据的标准方法
cat("预处理 ADT(CLR 归一化)...\n")
DefaultAssay(pbmc) <- "ADT"
# 使用所有 ADT 特征进行降维
VariableFeatures(pbmc) <- rownames(pbmc[["ADT"]])
pbmc <- NormalizeData(pbmc, normalization.method = "CLR", margin = 2, verbose = FALSE)
pbmc <- ScaleData(pbmc, verbose = FALSE)
pbmc <- RunPCA(pbmc, reduction.name = "apca", verbose = FALSE)
cat("ADT 预处理完成\n")

# ============================================================
# 4. 构建多模态邻居图 (FindMultiModalNeighbors)
# ============================================================
cat("\n## 4. 构建多模态邻居图 (WNN)\n\n")

# 计算加权最近邻
# RNA 使用 30 个 PC,ADT 使用 18 个 PC(因为蛋白面板较小)
cat("正在计算 WNN...\n")
pbmc <- FindMultiModalNeighbors(
  pbmc,
  reduction.list = list("pca", "apca"),
  dims.list = list(1:30, 1:min(18, nrow(pbmc[["ADT"]]) - 1)),
  modality.weight.name = "RNA.weight"
)
cat("WNN 计算完成\n")

# ============================================================
# 5. 基于 WNN 的 UMAP 和聚类
# ============================================================
cat("\n## 5. WNN UMAP 与聚类\n\n")

pbmc <- RunUMAP(pbmc, nn.name = "weighted.nn", 
                reduction.name = "wnn.umap", 
                reduction.key = "wnnUMAP_", verbose = FALSE)
pbmc <- FindClusters(pbmc, graph.name = "wsnn", algorithm = 3, 
                      resolution = 0.5, verbose = FALSE)
cat("WNN 聚类完成\n")

# WNN UMAP(聚类 vs 细胞类型)
p1 <- DimPlot(pbmc, reduction = "wnn.umap", label = TRUE, repel = TRUE, 
              label.size = 3) + NoLegend() + ggtitle("WNN UMAP(聚类)")
p2 <- DimPlot(pbmc, reduction = "wnn.umap", group.by = "celltype", 
              label = TRUE, repel = TRUE, label.size = 3) + NoLegend() + 
              ggtitle("WNN UMAP(已知细胞类型)")
save_plot("01_wnn_umap.png", p1 + p2, width = 16, height = 7)

# ============================================================
# 6. 单模态 UMAP 对比
# ============================================================
cat("\n## 6. 单模态 UMAP 对比\n\n")

# RNA UMAP
pbmc <- RunUMAP(pbmc, reduction = "pca", dims = 1:30, assay = "RNA",
                reduction.name = "rna.umap", reduction.key = "rnaUMAP_", 
                verbose = FALSE)
# ADT UMAP
pbmc <- RunUMAP(pbmc, reduction = "apca", dims = 1:min(18, nrow(pbmc[["ADT"]]) - 1), 
                assay = "ADT",
                reduction.name = "adt.umap", reduction.key = "adtUMAP_", 
                verbose = FALSE)

p3 <- DimPlot(pbmc, reduction = "rna.umap", group.by = "celltype",
              label = TRUE, repel = TRUE, label.size = 2.5) + NoLegend() + 
              ggtitle("仅 RNA UMAP")
p4 <- DimPlot(pbmc, reduction = "adt.umap", group.by = "celltype",
              label = TRUE, repel = TRUE, label.size = 2.5) + NoLegend() + 
              ggtitle("仅 ADT UMAP")
save_plot("02_rna_vs_adt_umap.png", p3 + p4, width = 16, height = 7)

# 三模态对比
p5 <- DimPlot(pbmc, reduction = "rna.umap", group.by = "celltype",
              label = TRUE, repel = TRUE, label.size = 2.5) + ggtitle("RNA")
p6 <- DimPlot(pbmc, reduction = "adt.umap", group.by = "celltype",
              label = TRUE, repel = TRUE, label.size = 2.5) + ggtitle("ADT")
p7 <- DimPlot(pbmc, reduction = "wnn.umap", group.by = "celltype",
              label = TRUE, repel = TRUE, label.size = 2.5) + ggtitle("WNN")
save_plot("03_three_modality_comparison.png", 
          (p5 + p6 + p7) & NoLegend() & theme(plot.title = element_text(hjust = 0.5)),
          width = 21, height = 7)

# ============================================================
# 7. Marker 基因/蛋白可视化
# ============================================================
cat("\n## 7. Marker 基因与蛋白可视化\n\n")

DefaultAssay(pbmc) <- "RNA"
# RNA marker 基因
p8 <- FeaturePlot(pbmc, features = c("CD3D", "MS4A1", "CD14", "GNLY"),
                  reduction = "wnn.umap", ncol = 2,
                  max.cutoff = 3, cols = c("lightgrey", "red"))
save_plot("04_rna_markers_wnn.png", p8, width = 12, height = 10)

# ADT 蛋白 marker
DefaultAssay(pbmc) <- "ADT"
p9 <- FeaturePlot(pbmc, features = c("adt-CD3D", "adt-MS4A1", "adt-CD14", "adt-GNLY"),
                  reduction = "wnn.umap", ncol = 2,
                  max.cutoff = 2, cols = c("lightgrey", "darkgreen"))
save_plot("05_adt_markers_wnn.png", p9, width = 12, height = 10)

cat("Marker 可视化完成\n")

# ============================================================
# 8. 模态权重分析
# ============================================================
cat("\n## 8. 模态权重分析\n\n")

# RNA 权重分布
p10 <- VlnPlot(pbmc, features = "RNA.weight", group.by = "celltype",
               sort = TRUE, pt.size = 0.1) +
  NoLegend() +
  labs(title = "各细胞类型 RNA 模态权重",
       x = "细胞类型", y = "RNA 权重") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("06_rna_weight_violin.png", p10, width = 10, height = 7)

# RNA 权重在 WNN UMAP 上着色
p11 <- FeaturePlot(pbmc, features = "RNA.weight", reduction = "wnn.umap",
                   cols = c("#3B528B", "#21918C", "#5EC962", "#FDE725")) +
  ggtitle("RNA 模态权重分布")
save_plot("07_rna_weight_umap.png", p11, width = 10, height = 8)

cat(sprintf("平均 RNA 权重: %.3f\n", mean(pbmc$RNA.weight)))
cat(sprintf("RNA 权重范围: [%.3f, %.3f]\n", min(pbmc$RNA.weight), max(pbmc$RNA.weight)))

# 各细胞类型平均 RNA 权重
cat("\n各细胞类型平均 RNA 权重:\n")
weight_by_type <- tapply(pbmc$RNA.weight, pbmc$celltype, mean)
weight_by_type <- sort(weight_by_type, decreasing = TRUE)
for (ct in names(weight_by_type)) {
  cat(sprintf("  %s: %.3f\n", ct, weight_by_type[ct]))
}

# ============================================================
# 9. ADT 蛋白表达热图
# ============================================================
cat("\n## 9. ADT 蛋白表达热图\n\n")

DefaultAssay(pbmc) <- "ADT"
adt_markers_subset <- c("adt-CD3D", "adt-CD8A", "adt-MS4A1", "adt-CD14",
                         "adt-FCGR3A", "adt-GNLY", "adt-IL7R", "adt-NCAM1",
                         "adt-ITGAX", "adt-PPBP")
adt_markers_subset <- adt_markers_subset[adt_markers_subset %in% rownames(pbmc[["ADT"]])]
p12 <- DotPlot(pbmc, features = adt_markers_subset, group.by = "celltype") +
  coord_flip() +
  labs(title = "ADT 蛋白表达 DotPlot",
       x = "蛋白标记", y = "细胞类型") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("08_adt_dotplot.png", p12, width = 10, height = 8)

# ============================================================
# 10. 多模态联合可视化
# ============================================================
cat("\n## 10. 多模态联合可视化\n\n")

# RNA vs ADT 同一基因对比
DefaultAssay(pbmc) <- "RNA"
p13 <- FeaturePlot(pbmc, features = "CD3D", reduction = "wnn.umap",
                   max.cutoff = 3, cols = c("lightgrey", "red")) +
  ggtitle("RNA: CD3D")
DefaultAssay(pbmc) <- "ADT"
p14 <- FeaturePlot(pbmc, features = "adt-CD3D", reduction = "wnn.umap",
                   max.cutoff = 2, cols = c("lightgrey", "darkgreen")) +
  ggtitle("ADT: CD3D")
save_plot("09_rna_vs_adt_cd3d.png", p13 | p14, width = 16, height = 7)

DefaultAssay(pbmc) <- "RNA"
p15 <- FeaturePlot(pbmc, features = "CD14", reduction = "wnn.umap",
                   max.cutoff = 3, cols = c("lightgrey", "red")) +
  ggtitle("RNA: CD14")
DefaultAssay(pbmc) <- "ADT"
p16 <- FeaturePlot(pbmc, features = "adt-CD14", reduction = "wnn.umap",
                   max.cutoff = 2, cols = c("lightgrey", "darkgreen")) +
  ggtitle("ADT: CD14")
save_plot("10_rna_vs_adt_cd14.png", p15 | p16, width = 16, height = 7)

# ============================================================
# 11. 保存结果
# ============================================================
cat("\n## 11. 保存结果\n\n")

# 恢复默认 assay
DefaultAssay(pbmc) <- "RNA"
saveRDS(pbmc, file.path(output_dir, "pbmc_wnn.rds"))
cat("已保存: pbmc_wnn.rds\n")

# 保存模态权重
write.csv(data.frame(
  cell = colnames(pbmc),
  celltype = pbmc$celltype,
  RNA.weight = pbmc$RNA.weight
), file.path(output_dir, "modality_weights.csv"), row.names = FALSE)
cat("已保存: modality_weights.csv\n")

cat("\n=== 阶段4-B 加权最近邻分析 (WNN) 完成!===\n")
cat(sprintf("共生成 10 张图 + 1 个CSV + 1 个RDS文件\n"))

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段4-B: 加权最近邻分析(WNN / CLR 归一化 / 模态权重 / 多模态可视化)

20260912 121054 Seurat V5 参考数据集映射教程

Seurat v5 参考数据集映射教程

基于 Seurat v5.5.1 官方教程,使用 PBMC 3k 数据模拟参考映射场景。
将数据拆分为参考集(70%)和查询集(30%),完整演示标签转移与 UMAP 投影流程。


目录


1. 背景与原理

1.1 什么是参考数据集映射?

当获得一批新的单细胞数据(查询集),但没有细胞类型注释时,可以利用已有的、已注释的参考数据集,通过标签转移(Label Transfer)将细胞类型注释从参考集映射到查询集。

1.2 核心函数

函数 作用
FindTransferAnchors 在参考集和查询集之间寻找锚点(anchor)对
TransferData 基于锚点转移标签或连续变量
MapQuery 一步完成标签转移 + 嵌入整合 + UMAP 投影

1.3 与数据整合的区别

  • 数据整合(阶段3):去除批次效应,让多批次数据在同一空间混合
  • 参考映射:将查询集投影到参考集的 UMAP 空间,转移已有注释

2. 数据准备与拆分

使用阶段1完成的 PBMC 3k 已注释数据,随机拆分为参考集(70%)和查询集(30%):

# 加载已注释的 PBMC 数据
pbmc <- readRDS("output/pbmc3k_final.rds")
pbmc$seurat_annotations <- Idents(pbmc)

# 随机拆分
set.seed(42)
ref_idx <- sample(1:ncol(pbmc), size = floor(ncol(pbmc) * 0.7))
pancreas.ref <- subset(pbmc, cells = cell_ids[ref_idx])      # 1846 细胞
pancreas.query <- subset(pbmc, cells = cell_ids[-ref_idx])   # 792 细胞

3. 构建参考集

对参考集进行标准预处理(归一化、找高变基因、PCA、聚类、UMAP):

pancreas.ref <- NormalizeData(pancreas.ref, verbose = FALSE)
pancreas.ref <- FindVariableFeatures(pancreas.ref, verbose = FALSE)
pancreas.ref <- ScaleData(pancreas.ref, verbose = FALSE)
pancreas.ref <- RunPCA(pancreas.ref, verbose = FALSE)
pancreas.ref <- FindNeighbors(pancreas.ref, dims = 1:30, verbose = FALSE)
pancreas.ref <- FindClusters(pancreas.ref, verbose = FALSE)
pancreas.ref <- RunUMAP(pancreas.ref, dims = 1:30, verbose = FALSE)

参考集 UMAP(已知注释)

参考集UMAP


4. 查找转移锚点

pancreas.anchors <- FindTransferAnchors(
  reference = pancreas.ref,
  query = pancreas.query,
  dims = 1:30,
  reference.reduction = "pca"
)

锚点的作用:锚点是参考集和查询集之间表达模式高度相似的细胞对,通过这些锚点可以建立跨数据集的映射关系,从而将参考集的标签转移到查询集。


5. 标签转移

predictions <- TransferData(
  anchorset = pancreas.anchors,
  refdata = pancreas.ref$seurat_annotations,
  dims = 1:30
)
pancreas.query <- AddMetaData(pancreas.query, metadata = predictions)

TransferData 不仅返回预测的细胞类型标签(predicted.id),还返回每个预测的置信度分数(prediction.score.max)。


6. 评估预测准确性

6.1 整体准确率

由于使用的是同一批 PBMC 数据拆分,真实标签已知,可以直接评估预测准确率:92.4%。

6.2 各细胞类型预测准确率

各细胞类型预测准确率

6.3 混淆矩阵

混淆矩阵展示了真实标签与预测标签的对应关系。对角线上的数值表示预测正确的细胞数:

混淆矩阵

准确率达 92.4%,少数错误主要发生在 Naive CD4 T 和 Memory CD4 T 之间的混淆,这是 T 细胞亚型本身表达模式接近导致的。


7. Marker 基因验证

用经典 marker 基因在小提琴图上验证预测结果是否合理:

markers <- c("MS4A1", "CD3D", "CD3E", "IL7R", "CD14", "LYZ", "GNLY", "NKG7", "FCGR3A", "PPBP")
VlnPlot(pancreas.query, features = markers, group.by = "predicted.id", ncol = 5, pt.size = 0)

Marker小提琴图

每种预测细胞类型的 marker 基因表达模式符合预期,进一步验证了标签转移的可靠性。


8. UMAP 投影(MapQuery)

MapQuery 是 Seurat v5 的新函数,一步完成标签转移 + 嵌入整合 + UMAP 投影:

# 参考集 UMAP 需要保存模型
pancreas.ref <- RunUMAP(pancreas.ref, dims = 1:30, reduction = "pca",
                        return.model = TRUE, verbose = FALSE)

# MapQuery 一步到位
pancreas.query <- MapQuery(
  anchorset = pancreas.anchors,
  reference = pancreas.ref,
  query = pancreas.query,
  refdata = list(celltype = "seurat_annotations"),
  reference.reduction = "pca",
  reduction.model = "umap"
)

MapQuery 的优势:
- 将查询集直接投影到参考集的 UMAP 空间
- 无需在查询集上重新计算 UMAP
- 保持查询集与参考集在同一坐标系中


9. 参考集与查询集对比

9.1 参考集注释 vs 查询集预测

参考vs查询

9.2 查询集:真实标签 vs 预测标签

真实vs预测

左侧为查询集的真实细胞类型标签,右侧为通过锚点转移得到的预测标签,两者高度一致。


10. 预测置信度分析

ggplot(pancreas.query@meta.data, aes(x = predicted.id, y = prediction.score.max)) +
  geom_boxplot(fill = "#4C9BE8", alpha = 0.7) +
  geom_jitter(width = 0.2, alpha = 0.3, size = 0.5)

预测置信度

不同细胞类型的预测置信度都很高,B 细胞和 Platelet 的预测最为确信,T 细胞亚型(Naive vs Memory)的置信度稍低,符合生物学预期。


11. 合并可视化

11.1 查询集投影到参考 UMAP

查询集投影

11.2 查询集在参考空间中的分布

查询集分布


12. 总结

核心流程

参考集(已注释)-> 预处理 + PCA + UMAP(保存模型)
                           |
查询集(未注释)-> 预处理 -> FindTransferAnchors
                           |
                    TransferData -> 预测标签 + 置信度
                           |
                    MapQuery -> 投影到参考 UMAP 空间

关键知识点

  1. FindTransferAnchors 是标签转移的核心,寻找跨数据集锚点对
  2. TransferData 可以转移分类标签或连续变量
  3. MapQuery 一步完成标签转移 + UMAP 投影,是 Seurat v5 推荐方法
  4. 预测准确率取决于参考集注释质量和数据集之间的生物学相似性
  5. 本教程使用同源数据拆分,准确率高达 92.4%;真实场景中准确率通常在 80-95% 之间

附录: 完整脚本

mapping_vignette.R

# ============================================================
# Seurat v5 阶段4-A:参考数据集映射 (Mapping & Annotation)
# ============================================================
# 基于官方教程:https://satijalab.org/seurat/articles/integration_mapping
# 由于 panc8 数据集下载超时,使用已有的 PBMC 3k 数据模拟参考映射场景
# 将 PBMC 数据拆分为参考集(70%)和查询集(30%),
# 演示完整的标签转移 + UMAP 投影流程
# ============================================================

library(Seurat)
library(ggplot2)
library(patchwork)

# 设置工作目录
setwd("E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial")
output_dir <- "output/mapping"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

cat("=== Seurat v5 阶段4-A:参考数据集映射 ===\n\n")

# ============================================================
# 1. 加载已注释的 PBMC 数据
# ============================================================
cat("## 1. 加载已注释的 PBMC 数据\n\n")

# 加载阶段1完成的带有细胞类型注释的 Seurat 对象
pbmc <- readRDS("output/pbmc3k_final.rds")
# 将 active.ident(细胞类型注释)保存为 metadata 列
pbmc$seurat_annotations <- Idents(pbmc)
cat(sprintf("加载完成:共 %d 个细胞,%d 个基因\n", ncol(pbmc), nrow(pbmc)))
cat(sprintf("已知细胞类型:%s\n", paste(levels(pbmc$seurat_annotations), collapse = ", ")))

# 保存图片函数
save_plot <- function(filename, plot_obj, width = 10, height = 8) {
  filepath <- file.path(output_dir, filename)
  ggsave(filepath, plot_obj, width = width, height = height, dpi = 150, bg = "white")
  cat(sprintf("  已保存: %s\n", filename))
}

# ============================================================
# 2. 拆分为参考集和查询集
# ============================================================
cat("\n## 2. 拆分为参考集和查询集\n\n")

set.seed(42)
cell_ids <- colnames(pbmc)
n_cells <- length(cell_ids)
# 70% 参考集,30% 查询集
ref_idx <- sample(1:n_cells, size = floor(n_cells * 0.7))
ref_cells <- cell_ids[ref_idx]
query_cells <- cell_ids[-ref_idx]

cat(sprintf("参考集:%d 个细胞\n", length(ref_cells)))
cat(sprintf("查询集:%d 个细胞\n", length(query_cells)))

# 提取参考集和查询集
pancreas.ref <- subset(pbmc, cells = ref_cells)
pancreas.query <- subset(pbmc, cells = query_cells)

# 确保使用 seurat_annotations 作为 Idents
Idents(pancreas.ref) <- "seurat_annotations"
Idents(pancreas.query) <- "seurat_annotations"

# 记录参考集的真实细胞类型(用于后续验证)
cat(sprintf("\n参考集细胞类型分布:\n"))
print(table(pancreas.ref$seurat_annotations))

cat(sprintf("\n查询集真实细胞类型分布(用于验证预测准确性):\n"))
print(table(pancreas.query$seurat_annotations))

# 保存查询集的真实标签用于后续比较
query_true_labels <- pancreas.query$seurat_annotations

# ============================================================
# 3. 构建参考集(预处理 + 聚类 + UMAP)
# ============================================================
cat("\n## 3. 构建参考集\n\n")

# 参考集预处理
pancreas.ref <- NormalizeData(pancreas.ref, verbose = FALSE)
pancreas.ref <- FindVariableFeatures(pancreas.ref, verbose = FALSE)
pancreas.ref <- ScaleData(pancreas.ref, verbose = FALSE)
pancreas.ref <- RunPCA(pancreas.ref, verbose = FALSE)
pancreas.ref <- FindNeighbors(pancreas.ref, dims = 1:30, verbose = FALSE)
pancreas.ref <- FindClusters(pancreas.ref, verbose = FALSE)
pancreas.ref <- RunUMAP(pancreas.ref, dims = 1:30, verbose = FALSE)

cat("参考集预处理完成\n")

# 参考集 UMAP(按已知细胞类型着色)
p1 <- DimPlot(pancreas.ref, reduction = "umap", group.by = "seurat_annotations",
              label = TRUE, label.size = 4, repel = TRUE) +
  ggtitle("参考集 UMAP(已知注释)")
save_plot("01_reference_umap.png", p1)

# ============================================================
# 4. 查找转移锚点 (FindTransferAnchors)
# ============================================================
cat("\n## 4. 查找转移锚点\n\n")

# 查询集预处理
pancreas.query <- NormalizeData(pancreas.query, verbose = FALSE)

# 查找锚点
cat("正在查找转移锚点...\n")
pancreas.anchors <- FindTransferAnchors(
  reference = pancreas.ref,
  query = pancreas.query,
  dims = 1:30,
  reference.reduction = "pca"
)
cat("锚点查找完成\n")

# ============================================================
# 5. 标签转移 (TransferData)
# ============================================================
cat("\n## 5. 标签转移 (TransferData)\n\n")

# 转移细胞类型标签
predictions <- TransferData(
  anchorset = pancreas.anchors,
  refdata = pancreas.ref$seurat_annotations,
  dims = 1:30
)
pancreas.query <- AddMetaData(pancreas.query, metadata = predictions)

cat("标签转移完成\n")
cat(sprintf("\n预测细胞类型分布:\n"))
print(table(pancreas.query$predicted.id))

# ============================================================
# 6. 评估预测准确性
# ============================================================
cat("\n## 6. 评估预测准确性\n\n")

# 比较预测标签和真实标签
pancreas.query$prediction.match <- pancreas.query$predicted.id == pancreas.query$seurat_annotations
match_result <- table(pancreas.query$prediction.match)
cat(sprintf("预测结果:\n"))
print(match_result)
cat(sprintf("\n预测准确率: %.1f%%\n", 
    round(match_result["TRUE"] / sum(match_result) * 100, 1)))

# 按细胞类型查看预测准确性
cat("\n各细胞类型预测准确性:\n")
acc_by_type <- sapply(levels(pancreas.query$seurat_annotations), function(ct) {
  idx <- pancreas.query$seurat_annotations == ct
  if (sum(idx) > 0) {
    correct <- sum(pancreas.query$predicted.id[idx] == ct)
    sprintf("%s: %d/%d (%.0f%%)", ct, correct, sum(idx), correct/sum(idx)*100)
  } else {
    sprintf("%s: 0/0", ct)
  }
})
print(acc_by_type)

# 预测准确性柱状图
acc_df <- data.frame(
  celltype = levels(pancreas.query$seurat_annotations),
  accuracy = sapply(levels(pancreas.query$seurat_annotations), function(ct) {
    idx <- pancreas.query$seurat_annotations == ct
    if (sum(idx) > 0) sum(pancreas.query$predicted.id[idx] == ct) / sum(idx) * 100
    else 0
  })
)
p2 <- ggplot(acc_df, aes(x = reorder(celltype, -accuracy), y = accuracy)) +
  geom_bar(stat = "identity", fill = "#4C9BE8") +
  geom_text(aes(label = sprintf("%.0f%%", accuracy)), vjust = -0.5, size = 3.5) +
  ylim(0, 105) +
  labs(title = "各细胞类型预测准确率", x = "细胞类型", y = "准确率 (%)") +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("02_prediction_accuracy.png", p2)

# 混淆矩阵热图
conf_matrix <- as.data.frame(table(pancreas.query$seurat_annotations, pancreas.query$predicted.id))
colnames(conf_matrix) <- c("True", "Predicted", "Count")
p3 <- ggplot(conf_matrix, aes(x = Predicted, y = True, fill = Count)) +
  geom_tile() +
  geom_text(aes(label = Count), size = 3) +
  scale_fill_gradient(low = "white", high = "#2166AC") +
  labs(title = "混淆矩阵:真实标签 vs 预测标签", x = "预测标签", y = "真实标签") +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("03_confusion_matrix.png", p3, width = 10, height = 9)

# ============================================================
# 7. Marker 基因验证
# ============================================================
cat("\n## 7. Marker 基因验证\n\n")

# 用经典 Marker 基因验证预测
markers <- c("MS4A1", "CD3D", "CD3E", "IL7R", "CD14", "LYZ", "GNLY", "NKG7", "FCGR3A", "PPBP")
p4 <- VlnPlot(pancreas.query, features = markers, group.by = "predicted.id",
              ncol = 5, pt.size = 0) +
  plot_layout(ncol = 1) &
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 8))
save_plot("04_marker_violin.png", p4, width = 15, height = 12)

cat("Marker 基因小提琴图已保存\n")

# ============================================================
# 8. UMAP 投影 (MapQuery)
# ============================================================
cat("\n## 8. UMAP 投影 (MapQuery)\n\n")

# 重新运行 UMAP 并保存模型
pancreas.ref <- RunUMAP(pancreas.ref, dims = 1:30, reduction = "pca",
                         return.model = TRUE, verbose = FALSE)

# 使用 MapQuery 一步完成标签转移 + 嵌入整合 + UMAP 投影
cat("正在执行 MapQuery...\n")
pancreas.query <- MapQuery(
  anchorset = pancreas.anchors,
  reference = pancreas.ref,
  query = pancreas.query,
  refdata = list(celltype = "seurat_annotations"),
  reference.reduction = "pca",
  reduction.model = "umap"
)
cat("MapQuery 完成\n")

# ============================================================
# 9. 参考集与查询集 UMAP 对比
# ============================================================
cat("\n## 9. 参考集与查询集 UMAP 对比\n\n")

# 参考集 UMAP
p5 <- DimPlot(pancreas.ref, reduction = "umap", group.by = "seurat_annotations",
              label = TRUE, label.size = 3, repel = TRUE) +
  NoLegend() +
  ggtitle("参考集注释")
# 查询集投影 UMAP
p6 <- DimPlot(pancreas.query, reduction = "ref.umap", group.by = "predicted.celltype",
              label = TRUE, label.size = 3, repel = TRUE) +
  NoLegend() +
  ggtitle("查询集预测标签")
p_combo <- p5 + p6
save_plot("05_ref_vs_query_umap.png", p_combo, width = 16, height = 7)

# 查询集:真实标签 vs 预测标签对比
p7 <- DimPlot(pancreas.query, reduction = "ref.umap", group.by = "seurat_annotations",
              label = TRUE, label.size = 3, repel = TRUE) +
  ggtitle("查询集真实标签")
p8 <- DimPlot(pancreas.query, reduction = "ref.umap", group.by = "predicted.celltype",
              label = TRUE, label.size = 3, repel = TRUE) +
  ggtitle("查询集预测标签")
p_truth_vs_pred <- p7 + p8
save_plot("06_truth_vs_predicted.png", p_truth_vs_pred, width = 16, height = 7)

# ============================================================
# 10. 预测置信度分析
# ============================================================
cat("\n## 10. 预测置信度分析\n\n")

# 查看预测分数分布
p9 <- ggplot(pancreas.query@meta.data, aes(x = predicted.id, y = prediction.score.max)) +
  geom_boxplot(fill = "#4C9BE8", alpha = 0.7) +
  geom_jitter(width = 0.2, alpha = 0.3, size = 0.5) +
  labs(title = "各细胞类型预测置信度", x = "预测细胞类型", y = "预测分数") +
  theme_bw() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))
save_plot("07_prediction_scores.png", p9, width = 10, height = 7)

cat(sprintf("平均预测分数: %.3f\n", mean(pancreas.query$prediction.score.max)))

# ============================================================
# 11. 合并参考集和查询集可视化
# ============================================================
cat("\n## 11. 合并参考集和查询集可视化\n\n")

# 给参考集和查询集添加来源标签
pancreas.ref$dataset <- "Reference"
pancreas.query$dataset <- "Query"

# 合并 UMAP 可视化
p10 <- DimPlot(pancreas.query, reduction = "ref.umap", group.by = "predicted.celltype",
               label = TRUE, label.size = 3, repel = TRUE) +
  labs(title = "查询集投影到参考 UMAP", color = "预测细胞类型") +
  theme(plot.title = element_text(size = 14))
save_plot("08_query_projected_umap.png", p10, width = 10, height = 8)

# 按数据来源分组着色
p11 <- DimPlot(pancreas.query, reduction = "ref.umap", group.by = "dataset",
               cols = c("Query" = "#E41A1C")) +
  labs(title = "查询集在参考 UMAP 空间中的分布", color = "数据集")
save_plot("09_query_on_ref_space.png", p11, width = 10, height = 8)

# ============================================================
# 12. 保存结果
# ============================================================
cat("\n## 12. 保存结果\n\n")

saveRDS(pancreas.ref, file.path(output_dir, "reference_dataset.rds"))
saveRDS(pancreas.query, file.path(output_dir, "query_mapped.rds"))
cat("已保存: reference_dataset.rds, query_mapped.rds\n")

# 保存预测结果
write.csv(pancreas.query@meta.data[, c("seurat_annotations", "predicted.id", 
                                        "prediction.score.max", "prediction.match")],
          file.path(output_dir, "mapping_predictions.csv"), row.names = TRUE)
cat("已保存: mapping_predictions.csv\n")

cat("\n=== 阶段4-A 参考数据集映射 完成!===\n")
cat(sprintf("共生成 9 张图 + 1 个CSV + 2 个RDS文件\n"))
cat(sprintf("预测准确率: %.1f%%\n", 
    round(match_result["TRUE"] / sum(match_result) * 100, 1)))

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段4-A: 参考数据集映射(标签转移 + MapQuery + 预测准确率 92.4%)

20260912 111444 Seurat V5 数据整合分析教程

Seurat v5 数据整合分析教程

基于 Seurat v5.5.1 官方教程,系统学习多批次单细胞数据的整合分析。
使用 PBMC 3k 数据模拟批次效应,完整演示 CCA / RPCA 整合、保守 Marker、跨批次差异表达等核心流程。


目录


1. 背景与原理

1.1 为什么需要数据整合?

在单细胞实验中,不同批次的样本由于技术差异(捕获效率、测序深度、试剂批次等)会产生批次效应。如果不进行整合,批次效应会导致:

  • 相同细胞类型因批次不同而被分到不同聚类
  • 批次特异的技术噪声干扰生物学信号
  • 跨批次比较不可靠

1.2 Seurat v5 整合核心特性

Seurat v5 引入了 layers 机制,可以在一个 Seurat 对象中管理多个批次的数据,无需创建多个对象:

操作 函数 说明
按批次分割 split(pbmc[["RNA"]], f = pbmc$batch) 将 counts 分为多个 layer
整合 IntegrateLayers(method = CCAIntegration) 一行代码完成整合
合并 JoinLayers(pbmc[["RNA"]]) 整合后合并回单 layer

1.3 两种整合方法对比

方法 函数 特点 适用场景
CCA CCAIntegration 经典方法,整合力度大 批次差异大、需要彻底去除批次效应
RPCA RPCAIntegration 更快更保守 批次差异小、希望保留更多生物学变异

2. 模拟批次效应

由于官方 ifnb 数据集(394MB)下载过慢,本教程使用已有的 PBMC 3k 数据,通过人为分割模拟两个「批次」。

2.1 数据准备

# 读取原始 10X 数据
pbmc_data <- Read10X(data.dir = data_dir)
pbmc <- CreateSeuratObject(counts = pbmc_data, project = "pbmc3k",
                            min.cells = 3, min.features = 200)

# QC 过滤
pbmc <- PercentageFeatureSet(pbmc, pattern = "^MT-", col.name = "percent.mt")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
# 过滤后细胞数: 2638

2.2 随机分批 + 添加技术偏差

# 随机分为两个批次
set.seed(42)
pbmc$batch <- sample(c("batch1", "batch2"), size = ncol(pbmc), replace = TRUE)
# batch1: 1348 个细胞 | batch2: 1290 个细胞

# 为 batch2 添加技术偏差:
# 1) 乘法噪声(0.7-1.3 的基因特异噪声因子,模拟不同捕获效率)
# 2) Dropout 500 个基因的 30% 细胞(模拟检测率差异)

2.3 分割 Layers(Seurat v5 核心)

# 按 batch 分割 RNA assay 的 counts layer
pbmc[["RNA"]] <- split(pbmc[["RNA"]], f = pbmc$batch)
# 分割后 pbmc 对象包含 counts.batch1 和 counts.batch2 两个 layer

3. 未整合分析(展示批次效应)

对每个 layer 独立进行标准流程(归一化、找高变基因、PCA),展示未整合时的批次效应。

pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc)
pbmc <- FindNeighbors(pbmc, dims = 1:30, reduction = "pca")
pbmc <- FindClusters(pbmc, resolution = 0.8, cluster.name = "unintegrated_clusters")
pbmc <- RunUMAP(pbmc, dims = 1:30, reduction = "pca", reduction.name = "umap.unintegrated")

3.1 未整合 UMAP - 按批次着色

可以清楚看到,两个批次在 UMAP 空间中明显分离,这就是批次效应。

未整合-按批次

3.2 未整合 UMAP - 按聚类着色

聚类结果受批次效应影响,相同细胞类型可能被分到不同聚类。

未整合-按聚类

3.3 未整合 - 按批次分割

用 split.by 参数分面展示,更直观地看到两个批次的分布差异。

未整合-按批次分割


4. CCA 整合

4.1 一行代码完成整合

Seurat v5 的 IntegrateLayers 函数封装了整个整合流程:

pbmc <- IntegrateLayers(
    object = pbmc,
    method = CCAIntegration,
    orig.reduction = "pca",
    new.reduction = "integrated.cca",
    verbose = FALSE
)

CCA 整合原理:
1. 在两个批次之间寻找典型相关分析(CCA)的「锚点」对
2. 基于锚点对计算批次校正向量
3. 将校正后的数据投影到新的降维空间(integrated.cca)

4.2 合并 Layers 并重新聚类

# 整合后必须合并 layers
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])

# 基于整合后的降维结果聚类
pbmc <- FindNeighbors(pbmc, reduction = "integrated.cca", dims = 1:30)
pbmc <- FindClusters(pbmc, resolution = 0.5, cluster.name = "cca_clusters")
pbmc <- RunUMAP(pbmc, dims = 1:30, reduction = "integrated.cca", reduction.name = "umap.cca")

4.3 整合后 UMAP - 按批次着色

整合后,两个批次在 UMAP 空间中充分混合,批次效应已消除。

CCA整合后-按批次

4.4 整合后 UMAP - 按聚类着色

聚类结果不再受批次影响,相同细胞类型被正确归入同一聚类。

CCA整合后-按聚类

4.5 整合后 - 按批次分割

分面展示可以看到两个批次的细胞类型分布高度一致。

CCA整合后-按批次分割


5. 整合前后对比

5.1 按批次着色对比

左:未整合(批次明显分离)| 右:CCA 整合后(批次充分混合)

整合前后-按批次

5.2 按聚类着色对比

左:未整合聚类(受批次影响)| 右:整合后聚类(反映真实细胞类型)

整合前后-按聚类


6. 细胞类型注释

6.1 寻找各聚类 Marker 基因

pbmc.markers <- FindAllMarkers(pbmc, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)
top_markers <- pbmc.markers %>%
    group_by(cluster) %>%
    slice_max(n = 5, order_by = avg_log2FC)

各聚类 top 5 marker(示例):

聚类 Top Markers 细胞类型
0 CD3D, CCR7, CD3E, LDHB, CD7 Naive CD4 T
1 IL32, CD3D, CD3E, IL7R, CD2 Memory CD4 T / T 细胞
2 LYZ, S100A9, S100A8 CD14+ Mono
3 MS4A1, CD79A, CD79B B 细胞
4 CD8A, GZMK, CCL5 CD8 T
5 FCGR3A, MS4A7 FCGR3A+ Mono
6 GNLY, NKG7, KLRD1 NK
7 FCER1A, CST3, LST1 DC
8 PPBP, PF4 Platelet

6.2 注释后 UMAP

new.cluster.ids <- c("Naive CD4 T", "CD14+ Mono", "Memory CD4 T", "B",
                     "CD8 T", "FCGR3A+ Mono", "NK", "DC", "Platelet")
names(new.cluster.ids) <- levels(pbmc)
pbmc <- RenameIdents(pbmc, new.cluster.ids)
pbmc$cell_type <- Idents(pbmc)

注释后UMAP

6.3 Marker FeaturePlot

经典 marker 基因在 UMAP 空间的表达分布:

FeaturePlot(pbmc, features = c("IL7R", "CD14", "MS4A1", "CD8A", "GNLY", "FCGR3A"),
            reduction = "umap.cca", ncol = 3)
  • IL7R: CD4 T 细胞
  • CD14: 单核细胞
  • MS4A1: B 细胞
  • CD8A: CD8 T 细胞
  • GNLY: NK 细胞
  • FCGR3A: FCGR3A+ 单核细胞

Marker FeaturePlot


7. 保守 Marker 基因(FindConservedMarkers)

7.1 原理

FindConservedMarkers 在两个批次中分别寻找某聚类的 marker 基因,然后用 meta-analysis 合并 p 值,找到不受批次效应影响的稳定 marker。

7.2 核心代码

# 注意:FindConservedMarkers 需要 layers 处于合并状态
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])

Idents(pbmc) <- "cca_clusters"
# 选择最大的聚类进行分析
conserved_markers <- FindConservedMarkers(
    pbmc,
    ident.1 = target_cluster,  # 聚类1
    grouping.var = "batch",     # 按批次分组
    verbose = FALSE
)

7.3 结果解读

聚类1(T 细胞)的保守 marker 基因(前 15 个):

基因 batch2 avg_log2FC batch1 avg_log2FC 说明
IL32 1.457 1.153 T 细胞活化标志
CD3D 1.056 1.067 T 细胞受体组件
CD3E 1.052 1.052 T 细胞受体组件
IL7R 1.260 1.711 CD4 T 细胞标志
CD2 1.848 1.389 T 细胞粘附分子
LDHB 0.970 1.038 代谢酶(泛表达)
LTB 1.235 1.438 淋巴毒素
LCK 0.957 0.682 T 细胞信号转导
AQP3 1.826 2.623 水通道蛋白(T 细胞活化)
PRDX2 1.185 0.871 抗氧化蛋白

这些基因在两个批次中都显著高表达,说明它们是稳定的生物学 marker,不受批次效应影响。

7.4 保守 Marker 可视化

小提琴图(按批次分割):展示 top 6 保守 marker 在两个批次中的表达一致性。

VlnPlot(pbmc, features = top_conserved, split.by = "batch", pt.size = 0, ncol = 3)

保守Marker小提琴图

DotPlot(按批次分割):展示 top 12 保守 marker 的表达比例和水平。

DotPlot(pbmc, features = markers_to_plot, split.by = "batch") + RotatedAxis()

保守Marker DotPlot


8. 跨批次差异表达(FindMarkers)

8.1 目的

比较同一细胞类型在两个批次中的差异表达基因,用于识别:
- 技术偏差导致的假阳性差异
- 真实的生物学差异(如果有处理条件不同)

8.2 核心代码

# 创建 celltype.batch 组合变量
pbmc$celltype.batch <- paste(pbmc$cell_type, pbmc$batch, sep = "_")
Idents(pbmc) <- "celltype.batch"

# 比较第一个共有细胞类型的两个批次
de_genes <- FindMarkers(pbmc, ident.1 = "Cluster0_batch2", ident.2 = "Cluster0_batch1")

8.3 结果解读

跨批次差异基因(top 10):

基因 avg_log2FC pct.1 pct.2 说明
B2M -0.722 1.0 1.0 MHC-I 组件(技术偏差)
RPL15 -0.634 1.0 1.0 核糖体蛋白(技术偏差)
RPL13 -0.558 1.0 1.0 核糖体蛋白(技术偏差)
TMSB4X -0.419 1.0 1.0 肌动蛋白结合(技术偏差)
RPL19 -0.487 1.0 1.0 核糖体蛋白(技术偏差)
RPL3 -0.468 1.0 1.0 核糖体蛋白(技术偏差)
RPS15A -0.510 0.993 1.0 核糖体蛋白(技术偏差)
RPS19 -0.597 1.0 1.0 核糖体蛋白(技术偏差)

关键发现:差异基因主要是核糖体蛋白和 MHC-I 分子,这些是典型的技术偏差而非生物学差异。这说明我们模拟的批次效应主要体现在检测效率上,符合预期。

8.4 差异基因可视化

VlnPlot(pbmc, features = top_de, split.by = "batch", pt.size = 0, ncol = 3)

跨批次差异基因小提琴图


9. RPCA 整合与 CCA 对比

9.1 RPCA 整合

RPCA(Reciprocal PCA)是一种更快速、更保守的整合方法:

# RPCA 需要 layers 处于分割状态
pbmc[["RNA"]] <- split(pbmc[["RNA"]], f = pbmc$batch)

pbmc <- IntegrateLayers(
    object = pbmc,
    method = RPCAIntegration,
    orig.reduction = "pca",
    new.reduction = "integrated.rpca",
    verbose = FALSE
)

# 合并 layers
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])

9.2 RPCA 整合后 UMAP

RPCA整合后-按批次

9.3 CCA vs RPCA 对比

左:CCA 整合 | 右:RPCA 整合

CCA vs RPCA

9.4 CCA vs RPCA 选择建议

指标 CCA RPCA
整合力度 强(可能过度校正) 适中(保留更多变异)
计算速度 较慢 较快
适用场景 批次差异大 批次差异小
生物学信号保留 可能丢失部分 保留更好
推荐 常规首选 大数据集/小批次差异

10. 总结与核心知识点

10.1 核心流程图

原始数据 → QC 过滤 → 模拟/识别批次
                           ↓
                    split(pbmc[["RNA"]], f = pbmc$batch)
                           ↓
              ┌─────────────┴──────────────┐
              ↓                           ↓
        未整合分析                    CCA/RPCA 整合
        (展示批次效应)              IntegrateLayers()
              ↓                           ↓
        UMAP(批次分离)           JoinLayers() + 聚类
                                      ↓
                              UMAP(批次混合)
                                      ↓
                    ┌─────────────────┴──────────────────┐
                    ↓                                    ↓
           FindConservedMarkers                    FindMarkers
           (跨批次保守 marker)                   (跨批次差异基因)

10.2 关键注意事项

  1. split vs JoinLayers 切换:
    - IntegrateLayers 需要 layers 分割状态
    - FindConservedMarkers 需要 layers 合并状态
    - 脚本中需要反复切换

  2. 依赖包:
    - FindConservedMarkers 需要 metap 包
    - metap 依赖 multtest(Bioconductor 包)
    - 安装:BiocManager::install("multtest") → install.packages("metap")

  3. 整合方法选择:
    - 默认使用 CCA(整合力度大)
    - 大数据集或小批次差异时优先使用 RPCA

10.3 输出文件清单

编号 文件名 内容
01 01_unintegrated_batch.png 未整合 UMAP - 按批次
02 02_unintegrated_cluster.png 未整合 UMAP - 按聚类
03 03_unintegrated_split.png 未整合 - 按批次分割
04 04_integrated_batch.png CCA 整合后 - 按批次
05 05_integrated_cluster.png CCA 整合后 - 按聚类
06 06_integrated_split.png CCA 整合后 - 按批次分割
07 07_compare_batch.png 整合前后对比 - 按批次
08 08_compare_cluster.png 整合前后对比 - 按聚类
09 09_annotated_umap.png 注释后 UMAP
10 10_marker_featureplot.png Marker FeaturePlot
11 11_conserved_markers_violin.png 保守 Marker 小提琴图
12 12_conserved_markers_dot.png 保守 Marker DotPlot
13 13_batch_de_violin.png 跨批次差异基因小提琴图
14 14_rpca_batch.png RPCA 整合后 - 按批次
15 15_cca_vs_rpca.png CCA vs RPCA 对比

附录: 完整脚本

integration_vignette.R

###############################################################################
# Seurat v5 阶段3:数据整合分析(Integration)
# 基于官方教程:
#   - https://satijalab.org/seurat/articles/integration_introduction
#   - https://satijalab.org/seurat/articles/seurat5_integration
#
# 由于 ifnb 数据集(394MB)从 Seurat 官方服务器下载过慢,
# 本脚本使用已有的 PBMC 3k 数据,通过人为分割模拟两个"批次",
# 完整演示 Seurat v5 整合流程的所有核心步骤:
#   1. 模拟批次效应
#   2. 未整合分析(展示批次效应)
#   3. CCA 整合
#   4. 整合前后对比
#   5. 保守 Marker 基因(FindConservedMarkers)
#   6. 跨条件差异表达(FindMarkers)
#   7. 整合方法对比(CCA vs RPCA)
###############################################################################

library(Seurat)
library(ggplot2)
library(patchwork)
library(dplyr)

# === 路径配置 ===
base_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial"
data_dir <- file.path(base_dir, "data/filtered_gene_bc_matrices/hg19")
output_dir <- file.path(base_dir, "output")
integ_dir <- file.path(output_dir, "integration")
dir.create(integ_dir, showWarnings = FALSE, recursive = TRUE)

# ============================================================================
# 1. 加载数据 & 模拟批次效应
# ============================================================================
cat("\n========== 1. 加载数据 & 模拟批次 ==========\n")

# 读取原始 10X 数据
pbmc_data <- Read10X(data.dir = data_dir)

# 创建 Seurat 对象
pbmc <- CreateSeuratObject(counts = pbmc_data, project = "pbmc3k",
                            min.cells = 3, min.features = 200)

# 计算线粒体百分比
pbmc <- PercentageFeatureSet(pbmc, pattern = "^MT-", col.name = "percent.mt")

# QC 过滤
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
cat("过滤后细胞数:", ncol(pbmc), "\n")

# --- 模拟批次效应 ---
# 将细胞随机分为两个"批次"
set.seed(42)
cell_ids <- colnames(pbmc)
batch_labels <- sample(c("batch1", "batch2"), size = length(cell_ids), replace = TRUE)
names(batch_labels) <- cell_ids
pbmc$batch <- batch_labels

# 为 batch2 人为添加技术偏差:
# 1) 对 batch2 的部分基因表达量添加随机噪声(模拟批次效应)
# 2) 降低 batch2 中某些基因的检测率
cat("正在模拟批次效应...\n")

# 获取 counts 矩阵
counts_mat <- GetAssayData(pbmc, layer = "counts")

# 找到 batch2 的细胞
batch2_cells <- which(pbmc$batch == "batch2")
batch1_cells <- which(pbmc$batch == "batch1")
cat("batch1:", length(batch1_cells), "个细胞\n")
cat("batch2:", length(batch2_cells), "个细胞\n")

# 对 batch2 添加乘法噪声(模拟不同的捕获效率)
set.seed(123)
noise_factor <- 0.7 + 0.6 * runif(nrow(counts_mat))  # 0.7-1.3 的基因特异噪声
noise_matrix <- matrix(noise_factor, nrow = nrow(counts_mat), ncol = length(batch2_cells))
# 对 batch2 的 counts 乘以噪声因子(向下取整)
counts_batch2 <- ceiling(as.matrix(counts_mat[, batch2_cells]) * noise_matrix)
counts_mat[, batch2_cells] <- as(counts_batch2, "dgCMatrix")

# 为 batch2 随机 dropout 部分基因(模拟检测率差异)
set.seed(456)
dropout_genes <- sample(rownames(pbmc), size = 500)  # 随机选 500 个基因
for (gene in dropout_genes) {
    dropout_cells <- sample(batch2_cells, size = floor(length(batch2_cells) * 0.3))
    counts_mat[gene, dropout_cells] <- 0
}

# 写回修改后的 counts
pbmc[["RNA"]]$counts <- counts_mat
cat("批次效应模拟完成\n")

# ============================================================================
# 2. 按批次分割 Layers(Seurat v5 核心特性)
# ============================================================================
cat("\n========== 2. 分割 Layers ==========\n")

# Seurat v5 核心特性:在一个对象中通过 layers 管理多个批次
pbmc[["RNA"]] <- split(pbmc[["RNA"]], f = pbmc$batch)
cat("分割后 layers:\n")
print(pbmc)

# ============================================================================
# 3. 未整合分析(展示批次效应)
# ============================================================================
cat("\n========== 3. 未整合分析 ==========\n")

# 标准流程(对每个 layer 独立归一化和找高变基因)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc)

# 聚类
pbmc <- FindNeighbors(pbmc, dims = 1:30, reduction = "pca")
pbmc <- FindClusters(pbmc, resolution = 0.8, cluster.name = "unintegrated_clusters")

# UMAP(未整合)
pbmc <- RunUMAP(pbmc, dims = 1:30, reduction = "pca", reduction.name = "umap.unintegrated")

# 3.1 未整合 UMAP - 按批次着色
p_unintegrated_batch <- DimPlot(pbmc, reduction = "umap.unintegrated", group.by = "batch") +
    ggtitle("未整合 - 按批次着色")
ggsave(file.path(integ_dir, "01_unintegrated_batch.png"), plot = p_unintegrated_batch, width = 8, height = 7, dpi = 150)
cat("已保存: 01_unintegrated_batch.png\n")

# 3.2 未整合 UMAP - 按聚类着色
p_unintegrated_cluster <- DimPlot(pbmc, reduction = "umap.unintegrated", group.by = "unintegrated_clusters") +
    ggtitle("未整合 - 按聚类着色")
ggsave(file.path(integ_dir, "02_unintegrated_cluster.png"), plot = p_unintegrated_cluster, width = 8, height = 7, dpi = 150)
cat("已保存: 02_unintegrated_cluster.png\n")

# 3.3 未整合 - split.by 批次
p_unintegrated_split <- DimPlot(pbmc, reduction = "umap.unintegrated", split.by = "batch") +
    ggtitle("未整合 - 按批次分割")
ggsave(file.path(integ_dir, "03_unintegrated_split.png"), plot = p_unintegrated_split, width = 14, height = 6, dpi = 150)
cat("已保存: 03_unintegrated_split.png\n")

# ============================================================================
# 4. CCA 整合
# ============================================================================
cat("\n========== 4. CCA 整合 ==========\n")
cat("正在运行 CCA 整合(可能需要几分钟)...\n")

# IntegrateLayers 一行代码完成整合
pbmc <- IntegrateLayers(
    object = pbmc,
    method = CCAIntegration,
    orig.reduction = "pca",
    new.reduction = "integrated.cca",
    verbose = FALSE
)
cat("CCA 整合完成\n")

# 重新合并 layers(整合后必须执行)
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])
cat("Layers 已重新合并\n")

# 基于整合后的降维结果聚类
pbmc <- FindNeighbors(pbmc, reduction = "integrated.cca", dims = 1:30)
pbmc <- FindClusters(pbmc, resolution = 0.5, cluster.name = "cca_clusters")

# 整合后 UMAP
pbmc <- RunUMAP(pbmc, dims = 1:30, reduction = "integrated.cca", reduction.name = "umap.cca")

# 4.1 整合后 UMAP - 按批次着色
p_integrated_batch <- DimPlot(pbmc, reduction = "umap.cca", group.by = "batch") +
    ggtitle("CCA 整合后 - 按批次着色")
ggsave(file.path(integ_dir, "04_integrated_batch.png"), plot = p_integrated_batch, width = 8, height = 7, dpi = 150)
cat("已保存: 04_integrated_batch.png\n")

# 4.2 整合后 UMAP - 按聚类着色
p_integrated_cluster <- DimPlot(pbmc, reduction = "umap.cca", group.by = "cca_clusters", label = TRUE) +
    ggtitle("CCA 整合后 - 按聚类着色")
ggsave(file.path(integ_dir, "05_integrated_cluster.png"), plot = p_integrated_cluster, width = 8, height = 7, dpi = 150)
cat("已保存: 05_integrated_cluster.png\n")

# 4.3 整合后 - split.by 批次
p_integrated_split <- DimPlot(pbmc, reduction = "umap.cca", split.by = "batch") +
    ggtitle("CCA 整合后 - 按批次分割")
ggsave(file.path(integ_dir, "06_integrated_split.png"), plot = p_integrated_split, width = 14, height = 6, dpi = 150)
cat("已保存: 06_integrated_split.png\n")

# ============================================================================
# 5. 整合前后对比图
# ============================================================================
cat("\n========== 5. 整合前后对比 ==========\n")

# 5.1 并排对比 - 按批次着色
p_compare_batch <- DimPlot(pbmc, reduction = "umap.unintegrated", group.by = "batch") +
    ggtitle("未整合") +
DimPlot(pbmc, reduction = "umap.cca", group.by = "batch") +
    ggtitle("CCA 整合后")
ggsave(file.path(integ_dir, "07_compare_batch.png"), plot = p_compare_batch, width = 16, height = 6, dpi = 150)
cat("已保存: 07_compare_batch.png\n")

# 5.2 并排对比 - 按聚类着色
p_compare_cluster <- DimPlot(pbmc, reduction = "umap.unintegrated", group.by = "unintegrated_clusters") +
    ggtitle("未整合聚类") +
DimPlot(pbmc, reduction = "umap.cca", group.by = "cca_clusters", label = TRUE) +
    ggtitle("整合后聚类")
ggsave(file.path(integ_dir, "08_compare_cluster.png"), plot = p_compare_cluster, width = 16, height = 6, dpi = 150)
cat("已保存: 08_compare_cluster.png\n")

# ============================================================================
# 6. 细胞类型注释
# ============================================================================
cat("\n========== 6. 细胞类型注释 ==========\n")

# 使用经典 marker 进行注释(与阶段1一致)
# 先寻找每个聚类的 marker 基因
pbmc.markers <- FindAllMarkers(pbmc, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)
top_markers <- pbmc.markers %>%
    group_by(cluster) %>%
    slice_max(n = 5, order_by = avg_log2FC)
cat("各聚类 top 5 marker:\n")
print(top_markers)

# 保存 marker 表
write.csv(pbmc.markers, file.path(integ_dir, "integration_markers.csv"), row.names = FALSE)
cat("已保存: integration_markers.csv\n")

# 基于阶段1的知识进行注释
# 查看各聚类的 marker 来确定细胞类型
new.cluster.ids <- c("Naive CD4 T", "CD14+ Mono", "Memory CD4 T", "B",
                      "CD8 T", "FCGR3A+ Mono", "NK", "DC", "Platelet")
# 根据实际聚类数调整
n_clusters <- length(unique(pbmc$cca_clusters))
cat("聚类数量:", n_clusters, "\n")

# 如果聚类数与注释数不匹配,使用通用注释
if (n_clusters == length(new.cluster.ids)) {
    names(new.cluster.ids) <- levels(pbmc)
    pbmc <- RenameIdents(pbmc, new.cluster.ids)
} else {
    cat("聚类数与预设注释数不一致,使用通用注释\n")
    # 使用数字标签
    new.cluster.ids <- paste0("Cluster", 0:(n_clusters - 1))
    names(new.cluster.ids) <- levels(pbmc)
    pbmc <- RenameIdents(pbmc, new.cluster.ids)
}
pbmc$cell_type <- Idents(pbmc)

# 6.1 注释后 UMAP
p_annotated <- DimPlot(pbmc, reduction = "umap.cca", label = TRUE, pt.size = 0.5) + NoLegend() +
    ggtitle("整合后 - 细胞类型注释")
ggsave(file.path(integ_dir, "09_annotated_umap.png"), plot = p_annotated, width = 8, height = 7, dpi = 150)
cat("已保存: 09_annotated_umap.png\n")

# 6.2 Marker FeaturePlot
p_markers_fp <- FeaturePlot(pbmc, features = c("IL7R", "CD14", "MS4A1", "CD8A", "GNLY", "FCGR3A"),
                             reduction = "umap.cca", ncol = 3)
ggsave(file.path(integ_dir, "10_marker_featureplot.png"), plot = p_markers_fp, width = 14, height = 10, dpi = 150)
cat("已保存: 10_marker_featureplot.png\n")

# ============================================================================
# 7. 保守 Marker 基因(FindConservedMarkers)
# ============================================================================
cat("\n========== 7. 保守 Marker 基因 ==========\n")

# FindConservedMarkers:在两个批次中分别寻找 marker,然后用 meta-analysis 合并 p 值
# 这可以找到不受批次效应影响的稳定 marker 基因

# 注意:FindConservedMarkers 需要 layers 处于合并状态
# (它在内部按 grouping.var 分组分析,不需要预先 split)
# 确保 layers 已合并
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])

# 以某个聚类为例(选择最大的聚类)
Idents(pbmc) <- "cca_clusters"
target_cluster <- as.character(which.max(table(pbmc$cca_clusters)))
cat("分析聚类", target_cluster, "的保守 marker\n")

conserved_markers <- FindConservedMarkers(
    pbmc,
    ident.1 = target_cluster,
    grouping.var = "batch",
    verbose = FALSE
)

cat("保守 marker 前 15 个:\n")
print(head(conserved_markers, 15))

# 保存保守 marker
write.csv(conserved_markers, file.path(integ_dir, "conserved_markers.csv"), row.names = TRUE)
cat("已保存: conserved_markers.csv\n")

# 7.1 保守 marker 小提琴图(按批次分割)
top_conserved <- rownames(head(conserved_markers, 6))
p_conserved_vln <- VlnPlot(pbmc, features = top_conserved, split.by = "batch", pt.size = 0, ncol = 3) +
    ggtitle("保守 Marker - 按批次分割")
ggsave(file.path(integ_dir, "11_conserved_markers_violin.png"), plot = p_conserved_vln, width = 14, height = 10, dpi = 150)
cat("已保存: 11_conserved_markers_violin.png\n")

# 7.2 保守 marker DotPlot(按批次分割)
# layers 已合并,直接使用
markers_to_plot <- rownames(head(conserved_markers, 12))
p_conserved_dot <- DotPlot(pbmc, features = markers_to_plot, split.by = "batch") + RotatedAxis() +
    ggtitle("保守 Marker DotPlot - 按批次对比")
ggsave(file.path(integ_dir, "12_conserved_markers_dot.png"), plot = p_conserved_dot, width = 12, height = 6, dpi = 150)
cat("已保存: 12_conserved_markers_dot.png\n")

# ============================================================================
# 8. 跨批次差异表达分析
# ============================================================================
cat("\n========== 8. 跨批次差异表达 ==========\n")

# 创建 celltype.batch 组合变量
pbmc$celltype.batch <- paste(pbmc$cell_type, pbmc$batch, sep = "_")
Idents(pbmc) <- "celltype.batch"

# 选择一个细胞类型,比较两个批次的差异
# 尝试找到 CD14+ Mono 或第一个细胞类型
unique_idents <- unique(pbmc$celltype.batch)
cat("可用的 celltype.batch 组合:\n")
print(table(pbmc$celltype.batch))

# 选择第一个细胞类型进行跨批次比较
# 找到同时存在于两个批次的细胞类型
batch1_types <- unique(pbmc$cell_type[pbmc$batch == "batch1"])
batch2_types <- unique(pbmc$cell_type[pbmc$batch == "batch2"])
common_types <- intersect(batch1_types, batch2_types)
cat("两个批次共有的细胞类型:\n")
print(common_types)

if (length(common_types) > 0) {
    target_type <- common_types[1]
    ident1 <- paste0(target_type, "_batch2")
    ident2 <- paste0(target_type, "_batch1")

    if (ident1 %in% unique_idents && ident2 %in% unique_idents) {
        de_genes <- FindMarkers(pbmc, ident.1 = ident1, ident.2 = ident2, verbose = FALSE)
        cat("\n", target_type, "的跨批次差异基因(前 15 个):\n")
        print(head(de_genes, 15))

        write.csv(de_genes, file.path(integ_dir, "batch_de_genes.csv"), row.names = TRUE)
        cat("已保存: batch_de_genes.csv\n")

        # 8.1 差异基因小提琴图
        top_de <- rownames(head(de_genes, 6))
        if (length(top_de) > 0) {
            p_de_vln <- VlnPlot(pbmc, features = top_de, split.by = "batch", pt.size = 0, ncol = 3) +
                ggtitle(paste0("跨批次差异基因 - ", target_type))
            ggsave(file.path(integ_dir, "13_batch_de_violin.png"), plot = p_de_vln, width = 14, height = 10, dpi = 150)
            cat("已保存: 13_batch_de_violin.png\n")
        }
    }
}

# ============================================================================
# 9. RPCA 整合(更快速、更保守的整合方法)
# ============================================================================
cat("\n========== 9. RPCA 整合 ==========\n")

# RPCA 整合需要按批次分割 layers
pbmc[["RNA"]] <- split(pbmc[["RNA"]], f = pbmc$batch)

# RPCA 整合(比 CCA 更快、更保守)
cat("正在运行 RPCA 整合...\n")
pbmc <- IntegrateLayers(
    object = pbmc,
    method = RPCAIntegration,
    orig.reduction = "pca",
    new.reduction = "integrated.rpca",
    verbose = FALSE
)
cat("RPCA 整合完成\n")

# 合并 layers
pbmc[["RNA"]] <- JoinLayers(pbmc[["RNA"]])

# RPCA 聚类和 UMAP
pbmc <- FindNeighbors(pbmc, reduction = "integrated.rpca", dims = 1:30)
pbmc <- FindClusters(pbmc, resolution = 0.5, cluster.name = "rpca_clusters")
pbmc <- RunUMAP(pbmc, dims = 1:30, reduction = "integrated.rpca", reduction.name = "umap.rpca")

# 9.1 RPCA 整合后 UMAP - 按批次着色
p_rpca_batch <- DimPlot(pbmc, reduction = "umap.rpca", group.by = "batch") +
    ggtitle("RPCA 整合后 - 按批次着色")
ggsave(file.path(integ_dir, "14_rpca_batch.png"), plot = p_rpca_batch, width = 8, height = 7, dpi = 150)
cat("已保存: 14_rpca_batch.png\n")

# 9.2 CCA vs RPCA 对比
p_cca_rpca <- DimPlot(pbmc, reduction = "umap.cca", group.by = "batch") +
    ggtitle("CCA 整合") +
DimPlot(pbmc, reduction = "umap.rpca", group.by = "batch") +
    ggtitle("RPCA 整合")
ggsave(file.path(integ_dir, "15_cca_vs_rpca.png"), plot = p_cca_rpca, width = 16, height = 6, dpi = 150)
cat("已保存: 15_cca_vs_rpca.png\n")

# ============================================================================
# 10. 保存结果
# ============================================================================
cat("\n========== 10. 保存结果 ==========\n")

saveRDS(pbmc, file.path(output_dir, "pbmc3k_integrated.rds"))
cat("已保存整合后对象: pbmc3k_integrated.rds\n")

# ============================================================================
# 总结
# ============================================================================
cat("\n")
cat("=======================================================\n")
cat("  阶段3 数据整合教程完成!\n")
cat("=======================================================\n")
cat("  输出目录:", integ_dir, "\n")
cat("  图片数量: 15 张\n")
cat("  覆盖内容:\n")
cat("    01-03: 未整合分析(展示批次效应)\n")
cat("    04-06: CCA 整合结果\n")
cat("    07-08: 整合前后对比\n")
cat("    09-10: 细胞类型注释\n")
cat("    11-12: 保守 Marker 基因\n")
cat("    13:    跨批次差异表达\n")
cat("    14-15: RPCA 整合与 CCA 对比\n")
cat("  核心知识点:\n")
cat("    - Seurat v5 layers 机制(split / Join)\n")
cat("    - IntegrateLayers 一行代码整合\n")
cat("    - CCA vs RPCA 两种整合方法\n")
cat("    - FindConservedMarkers 跨批次保守 marker\n")
cat("    - FindMarkers 跨批次差异表达\n")
cat("    - 整合前后对比可视化\n")
cat("=======================================================\n")

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段3: 数据整合分析(CCA / RPCA / 保守 Marker / 跨批次差异表达)

20260912 093449 Seurat V5 数据可视化方法与 Sctransform 归一化教程

Seurat v5 数据可视化方法与 SCTransform 归一化教程

基于 Seurat v5.5.1 官方教程,系统学习单细胞数据的可视化技巧与 SCTransform 先进归一化方法。
阶段2-A:数据可视化方法(20张图,5大板块)
阶段2-B:SCTransform 归一化(9张图,7个步骤)


目录


Part A: 数据可视化方法

基于 PBMC 3k 数据集(阶段1的 pbmc3k_final.rds),演示 Seurat v5 的五大可视化功能。

环境准备

library(Seurat)
library(ggplot2)
library(patchwork)

# 加载阶段1保存的最终对象
pbmc <- readRDS("output/pbmc3k_final.rds")

# 添加随机分组变量(用于演示 split.by 功能)
set.seed(42)
pbmc$groups <- sample(c("group1", "group2"), size = ncol(pbmc), replace = TRUE)

# 演示用基因列表
features <- c("LYZ", "CCL5", "IL32", "PTPRCAP", "FCGR3A", "PF4")

1. 五种核心可视化方法

Seurat 提供了五种主要的基因表达可视化方法,各有侧重:

方法 函数 特点 适用场景
山脊图 RidgePlot() 展示表达密度分布 比较多个基因的表达模式
小提琴图 VlnPlot() 展示表达分布+密度 各聚类间基因表达比较
Feature Plot FeaturePlot() 在低维空间展示表达 空间分布模式
气泡图 DotPlot() 大小=比例,颜色=表达量 大量基因×聚类的概览
热图 DoHeatmap() 颜色梯度展示表达 多基因多聚类综合展示

1.1 Ridge Plot(山脊图)

展示各聚类中基因表达的密度分布。每个「山脊」代表一个聚类,宽度反映表达密度。

p_ridge <- RidgePlot(pbmc, features = features, ncol = 2)

山脊图

1.2 Violin Plot(小提琴图)

展示各聚类中基因表达的完整分布(中位数、四分位数、密度)。

p_vln <- VlnPlot(pbmc, features = features)

小提琴图

1.3 Feature Plot

在 UMAP 降维空间上展示基因表达水平,颜色深浅代表表达量。

p_feature <- FeaturePlot(pbmc, features = features)

Feature Plot

1.4 Dot Plot(气泡图)

点的大小表示该聚类中表达该基因的细胞比例,颜色深浅表示平均表达水平。适合快速概览多个基因在所有聚类中的表达模式。

p_dot <- DotPlot(pbmc, features = features) + RotatedAxis()

气泡图

1.5 Heatmap(热图)

对每个聚类降采样后展示基因表达热图,适合同时查看多个基因在多个聚类中的表达模式。

p_heat <- DoHeatmap(subset(pbmc, downsample = 100), features = features, size = 3)

热图


2. 自定义 FeaturePlot

FeaturePlot 是最灵活的可视化函数,支持多种自定义选项。

2.1 带图例的 FeaturePlot

默认带图例,展示连续的颜色梯度:

FeaturePlot(pbmc, features = "MS4A1")

带图例 FeaturePlot

2.2 调整对比度(min/max cutoff)

设定表达量的最小/最大截断值,增强视觉对比度:

FeaturePlot(pbmc, features = "MS4A1", min.cutoff = 1, max.cutoff = 3)

调整对比度

2.3 分位数 cutoff

使用分位数 cutoff(如 q10-q90),适合多基因同时绘图时统一色彩范围:

FeaturePlot(pbmc, features = c("MS4A1", "PTPRCAP"), min.cutoff = "q10", max.cutoff = "q90")

分位数 cutoff

2.4 混合模式(blend)

混合展示两个基因的共表达,红色和绿色分别代表两个基因,黄色表示共表达:

FeaturePlot(pbmc, features = c("MS4A1", "CD79A"), blend = TRUE)

混合模式

2.5 按分组分割

使用 split.by 参数按分组变量分割展示同一基因的表达:

FeaturePlot(pbmc, features = c("MS4A1", "CD79A"), split.by = "groups")

分组分割


3. 分组分割与热图

3.1 按分组分割的小提琴图

VlnPlot(pbmc, features = "percent.mt", split.by = "groups")

分组小提琴图

3.2 按分组分割的气泡图

DotPlot(pbmc, features = features, split.by = "groups") + RotatedAxis()

分组气泡图

3.3 DimPlot 默认 UMAP

DimPlot(pbmc)

UMAP

3.4 带分组条的热图

展示前 100 个高变基因在前 500 个细胞中的表达,使用 angle = 90 旋转基因标签:

DoHeatmap(pbmc, features = VariableFeatures(pbmc)[1:100], cells = 1:500, size = 4, angle = 90) + NoLegend()

分组热图


4. 主题定制

Seurat 的图表基于 ggplot2,支持完整的 ggplot2 主题系统。

4.1 添加标题

baseplot <- DimPlot(pbmc, reduction = "umap")
baseplot + labs(title = "Clustering of 2,700 PBMCs")

添加标题

4.2 暗色主题

baseplot + DarkTheme()

暗色主题

4.3 链式主题

多个主题元素可以链式叠加,同时调整字体大小、图例等:

baseplot + DarkTheme() +
    theme(axis.title = element_text(size = 18), legend.text = element_text(size = 18)) +
    guides(colour = guide_legend(override.aes = list(size = 10)))

链式主题


5. 图组合并

使用 patchwork 包的 + 和 & 运算符组合多个图表。

5.1 拼接两图

plot1 <- DimPlot(pbmc)
plot2 <- FeatureScatter(pbmc, feature1 = "LYZ", feature2 = "CCL5")
plot1 + plot2

图组合并

5.2 移除所有图例

& 运算符对组合图中的所有子图应用同一操作:

(plot1 + plot2) & NoLegend()

移除图例


6. 聚类标签

使用 LabelClusters() 在降维图上直接标注聚类编号:

p_label <- DimPlot(pbmc, reduction = "pca") + NoLegend()
p_label <- LabelClusters(plot = p_label, id = "ident")

聚类标签


Part B: SCTransform 归一化

7. SCTransform 原理与流程

7.1 为什么需要 SCTransform?

传统标准流程使用 NormalizeData() + ScaleData() + FindVariableFeatures() 三步处理:
- LogNormalize:简单但对测序深度差异敏感
- 高变基因选择:可能受技术因素影响
- 标准化:无法完全去除批次效应

SCTransform(v2)使用负二项回归模型一步完成:
1. 对每个基因拟合 count 模型(考虑测序深度等协变量)
2. 计算 Pearson 残差(去除技术变异后的生物学信号)
3. 选择高变基因(默认 3000 个,比标准的 2000 个更多)
4. 生成「校正后」的 UMI counts(可理解为统一测序深度后的预期值)

7.2 核心代码

library(Seurat)
library(ggplot2)
library(sctransform)

# 从原始数据开始(独立于阶段1的标准流程)
pbmc_data <- Read10X(data.dir = data_dir)
pbmc <- CreateSeuratObject(counts = pbmc_data)

# 计算线粒体百分比
pbmc <- PercentageFeatureSet(pbmc, pattern = "^MT-", col.name = "percent.mt")

# QC 过滤
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)

# SCTransform 一步完成归一化 + 变量特征选择 + 标准化
# 同时回归线粒体百分比(去除技术变异)
pbmc <- SCTransform(pbmc, vars.to.regress = "percent.mt", verbose = FALSE)

7.3 SCT Assay 结构

运行 SCTransform() 后,创建了一个名为 SCT 的新 Assay 并设为默认:

层级 内容 用途
scale.data Pearson 残差 PCA 输入
counts 校正后的 UMI counts 重新计算归一化值
data 校正 counts 的 log 归一化 可视化
> DefaultAssay(pbmc)
[1] "SCT"

> pbmc[["SCT"]]
SCTAssay data with 12519 features for 2638 cells, and 1 SCTModel(s)
Top 10 variable features:
 S100A9, GNLY, LYZ, S100A8, NKG7, FTL, GZMB, IGLL5, FTH1, CCL5

7.4 运行信息

  • 细胞数:2,700 -> 2,638(QC 过滤后)
  • 基因数:32,738 -> 12,519(过滤后参与建模)
  • 耗时:约 5 分钟(未安装 glmGamPoi 加速包,使用原生实现)
  • 变量特征:3,000 个(默认,比标准流程多 1,000 个)
  • 模型版本:v2(Seurat v5 默认,固定斜率模型 + 排除 Poisson 基因)

8. 降维与聚类

8.1 PCA

pbmc <- RunPCA(pbmc, verbose = FALSE)
DimPlot(pbmc, reduction = "pca")

SCT PCA

8.2 UMAP(使用前 30 个 PC)

SCTransform 推荐使用更多 PC(30 个),因为更有效的归一化使得高阶 PC 更可能代表真实的生物学变异。

pbmc <- RunUMAP(pbmc, dims = 1:30, verbose = FALSE)
DimPlot(pbmc, reduction = "umap")

SCT UMAP

8.3 Elbow Plot

ElbowPlot(pbmc, ndims = 50)

SCT Elbow Plot

8.4 聚类

pbmc <- FindNeighbors(pbmc, dims = 1:30, verbose = FALSE)
pbmc <- FindClusters(pbmc, verbose = FALSE)
DimPlot(pbmc, reduction = "umap", label = TRUE)

SCT 聚类标注


9. SCTransform 优势展示

SCTransform 归一化能揭示比标准流程更精细的细胞亚群。

9.1 T 细胞亚群标志基因

SCTransform 能更清晰地区分 CD8 T 细胞的三个亚群(naive / memory / effector):

亚群 标志基因 说明
Naive CD8 T CD8A, CCR7 初始 T 细胞
Memory CD8 T GZMK 记忆 T 细胞
Effector CD8 T CCL5 效应 T 细胞

同时区分 CD4 T 细胞的三亚群(naive / memory / IFN-activated):

亚群 标志基因 说明
Naive CD4 T CCR7, IL32 初始 T 细胞
Memory CD4 T S100A4, ANXA1 记忆 T 细胞
IFN-activated ISG15 干素激活

小提琴图:

VlnPlot(pbmc, features = c("CD8A", "GZMK", "CCL5", "S100A4", "ANXA1", "CCR7", "ISG15", "CD3D"),
         pt.size = 0.2, ncol = 4)

T细胞标志基因小提琴图

FeaturePlot(T 细胞标志):

FeaturePlot(pbmc, features = c("CD8A", "GZMK", "CCL5", "S100A4", "ANXA1", "CCR7"),
            pt.size = 0.2, ncol = 3)

T细胞FeaturePlot

9.2 B 细胞与 NK 细胞亚群

SCTransform 还揭示了:
- B 细胞发育亚结构(TCL1A, FCER2)
- NK 细胞 CD56dim vs. bright 亚群(XCL1, FCGR3A)

FeaturePlot(pbmc, features = c("CD3D", "ISG15", "TCL1A", "FCER2", "XCL1", "FCGR3A"),
            pt.size = 0.2, ncol = 3)

B/NK细胞FeaturePlot


10. SCT vs 标准流程对比

10.1 变量特征

SCTransform 默认返回 3,000 个变量特征(标准流程为 2,000 个):

length(VariableFeatures(pbmc))  # 3000

SCT变量特征

10.2 SCT UMAP(30 PCs)

DimPlot(pbmc, reduction = "umap", group.by = "seurat_clusters") +
    ggtitle("SCT: 30 PCs (推荐)")

SCT UMAP 30PC

10.3 对比总结

指标 标准流程 SCTransform
归一化方法 LogNormalize 负二项回归(v2)
变量特征数 2,000 3,000
推荐 PC 数 10 30
回归变量 可选 内置支持
测序深度影响 仍有残留 显著减弱
细胞亚群分辨率 基础 更精细(CD8 naive/memory/effector)
计算耗时 秒级 分钟级(约5分钟)
内存占用 较低 较高(非稀疏矩阵)
适用场景 快速探索 正式分析/发表

10.4 何时选择 SCTransform?

  • 数据中测序深度差异较大时
  • 需要检测精细细胞亚群时
  • 需要同时回归多个协变量时
  • 准备发表正式结果时
  • 跨数据集整合分析时(SCT 是整合分析的前提)

10.5 一行代码完成全流程

SCTransform 与管道操作符配合,可以一行代码完成完整分析:

library(dplyr)
pbmc <- CreateSeuratObject(pbmc_data) %>%
    PercentageFeatureSet(pattern = "^MT-", col.name = "percent.mt") %>%
    SCTransform(vars.to.regress = "percent.mt") %>%
    RunPCA() %>%
    FindNeighbors(dims = 1:30) %>%
    RunUMAP(dims = 1:30) %>%
    FindClusters()

附录: 完整脚本

A. 可视化教程脚本(visualization_vignette.R)

###############################################################################
# Seurat v5 阶段2-A:数据可视化方法
# 基于官方教程: https://satijalab.org/seurat/articles/visualization_vignette
# 使用阶段1保存的 pbmc3k_final.rds 对象
###############################################################################

library(Seurat)
library(ggplot2)
library(patchwork)

# === 路径配置 ===
output_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output"
viz_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output/visualization"
dir.create(viz_dir, showWarnings = FALSE, recursive = TRUE)

# === 加载阶段1的最终对象 ===
pbmc <- readRDS(file.path(output_dir, "pbmc3k_final.rds"))
cat("已加载 pbmc3k_final.rds\n")
print(pbmc)

# 添加分组变量(用于演示 split.by 功能)
set.seed(42)
pbmc$groups <- sample(c("group1", "group2"), size = ncol(pbmc), replace = TRUE)

# 定义演示用基因列表
features <- c("LYZ", "CCL5", "IL32", "PTPRCAP", "FCGR3A", "PF4")


# ============================================================================
# 1. 五种可视化方法展示 Marker 基因表达
# ============================================================================

cat("\n========== 1. 五种可视化方法 ==========\n")

# 1.1 Ridge Plot(山脊图)- 展示各聚类的基因表达分布
p_ridge <- RidgePlot(pbmc, features = features, ncol = 2)
ggsave(file.path(viz_dir, "01_ridge_plot.png"), plot = p_ridge, width = 12, height = 10, dpi = 150)
cat("已保存: 01_ridge_plot.png\n")

# 1.2 Violin Plot(小提琴图)- 展示各聚类的基因表达分布
p_vln <- VlnPlot(pbmc, features = features)
ggsave(file.path(viz_dir, "02_violin_plot.png"), plot = p_vln, width = 14, height = 8, dpi = 150)
cat("已保存: 02_violin_plot.png\n")

# 1.3 Feature Plot - 在低维空间展示基因表达
p_feature <- FeaturePlot(pbmc, features = features)
ggsave(file.path(viz_dir, "03_feature_plot.png"), plot = p_feature, width = 14, height = 10, dpi = 150)
cat("已保存: 03_feature_plot.png\n")

# 1.4 Dot Plot(气泡图)- 点大小=表达比例,颜色=平均表达水平
p_dot <- DotPlot(pbmc, features = features) + RotatedAxis()
ggsave(file.path(viz_dir, "04_dot_plot.png"), plot = p_dot, width = 10, height = 6, dpi = 150)
cat("已保存: 04_dot_plot.png\n")

# 1.5 Heatmap(热图)- 降采样后展示基因表达
p_heat <- DoHeatmap(subset(pbmc, downsample = 100), features = features, size = 3)
ggsave(file.path(viz_dir, "05_heatmap.png"), plot = p_heat, width = 10, height = 6, dpi = 150)
cat("已保存: 05_heatmap.png\n")


# ============================================================================
# 2. 自定义 FeaturePlot
# ============================================================================

cat("\n========== 2. 自定义 FeaturePlot ==========\n")

# 2.1 带图例的 FeaturePlot
p_fp1 <- FeaturePlot(pbmc, features = "MS4A1")
ggsave(file.path(viz_dir, "06_featureplot_legend.png"), plot = p_fp1, width = 7, height = 6, dpi = 150)
cat("已保存: 06_featureplot_legend.png\n")

# 2.2 调整对比度(设定 min/max cutoff)
p_fp2 <- FeaturePlot(pbmc, features = "MS4A1", min.cutoff = 1, max.cutoff = 3)
ggsave(file.path(viz_dir, "07_featureplot_contrast.png"), plot = p_fp2, width = 7, height = 6, dpi = 150)
cat("已保存: 07_featureplot_contrast.png\n")

# 2.3 分位数 cutoff(适合多基因同时绘图)
p_fp3 <- FeaturePlot(pbmc, features = c("MS4A1", "PTPRCAP"), min.cutoff = "q10", max.cutoff = "q90")
ggsave(file.path(viz_dir, "08_featureplot_quantile.png"), plot = p_fp3, width = 12, height = 5, dpi = 150)
cat("已保存: 08_featureplot_quantile.png\n")

# 2.4 混合展示两个基因的共表达(blend 模式)
p_fp4 <- FeaturePlot(pbmc, features = c("MS4A1", "CD79A"), blend = TRUE)
ggsave(file.path(viz_dir, "09_featureplot_blend.png"), plot = p_fp4, width = 14, height = 5, dpi = 150)
cat("已保存: 09_featureplot_blend.png\n")

# 2.5 按分组分割展示
p_fp5 <- FeaturePlot(pbmc, features = c("MS4A1", "CD79A"), split.by = "groups")
ggsave(file.path(viz_dir, "10_featureplot_split.png"), plot = p_fp5, width = 14, height = 10, dpi = 150)
cat("已保存: 10_featureplot_split.png\n")


# ============================================================================
# 3. 其他可视化功能
# ============================================================================

cat("\n========== 3. 其他可视化功能 ==========\n")

# 3.1 按分组分割的小提琴图
p_vln_split <- VlnPlot(pbmc, features = "percent.mt", split.by = "groups")
ggsave(file.path(viz_dir, "11_violin_split.png"), plot = p_vln_split, width = 8, height = 5, dpi = 150)
cat("已保存: 11_violin_split.png\n")

# 3.2 按分组分割的气泡图
p_dot_split <- DotPlot(pbmc, features = features, split.by = "groups") + RotatedAxis()
ggsave(file.path(viz_dir, "12_dotplot_split.png"), plot = p_dot_split, width = 12, height = 6, dpi = 150)
cat("已保存: 12_dotplot_split.png\n")

# 3.3 DimPlot 默认使用 UMAP
p_dim1 <- DimPlot(pbmc)
ggsave(file.path(viz_dir, "13_dimplot_umap.png"), plot = p_dim1, width = 8, height = 7, dpi = 150)
cat("已保存: 13_dimplot_umap.png\n")

# 3.4 DoHeatmap 带分组条
p_heat2 <- DoHeatmap(pbmc, features = VariableFeatures(pbmc)[1:100], cells = 1:500, size = 4, angle = 90) + NoLegend()
ggsave(file.path(viz_dir, "14_heatmap_grouped.png"), plot = p_heat2, width = 10, height = 8, dpi = 150)
cat("已保存: 14_heatmap_grouped.png\n")


# ============================================================================
# 4. 应用主题(Theme)
# ============================================================================

cat("\n========== 4. 应用主题 ==========\n")

baseplot <- DimPlot(pbmc, reduction = "umap")

# 4.1 添加标题
p_theme1 <- baseplot + labs(title = "Clustering of 2,700 PBMCs")
ggsave(file.path(viz_dir, "15_theme_title.png"), plot = p_theme1, width = 8, height = 7, dpi = 150)
cat("已保存: 15_theme_title.png\n")

# 4.2 暗色主题
p_theme2 <- baseplot + DarkTheme()
ggsave(file.path(viz_dir, "16_theme_dark.png"), plot = p_theme2, width = 8, height = 7, dpi = 150)
cat("已保存: 16_theme_dark.png\n")

# 4.3 链式主题
p_theme3 <- baseplot + DarkTheme() +
    theme(axis.title = element_text(size = 18), legend.text = element_text(size = 18)) +
    guides(colour = guide_legend(override.aes = list(size = 10)))
ggsave(file.path(viz_dir, "17_theme_chained.png"), plot = p_theme3, width = 10, height = 8, dpi = 150)
cat("已保存: 17_theme_chained.png\n")


# ============================================================================
# 5. 图组合并
# ============================================================================

cat("\n========== 5. 图组合并 ==========\n")

plot1 <- DimPlot(pbmc)
plot2 <- FeatureScatter(pbmc, feature1 = "LYZ", feature2 = "CCL5")

# 5.1 拼接两图
p_combine1 <- plot1 + plot2
ggsave(file.path(viz_dir, "18_combine_plots.png"), plot = p_combine1, width = 14, height = 6, dpi = 150)
cat("已保存: 18_combine_plots.png\n")

# 5.2 移除所有图例
p_combine2 <- (plot1 + plot2) & NoLegend()
ggsave(file.path(viz_dir, "19_combine_nolegend.png"), plot = p_combine2, width = 14, height = 6, dpi = 150)
cat("已保存: 19_combine_nolegend.png\n")


# ============================================================================
# 6. 聚类标签
# ============================================================================

cat("\n========== 6. 聚类标签 ==========\n")

# 6.1 标注聚类
p_label1 <- DimPlot(pbmc, reduction = "pca") + NoLegend()
p_label1 <- LabelClusters(plot = p_label1, id = "ident")
ggsave(file.path(viz_dir, "20_label_clusters.png"), plot = p_label1, width = 8, height = 7, dpi = 150)
cat("已保存: 20_label_clusters.png\n")


# ============================================================================
# 总结
# ============================================================================

cat("\n")
cat("=======================================================\n")
cat("  阶段2-A 可视化教程完成!\n")
cat("=======================================================\n")
cat("  输出目录:", viz_dir, "\n")
cat("  图片数量: 20 张\n")
cat("  覆盖内容:\n")
cat("    01-05: 五种核心可视化方法\n")
cat("    06-10: FeaturePlot 自定义技巧\n")
cat("    11-14: 分组分割与热图\n")
cat("    15-17: 主题定制\n")
cat("    18-19: 图组合并\n")
cat("    20:    聚类标签\n")
cat("=======================================================\n")

B. SCTransform 脚本(sctransform_vignette.R)

###############################################################################
# Seurat v5 阶段2-B:SCTransform 归一化分析
# 基于官方教程: https://satijalab.org/seurat/articles/sctransform_vignette
# SCTransform 替代 NormalizeData + ScaleData + FindVariableFeatures 三步
# 使用 PBMC 3k 数据从头创建新对象(独立于阶段1的标准流程)
###############################################################################

library(Seurat)
library(ggplot2)
library(sctransform)
library(patchwork)

# === 路径配置 ===
data_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/data/filtered_gene_bc_matrices/hg19"
output_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output"
sct_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output/sctransform"
dir.create(sct_dir, showWarnings = FALSE, recursive = TRUE)

# ============================================================================
# 1. 加载数据并创建 Seurat 对象
# ============================================================================
cat("\n========== 1. 加载数据 ==========\n")

pbmc_data <- Read10X(data.dir = data_dir)
pbmc <- CreateSeuratObject(counts = pbmc_data)
cat("已创建 Seurat 对象\n")
print(pbmc)

# ============================================================================
# 2. 计算线粒体百分比并应用 SCTransform
# ============================================================================
cat("\n========== 2. SCTransform 归一化 ==========\n")

# 计算线粒体基因百分比
pbmc <- PercentageFeatureSet(pbmc, pattern = "^MT-", col.name = "percent.mt")
cat("已计算线粒体百分比\n")

# QC 过滤(与阶段1一致)
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
cat("QC 过滤后细胞数:\n")
cat(ncol(pbmc), "\n")

# 应用 SCTransform(同时回归线粒体百分比)
# SCTransform 一步完成:归一化 + 变量特征选择 + 标准化
cat("正在运行 SCTransform(可能需要1-2分钟)...\n")
pbmc <- SCTransform(pbmc, vars.to.regress = "percent.mt", verbose = TRUE)
cat("SCTransform 完成!\n")
cat("当前默认 assay:\n")
print(DefaultAssay(pbmc))

# ============================================================================
# 3. 降维分析:PCA + UMAP
# ============================================================================
cat("\n========== 3. 降维分析 ==========\n")

# PCA(使用 SCT residuals)
pbmc <- RunPCA(pbmc, verbose = FALSE)
cat("PCA 完成\n")

# UMAP(使用前30个PC,SCTransform 推荐使用更多PC)
pbmc <- RunUMAP(pbmc, dims = 1:30, verbose = FALSE)
cat("UMAP 完成\n")

# 3.1 PCA 可视化
p_pca <- DimPlot(pbmc, reduction = "pca")
ggsave(file.path(sct_dir, "01_pca.png"), plot = p_pca, width = 8, height = 7, dpi = 150)
cat("已保存: 01_pca.png\n")

# 3.2 UMAP 可视化
p_umap <- DimPlot(pbmc, reduction = "umap")
ggsave(file.path(sct_dir, "02_umap.png"), plot = p_umap, width = 8, height = 7, dpi = 150)
cat("已保存: 02_umap.png\n")

# 3.3 PCA Elbow Plot(选择PC数量参考)
p_elbow <- ElbowPlot(pbmc, ndims = 50)
ggsave(file.path(sct_dir, "03_elbow_plot.png"), plot = p_elbow, width = 8, height = 5, dpi = 150)
cat("已保存: 03_elbow_plot.png\n")

# ============================================================================
# 4. 聚类分析
# ============================================================================
cat("\n========== 4. 聚类分析 ==========\n")

pbmc <- FindNeighbors(pbmc, dims = 1:30, verbose = FALSE)
pbmc <- FindClusters(pbmc, verbose = FALSE)
cat("聚类完成\n")

# 4.1 带标签的 UMAP
p_umap_label <- DimPlot(pbmc, reduction = "umap", label = TRUE)
ggsave(file.path(sct_dir, "04_umap_labeled.png"), plot = p_umap_label, width = 8, height = 7, dpi = 150)
cat("已保存: 04_umap_labeled.png\n")

# ============================================================================
# 5. SCTransform 优势展示:更精细的细胞亚群
# ============================================================================
cat("\n========== 5. SCTransform 优势展示 ==========\n")

# 5.1 T 细胞亚群标志基因小提琴图
# SCTransform 能更清晰地区分 CD8 T 细胞亚群(naive/memory/effector)
p_vln_t <- VlnPlot(pbmc, features = c("CD8A", "GZMK", "CCL5", "S100A4", "ANXA1", "CCR7", "ISG15", "CD3D"),
                   pt.size = 0.2, ncol = 4)
ggsave(file.path(sct_dir, "05_violin_tcell_markers.png"), plot = p_vln_t, width = 16, height = 10, dpi = 150)
cat("已保存: 05_violin_tcell_markers.png\n")

# 5.2 T 细胞标志基因 FeaturePlot
p_fp_t <- FeaturePlot(pbmc, features = c("CD8A", "GZMK", "CCL5", "S100A4", "ANXA1", "CCR7"),
                       pt.size = 0.2, ncol = 3)
ggsave(file.path(sct_dir, "06_featureplot_tcell.png"), plot = p_fp_t, width = 14, height = 10, dpi = 150)
cat("已保存: 06_featureplot_tcell.png\n")

# 5.3 B 细胞与 NK 细胞标志基因 FeaturePlot
p_fp_bnk <- FeaturePlot(pbmc, features = c("CD3D", "ISG15", "TCL1A", "FCER2", "XCL1", "FCGR3A"),
                         pt.size = 0.2, ncol = 3)
ggsave(file.path(sct_dir, "07_featureplot_bnk.png"), plot = p_fp_bnk, width = 14, height = 10, dpi = 150)
cat("已保存: 07_featureplot_bnk.png\n")

# ============================================================================
# 6. SCTransform 与标准归一化对比
# ============================================================================
cat("\n========== 6. 对比分析 ==========\n")

# 6.1 SCTransform 变量特征数量(默认3000个,比标准流程的2000更多)
sct_features <- VariableFeatures(pbmc)
cat("SCTransform 变量特征数量:", length(sct_features), "\n")

# 6.2 展示前20个变量特征
p_var <- LabelPoints(plot = VariableFeaturePlot(pbmc), points = sct_features[1:20], repel = TRUE)
ggsave(file.path(sct_dir, "08_variable_features.png"), plot = p_var, width = 10, height = 8, dpi = 150)
cat("已保存: 08_variable_features.png\n")

# 6.3 SCT assay 结构说明
cat("\nSCT Assay 层级结构:\n")
print(pbmc[["SCT"]])

# 6.4 标准流程对比图:使用前10个PC(标准流程推荐)vs 前30个PC(SCT推荐)
p_umap_10 <- DimPlot(pbmc, reduction = "umap", group.by = "seurat_clusters") +
    ggtitle("SCT: 30 PCs (推荐)")
ggsave(file.path(sct_dir, "09_umap_sct_30pc.png"), plot = p_umap_10, width = 8, height = 7, dpi = 150)
cat("已保存: 09_umap_sct_30pc.png\n")

# ============================================================================
# 7. 保存 SCTransform 结果
# ============================================================================
cat("\n========== 7. 保存结果 ==========\n")

saveRDS(pbmc, file.path(output_dir, "pbmc3k_sct.rds"))
cat("已保存 SCTransform 对象: pbmc3k_sct.rds\n")

# ============================================================================
# 总结
# ============================================================================
cat("\n")
cat("=======================================================\n")
cat("  阶段2-B SCTransform 教程完成!\n")
cat("=======================================================\n")
cat("  输出目录:", sct_dir, "\n")
cat("  图片数量: 9 张\n")
cat("  覆盖内容:\n")
cat("    01-03: PCA + UMAP + Elbow Plot\n")
cat("    04:    聚类标注\n")
cat("    05-07: T/B/NK 细胞亚群标志基因可视化\n")
cat("    08:    SCTransform 变量特征\n")
cat("    09:    SCT UMAP(30 PCs)\n")
cat("  SCT vs 标准流程:\n")
cat("    - 变量特征: 3000(SCT) vs 2000(标准)\n")
cat("    - 推荐PC数: 30(SCT) vs 10(标准)\n")
cat("    - 归一化方法: 负二项回归 vs LogNormalize\n")
cat("    - 能揭示更精细的细胞亚群(CD8 naive/memory/effector)\n")
cat("=======================================================\n")

输出文件清单

Part A: 可视化教程(20张图)

编号 文件名 内容
01 01_ridge_plot.png 山脊图
02 02_violin_plot.png 小提琴图
03 03_feature_plot.png Feature Plot
04 04_dot_plot.png 气泡图
05 05_heatmap.png 热图
06 06_featureplot_legend.png 带图例 FeaturePlot
07 07_featureplot_contrast.png 调整对比度
08 08_featureplot_quantile.png 分位数 cutoff
09 09_featureplot_blend.png 混合模式
10 10_featureplot_split.png 分组分割
11 11_violin_split.png 分组小提琴图
12 12_dotplot_split.png 分组气泡图
13 13_dimplot_umap.png UMAP
14 14_heatmap_grouped.png 分组热图
15 15_theme_title.png 添加标题
16 16_theme_dark.png 暗色主题
17 17_theme_chained.png 链式主题
18 18_combine_plots.png 图组合并
19 19_combine_nolegend.png 移除图例
20 20_label_clusters.png 聚类标签

Part B: SCTransform(9张图)

编号 文件名 内容
01 01_pca.png PCA
02 02_umap.png UMAP
03 03_elbow_plot.png Elbow Plot
04 04_umap_labeled.png 聚类标注 UMAP
05 05_violin_tcell_markers.png T 细胞标志小提琴图
06 06_featureplot_tcell.png T 细胞 FeaturePlot
07 07_featureplot_bnk.png B/NK 细胞 FeaturePlot
08 08_variable_features.png 变量特征图
09 09_umap_sct_30pc.png SCT UMAP(30 PCs)

本教程基于 Seurat v5.5.1 官方文档整理
R 4.6.1 + Seurat 5.5.1 + Windows 环境
阶段2-A: 数据可视化方法 | 阶段2-B: SCTransform 归一化

20260912 091245 Seurat V5 Pbmc 3K 单细胞分析完整教程

Seurat v5 PBMC 3k 单细胞分析完整教程

基于 Seurat v5.5.1 官方教程,从零开始跑通单细胞 RNA-seq 标准分析全流程。


1. 概述

1.1 数据集

项目 说明
数据来源 10X Genomics PBMC(外周血单核细胞)
细胞数 2,700
测序平台 Illumina NextSeq 500
参考基因组 hg19
数据格式 10X CellRanger 输出(barcodes.tsv, genes.tsv, matrix.mtx)

1.2 分析流程

读取数据 -> QC过滤 -> 归一化 -> 高变基因 -> 标准化 -> PCA
-> 确定维度 -> 聚类 -> UMAP -> 寻找Marker -> 细胞类型注释

1.3 环境信息

软件 版本
R 4.6.1 (2026-06-24, “Happy Hop”)
Seurat 5.5.1
SeuratObject 5.4.0
操作系统 Windows (D盘安装: D:\r)

2. 环境准备

2.1 安装 R 和 RStudio

R 和 RStudio 安装在 D:\r 目录下:

  • R 4.6.1: D:\r\R-4.6.1
  • RStudio 2026.09.0: D:\r\RStudio

2.2 配置 CRAN 国内镜像

编辑 D:\r\R-4.6.1\etc\Rprofile.site,添加:

# set a CRAN mirror (TUNA Tsinghua)
local({r <- getOption("repos")
      r["CRAN"] <- "https://mirrors.tuna.tsinghua.edu.cn/CRAN"
      options(repos=r)})

2.3 安装 Seurat

install.packages("Seurat")

从清华镜像下载,100+ 依赖包全部自动安装,无报错。


3. 完整分析流程

3.1 读取数据 & 创建 Seurat 对象

library(dplyr)
library(Seurat)
library(patchwork)
library(ggplot2)

# 设置路径
data_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/data/filtered_gene_bc_matrices/hg19"
output_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)

# 读取 10X 数据
pbmc.data <- Read10X(data.dir = data_dir)

# 创建 Seurat 对象
# min.cells = 3: 至少在3个细胞中检测到的基因才保留
# min.features = 200: 每个细胞至少检测到200个基因才保留
pbmc <- CreateSeuratObject(counts = pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)

结果:13,714 个基因,2,700 个细胞,1 个 Assay(RNA)。

稀疏矩阵示例(. 代表 0,节省内存约 24 倍):

        AAACATACAACCAC-1 AAACATTGAGCTAC-1 AAACATTGATCAGC-1 ...
CD3D    4                .                10               ...
TCL1A   .                .                .                ...
MS4A1   .                6                .                ...

3.2 质量控制(QC)& 细胞过滤

# 计算线粒体基因表达比例
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")

# 过滤条件:
#   nFeature_RNA > 200   (去除空液滴)
#   nFeature_RNA < 2500  (去除 doublet)
#   percent.mt < 5       (去除线粒体污染严重的细胞)
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)

过滤后:2,700 到 2,638 个细胞(去除 62 个低质量细胞)。

QC 小提琴图 – 展示 nFeature_RNA、nCount_RNA、percent.mt 的分布:

QC小提琴图

QC 散点图 – 展示 nCount_RNA 与 percent.mt / nFeature_RNA 的关系:

QC散点图

3.3 数据归一化

# LogNormalize: 每个细胞表达量除以总UMI,乘10000,再取log
pbmc <- NormalizeData(pbmc, normalization.method = "LogNormalize", scale.factor = 10000)

3.4 识别高变基因

# 默认选择 2000 个高变基因,用于下游 PCA
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)

Top 10 高变基因:PPBP, LYZ, S100A9, IGLL5, GNLY, FTL, PF4, FTH1, GNG11, S100A8

高变基因图

3.5 数据标准化

# 对每个基因做 Z-score 标准化(均值=0, 方差=1)
all.genes <- rownames(pbmc)
pbmc <- ScaleData(pbmc, features = all.genes)

3.6 线性降维(PCA)

# 对高变基因做 PCA
pbmc <- RunPCA(pbmc, features = VariableFeatures(object = pbmc))

前5个主成分的标记基因:

PC Positive(正加载) Negative(负加载)
PC1 CST3, TYROBP, LST1, AIF1, FTL MALAT1, LTB, IL32, IL7R, CD2
PC2 CD79A, MS4A1, TCL1A, HLA-DQA1, HLA-DQB1 NKG7, PRF1, CST7, GZMB, GZMA
PC3 HLA-DQA1, CD79A, CD79B, HLA-DQB1, HLA-DPB1 PPBP, PF4, SDPR, SPARC, GNG11
PC4 HLA-DQA1, CD79B, CD79A, MS4A1, HLA-DQB1 VIM, IL7R, S100A6, IL32, S100A8
PC5 GZMB, NKG7, S100A8, FGFBP2, GNLY LTB, IL7R, CKB, VIM, MS4A7

PCA散点图

PCA负载图

PCA热图

3.7 确定数据维度

# ElbowPlot: 按方差解释比例排序各 PC,拐点处即为建议维度
ElbowPlot(pbmc)

拐点约在 PC 9-10,本教程选取 10 个 PC 用于下游分析。

Elbow拐点图

3.8 细胞聚类

# 基于 PCA 空间构建 KNN 图
pbmc <- FindNeighbors(pbmc, dims = 1:10)

# Louvain 算法聚类,resolution=0.5 适合约3K细胞
pbmc <- FindClusters(pbmc, resolution = 0.5)

聚类结果:9 个聚类,模块化度 0.8728

Number of nodes: 2638
Number of edges: 95927
Number of communities: 9

3.9 非线性降维(UMAP)

pbmc <- RunUMAP(pbmc, dims = 1:10)
DimPlot(pbmc, reduction = "umap")

UMAP聚类图

3.10 寻找差异表达基因(Marker Genes)

# 为每个聚类寻找特异性标记基因
pbmc.markers <- FindAllMarkers(pbmc, only.pos = TRUE)

各聚类 Marker 数量:

聚类 Marker 数(log2FC > 1)
0 214
1 1,190
2 384
3 543
4 344
5 1,241
6 891
7 1,589
8 623

Marker 热图 – 每个聚类 top 10 基因:

Marker热图

FeaturePlot – 经典 marker 基因在 UMAP 上的表达:

FeaturePlot

小提琴图 – 特定 marker 的表达分布:

小提琴图

3.11 细胞类型注释

根据 canonical markers 将聚类映射到已知细胞类型:

聚类 Marker 基因 细胞类型
0 IL7R, CCR7 Naive CD4+ T
1 CD14, LYZ CD14+ Mono(单核细胞)
2 IL7R, S100A4 Memory CD4+ T
3 MS4A1 B(B细胞)
4 CD8A CD8+ T
5 FCGR3A, MS4A7 FCGR3A+ Mono
6 GNLY, NKG7 NK(自然杀伤细胞)
7 FCER1A, CST3 DC(树突状细胞)
8 PPBP Platelet(血小板)
new.cluster.ids <- c("Naive CD4 T", "CD14+ Mono", "Memory CD4 T", "B",
                      "CD8 T", "FCGR3A+ Mono", "NK", "DC", "Platelet")
names(new.cluster.ids) <- levels(pbmc)
pbmc <- RenameIdents(pbmc, new.cluster.ids)
DimPlot(pbmc, reduction = "umap", label = TRUE, pt.size = 0.5) + NoLegend()

注释后UMAP图


4. 分析总结

指标 数值
输入细胞数 2,700
过滤后细胞数 2,638
基因数 13,714
高变基因数 2,000
PCA 维度 10
聚类数 9
细胞类型数 9
Marker 基因总数 7,019(log2FC > 1)

5. 输出文件清单

文件 说明
01_qc_violin.png QC 小提琴图
02_qc_scatter.png QC 散点图
03_variable_features.png 高变基因图
04_pca.png PCA 散点图
05_pca_loadings.png PCA 负载图
06_pca_heatmap.png PCA 热图
07_elbow_plot.png Elbow 拐点图
08_umap.png UMAP 聚类图
09_marker_heatmap.png Marker 热图
10_feature_plot.png Feature 表达图
11_violin_markers.png Marker 小提琴图
12_umap_labeled.png 注释后 UMAP 图
all_markers.csv 全部 Marker 基因表
pbmc_tutorial.rds 中间结果(Seurat 对象)
pbmc3k_final.rds 最终结果(Seurat 对象)

6. 关键知识点回顾

6.1 Seurat 对象结构(v5)

Seurat 对象
|-- assays
|   +-- RNA
|       |-- layers
|       |   |-- counts    # 原始 UMI 计数矩阵
|       |   |-- data       # 归一化后的矩阵
|       |   +-- scale.data # 标准化后的矩阵
|       +-- VariableFeatures  # 高变基因列表
|-- reductions
|   |-- pca               # PCA 降维结果
|   +-- umap              # UMAP 降维结果
|-- meta.data             # 细胞元数据(nCount, nFeature, percent.mt 等)
+-- active.ident          # 当前聚类标签

6.2 标准分析步骤要点

步骤 核心函数 关键参数 目的
读取数据 Read10X() data.dir 读取 10X 输出
创建对象 CreateSeuratObject() min.cells, min.features 初始过滤
QC PercentageFeatureSet() pattern=”^MT-“ 计算线粒体比例
归一化 NormalizeData() LogNormalize, scale.factor=10000 消除细胞间测序深度差异
高变基因 FindVariableFeatures() vst, nfeatures=2000 识别信息量最大的基因
标准化 ScaleData() features=all.genes Z-score 标准化
降维 RunPCA() features=VariableFeatures 线性降维
确定维度 ElbowPlot() - 选择 PC 数量
聚类 FindNeighbors() + FindClusters() dims, resolution 图聚类
可视化 RunUMAP() + DimPlot() dims 2D 可视化
Marker FindAllMarkers() only.pos=TRUE 差异基因分析
注释 RenameIdents() - 映射细胞类型

6.3 常用参数调优建议

  • resolution:0.4-1.2 适合约3K细胞;更大数据集可适当提高
  • PC 数量:建议 7-15 之间,宁多勿少(太少会丢失信号)
  • percent.mt 阈值:常用 5%-10%,不同组织类型可能需要调整
  • nFeature_RNA 过滤:下限 200-500,上限通常取 2500-6000

7. 完整脚本

以下是完整的 R 分析脚本,可直接运行:

###############################################################################
# Seurat v5 PBMC 3k 标准分析流程
# 基于官方教程: https://satijalab.org/seurat/articles/pbmc3k_tutorial
# 数据: 10X Genomics PBMC 2,700 个外周血单核细胞
###############################################################################
#
# 学习目标: 跑通单细胞 RNA-seq 标准分析全流程
# 流程链路: 读取数据 -> QC过滤 -> 归一化 -> 高变基因 -> 标准化 -> PCA
#           -> 确定维度 -> 聚类 -> UMAP -> 寻找marker -> 细胞类型注释
#
###############################################################################


# ============================================================================
# 0. 环境准备
# ============================================================================

library(dplyr)
library(Seurat)
library(patchwork)
library(ggplot2)

# 设置工作目录(请根据实际路径修改)
data_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/data/filtered_gene_bc_matrices/hg19"
output_dir <- "E:/BaiduNetdiskDownload/生物信息学/pbmc3k_tutorial/output"
dir.create(output_dir, showWarnings = FALSE, recursive = TRUE)


# ============================================================================
# 1. 读取数据 & 创建 Seurat 对象
# ============================================================================
# Read10X() 读取 10X CellRanger 输出目录(包含 barcodes.tsv, genes.tsv, matrix.mtx)
# 返回一个稀疏矩阵: 行=基因, 列=细胞, 值=UMI计数

cat("\n========== 1. 读取数据 ==========\n")
pbmc.data <- Read10X(data.dir = data_dir)

# 创建 Seurat 对象
# min.cells = 3:   至少在 3 个细胞中检测到的基因才保留
# min.features = 200: 每个细胞至少检测到 200 个基因才保留
pbmc <- CreateSeuratObject(counts = pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
print(pbmc)

# 查看计数矩阵的稀疏性(大多数值为 0,用稀疏矩阵存储节省内存)
cat("\n--- 稀疏矩阵示例(前30个细胞中3个基因)---\n")
print(pbmc.data[c("CD3D", "TCL1A", "MS4A1"), 1:30])


# ============================================================================
# 2. 质量控制(QC)& 细胞过滤
# ============================================================================
# 常用 QC 指标:
#   - nFeature_RNA: 每个细胞检测到的独特基因数(过低=空液滴/低质量,过高=doublet)
#   - nCount_RNA:   每个细胞检测到的分子总数(UMI总数)
#   - percent.mt:   线粒体基因占比(过高=低质量/凋亡细胞)

cat("\n========== 2. 质量控制 ==========\n")

# 计算线粒体基因表达比例(基因名以 MT- 开头)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")

# 查看 QC 统计
cat("\n--- QC 指标(前5个细胞)---\n")
print(head(pbmc@meta.data, 5))

# 可视化 QC 指标(小提琴图)
p_qc_vln <- VlnPlot(pbmc, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"), ncol = 3)
ggsave(file.path(output_dir, "01_qc_violin.png"), plot = p_qc_vln, width = 12, height = 5, dpi = 150)
cat("已保存: 01_qc_violin.png\n")

# FeatureScatter: 查看指标间关系
p_scatter1 <- FeatureScatter(pbmc, feature1 = "nCount_RNA", feature2 = "percent.mt")
p_scatter2 <- FeatureScatter(pbmc, feature1 = "nCount_RNA", feature2 = "nFeature_RNA")
p_qc_scatter <- p_scatter1 + p_scatter2
ggsave(file.path(output_dir, "02_qc_scatter.png"), plot = p_qc_scatter, width = 12, height = 5, dpi = 150)
cat("已保存: 02_qc_scatter.png\n")

# 过滤细胞:
#   nFeature_RNA > 200  (已有 min.features=200 过滤,这里二次确认)
#   nFeature_RNA < 2500 (去除 doublet)
#   percent.mt < 5      (去除线粒体污染严重的细胞)
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
cat("过滤后细胞数:", ncol(pbmc), "\n")


# ============================================================================
# 3. 数据归一化(Normalization)
# ============================================================================
# LogNormalize: 每个细胞的表达量除以该细胞总UMI,乘以10000,再取log
# 结果存储在 pbmc[["RNA"]]$data

cat("\n========== 3. 归一化 ==========\n")
pbmc <- NormalizeData(pbmc, normalization.method = "LogNormalize", scale.factor = 10000)
cat("归一化完成\n")


# ============================================================================
# 4. 识别高变基因(Feature Selection)
# ============================================================================
# FindVariableFeatures: 识别在不同细胞间表达变异最大的基因
# 默认选择 2000 个高变基因,用于下游 PCA 降维

cat("\n========== 4. 高变基因识别 ==========\n")
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)

# 查看前10个高变基因
top10 <- head(VariableFeatures(pbmc), 10)
cat("Top 10 高变基因:", paste(top10, collapse = ", "), "\n")

# 可视化高变基因
p_var <- VariableFeaturePlot(pbmc)
p_var_label <- LabelPoints(plot = p_var, points = top10, repel = TRUE)
ggsave(file.path(output_dir, "03_variable_features.png"), plot = p_var + p_var_label, width = 10, height = 7, dpi = 150)
cat("已保存: 03_variable_features.png\n")


# ============================================================================
# 5. 数据标准化(Scaling)
# ============================================================================
# ScaleData: 对每个基因做 Z-score 标准化(均值=0, 方差=1)
# 使下游 PCA 分析中各基因权重一致
# 结果存储在 pbmc[["RNA"]]$scale.data

cat("\n========== 5. 标准化 ==========\n")
all.genes <- rownames(pbmc)
pbmc <- ScaleData(pbmc, features = all.genes)
cat("标准化完成(", length(all.genes), "个基因)\n")


# ============================================================================
# 6. 线性降维(PCA)
# ============================================================================
# 对标准化后的高变基因做 PCA
# 每个 PC 代表一组相关基因的「元特征」

cat("\n========== 6. PCA 降维 ==========\n")
pbmc <- RunPCA(pbmc, features = VariableFeatures(object = pbmc))

# 查看前5个 PC 的 top 基因
cat("\n--- 前5个主成分的标记基因---\n")
print(pbmc[["pca"]], dims = 1:5, nfeatures = 5)

# 可视化 PCA
p_pca <- DimPlot(pbmc, reduction = "pca") + NoLegend()
ggsave(file.path(output_dir, "04_pca.png"), plot = p_pca, width = 7, height = 6, dpi = 150)
cat("已保存: 04_pca.png\n")

# PCA 负载图(前2个 PC)
p_pca_load <- VizDimLoadings(pbmc, dims = 1:2, reduction = "pca")
ggsave(file.path(output_dir, "05_pca_loadings.png"), plot = p_pca_load, width = 10, height = 8, dpi = 150)
cat("已保存: 05_pca_loadings.png\n")

# PCA 热图(前15个 PC)
p_pca_heat <- DimHeatmap(pbmc, dims = 1:15, cells = 500, balanced = TRUE)
ggsave(file.path(output_dir, "06_pca_heatmap.png"), plot = p_pca_heat, width = 12, height = 15, dpi = 150)
cat("已保存: 06_pca_heatmap.png\n")


# ============================================================================
# 7. 确定数据维度
# ============================================================================
# 选择用于下游分析的主成分数量
# ElbowPlot: 按方差解释比例排序各 PC,拐点处即为建议维度

cat("\n========== 7. 确定维度 ==========\n")
p_elbow <- ElbowPlot(pbmc)
ggsave(file.path(output_dir, "07_elbow_plot.png"), plot = p_elbow, width = 7, height = 5, dpi = 150)
cat("已保存: 07_elbow_plot.png\n")
cat("拐点约在 PC 9-10,本教程选取 10 个 PC\n")


# ============================================================================
# 8. 细胞聚类(Clustering)
# ============================================================================
# FindNeighbors: 基于 PCA 空间构建 KNN 图(K近邻图)
# FindClusters:  使用 Louvain 算法做模块化优化聚类
#   resolution: 聚类粒度,0.4-1.2 适合约 3K 细胞的数据集

cat("\n========== 8. 聚类 ==========\n")
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)

# 查看聚类结果
cat("\n--- 前5个细胞的聚类 ID ---\n")
print(head(Idents(pbmc), 5))
cat("聚类数量:", length(levels(pbmc)), "\n")


# ============================================================================
# 9. 非线性降维(UMAP)
# ============================================================================
# UMAP: 将高维数据投射到 2D 空间用于可视化
# 注意: UMAP 用于可视化探索,不应仅凭图形下生物学结论

cat("\n========== 9. UMAP ==========\n")
pbmc <- RunUMAP(pbmc, dims = 1:10)

p_umap <- DimPlot(pbmc, reduction = "umap")
ggsave(file.path(output_dir, "08_umap.png"), plot = p_umap, width = 8, height = 7, dpi = 150)
cat("已保存: 08_umap.png\n")

# 保存中间结果(可重新加载,无需重跑前面的步骤)
saveRDS(pbmc, file.path(output_dir, "pbmc_tutorial.rds"))
cat("已保存中间结果: pbmc_tutorial.rds\n")


# ============================================================================
# 10. 寻找差异表达基因(Marker Genes)
# ============================================================================
# FindAllMarkers: 为每个聚类寻找特异性标记基因(与其他所有细胞比较)
# presto 包加速 DE 分析(Seurat v5 自动调用)

cat("\n========== 10. Marker 基因 ==========\n")
pbmc.markers <- FindAllMarkers(pbmc, only.pos = TRUE)

# 筛选 log2FC > 1 的显著 marker
pbmc.markers %>%
    group_by(cluster) %>%
    dplyr::filter(avg_log2FC > 1) -> top_markers

cat("\n--- 各聚类 marker 数量 ---\n")
print(table(top_markers$cluster))

# 保存 marker 表
write.csv(pbmc.markers, file.path(output_dir, "all_markers.csv"), row.names = FALSE)
cat("已保存: all_markers.csv\n")

# 每个聚类取 top 10 基因做热图
top10_all <- pbmc.markers %>%
    group_by(cluster) %>%
    dplyr::filter(avg_log2FC > 1) %>%
    slice_head(n = 10) %>%
    ungroup()

p_heatmap <- DoHeatmap(pbmc, features = top10_all$gene) + NoLegend()
ggsave(file.path(output_dir, "09_marker_heatmap.png"), plot = p_heatmap, width = 12, height = 15, dpi = 150)
cat("已保存: 09_marker_heatmap.png\n")

# FeaturePlot: 在 UMAP 上展示经典 marker 基因表达
p_feature <- FeaturePlot(pbmc, features = c("MS4A1", "GNLY", "CD3E", "CD14",
                                              "FCER1A", "FCGR3A", "LYZ", "PPBP", "CD8A"))
ggsave(file.path(output_dir, "10_feature_plot.png"), plot = p_feature, width = 14, height = 12, dpi = 150)
cat("已保存: 10_feature_plot.png\n")

# VlnPlot: 小提琴图展示特定 marker
p_vln <- VlnPlot(pbmc, features = c("MS4A1", "CD79A"))
ggsave(file.path(output_dir, "11_violin_markers.png"), plot = p_vln, width = 8, height = 5, dpi = 150)
cat("已保存: 11_violin_markers.png\n")


# ============================================================================
# 11. 细胞类型注释
# ============================================================================
# 根据已知 canonical markers 将聚类映射到细胞类型
#
# 聚类  Marker 基因          细胞类型
# 0    IL7R, CCR7           Naive CD4+ T
# 1    CD14, LYZ            CD14+ Mono(单核细胞)
# 2    IL7R, S100A4         Memory CD4+ T
# 3    MS4A1                 B(B细胞)
# 4    CD8A                  CD8+ T
# 5    FCGR3A, MS4A7         FCGR3A+ Mono
# 6    GNLY, NKG7            NK(自然杀伤细胞)
# 7    FCER1A, CST3          DC(树突状细胞)
# 8    PPBP                  Platelet(血小板)

cat("\n========== 11. 细胞类型注释 ==========\n")

new.cluster.ids <- c("Naive CD4 T", "CD14+ Mono", "Memory CD4 T", "B",
                      "CD8 T", "FCGR3A+ Mono", "NK", "DC", "Platelet")
names(new.cluster.ids) <- levels(pbmc)
pbmc <- RenameIdents(pbmc, new.cluster.ids)

# 带标签的 UMAP 图
p_umap_labeled <- DimPlot(pbmc, reduction = "umap", label = TRUE, pt.size = 0.5) + NoLegend()
ggsave(file.path(output_dir, "12_umap_labeled.png"), plot = p_umap_labeled, width = 8, height = 7, dpi = 150)
cat("已保存: 12_umap_labeled.png\n")

# 保存最终结果
saveRDS(pbmc, file.path(output_dir, "pbmc3k_final.rds"))
cat("已保存最终结果: pbmc3k_final.rds\n")


# ============================================================================
# 12. 总结
# ============================================================================

cat("\n")
cat("=======================================================\n")
cat("  PBMC 3k 标准分析流程完成!\n")
cat("=======================================================\n")
cat("  输入细胞数: 2,700\n")
cat("  过滤后细胞数:", ncol(pbmc), "\n")
cat("  聚类数:", length(levels(pbmc)), "\n")
cat("  细胞类型: Naive CD4 T / CD14+ Mono / Memory CD4 T / B /\n")
cat("           CD8 T / FCGR3A+ Mono / NK / DC / Platelet\n")
cat("  输出目录:", output_dir, "\n")
cat("  输出文件:\n")
cat("    01_qc_violin.png       - QC小提琴图\n")
cat("    02_qc_scatter.png      - QC散点图\n")
cat("    03_variable_features.png - 高变基因图\n")
cat("    04_pca.png             - PCA散点图\n")
cat("    05_pca_loadings.png    - PCA负载图\n")
cat("    06_pca_heatmap.png     - PCA热图\n")
cat("    07_elbow_plot.png      - 拐点图\n")
cat("    08_umap.png            - UMAP聚类图\n")
cat("    09_marker_heatmap.png  - Marker热图\n")
cat("    10_feature_plot.png    - Feature图\n")
cat("    11_violin_markers.png  - Marker小提琴图\n")
cat("    12_umap_labeled.png    - 注释后UMAP图\n")
cat("    all_markers.csv        - 全部Marker表\n")
cat("    pbmc_tutorial.rds      - 中间结果\n")
cat("    pbmc3k_final.rds       - 最终结果\n")
cat("=======================================================\n")

8. 下一步学习

阶段 教程 技能目标
阶段2 Visualization Vignette 掌握专业级图表制作
阶段2 SCTransform 替代传统归一化的先进方法
阶段3 Integration / Mapping 跨批次整合 + 自动注释
阶段4 WNN / ATAC / Mixscape 多模态与扰动分析
阶段5 Sketch / BPCells 百万级细胞分析
阶段6 Visium HD / 空间组学 空间转录组全链路

本教程基于 Seurat v5.5.1 官方文档整理,数据来自 10X Genomics 公开数据集。
R 4.6.1 + Seurat 5.5.1 + Windows 环境

20260910 144229 蛋白质结构预测与虚拟筛选全流程实现详解

蛋白质结构预测与虚拟筛选全流程实现详解

AI生成 本文档由星辰超级智能体(TeleAgent)基于 bioSkills 技能库自动生成,内容来源于以下技能的 SKILL.md 文件,经组织编排为端到端工作流教程。


场景假设

你是一名药物化学研究员,收到一个新靶点蛋白——人源 BRD4 溴区结构域(BRD4-BD1, UniProt: O60885),需要:

  1. 获取该蛋白的三维结构(优先使用实验结构,无则用 AlphaFold 预测)
  2. 评估结构质量,判断哪些区域可信
  3. 制备受体结构(加氢、质子化、修补缺失原子)
  4. 检测结合口袋(如果是 apo 结构)
  5. 准备配体库(从 SMILES 生成 3D 构象)
  6. 进行层次化虚拟筛选(Vina → GNINA 重打分)
  7. 对 top hits 进行 PoseBusters 物理合理性验证
  8. ADMET 性质预测与类药性筛选
  9. 对最终候选化合物进行自由能计算(FEP)精排序

全流程总览

靶点蛋白序列/PDB ID
        │
        ▼
┌──────────────────────────┐
│ Step 1: 结构获取           │  技能: bio-structural-biology-alphafold-predictions
│ · PDB 实验结构 → 直接使用  │
│ · 无实验结构 → AlphaFold DB│
│ · 需要复合物 → AF3/Boltz   │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 2: 结构质量评估       │  技能: bio-structural-biology-alphafold-predictions
│ · pLDDT 分段读取           │
│ · PAE 域间置信度           │
│ · 决策:AFDB vs 新预测     │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 3: 受体结构制备       │  技能: bio-structural-biology-structure-preparation
│ · PDBFixer 补缺失原子     │
│ · PROPKA 预测 pKa         │
│ · reduce 优化翻转/互变异构  │
│ · PDB2PQR 生成 PDBQT      │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 4: 结合口袋检测       │  技能: bio-structural-biology-binding-site-detection
│ · fpocket 几何枚举         │
│ · P2Rank ML 配体性评分     │
│ · 交叉验证,确定 docking 框 │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 5: 配体库制备         │  技能: bio-virtual-screening (ligand prep)
│ · RDKit SMILES → 3D       │
│ · ETKDGv3 构象嵌入         │
│ · MMFF94 能量最小化        │
│ · meeko → PDBQT           │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 6: 层次化虚拟筛选     │  技能: bio-virtual-screening
│ · Stage 1: Vina 筛全库     │
│ · Stage 2: GNINA CNN 重排  │
│ · Stage 3: top-1000 候选   │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 7: Pose 物理验证      │  技能: bio-pose-validation
│ · PoseBusters 全检         │
│ · 应变能量化               │
│ · 芳香环平面度             │
│ · 过滤 PB-invalid 构象     │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 8: ADMET 预测         │  技能: bio-admet-prediction
│ · ADMETlab 3.0 批量预测    │
│ · Lipinski/Veber 类药性    │
│ · hERG/CYP/AMES 毒性       │
│ · PAINS 结构警报            │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│ Step 9: 自由能精排序       │  技能: bio-free-energy-calculations
│ · RBFE 热力学循环          │
│ · MBAR 分析                │
│ · 闭环验证                 │
│ · 或 Boltz-2 快速替代      │
└──────────────────────────┘

Step 1: 结构获取

技能:bio-structural-biology-alphafold-predictions

目标

获取 BRD4-BD1 的三维结构坐标文件。优先策略:PDB 有实验结构则直接用;无则从 AlphaFold DB 获取预测模型。

决策表:AFDB 使用 vs 重新预测

情况 用 AFDB 重新预测
单条 UniProt 单体,折叠层面问题 是 否
需要复合物/界面 否(仅单体) 是 — AF3 / AF-Multimer
需要配体/离子/辅因子/PTM 否 是 — AF3 或对接到实验结构
设计突变体不在 UniProt 中 否 是
需要特定构象状态 否(单一状态) 是(子采样 MSA)

BRD4-BD1 决策:PDB 中有多个 BRD4-BD1 实验结构(如 3MXF, 4LYI),优先使用实验 holo 结构(含已知配体)。若需 AlphaFold 模型作为补充:

代码:从 AlphaFold DB 获取模型

import requests

def afdb_metadata(accession):
    """查询 AFDB 元数据接口,返回当前下载 URL"""
    url = f'https://alphafold.ebi.ac.uk/api/prediction/{accession}'
    r = requests.get(url)
    r.raise_for_status()
    return r.json()  # list;每个片段/异构体一个对象

def fetch_afdb(accession, out_dir='.'):
    """下载坐标文件和 PAE 矩阵"""
    entries = afdb_metadata(accession)
    if not entries:
        return None  # >2700aa 非人源蛋白或非 UniProt 序列常缺失
    entry = entries[0]
    cif_text = requests.get(entry['cifUrl']).text
    pae_json = requests.get(entry['paeDocUrl']).json()
    cif_path = f"{out_dir}/AF-{accession}.cif"
    with open(cif_path, 'w') as f:
        f.write(cif_text)
    return cif_path, pae_json

# BRD4 full-length UniProt: O60885
result = fetch_afdb('O60885')

关键注意点

陷阱 原因 正确做法
版本后缀 URL 404 AFDB 版本号会变(v4→v6) 从 /api/prediction/{accession} 动态获取 URL,不要手工拼
长蛋白只返回部分残基 人源 >2700aa 的蛋白会被分段(F1, F2…) 遍历所有 metadata 条目,不同片段间的相对位置不可信
AFDB 模型无配体/离子/辅因子 AFDB 是真空中单链预测 需要 holo 结构时用实验 PDB;需要复合物时用 AF3/Boltz
404/空列表 非人源大蛋白或非 UniProt 序列 重新预测或查 ESM Metagenomic Atlas

Step 2: 结构质量评估

技能:bio-structural-biology-alphafold-predictions

目标

读取 AlphaFold 模型的 pLDDT 和 PAE,判断哪些区域可信、哪些需裁剪。

pLDDT 置信度分级

pLDDT 等级 操作含义 可信用于
> 90 Very high 骨架和侧链方向均可信 侧链细节、催化几何假设、MR 核心
70-90 Confident 骨架基本正确 折叠、域拓扑、骨架级 MR;侧链需谨慎
50-70 Low 骨架不确定 仅粗略拓扑;不可信细节
< 50 Very low 占位符;通常是固有无序区 无序信号,不是构象

代码:提取 pLDDT 并分级

from Bio.PDB import MMCIFParser

def extract_plddt(cif_file):
    """从 B-factor 列读取 pLDDT(注意:与热运动 B-factor 极性相反)"""
    parser = MMCIFParser(QUIET=True)
    structure = parser.get_structure('af', cif_file)
    plddt = {}
    for residue in structure[0].get_residues():
        if residue.id[0] == ' ' and 'CA' in residue:
            # pLDDT 存在 B-factor 列中,但是置信度(0-100,越高越好)
            # 与热运动 B-factor 极性相反——切勿当作热运动读取
            plddt[residue.id[1]] = residue['CA'].get_bfactor()
    return plddt

def plddt_band(score):
    if score > 90:
        return 'very_high'
    if score >= 70:
        return 'confident'
    if score >= 50:
        return 'low'
    return 'very_low'   # <50: 通常是无序区(IDR),不是建模错误

plddt = extract_plddt('AF-O60885.cif')
mean_plddt = sum(plddt.values()) / len(plddt)
disordered = [res for res, s in plddt.items() if s < 50]  # 候选 IDR

代码:读取 PAE 判断域间可靠性

import numpy as np

def load_pae(pae_json):
    entry = pae_json[0] if isinstance(pae_json, list) else pae_json
    # 紧凑格式(2023+): 2D num_res × num_res 数组
    # 遗留 1D 'distance'/'residue1'/'residue2' 字段已移除
    return np.array(entry['predicted_aligned_error'])

def interdomain_confidence(pae, domain_a, domain_b):
    """PAE 不对称,取两个 off-diagonal block 的平均"""
    block = np.concatenate([pae[np.ix_(domain_a, domain_b)].ravel(),
                            pae[np.ix_(domain_b, domain_a)].ravel()])
    return block.mean()  # 低(约 <5Å)= 域间相对位置可信

pae = load_pae(result[1])

核心原则

pLDDT 是局部逐残基置信度,高均值 pLDDT 不保证域间相对位置正确。 两个域各 95 pLDDT 仍可能以未知相对方向飘浮——读 PAE 才能判断域间几何。

低 pLDDT 长段通常是真实的固有无序区(IDR),不是建模失败。 裁剪掉它会丢失真实生物学信息。pLDDT 与专门的 IDR 预测器具有竞争力(Akdel 2022, Piovesan 2022)。

关键陷阱

症状 原因 修复
模型”按柔度着色”看起来反了 B-factor 列被当作热运动读 它是 pLDDT(置信度,越高越好);按 pLDDT band 着色
删掉低 pLDDT 尾巴后已知 IDR 消失 把低 pLDDT 当”错误” 低 pLDDT 通常= 无序;保留并注释为 IDR
域间距离无意义但 pLDDT 都很高 信任了拼接卡通图 pLDDT 是局部的;读 PAE——高 off-diagonal PAE = 域间无约束
对接打分很好但验证差 对接到 apo AFDB 口袋 口袋缺配体/辅因子,rotamer 不可靠;用 holo 结构或柔性对接

Step 3: 受体结构制备

技能:bio-structural-biology-structure-preparation

目标

将原始 PDB/mmCIF 转化为加氢完全、质子化正确、缺失原子补齐的 docking-ready 受体。

核心原则

沉积或预测的结构不是模拟就绪的。 每个添加的原子、每个放置的质子、每段补建的 loop 都是假设而非测量。制备条件(pH、质子化模型、补建内容)必须随文件一起记录,否则下游 H-bond、盐桥、界面和对接结果不可复现。

工具选择决策表

任务 工具 最佳场景 失败/误导场景
补缺失重原子、短内部 loop PDBFixer 截短侧链、1-3 残基缺口 长/末端缺口、域级缺失
加 pH 氢、替换非标准残基 PDBFixer addMissingHydrogens(pH) 快速一键制备 忽略微环境 pKa 偏移
优化 Asn/Gln/His 翻转和 His 互变异构 reduce -build 解决酰胺/环方向歧义 当作 pKa 预测器——它优化几何而非滴定
预测残基 pKa / pH 依赖质子化 PROPKA (propka3) 判断哪些可滴定残基偏离标准态 当作精确实验 pKa
指定状态 + 写 PQR PDB2PQR --with-ph --ff --titration-state-method propka APBS/泊松-玻尔兹曼静电 力场不匹配时用作通用加氢
力场一致氢 + 显式变体 OpenMM Modeller.addHydrogens MD 系统构建 需要非标准态时变体留默认

质子化决策表:标准态 vs pKa 预测器

残基/情形 pH 7 标准态通常可用 需 pKa 预测器 + H-bond 优化
表面 Asp/Glu 去质子化 (-1) 仅当靠近金属或 H-bond 伙伴
埋藏或盐桥 Asp/Glu — pKa 可升数个单位 → 可能中性/质子化
表面 Lys/Arg 质子化 (+1) 埋藏 Lys 靠近酸性残基可中性
任意位置 His 默认歧义 几乎总是:按局部 H-bond 和金属配位选 HID/HIE/HIP
游离 Cys 中性硫醇 催化/金属配位 Cys 常为硫醇盐
二硫键 Cys S 上无 H (CYX) 先检测 SS 键;不要质子化

一句话规则:标准态仅对体相溶剂环境中的残基可辩护;任何埋藏、电荷簇集、金属邻接或口袋内的可滴定残基都需要预测器(PROPKA/H++)和 H-bond 网络 pass(reduce)。

代码:PDBFixer 制备管线

from pdbfixer import PDBFixer
from openmm.app import PDBFile

# 注意:finder 调用顺序很重要——残基 → 非标准 → 原子
# hydrogens 必须最后加(重原子存在后才能加氢)
fixer = PDBFixer(filename='receptor.pdb')

fixer.findMissingResidues()           # 短内部缺口 + 末端
fixer.findNonstandardResidues()       # 如 MSE(硒代蛋氨酸)
fixer.replaceNonstandardResidues()    # 映射回标准母体
fixer.removeHeterogens(keepWater=False)  # 去缓冲离子/冷冻保护剂
fixer.findMissingAtoms()              # 截短侧链 + 上面找到的残基
fixer.addMissingAtoms()               # 补重原子;补建的 loop 是假设,需记录

# pH 7.0 是一个选择,不是安全默认——需声明并匹配实验/生物条件
# addMissingHydrogens 检测二硫键并将那些 Cys 留为 CYX
fixer.addMissingHydrogens(pH=7.0)

with open('receptor_prepared.pdb', 'w') as out:
    PDBFile.writeFile(fixer.topology, fixer.positions, out, keepIds=True)

# 记录 provenance
built = [(ci, pos, name) for (ci, pos), names in fixer.missingResidues.items() for name in names]
print(f'built {len(fixer.missingResidues)} missing-residue segment(s), {len(built)} residue(s); pH=7.0')

代码:reduce 优化翻转和互变异构

import subprocess

# -build 执行 -OH -ROTEXOH -HIS -FLIP:加氢并优化 OH/His 旋转 + Asn/Gln/His 翻转
# reduce 通过小探针全原子接触评分优化方向(Word 1999)
with open('receptor_reduced.pdb', 'w') as out:
    subprocess.run(['reduce', '-build', 'receptor_prepared.pdb'], stdout=out, check=True)

# Reduce2(CCTBX/Phenix)是维护中的继承者:mmtbx.reduce2 receptor_prepared.pdb

代码:PROPKA pKa 预测 + PDB2PQR 生成 PQR/PDBQT

import subprocess

# propka3 写 receptor_prepared.pka;SUMMARY 列出预测 pKa vs 模型(标准)pKa
subprocess.run(['propka3', 'receptor_prepared.pdb'], check=True)

# PDB2PQR 通过 PROPKA 在 --with-ph 指定 pH 下指定质子化态,写出电荷/半径
# --ff 必须匹配下游力场;--with-ph 7.0 是声明的滴定条件
subprocess.run([
    'pdb2pqr30', '--ff=AMBER', '--with-ph', '7.0',
    '--titration-state-method', 'propka', '--keep-chain',
    'receptor_prepared.pdb', 'receptor.pqr',
], check=True)

# 再用 meeko mk_prepare_receptor.py 转 PDBQT
subprocess.run([
    'mk_prepare_receptor.py', '--read_pqr', 'receptor.pqr',
    '-o', 'receptor', '-p'
], check=True)

预测模型制备的特殊考虑

AlphaFold/ESMFold 模型制备时必须先裁剪低 pLDDT 区域,再加氢/质子化。口袋 rotamer 即使在骨架 pLDDT 高时也是最不可靠的原子。

# 1) 先裁剪低 pLDDT 残基(<50-70 = 不可靠)
#    phenix.process_predicted_model 做这个 + PAE 域分割
# 2) 然后对裁剪后模型运行 PDBFixer 管线
fixer = PDBFixer(filename='af_model_trimmed.pdb')
fixer.findMissingAtoms()
fixer.addMissingAtoms()
fixer.addMissingHydrogens(pH=7.0)  # 预测模型从不携带实验氢

关键陷阱

症状 原因 修复
制备文件没有氢 X-ray 模型极少分辨氢 显式调用 addMissingHydrogens(pH=...)
His H-bond/金属配位错 默认 HIE/HID 猜测未考虑局部网络 让 reduce 选中性互变异构(HID vs HIE);PROPKA 仅决定电荷态(HIP vs 中性)
埋藏 Asp/Glu 去质子化但应为中性 假设标准 pKa 7 预测 pKa(PROPKA/H++);质子化该残基(ASH/GLH)
addMissingAtoms 建出离谱 loop 长/末端缺口交给只做短 loop 的补全器 不在这里补长缺口;用 loop/同源建模器并标记
催化金属/辅因子被去掉 removeHeterogens() 清掉所有非水 heterogen 有目的地保留:显式过滤,不要批量移除
下游结果不可复现 制备文件未记录假设 记录 pH、质子化模型、每个补建原子/loop 作为 provenance

Step 4: 结合口袋检测

技能:bio-structural-biology-binding-site-detection

目标

在 apo 结构上检测候选口袋并评分,确定 docking 搜索框的中心坐标和大小。

核心原则

几何凹陷不自动等于功能或可成药位点。 fpocket 的 alpha 球或 CASTp 的表面拓扑会找到表面所有凹陷——包括结晶添加剂位点、非功能表面口袋和域间裂缝。口袋排名是假设而非结合位点。最可靠的位点是已被配体占据的位点(holo 结构)。

可成药性评分基于 holo 训练集,系统性低估 apo、浅口袋、别构位点和 PPI 口袋。 隐口袋在 apo 快照中完全不可见——仅在 MD 或构象系综中出现。

方法选择决策表

问题/情况 方法 最佳场景 失败/误导场景
快速枚举所有几何凹陷 fpocket 或 CASTp apo 或 holo 全表面扫描 报告添加剂位点和非功能口袋
按配体性/可成药性排名 P2Rank, fpocket 药性分, DoGSiteScorer 球状结构域上多口袋优先排序 训练于 holo/可成药集 → 低估 apo/shallow/allosteric
检测隐口袋/瞬态口袋 mdpocket over MD trajectory 位点仅在结合时打开 单一静态结构——隐口袋不可见

代码:fpocket 检测和排名

import subprocess
from pathlib import Path

def run_fpocket(pdb_path):
    subprocess.run(['fpocket', '-f', pdb_path], check=True)
    stem = Path(pdb_path).stem
    return Path(pdb_path).with_name(f'{stem}_out')

def parse_fpocket_info(out_dir):
    info = next(Path(out_dir).glob('*_info.txt'))
    pockets, current = [], None
    for raw in info.read_text().splitlines():
        line = raw.strip()
        if line.startswith('Pocket'):
            current = {'pocket': int(line.split()[1])}
            pockets.append(current)
        elif ':' in line and current is not None:
            key, val = (part.strip() for part in line.split(':', 1))
            try:
                current[key] = float(val)
            except ValueError:
                pass
    return pockets

out_dir = run_fpocket('receptor_prepared.pdb')
pockets = sorted(parse_fpocket_info(out_dir),
                 key=lambda p: p.get('Druggability Score', 0), reverse=True)
# 0.5 药性分是分诊线,不是定律
druggable = [p for p in pockets if p.get('Druggability Score', 0) >= 0.5]
for p in pockets[:5]:
    print(f"pocket {p['pocket']}: drug={p.get('Druggability Score', 0):.2f}")

代码:P2Rank ML 配体性评分

import csv
import subprocess
from pathlib import Path

def run_p2rank(pdb_path, out_dir='p2rank_out'):
    subprocess.run(['prank', 'predict', '-f', pdb_path, '-o', out_dir], check=True)
    return Path(out_dir) / f'{Path(pdb_path).name}_predictions.csv'

def parse_p2rank(csv_path):
    with open(csv_path) as fh:
        reader = csv.DictReader(fh, skipinitialspace=True)  # P2Rank 列名有空格
        rows = [{k.strip(): v.strip() for k, v in row.items()} for row in reader]
    return [{'rank': int(r['rank']), 'score': float(r['score']),
             'probability': float(r['probability'])} for r in rows]

predictions = parse_p2rank(run_p2rank('receptor_prepared.pdb'))
top = predictions[0]
print(f"top pocket: probability={top['probability']:.2f} score={top['score']:.2f}")

正交验证信号

口袋检测排名需要以下信号交叉验证:

  1. 独立检测器一致性:fpocket 和 P2Rank 是否排名同一个口袋为 top
  2. 口袋内衬残基的进化保守性(ConSurf):功能位点通常保守,添加剂/晶体裂缝不保守
  3. 热点/混合溶剂作图(FTMap, mixed-solvent MD)
  4. 同源 holo 结构
  5. 已知生物学信息

关键陷阱

症状 原因 修复
排名第一的口袋是表面添加剂位点 几何找到所有凹陷 交叉检查口袋原子 vs 输入中的 HETATM/添加剂
已知结合位点完全漏掉 位点是隐口袋——apo 快照中关闭 运行 mdpocket over MD/NMR 系综
真实靶点药性分全低 评分训练于 holo/可成药集 低分不等于”无位点”;用独立方法佐证
P2Rank parse KeyError CSV 列名有前导空格 用 csv.DictReader(skipinitialspace=True) 并 strip keys
AlphaFold 口袋对接差 口袋内衬 rotamer 最不可靠 先验证/制备模型;优先用 holo 实验结构

Step 5: 配体库制备

技能:bio-virtual-screening(配体制备部分)

目标

从 SMILES 生成 3D docking-ready 配体文件,包含正确的质子化和构象。

代码:RDKit + meeko 配体制备

from rdkit import Chem
from rdkit.Chem import AllChem
from meeko import MoleculePreparation, PDBQTWriterLegacy

def prepare_ligand(smiles, pdbqt_out):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        raise ValueError(f'invalid SMILES: {smiles}')
    mol = Chem.AddHs(mol)

    # ETKDGv3 3D 构象嵌入
    embed_status = AllChem.EmbedMolecule(mol, AllChem.ETKDGv3())
    if embed_status != 0:
        raise RuntimeError('ETKDGv3 failed to generate a ligand conformer')

    # MMFF94 能量最小化
    if not AllChem.MMFFHasAllMoleculeParams(mol):
        raise ValueError('MMFF94 parameters unavailable for this ligand')
    optimization_status = AllChem.MMFFOptimizeMolecule(mol)
    if optimization_status != 0:
        raise RuntimeError('MMFF94 optimization did not converge')

    # meeko 0.5+ API:prepare() 返回 MoleculeSetup 列表
    # 用 PDBQTWriterLegacy.write_string() 生成 PDBQT
    mk_prep = MoleculePreparation()
    setups = mk_prep.prepare(mol)
    pdbqt_text, is_ok, err = PDBQTWriterLegacy.write_string(setups[0])
    if not is_ok:
        raise RuntimeError(f'meeko PDBQT export failed: {err}')
    with open(pdbqt_out, 'w') as f:
        f.write(pdbqt_text)
    return pdbqt_out

# 批量制备
library = [
    'CC(=O)c1ccccc1',      # acetophenone
    'O=C(O)c1ccccc1O',     # salicylic acid
    # ... 更多 SMILES
]
for i, smi in enumerate(library):
    prepare_ligand(smi, f'ligand_{i:04d}.pdbqt')

关键注意点

陷阱 原因 修复
MolFromSmiles 不预测 pH 7.4 质子化 它只解析提供的 SMILES 状态 需先用 pKa/protomer 工具生成正确质子化态
Uncharger 不预测质子化 它仅中和形式电荷 用专门工具预测 pH 7.4 下的质子化
meeko 0.5+ API 变化 prep.write_pdbqt_file() 已废弃 用 PDBQTWriterLegacy.write_string(setups[0])
Histidine 质子化不一致 受体用 pH 7.4,配体用 pH 7.0 统一 pH,用 PROPKA 检查催化残基

Step 6: 层次化虚拟筛选

技能:bio-virtual-screening

目标

对 10M+ 化合物库进行三阶段过滤,从全库筛到 top-1000 候选。

Docking 工具分类

工具 评分函数 最佳场景 失败场景
AutoDock Vina 1.2 Vina(经验) 开放、表征良好的基线 交叉对接;隐口袋;金属中心
SMINA Vina + 灵活 + 自定义 自定义评分;柔性侧链 Vina 评分同样的注意事项
GNINA 1.1 CNN 或 Vina 评分 CNN 辅助 pose 排序 新化学型 OOD
DiffDock-L 扩散生成 交叉对接 pose 采样 需 PoseBusters 验证
Boltz-2 + GNINA rescore 基础模型 + CNN 实验性多模型工作流 独立基准每个证据流

场景决策树

场景 推荐工作流
自对接(已知配体口袋) GNINA --cnn_scoring rescore
交叉对接(apo 或相关靶点结构) DiffDock-L pose + GNINA rescore + PoseBusters
超大库(10M+) 校准层次筛选:性质/警报分诊 → Vina → GNINA → 高成本后续
别构/未定义位点 P2Rank 口袋检测 → 对所有口袋做系综对接
金属配位配体 GOLD(商业)或手动参数化 Vina 金属评分
Hit-to-lead 精修 用共晶结构;MD 弛豫受体;FEP 亲和力

代码:Vina 单配体对接

from vina import Vina

def dock_single(receptor_pdbqt, ligand_pdbqt, center, box_size,
                exhaustiveness=8, n_poses=10):
    """
    center: (x, y, z) 口袋中心坐标
    box_size: (x, y, z) 搜索框大小
    """
    v = Vina(sf_name='vina')
    v.set_receptor(receptor_pdbqt)
    v.set_ligand_from_file(ligand_pdbqt)
    v.compute_vina_maps(center=center, box_size=box_size)
    v.dock(exhaustiveness=exhaustiveness, n_poses=n_poses)
    return v.energies(), v.poses()

代码:GNINA CNN 重打分

gnina -r receptor.pdb -l ligand.sdf \
      --autobox_ligand reference_ligand.sdf \
      --cnn_scoring rescore \
      -o poses.sdf.gz \
      --num_modes 9 --exhaustiveness 8

--cnn_scoring 模式说明:

模式 行为 计算开销
none 无 CNN;全程经验评分 最低
rescore(默认) 经验搜索 + CNN 重排最终 pose CNN 选项中最低
refinement CNN 精修 pose 并排名 ~10x rescore
metrorescore CNN 在 Metropolis 搜索中评分 中等
metrorefine CNN 在搜索和精修中均评分 高
all 全程 CNN 极高,不推荐

代码:层次化虚拟筛选管线

import pandas as pd
from concurrent.futures import ProcessPoolExecutor
from functools import partial

def vs_pipeline(library_smi, receptor_pdbqt, center, box, output_dir, n_workers=16):
    # Stage 0: 读取化合物库
    df = pd.read_csv(library_smi)

    # Stage 1: 类药性过滤(Lipinski + Veber + PAINS)
    df_stage1 = drug_like_filter(df)  # 委托给 admet-prediction 技能

    # Stage 2: Vina 对接全过滤后库
    worker = partial(vina_dock, receptor_pdbqt=receptor_pdbqt,
                     center=center, box=box)
    with ProcessPoolExecutor(max_workers=n_workers) as ex:
        affinities = list(ex.map(worker, df_stage1['smiles']))
    df_stage1['vina_affinity'] = affinities

    # 取 Vina top 1%
    df_stage2 = df_stage1.nsmallest(int(len(df_stage1) * 0.01), 'vina_affinity')

    # Stage 3: GNINA CNN 重打分 top 1%
    df_stage2['gnina_affinity'] = df_stage2['smiles'].apply(
        lambda smi: gnina_rescore(smi, receptor_pdbqt, center, box))

    # 取 GNINA top 1000
    df_stage3 = df_stage2.nsmallest(1000, 'gnina_affinity')

    # Stage 4: Pose 物理验证
    return pose_validate(df_stage3)  # 委托给 pose-validation 技能

Vina vs GNINA 分歧调解表

Vina top pose GNINA top pose 行动
同一 pose,分数相似 同一 pose,分数相似 视为支持证据;仍需物理验证
不同 pose 同口袋,不同方向 保留两者,对照靶点相关对照比较
Vina 极好,GNINA 一般 不同 pose,分数差异大 检查两个 pose;不要仅从分数分歧推断谁对
两者都差 大量配体分数相似偏低 可能口袋/蛋白构象错;重新考虑受体

关键陷阱

症状 原因 修复
Vina segfault PDBQT 损坏(原子名) 用 meeko 重新制备
GNINA 挂起 GPU OOM 减少并发;--num_modes 5
所有色预测结合能很差(-3 到 -5) 质子化错误;配体对框太大 重检 pKa;扩大框
配体间亲和力完全相同 受体 grid 未计算 dock 前调 v.compute_vina_maps()
GPU 模式慢 Vina 仅 CPU;仅 GNINA 用 GPU GNINA 用 GPU;Vina 用多核 CPU

Step 7: Pose 物理验证

技能:bio-pose-validation

目标

用 PoseBusters 对 docked poses 进行物理合理性验证,过滤 PB-invalid 构象。

核心原则

Pose 准确性和化学合理性是不同的维度。 在 PoseBusters Benchmark 上,DiffDock 的 top-pose RMSD ≤ 2Å 成功率为 72%,但 RMSD+PB-valid 联合成功率仅 47%——25 个百分点的差距。不要将此百分比泛化到其他数据集或更新模型版本。

PoseBusters 检查组

检查组 测试内容 2024 基准标准
Sanity 配体化学合理性 RDKit sanitization 通过
Bond lengths 键长在参考范围内 0.75–1.25 × RDKit 距离几何下界
Bond angles 1-3 距离在参考范围内 0.75–1.25 × RDKit 下界
Internal steric 无配体内 clash 对距离 > 0.70 × RDKit 下界
Aromatic planarity 芳香环平面 最大偏离拟合平面 ≤ 0.25 Å
Double-bond stereo Z/E 保持 匹配输入 SMILES
Internal energy 能量 vs 生成构象 UFF 能量比 ≤ 100 × 50 个生成弛豫构象均值
Volume overlap 与蛋白 vdW 重叠 < 7.5% 配体 vdW 体积
Minimum distance 无严重蛋白-配体 clash 距离 ≥ 0.75 × vdW 半径和
Chirality R/S 保持 匹配输入 SMILES

通过所有检查 = “PB-valid”。PB-valid + RMSD ≤ 2Å = 现代标准。

代码:PoseBusters 验证

from posebusters import PoseBusters
import pandas as pd

bust = PoseBusters(config='dock')  # 'dock' = 全检除 RMSD 参考

results = bust.bust(
    mol_pred='/path/to/docked_poses.sdf',
    mol_cond='/path/to/receptor.pdb',
)

# 聚合所有 bool 列为 pb_valid 标志
check_cols = [
    col for col in results.select_dtypes(include='bool').columns
    if not col.lower().startswith('rmsd')
]
results['pb_valid'] = results[check_cols].all(axis=1)
valid = results[results['pb_valid']]
print(f'{len(valid)} / {len(results)} poses are PB-valid')

配置选择

配置 包含检查 使用场景
redock 全检 + RMSD vs 参考 + 蛋白 vdW 重叠 自对接基准、回溯验证
dock 全检除 RMSD 参考 盲对接、前瞻性虚拟筛选
mol 仅配体内(sanity, bonds, angles, rings, stereo, energy) 构象 QC;无蛋白上下文

代码:应变能量化

from rdkit import Chem
from rdkit.Chem import AllChem

def ligand_strain(docked_sdf, n_ref=20):
    """量化 docked pose 偏离最低能量自由构象的程度"""
    suppl = Chem.SDMolSupplier(docked_sdf, removeHs=False)
    strains = []
    for docked in suppl:
        if docked is None:
            continue
        smi = Chem.MolToSmiles(docked)
        ref = Chem.MolFromSmiles(smi)
        if ref is None:
            strains.append({'strain': None, 'note': 'reference_parse_failed'})
            continue
        ref = Chem.AddHs(ref)
        props_ref = AllChem.MMFFGetMoleculeProperties(ref)
        if props_ref is None:
            strains.append({'strain': None, 'note': 'no_mmff_params'})
            continue
        conf_ids = list(AllChem.EmbedMultipleConfs(
            ref, numConfs=n_ref, params=AllChem.ETKDGv3()))
        if not conf_ids:
            strains.append({'strain': None, 'note': 'embedding_failed'})
            continue
        AllChem.MMFFOptimizeMoleculeConfs(ref)

        ref_energies = []
        for c in conf_ids:
            ff = AllChem.MMFFGetMoleculeForceField(ref, props_ref, confId=c)
            if ff is not None:
                ref_energies.append(ff.CalcEnergy())
        min_ref = min(ref_energies) if ref_energies else None

        # 加 H 坐标,弛豫 H 同时固定重原子
        docked_h = Chem.AddHs(Chem.Mol(docked), addCoords=True)
        if docked_h.GetNumAtoms() != ref.GetNumAtoms():
            strains.append({'strain': None, 'note': 'atom_mismatch'})
            continue
        props_docked = AllChem.MMFFGetMoleculeProperties(docked_h)
        docked_ff = AllChem.MMFFGetMoleculeForceField(docked_h, props_docked) \
            if props_docked is not None else None
        if docked_ff is not None:
            for atom in docked_h.GetAtoms():
                if atom.GetAtomicNum() != 1:
                    docked_ff.AddFixedPoint(atom.GetIdx())
            docked_ff.Minimize(maxIts=200)
            docked_e = docked_ff.CalcEnergy()
            strains.append({
                'min_ref_energy': min_ref,
                'docked_energy': docked_e,
                'strain': docked_e - min_ref,
                'note': 'ok'
            })
        else:
            strains.append({'strain': None, 'note': 'docked_ff_failed'})
    return strains

RMSD vs PB-valid 调解表

RMSD ≤ 2Å PB-valid 行动
是 是 物理合理且接近参考;仍需验证下游任务适用性
是 否 接近参考但某项合理性检查未过;检查失败项
否 是 物理合理但与参考不同;调查对齐、蛋白状态和替代结合模式
否 否 与参考不同且不合理;检查两方面再决定是否拒绝

关键陷阱

症状 原因 修复
期望的检查缺失 输入加载失败或配置省略 检查返回 DataFrame、加载状态列和输入格式
全检通过但 pose 无效 受体文件格式错 用带氢的 PDB;PDBQT 可能不工作
vdW 重叠假阳性(共价) 共价键被计为 clash 用共价对接专用验证
应变计算慢 参考构象太多 n_ref 减到 5-10

Step 8: ADMET 预测

技能:bio-admet-prediction

目标

对 top hits 进行 ADMET 性质预测和类药性筛选,排除有毒性或药代性质差的化合物。

ADMET 模型分类

工具 端点数 架构 不确定性 访问方式 失败场景
ADMETlab 3.0 119 特征(77 模型 + 34 计算性质 + 8 规则) 多任务 DMPNN + 描述符 证据不确定性 Web 服务/ API OOD;金属;大环
ADMET-AI TDC 衍生 Chemprop D-MPNN 版本相关 Python 包 v2 与 v1/服务器不同
chemprop(自建) 用户定义 D-MPNN ± 描述符 集成 Python 包 需充足训练数据
ProTox-3.0 61 毒性端点 RF/DNN + 相似性 置信分 Web 服务 仅毒性

场景决策树

场景 工作流 理由
库分诊,无内部数据 ADMETlab 3.0 API 批量 广覆盖 + 模型不确定性
单端点,有内部数据 chemprop D-MPNN 基准 按部署相关验证选择
需校准概率 chemprop 集成 + Platt DL 原生概率很少校准
心脏毒性倾向 ADMETlab hERG + ProTox-3.0 + 文献 多模型三角验证
药物-药物相互作用(CYP) CYP1A2/2C9/2C19/2D6/3A4 标准 5 个 CYP

代码:Lipinski/Veber 类药性筛选

from rdkit.Chem import Descriptors, Lipinski, QED
from rdkit.Chem.FilterCatalog import FilterCatalog, FilterCatalogParams

def druglike_score(mol):
    """计算类药性指标"""
    mw = Descriptors.MolWt(mol)
    logp = Descriptors.MolLogP(mol)
    hbd = Lipinski.NumHDonors(mol)
    hba = Lipinski.NumHAcceptors(mol)
    tpsa = Descriptors.TPSA(mol)
    rotbonds = Lipinski.NumRotatableBonds(mol)
    qed = QED.qed(mol)

    lipinski_violations = sum([mw > 500, logp > 5, hbd > 5, hba > 10])
    veber_pass = rotbonds <= 10 and tpsa <= 140
    bbb_simple_screen = tpsa <= 90 and mw <= 500 and hbd <= 3  # BBB 启发式

    return {'MW': mw, 'LogP': logp, 'HBD': hbd, 'HBA': hba,
            'TPSA': tpsa, 'RotBonds': rotbonds, 'QED': qed,
            'Lipinski_violations': lipinski_violations,
            'Veber_pass': veber_pass,
            'BBB_simple_screen': bbb_simple_screen}

def alerts(mol, catalogs=('PAINS_A', 'BRENK', 'ZINC')):
    """PAINS/BRENK/REOS 结构警报"""
    params = FilterCatalogParams()
    for cat in catalogs:
        params.AddCatalog(getattr(FilterCatalogParams.FilterCatalogs, cat))
    catalog = FilterCatalog(params)
    hits = catalog.GetMatches(mol)
    return [h.GetDescription() for h in hits]

ADMETlab 3.0 端点样例

类别 端点示例
吸收 Caco-2 通透性、HIA (%)、Pgp 抑制剂/底物、MDCK
分布 BBB+、PPB (%)、VDss (L/kg)、Fu(游离分数)
代谢 CYP1A2/2C9/2C19/2D6/3A4 抑制剂/底物
排泄 CL (mL/min/kg)、T1/2 (h)
毒性 hERG、AMES、肝毒性(DILI)、致癌性、免疫毒性、致突变性
类药性 Lipinski, Veber, Ghose, Egan, Muegge, QED, SAscore

hERG 心脏毒性端点

模型 架构 训练数据 AUC
Shan et al. D-MPNN + MOE D-MPNN + 206 MOE 7,889 化合物 0.956
CardioTox-net 五 DL + 神经元集成 BindingDB + ChEMBL 0.930
ADMETlab 3.0 hERG DMPNN 多任务 内部 0.92
ProTox-3.0 心脏毒性 RF 5,252 ChEMBL 0.86 CV / 0.95 外部

单模型概率 > 0.5 不是 kill signal。 需多模型三角验证 + 文献检索 + 考虑暴露量 vs hERG 效力。

PAINS 作为 kill filter 的陷阱

PAINS 校准的是 HTS 分析干扰,不预测药物开发失败。将 PAINS 匹配作为排除标准会从库中清除有效先导物(如姜黄素类似物、多酚天然产物)。正确做法是标记 PAINS 命中以便正交分析确认,而非预先排除。

模型间调解

当 ADMETlab、ProTox-3.0 和 chemprop 在 hERG 上不一致:

情况 行动
全预测 hERG+ 更高关注;计划体外膜片钳
结果分歧 检查适用域、活性阈值和测定定义再决定
全预测 hERG- 较低关注,但临床候选和新化学型仍考虑体外筛选
不要将相关模型当作独立证据 仅仅因为托管在不同服务上

OECD QSAR 五支柱(监管级)

  1. 明确定义端点 — 具体生物测定、单位、条件
  2. 明确算法 — 可复现模型 + 代码
  3. 定义适用域(AD) — 模型有效的范围
  4. 适当统计验证 — 外部测试集、交叉验证
  5. 机制解释 — 生物/化学原理

关键陷阱

症状 原因 修复
ADMETlab API 超时 服务负载/配额 遵循批量限制;退避重试
chemprop 过拟合 随机拆分 用骨架拆分 --split scaffold_balanced
hERG 预测 50/50 OOD 检查适用域(kNN 距离、集成方差)
QED 计算失败 分子缺失/未 sanitize 拒绝解析失败;sanitize 输入
预测跨运行不一致 chemprop 集成随机种子 --seed 42 并复用模型
校准不匹配 DL 原生概率未校准 在验证集上 Platt scaling

Step 9: 自由能精排序

技能:bio-free-energy-calculations

目标

对 top 候选化合物进行相对结合自由能(RBFE)计算,以物理方法精确排序类似物。

FEP 方法分类

方法 成本/对 精度 适用场景 失败场景
FEP+(Schrödinger) 系统/协议相关 GPU 商业 RBFE 商业先导优化 许可和复现约束
OpenFE RBFE 系统/协议相关 GPU 开源 RBFE 开源项目 映射/设置/采样需审查
GROMACS/AMBER RBFE 实现相关 自定义 专家控制 手动验证负担
MM/PBSA / MM/GBSA 低成本端点 近似端点分 系列内探索排序 熵、采样、模型依赖
Boltz-2 affinity 秒级 GPU 基准相关 ML 替代;≥1000x 更快 新化学型

场景决策树

场景 推荐工作流
排序近似物(R-group SAR) RBFE via OpenFE(循环:lig1↔lig2↔lig3)
跨骨架排序 ABFE 每个配体;或协调 RBFE 星形网络
同源先导优化集 RBFE + 连通冗余感知扰动图
单配体亲和力 ABFE(无需参考化合物)
低成本探索排序 项目验证端点或 ML 方法 + 正交确认
选择性(靶点 vs 脱靶) 两个蛋白上 RBFE;报告 delta-delta-G
别构 vs 正构 ABFE 可比;用 MD 检查 pose 稳定性

RBFE 热力学循环

ΔΔG_binding = ΔG_lig1→lig2(bound) - ΔG_lig1→lig2(solvent)

代码:OpenFE RBFE 设置

from openfe import SmallMoleculeComponent, ProteinComponent, SolventComponent
from openfe.protocols.openmm_rfe import RelativeHybridTopologyProtocol

protein = ProteinComponent.from_pdb_file('receptor_prepared.pdb')
ligA = SmallMoleculeComponent.from_sdf_file('ligand_A.sdf')
ligB = SmallMoleculeComponent.from_sdf_file('ligand_B.sdf')
solvent = SolventComponent()

protocol = RelativeHybridTopologyProtocol(
    RelativeHybridTopologyProtocol.default_settings()
)
# 需创建/检查原子映射(Kartograf 是 OpenFE 1.7 CLI 默认映射器)
# 构建 bound 和 solvent Transformation 对象
# 通过 openfe quickrun 或 Python 执行接口运行

Lambda 窗口调度

阶段 Lambda 值 目的
解耦(vdW) 0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0 关闭配体 vdW
充电(Coulomb) 0.0, 0.25, 0.5, 0.75, 1.0 关闭配体部分电荷
约束(仅 ABFE) 0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0 Boresch 式约束

代码:MBAR 分析

from alchemlyb import concat
from alchemlyb.parsing import gmx
from alchemlyb.estimators import MBAR
from alchemlyb.postprocessors.units import to_kcalmol

u_nks = []
for window in range(12):
    df = gmx.extract_u_nk(f'window_{window}.xvg', T=300)
    u_nks.append(df)

u_nk = concat(u_nks)
mbar = MBAR().fit(u_nk)
delta_g = to_kcalmol(mbar.delta_f_).iloc[0, -1]
d_delta_g = to_kcalmol(mbar.d_delta_f_).iloc[0, -1]
print(f'ΔG: {delta_g:.2f} ± {d_delta_g:.2f} kcal/mol')

代码:闭环验证

def cycle_closure_residual(cycle):
    """cycle = [(lig_i, lig_j, delta_g, sd), ...]"""
    total = sum(d_g for _, _, d_g, _ in cycle)
    total_var = sum(sd**2 for _, _, _, sd in cycle)
    return total, total_var ** 0.5

# 残差应与传播不确定性一致(在 ~2σ 内)

力场选择

力场 用于 备注
OPLS4(Schrödinger) FEP+ 默认 商业;测试充分
OpenFF 2.1.1 (Sage) OpenFE 1.7 默认 检查序列化设置
GAFF2 AMBER FEP 仅配体;蛋白 FF14SB
CGenFF CHARMM 系 FEP CHARMM 力场族

Boltz-2 快速替代

Boltz-2 报告在评估基准上亲和力精度接近 FEP,至少 1000 倍速度优势。但结果是基于特定基准的,不建立对任意 ChEMBL 数据的通用 RMSE 或相关性。新化学型或模态超出演示的训练/基准域时不应单独依赖。

  • 何时用 Boltz-2:hit 发现分诊用 affinity_probability_binary;hit-to-lead 用 affinity_pred_value
  • 何时不依赖 Boltz-2:新化学型/模态超出版本域,或无靶点相关验证的生产决策

关键陷阱

症状 原因 修复
Lambda 窗口模拟发散 初始 pose 差 先用 MM 最小化弛豫 pose
闭环残差与不确定性不一致 采样/映射/力场/相关边问题 检查有符号残差、重叠、映射和独立重复
MBAR 返回 NaN 窗口间重叠不足 添加中间 lambda 窗口
ABFE 系统偏移 约束/标准态/采样/力场问题 检查协议文档化的约束/自由能项和符号
MM/GBSA 排序与对接不符 不同轨迹帧 在 MD 弛豫 pose 上计算 MM/GBSA
Boltz-2 亲和力全 0 输入格式错 检查 SMILES 有效性;先标准化

全流程总结图

靶点蛋白 BRD4-BD1 (O60885)
    │
    ├─ PDB 实验结构 (h3MXF) ──────────────────────────────┐
    │                                                      │
    └─ AlphaFold DB (O60885) ─┐                           │
         │                     │                           │
         ▼                     │                           │
    pLDDT/PAE 评估              │                           │
    裁剪低置信区域               │                           │
         │                     │                           │
         ▼                     ▼                           ▼
    ┌────────────────────────────────────────────────────────┐
    │           受体结构制备 (PDBFixer + PROPKA + reduce)     │
    │           补缺失原子 → 加氢 → 质子化 → PDBQT            │
    └────────────────────────┬───────────────────────────────┘
                             │
    ┌────────────────────────▼───────────────────────────────┐
    │  口袋检测 (fpocket + P2Rank)  ← 如果是 apo             │
    │  或从 holo 共晶配体中心定义搜索框                       │
    └────────────────────────┬───────────────────────────────┘
                             │
                  ┌──────────┴──────────┐
                  │                     │
            配体库制备               受体 PDBQT
          (RDKit + meeko)              │
                  │                     │
                  └──────────┬──────────┘
                             │
    ┌────────────────────────▼───────────────────────────────┐
    │  Stage 1: 类药性过滤 (Lipinski + Veber + PAINS)        │
    │  Stage 2: Vina 对接 (exhaustiveness=8, 并行)           │
    │  Stage 3: GNINA CNN rescore top 1%                    │
    │  Stage 4: PoseBusters 物理验证 top 1000               │
    │  Stage 5: ADMET 预测 (ADMETlab 3.0 + hERG + CYP)      │
    └────────────────────────┬───────────────────────────────┘
                             │
                    top ~50 候选
                             │
    ┌────────────────────────▼───────────────────────────────┐
    │  自由能精排序                                          │
    │  · 同系列: RBFE (OpenFE, MBAR, 闭环验证)              │
    │  · 跨骨架: ABFE 或 Boltz-2 快速分诊                    │
    └────────────────────────┬───────────────────────────────┘
                             │
                    top 5-10 候选
                             │
                    ▼
              实验验证 (SPR / ITC)

涉及技能一览表

步骤 技能名称 主要工具 核心功能
Step 1 bio-structural-biology-alphafold-predictions requests, Bio.PDB AFDB 模型获取与置信度解读
Step 2 bio-structural-biology-alphafold-predictions numpy pLDDT 分级、PAE 域间分析
Step 3 bio-structural-biology-structure-preparation PDBFixer, PROPKA, reduce, PDB2PQR 受体制备:加氢、质子化、补原子
Step 4 bio-structural-biology-binding-site-detection fpocket, P2Rank 结合口袋检测与可成药性评分
Step 5 bio-virtual-screening (ligand prep) RDKit, meeko SMILES → 3D PDBQT
Step 6 bio-virtual-screening AutoDock Vina, GNINA 层次化虚拟筛选
Step 7 bio-pose-validation PoseBusters, RDKit Pose 物理合理性验证 + 应变能
Step 8 bio-admet-prediction ADMETlab 3.0, RDKit, chemprop ADMET 预测与类药性筛选
Step 9 bio-free-energy-calculations OpenFE, alchemlyb, MBAR RBFE/ABFE 自由能精排序
补充 bio-ml-docking-rescoring DiffDock-L, Boltz-2, Chai-1 ML 对接与重打分(替代/补充)

量化阈值速查表

指标 阈值 含义 来源
pLDDT > 90 Very high 骨架+侧链方向可信 Jumper 2021
pLDDT 70-90 Confident 骨架基本正确 AFDB 定义
pLDDT 50-70 Low 仅粗略拓扑 AFDB 定义
pLDDT < 50 Very low 通常是无序区(IDR) Akdel 2022
PAE < 5Å(域间) 可信 域间相对位置有约束 AFDB
Druggability Score ≥ 0.5 可成药分诊线 药物分子可结合 Schmidtke 2010
Lipinski violations ≤ 1 类药 MW≤500, LogP≤5, HBD≤5, HBA≤10 Lipinski
Veber pass 可药 RotBonds≤10, TPSA≤140 Veber
PoseBusters PB-valid 物理合理 通过全部 ~20 项检查 Buttenschoen 2024
RMSD ≤ 2Å + PB-valid 现代标准 准确且合理 Buttenschoen 2024
芳香环偏离 ≤ 0.25Å 平面 芳香环非平面 = 不合理 PoseBusters
vdW 体积重叠 < 7.5% 无严重 clash 与蛋白的体积重叠 PoseBusters
内部应变能 ≤ 3 kcal/mol 低应变 ~70% 蛋白结合配体满足 Boström 1998
Vina affinity < -7 kcal/mol 可能活性 经验阈值(非保证) Vina 手册
闭环残差 < 2σ 一致 热力学循环闭合 最佳实践
hERG prob < 0.5 较低关注 需多模型三角验证 ADMETlab 3.0
BBB simple screen 可过 BBB TPSA≤90, MW≤500, HBD≤3 启发式
Exhaustiveness = 8 Vina 默认 可增到 16/32/64 Vina 手册

关键决策点汇总

1. 实验结构 vs 预测模型

  • PDB 有 holo 实验结构 → 直接用实验结构
  • PDB 只有 apo → 用 apo + 口袋检测,或考虑 MD 开口袋袋
  • 无实验结构 → AlphaFold DB → 检查 pLDDT/PAE → 裁剪后制备
  • 需要复合物 → AF3/Boltz-1/Chai-1

2. 对接策略选择

  • 有 holo 受体 + 已知口袋 → GNINA --cnn_scoring rescore
  • Apo 或预测受体 → DiffDock-L + GNINA rescore + PoseBusters
  • 超大库 → Vina → top 1% GNINA → top 0.1% Boltz-2/FEP
  • 金属/辅因子 → 需要专门力场(ZAFF, MCPB.py)

3. 自由能方法选择

  • 同系列类似物(R-group SAR)→ RBFE
  • 跨骨架 → ABFE
  • 快速分诊 → Boltz-2 affinity
    -低成本探索 → MM/GBSA(仅系列内排序)

4. ML vs 经典对接

  • Vina PB-valid + RMSD ≤ 2Å = 58%(PoseBusters Benchmark)
  • DiffDock PB-valid + RMSD ≤ 2Å = 12%(原始版本)
  • ML 模型 pose 必须 PoseBusters 验证(不是可选)
  • 保留采样器置信度、独立评分和物理验证作为分离的证据列

参考文献

  1. Jumper J, et al. (2021) Highly accurate protein structure prediction with AlphaFold. Nature 596:583-589.
  2. Varadi M, et al. (2024) AlphaFold Protein Structure Database in 2024. Nucleic Acids Res 52:D368-D375.
  3. Akdel M, et al. (2022) A structural biology community assessment of AlphaFold2 applications. Nat Struct Mol Biol 29:1056-1067.
  4. Word JM, et al. (1999) Asparagine and glutamine: using hydrogen atom contacts in the choice of side-chain amide orientation. J Mol Biol 285:1735-1747.
  5. Olsson MHM, et al. (2011) PROPKA3. J Chem Theory Comput 7:525-537.
  6. Dolinsky TJ, et al. (2004) PDB2PQR. Nucleic Acids Res 32:W665-W667.
  7. Le Guilloux V, et al. (2009) Fpocket. BMC Bioinformatics 10:168.
  8. Krivak R, Hoksza D (2018) P2Rank. J Cheminform 10:39.
  9. Trott O, Olson AJ (2010) AutoDock Vina. J Comput Chem 31:455-461.
  10. Eberhardt J, et al. (2021) Vina 1.2 features. J Chem Inf Model 61:3891-3898.
  11. McNutt AT, et al. (2021) GNINA 1.0 CNN docking. J Cheminformatics 13:43.
  12. Buttenschoen M, et al. (2024) PoseBusters. Chem Sci 15:3130-3139.
  13. Corso G, et al. (2024) DiffDock-L. ICLR.
  14. Passaro S, et al. (2025) Boltz-2. bioRxiv.
  15. Abramson J, et al. (2024) AlphaFold 3. Nature 630:493-500.
  16. Fu et al. (2024) ADMETlab 3.0. Nucleic Acids Res 52:W422-W431.
  17. Capuzzi et al. (2017) PAINS reality check. J Chem Inf Model 57:417.
  18. Wang L, et al. (2015) FEP+ method. J Am Chem Soc 137:2695-2703.
  19. Mey ASJS, et al. (2020) Alchemical free-energy best practices. Living J Comput Mol Sci 2:18378.
  20. Shirts MR, Chodera JD (2008) MBAR. J Chem Phys 129:124105.
  21. Boström J, et al. (1998) Conformational energy penalties of protein-bound ligands. J Comput-Aided Mol Des 12:383-396.
  22. Schmidtke P, Barril X (2010) Understanding and predicting druggability. J Med Chem 53:5858-5867.
  23. Cimermancic P, et al. (2016) CryptoSite: cryptic binding sites. J Mol Biol 428:709-719.
  24. Lyu et al. (2019) Ultralarge virtual-screening proof of concept. Nature 566:224-229.

AI生成 本文档由星辰超级智能体(TeleAgent)基于 bioSkills 技能库中以下 SKILL.md 文件编写:bio-structural-biology-alphafold-predictions、bio-structural-biology-structure-preparation、bio-structural-biology-binding-site-detection、bio-virtual-screening、bio-pose-validation、bio-admet-prediction、bio-free-energy-calculations、bio-ml-docking-rescoring。所有命令、参数和陷阱警告均来源于技能文件的原始内容。

AI生成

20260910 143546 Crispr 筛选 Hits 鉴定全流程实现详解

CRISPR 筛选 Hits 鉴定全流程实现详解

基于 bio-research-suite 技能库中 15 个 CRISPR 筛选相关技能的实际内容编写
涵盖从 sgRNA 库设计 → FASTQ 计数 → QC → 拷贝数校正 → Hit Calling → 通路富集的端到端流程


场景假设

你在研究一种新型靶向抗癌药物 compound-X,作用于某癌症细胞系 HCT116。需要设计并执行一个全基因组 CRISPR-Cas9 敲除筛选,找出:
- 致敏基因(sensitizer):敲除后使细胞对药物更敏感(合成致死)
- 抗性基因(resistance):敲除后使细胞耐受药物

实验设计:药物处理组 vs. 载体对照组,各 2 个生物学重复, brunello 文库(4 条 sgRNA/基因,约 76,000 条 sgRNA),Day 14 收集细胞。


全流程概览

 sgRNA库设计 → FASTQ → 计数 → QC → 拷贝数校正 → Hit Calling → 通路富集 → 可视化
     |           |        |       |         |            |           |          |
  Step 1      Step 2   Step 3  Step 4    Step 5       Step 6      Step 7    Step 8

Step 1: sgRNA 文库设计

涉及技能

技能 功能
bio-crispr-screens-library-design 文库设计全流程(化学体系选择、on-target 打分、control 设计)
bio-genome-engineering-grna-design 单条 gRNA 设计

关键决策:选什么文库

文库 化学体系 每基因 sgRNA 特点
Brunello SpCas9 KO 4 Doench Rule Set 2 打分,on-target 效率高
Avana SpCas9 KO 6 DepMap 使用,更密覆盖
TKOv3 SpCas9 KO 4 加入了增强子靶向 sgRNA
Dolcetto CRISPRi 6 不切割 DNA,适合剂量敏感基因

本场景选择 Brunello 文库(全基因组 KO 筛选标准选择)。

文库 QC 要点

  • Gini 指数 < 0.1(质粒库均匀度)
  • 零计数 sgRNA < 0.5%
  • 每条 sgRNA 覆盖 ≥ 500x 细胞(MOI ≈ 0.3)
  • 非靶向对照(NTC)≥ 500 条
# 检查文库均匀度
import pandas as pd
import numpy as np

def library_qc(counts_df):
    """文库质量控制指标"""
    out = pd.DataFrame(index=counts_df.columns)
    out['n_detected'] = (counts_df > 0).sum()
    out['pct_zero'] = (counts_df == 0).sum() / len(counts_df) * 100
    out['pct_lowcount'] = (counts_df < 30).sum() / len(counts_df) * 100
    out['gini'] = counts_df.apply(gini_coefficient)
    out['median_count'] = counts_df.median()
    out['p90_p10_ratio'] = counts_df.quantile(0.9) / counts_df.quantile(0.1).replace(0, np.nan)
    return out

def gini_coefficient(x):
    """计算 Gini 指数"""
    x = np.sort(x[x > 0])
    n = len(x)
    cumsum = np.cumsum(x)
    return (2 * np.sum((np.arange(1, n+1)) * x) - (n + 1) * cumsum[-1]) / (n * cumsum[-1])

关键陷阱

  • CRISPRi/a 必须用 FANTOM5 CAGE TSS:靶向没有 CAGE 证据的 TSS 会丢失大部分敲低效率
  • Cas9 KO 对旁系同源基因不敏感:42% 组成型表达基因因旁系同源缓冲而无法检出(Dede 2020),需用 Cas12a 多重文库
  • 统计预测的 guide 效率不等于实验效率:Azimuth/DeepSpCas9 打分仅作初筛,仍需实验验证

Step 2: FASTQ 预处理

涉及技能

技能 功能
bio-read-qc-fastp-workflow fastp 一站式预处理
bio-read-qc-quality-reports FastQC + MultiQC
# fastp 预处理
fastp \
    --input_plasmid_R1.fq.gz --input_plasmid_R2.fq.gz \
    --output_Day0_R1.fq.gz --output_Day0_R2.fq.gz \
    --detect_adapter_for_pe \
    --correction \
    --cut_front --cut_tail \
    --thread 16

# 生成质量报告
fastqc *.fq.gz -t 16 -o qc_reports/
multiqc qc_reports/ -o multiqc_report/

关键陷阱

  • fastp 自动接头检测不通用:若使用自定义文库构建 kit,需显式指定 --adapter_sequence
  • CRISPR 文库的 sgRNA 区段通常固定长度:不依赖 fastp 自动 trim,直接在 MAGeCK count 步骤用 --trim-5 AUTO

Step 3: sgRNA 计数

涉及技能

技能 功能
bio-crispr-screens-mageck-analysis MAGeCK count:FASTQ → sgRNA 计数矩阵

MAGeCK count 命令

mageck count \
    --list-seq brunello_library.csv \
    --sample-label Plasmid,Day0,Veh_r1,Veh_r2,Drug_r1,Drug_r2 \
    --fastq Plasmid.fq.gz Day0.fq.gz Veh_r1.fq.gz Veh_r2.fq.gz Drug_r1.fq.gz Drug_r2.fq.gz \
    --norm-method median \
    --output-prefix screen \
    --trim-5 AUTO

文库文件格式(CSV,列顺序固定):

sgRNA,Sequence,Gene
BRCA1_1,ATGGATTTATCTGCTCTTCG,BRCA1
BRCA1_2,CAGCAGATACTTGATGCATC,BRCA1
NTC_0001,GACGCATCGAATCAATAGCC,NonTargeting_0001

输出文件

文件 内容
screen.count.txt 原始计数矩阵
screen.count_normalized.txt 中位数归一化计数
screen.countsummary.txt 每样本 QC(Gini、reads、mapping rate)

归一化方法选择

--norm-method 适用场景 机制 何时失效
median(默认) 标准筛选,<40% sgRNA 变化 缩放到所有 sgRNA 中位数 重度筛选(>40% 变化)时中位数偏移
control 重度筛选,有 ≥500 条 NTC 用 NTC 中位数缩放 必须提供 --control-sgrna ntcs.txt
total 等测序深度假设 缩放到总 reads 对 PCR jackpot 敏感

诊断流程:先用 median 跑一遍,如果 QC 显示 CEGv2 PR-AUC < 0.5 但 Gini 正常,换用 control 重新归一化。


Step 4: 质量控制

涉及技能

技能 功能
bio-crispr-screens-screen-qc 全方位 QC(Gini、replicate correlation、CEGv2 PR-AUC)

六阶段瓶颈检测

阶段 关键指标 合格阈值 不及格后果
质粒库 Gini, skew, %zero Gini < 0.1, skew < 2, zero < 0.5% sgRNA 丢失无法区分
Day-0 感染 文库覆盖, MOI ≥99% guide 检出, MOI 0.3 奠基者效应
筛选 (puro/blast) 存活率, Gini 漂移 30-40% 存活, ΔGini < 0.05 筛选假象
终点 Replicate 相关性 Pearson ≥ 0.8, Spearman > 0.7 噪声主导, FDR 膨胀
生物学信号 CEGv2 PR-AUC > 0.7 at FDR 5% 筛选无信号
拷贝数假象 扩增区富集 sgRNA 切割数与 depletion 无相关 扩增区假阳性
# Replicate 一致性检查
import pandas as pd
import numpy as np
from scipy.stats import pearsonr, spearmanr

def replicate_concordance(counts_df, groups):
    """检查生物学重复的一致性"""
    results = []
    for group_name, samples in groups.items():
        if len(samples) >= 2:
            log_counts = np.log2(counts_df[samples] + 1)
            for i in range(len(samples)):
                for j in range(i+1, len(samples)):
                    r_pearson, p_p = pearsonr(
                        log_counts[samples[i]], log_counts[samples[j]])
                    r_spearman, p_s = spearmanr(
                        log_counts[samples[i]], log_counts[samples[j]])
                    results.append({
                        'group': group_name,
                        'pair': f"{samples[i]} vs {samples[j]}",
                        'pearson_r': r_pearson,
                        'spearman_r': r_spearman
                    })
    return pd.DataFrame(results)

# 使用
groups = {
    'Vehicle': ['Veh_r1', 'Veh_r2'],
    'Drug': ['Drug_r1', 'Drug_r2']
}
concordance = replicate_concordance(counts, groups)
print(concordance)
# 要求: Pearson >= 0.8, Spearman > 0.7

关键陷阱

  • Plasmid Gini > 0.1 → 文库合成或扩增偏差,筛选不可用
  • Endpoint Gini > 0.30 → 需区分是强生物学选择(药物筛选可接受)还是瓶颈效应
  • CEGv2 PR-AUC < 0.7 → 筛选质量不达标,hit 列表不可信

Step 5: 拷贝数校正(癌症细胞系必备)

涉及技能

技能 功能
bio-crispr-screens-copy-number-correction CRISPRcleanR / CERES / Chronos

为什么必须做

机制(Aguirre 2016, Munoz 2016):
1. 局部扩增 → 某区域有 4-50+ 个拷贝
2. 每条 sgRNA 同时切割所有拷贝
3. 多重切割 → DNA 损伤反应 → G2 期阻滞
4. 细胞停滞 → sgRNA 表现为”必需”(假阳性)

影响范围:所有 Cas9-KO 癌症细胞系筛选,普遍性的,不可通过增加测序深度或重复数修复。

典型案例:ERBB2 在 HER2 扩增的 SK-BR-3 中表现为必需(假阳性)

方法选择

可用数据 推荐方法 理由
无匹配 CN 数据 CRISPRcleanR 无监督,仅用基因组位置
单细胞系 + 匹配 CN CRISPRcleanR 或 Chronos Chronos 更严谨
DepMap 规模(1000+ 系) Chronos 群体动力学 + CN + 筛选质量

CRISPRcleanR 预校正(R)

library(CRISPRcleanR)

# 加载文库注释
data(KY_Library_v1.0)

# 归一化 + 计算折变化
norm_counts <- ccr.NormfoldChanges(
    filename = 'counts.txt',
    min_reads = 30,
    EXPname = 'hct116_screen',
    libraryAnnotation = KY_Library_v1.0
)

# 基因组位置校正
cleaned <- ccr.GWclean(
    norm_counts,
    libraryAnnotation = KY_Library_v1.0,
    saveToPath = 'corrected/'
)

# 校正后的计数可用于 MAGeCK 下游分析

关键陷阱

  • CRISPRi/CRISPRa 无需 CN 校正:不切割 DNA,无 DNA 损伤假象
  • CRISPRcleanR 输出的是校正后的计数,兼容任何下游工具(MAGeCK/BAGEL2/drugZ)
  • MYC 扩增的结直肠细胞系(10+ 拷贝)、FGFR1 扩增的头颈鳞癌细胞系均为典型假阳性

Step 6: Hit Calling

涉及技能

技能 功能
bio-crispr-screens-mageck-analysis MAGeCK RRA(两条件)或 MLE(多条件)
bio-crispr-screens-drugz-chemogenomic drugZ(药物筛选专用)
bio-crispr-screens-bagel-essentiality BAGEL2(必需性分类)
bio-crispr-screens-hit-calling 跨方法决策树 + 共识策略

方法选择决策树

实验设计 首选方法 原因
两条件(药物 vs 载体),单细胞系 drugZ 药物筛选专用,双向 Z,对小效应更敏感
多时间点(Day 0→7→14→21) MAGeCK MLE RRA 无法建模多时间点
多细胞系面板 Chronos 联合建模 CN + 筛选质量
必需性分类(Day 0 → 终点) BAGEL2 贝叶斯分类,CEGv2/NEGv1 校准
多筛联合分析 JACKS 共享 sgRNA 效率

本场景:drugZ + MAGeCK RRA 共识策略

为什么用 drugZ 而非 MAGeCK 作为药物筛选首选:
- drugZ 是双向 Z-score,同时检测致敏(synth)和抗性(supp)
- 对小效应(药物筛选典型 10-30% sgRNA 变化)比 RRA 更敏感
- Colic et al. 2019 基准测试:drugZ hits 对预期 DDR 通路富集远超 MAGeCK/STARS/edgeR

drugZ 分析
python drugz.py \
    -i screen.count.txt \
    -o drugz_results.txt \
    -c Veh_r1,Veh_r2 \
    -x Drug_r1,Drug_r2

drugZ 输出列:

列 含义
gene 基因名
sumZ sgRNA 级 Z-score 之和
normZ 归一化后的 per-gene Z
synth_pval / synth_fdr 致敏方向 p 值 / FDR
supp_pval / supp_fdr 抗性方向 p 值 / FDR
MAGeCK RRA 分析(对照验证)
mageck test \
    --count-table screen.count.txt \
    --treatment-id Drug_r1,Drug_r2 \
    --control-id Veh_r1,Veh_r2 \
    --norm-method median \
    --output-prefix drug_vs_veh \
    --gene-lfc-method median \
    --sort-criteria pos

MAGeCK 输出解读:

列 含义 判定标准
neg\|fdr 负选择(depletion)FDR < 0.05 → 敏感基因
neg\|lfc 负选择 log2FC < -1 → 2 倍以上消耗
pos\|fdr 正选择(enrichment)FDR < 0.05 → 抗性基因
pos\|lfc 正选择 log2FC > 1 → 2 倍以上富集
BAGEL2 必需性分析(补充)
# 1. 计算折叠变化
BAGEL.py fc \
    -i screen.count_normalized.txt \
    -o bagel_fc.txt \
    -c Day0 \
    -e Veh_r1,Veh_r2,Drug_r1,Drug_r2

# 2. 计算 Bayes Factor
BAGEL.py bf \
    -i bagel_fc.txt \
    -o bagel_bf.txt \
    -e CEGv2.txt \
    -n NEGv1.txt

# 3. 精确率-召回率曲线
BAGEL.py pr \
    -i bagel_bf.txt \
    -o bagel_pr \
    -e CEGv2.txt \
    -n NEGv1.txt

BAGEL2 校准:BF > 6 ≈ 90% 后验概率,~5% FDR(Hart 2017 惯例)

共识 Hit 调用

import pandas as pd

def consensus_hits(drugz_path, mageck_path, fdr_threshold=0.05):
    """跨方法共识 hit 调用"""
    # drugZ hits
    drugz = pd.read_csv(drugz_path, sep='\t')
    drugz_synth = drugz[drugz['synth_fdr'] < fdr_threshold]['gene'].tolist()
    drugz_supp = drugz[drugz['supp_fdr'] < fdr_threshold]['gene'].tolist()

    # MAGeCK hits
    mageck = pd.read_csv(mageck_path, sep='\t')
    mageck_neg = mageck[mageck['neg|fdr'] < fdr_threshold]['id'].tolist()
    mageck_pos = mageck[mageck['pos|fdr'] < fdr_threshold]['id'].tolist()

    # 共识
    consensus_sensitizer = set(drugz_synth) & set(mageck_neg)  # 敏感基因
    consensus_resistance = set(drugz_supp) & set(mageck_pos)   # 抗性基因

    # 单方法 hits(需正交验证)
    drugz_only_synth = set(drugz_synth) - set(mageck_neg)
    mageck_only_neg = set(mageck_neg) - set(drugz_synth)

    return {
        'consensus_sensitizers': consensus_sensitizer,
        'consensus_resistance': consensus_resistance,
        'drugz_only': drugz_only_synth,
        'mageck_only': mageck_only_neg
    }

跨方法分歧时的处理

分歧模式 可能原因 处理方式
MAGeCK FDR 显著, BAGEL2 不显著 参考集不同;BAGEL2 训练于 CEGv2/NEGv1 必需性判断信 BAGEL2;一般 LFC 信 MAGeCK
MAGeCK 显著, drugZ 不显著 RRA 对小效应不敏感 药物筛选中信 drugZ
MAGeCK 和 Chronos top 100 一致, 100-300 分歧 Chronos 校正了 CN,MAGeCK 没有 癌症细胞系信 Chronos
RRA 和 MLE 在同一数据上分歧 RRA 对离群值更鲁棒;MLE 更敏感 两方法都排名靠前 = 高可信

Step 7: 通路富集

涉及技能

技能 功能
bio-pathway-go-enrichment GO 富集分析
bio-pathway-kegg-pathways KEGG 通路分析
bio-pathway-gsea GSEA(fgsea)

分析方式

library(clusterProfiler)

# 对 hit 基因做 GO 富集
go_result <- enrichGO(
    gene = hit_genes,
    OrgDb = org.Hs.eg.db,
    ont = "BP",
    pAdjustMethod = "BH",
    pvalueCutoff = 0.05,
    qvalueCutoff = 0.2
)

# KEGG 通路富集
kegg_result <- enrichKEGG(
    gene = hit_genes,
    organism = "hsa",
    pAdjustMethod = "BH"
)

# GSEA:用 MAGeCK 排名(neg|score 或 neg|lfc)
library(fgsea)
ranks <- mageck_results$`neg|lfc`
names(ranks) <- mageck_results$id
fgsea_result <- fgsea(
    pathways = kegg_pathways,
    stats = ranks,
    minSize = 15,
    maxSize = 500
)

关键陷阱

  • GSEA 用排序列表,不用 p 值过滤列表:按 log2FC 或 signed -log10(p) 排序所有基因
  • 对 hit 基因做超几何检验时使用背景基因 = 文库中所有基因,不是全基因组

Step 8: 可视化

涉及技能

技能 功能
bio-data-visualization-volcano-and-ma-plots 火山图
bio-data-visualization-heatmaps-clustering 热图
bio-data-visualization-rank-plots 排名图
import matplotlib.pyplot as plt
import numpy as np

def crispr_volcano(gene_summary_path, direction='neg', fdr_threshold=0.05, lfc_threshold=1.0):
    """CRISPR 筛选火山图"""
    df = pd.read_csv(gene_summary_path, sep='\t')
    lfc_col = f'{direction}|lfc'
    fdr_col = f'{direction}|fdr'

    fig, ax = plt.subplots(figsize=(10, 8))
    sig = (df[fdr_col] < fdr_threshold) & (np.abs(df[lfc_col]) > lfc_threshold)

    ax.scatter(df.loc[~sig, lfc_col],
               -np.log10(df.loc[~sig, fdr_col].clip(lower=1e-10)),
               c='lightgray', alpha=0.3, s=8, label='n.s.')
    ax.scatter(df.loc[sig, lfc_col],
               -np.log10(df.loc[sig, fdr_col].clip(lower=1e-10)),
               c='red' if direction == 'neg' else 'blue',
               alpha=0.7, s=20, label=f'FDR<{fdr_threshold}, |LFC|>{lfc_threshold}')

    top = df.loc[sig].nsmallest(20, fdr_col)
    for _, r in top.iterrows():
        ax.annotate(r['id'], (r[lfc_col], -np.log10(max(r[fdr_col], 1e-10))),
                    fontsize=7, ha='center', va='bottom')

    ax.axhline(-np.log10(fdr_threshold), ls='--', c='black', lw=0.5)
    ax.axvline(0, c='gray', lw=0.5)
    ax.axvline(-lfc_threshold, ls=':', c='gray', lw=0.5)
    ax.axvline(lfc_threshold, ls=':', c='gray', lw=0.5)
    ax.set_xlabel(f'{direction} LFC (log2 fold change)')
    ax.set_ylabel('-log10(FDR)')
    ax.legend()
    return fig

全流程总结图

 ┌────────────┐   ┌────────────┐   ┌─────────────────┐
 │  Step 1    │   │  Step 2    │   │   Step 3        │
 │ 文库设计    │──►│ FASTQ 预处理│──►│  MAGeCK count   │
 │ Brunello   │   │ fastp      │   │  FASTQ→计数矩阵  │
 └────────────┘   └────────────┘   └────────┬────────┘
                                            │
                    ┌───────────────────────▼──────────────┐
                    │         Step 4: QC                   │
                    │  Gini / replicate / CEGv2 PR-AUC     │
                    └───────────────┬──────────────────────┘
                                    │
                    ┌───────────────▼──────────────────────┐
                    │    Step 5: 拷贝数校正 (癌症细胞系)    │
                    │    CRISPRcleanR / Chronos             │
                    └───────────────┬──────────────────────┘
                                    │
              ┌─────────────────────▼──────────────────────┐
              │            Step 6: Hit Calling              │
              │  ┌─────────┐  ┌──────────┐  ┌───────────┐  │
              │  │ drugZ   │  │ MAGeCK   │  │ BAGEL2    │  │
              │  │ (药物)  │  │ RRA/MLE  │  │ (必需性)  │  │
              │  └────┬────┘  └────┬─────┘  └─────┬─────┘  │
              │       └─────┬──────┘              │        │
              │         共识 hits                │        │
              └─────────────────┬─────────────────────────┘
                                │
              ┌─────────────────▼──────────────┐
              │     Step 7: 通路富集            │
              │  GO / KEGG / GSEA               │
              └─────────────────┬──────────────┘
                                │
              ┌─────────────────▼──────────────┐
              │     Step 8: 可视化              │
              │  Volcano / Heatmap / Rank plot  │
              └────────────────────────────────┘

涉及技能一览表

步骤 技能 库 主要功能
Step 1 bio-crispr-screens-library-design bio sgRNA 文库设计
Step 1 bio-genome-engineering-grna-design bio gRNA 设计
Step 2 bio-read-qc-fastp-workflow bio fastp 预处理
Step 2 bio-read-qc-quality-reports bio FastQC/MultiQC
Step 3 bio-crispr-screens-mageck-analysis bio MAGeCK count
Step 4 bio-crispr-screens-screen-qc bio 全方位 QC
Step 5 bio-crispr-screens-copy-number-correction bio CN 校正
Step 6 bio-crispr-screens-mageck-analysis bio MAGeCK RRA/MLE
Step 6 bio-crispr-screens-drugz-chemogenomic bio drugZ 药物筛选
Step 6 bio-crispr-screens-bagel-essentiality bio BAGEL2 必需性
Step 6 bio-crispr-screens-hit-calling bio 跨方法决策树
Step 7 bio-pathway-go-enrichment bio GO 富集
Step 7 bio-pathway-kegg-pathways bio KEGG 通路
Step 7 bio-pathway-gsea bio GSEA
Step 8 bio-data-visualization-volcano-and-ma-plots bio 火山图
Step 8 bio-data-visualization-heatmaps-clustering bio 热图
Pipeline bio-workflows-crispr-screen-pipeline bio 端到端流程

量化阈值速查

阈值 值 来源
Hit FDR(基因级) < 0.05 MAGeCK (Li 2014)
Hit LFC 绝对值 > 1(2 倍) 生物学效应大小
中位数归一化适用条件 < 40% sgRNA 变化 中位数假设
文库覆盖 ≥ 500x/sgRNA Joung 2017
MOI ~0.3 低 MOI 确保单插入
Plasmid Gini < 0.1 MAGeCK-VISPR
Endpoint Gini < 0.30 MAGeCK-VISPR
Replicate Pearson ≥ 0.8 MAGeCK-VISPR
CEGv2 PR-AUC > 0.7 Hart 2017
BAGEL2 BF > 6 ≈ 90% posterior Hart 2017
NTC 数量 ≥ 500 归一化可靠性
每基因 sgRNA 数(MLE) ≥ 3 非零 MAGeCK 收敛要求

本文由 AI 生成,基于 bio-research-suite 技能库中 15 个 CRISPR 筛选相关技能的真实内容(SKILL.md)编写。所有命令、参数、阈值均来源于技能文档原文。

AI生成

20260910 113051 单细胞转录组细胞类型鉴定与轨迹分析全流程实现详解

单细胞转录组细胞类型鉴定与轨迹分析全流程实现详解

基于 bioSkills 技能库(561 个生物信息学技能)的案例 2 实现
涉及技能:7 个 bio-single-cell-* 技能 | 场景:肿瘤组织 10x 单细胞数据


场景假设

研究者从肿瘤组织中获取 10x Genomics 单细胞转录组数据(多个患者样本,不同批次),目标是:

  1. 鉴定组织中存在的细胞亚群(细胞类型注释)
  2. 推断细胞分化轨迹(如肿瘤干细胞 → 分化的肿瘤细胞)
  3. 分析细胞间的通讯关系

数据特征:droplet 平台捕获,含空液滴和环境 RNA 污染,跨样本批次效应明显。


第一步:数据读取与质控

技能:bio-single-cell-data-io + bio-single-cell-preprocessing
工具:Scanpy (Python) / Seurat (R)

1.1 数据读取

import scanpy as sc

# 读取 10x 原始矩阵(未过滤),保留空液滴用于环境RNA估计
adata = sc.read_10x_h5('raw_feature_bc_matrix.h5')
adata.var_names_make_unique()

关键点:读取的是原始(未过滤)液滴矩阵,因为空液滴调取(EmptyDrops)和环境 RNA 去除需要原始计数。

1.2 环境 RNA 去除(SoupX)

library(SoupX)
sc_ <- load10X('cellranger_outs/')   # 需要 raw + filtered 两个矩阵
sc_ <- autoEstCont(sc_)               # 估计污染比例 rho
counts_adj <- adjustCounts(sc_, roundToInt = TRUE)   # 输出非整数,取整给NB模型

关键点:环境 RNA 去除必须在质控之前做,因为它需要 soup 估计(一旦 subet 到细胞,soup 估计就丢失了)。对于固体肿瘤和血液样本尤其重要(血红蛋白、白蛋白等高表达分泌基因会泄漏到每个 cluster)。

1.3 质控过滤

核心原则:不要用固定阈值(如 mito<20%),而要用 MAD(中位数绝对偏差)自适应阈值,且逐样本计算(不同批次深度不同)。

import scanpy as sc
import numpy as np
from scipy.stats import median_abs_deviation

adata.var['mt'] = adata.var_names.str.startswith('MT-')    # 线粒体基因
adata.var['ribo'] = adata.var_names.str.startswith(('RPS', 'RPL'))
adata.var['hb'] = adata.var_names.str.contains(r'^HB[ABDEGMQZ]\d*(?!\w)')   # 血红蛋白亚基

sc.pp.calculate_qc_metrics(adata, qc_vars=['mt', 'ribo', 'hb'],
                           percent_top=[20], log1p=True, inplace=True)

def is_outlier(adata, metric, nmads):
    M = adata.obs[metric]
    return (M < np.median(M) - nmads * median_abs_deviation(M)) | \
           (np.median(M) + nmads * median_abs_deviation(M) < M)

adata.obs['outlier'] = (is_outlier(adata, 'log1p_total_counts', 5) |
                        is_outlier(adata, 'log1p_n_genes_by_counts', 5) |
                        is_outlier(adata, 'pct_counts_in_top_20_genes', 5))
adata.obs['mt_outlier'] = is_outlier(adata, 'pct_counts_mt', 3) | (adata.obs['pct_counts_mt'] > 8)
adata = adata[~(adata.obs['outlier'] | adata.obs['mt_outlier'])].copy()
sc.pp.filter_genes(adata, min_cells=3)   # 去掉在<3个细胞中出现的基因

关键注意点:

指标 参考值 说明
min_genes 200 低于此多为空液滴/碎片
log1p_total_counts 5 MAD 在对数尺度过滤(深度右偏)
pct_counts_in_top_20_genes 5 MAD 高值标记低复杂度/垂死细胞
pct_counts_mt 3 MAD + 硬 >8% 组织依赖:心肌/肝细胞/肌肉天然 20-40% 且健康;细胞核 ~0-2%
min_cells(基因) 3 去掉稀有基因

特别警示:心脏/肝脏/肌肉的实质细胞天然高线粒体(20-40%),固定阈值会整群删掉健康细胞。且高线粒体可能是凋亡、真实代谢活动、解离应激或技术污染的混合,必须组织自适应处理。

1.4 解离应激处理

高温解离会诱导 FOS/JUN/JUNB/EGR1/HSPA1A/B 等即时早期基因表达,制造一个虚假的”活化/应激”cluster。处理方式:先给每个细胞打分解离模块,再从 HVG/聚类中排除这些基因。

1.5 双细胞检测(可选但重要)

在归一化之前、基于原始计数、逐样本进行(双细胞会形成虚假的中间 cluster)。


二、归一化与高变基因

技能:bio-single-cell-preprocessing
工具:Scanpy (Python)

2.1 归一化

# 先保存原始计数(HVG 和 doublet 检测需要)
adata.layers['counts'] = adata.X.copy()

# shifted-log 归一化(默认),sc.pp.normalize_total 将每个细胞缩放到中位数
sc.pp.normalize_total(adata)          # target_sum=None 用中位数
sc.pp.log1p(adata)                     # 自然对数(1+x)

归一化方法选择:

方法 模型/假设 何时用 何时失效
shifted-log (CP10k) 常数总 mRNA 通用默认 组成差异大的细胞类型(浆细胞、增殖细胞)
scran deconvolution pooled size factors 低深度、高 dropout R-only,需预聚类
sctransform v1/v2 NB 正则回归 Seurat 深度去除 慢,v1 过拟合
Pearson residuals 解析残差 UMI 计数 实验性

核心洞察:Ahlmann-Eltze & Huber (2023) 发现 plain shifted-log + PCA 性能与 sctransform 相当甚至更好,所以 shifted-log 是可辩护的默认选择。

2.2 高变基因选择

# seurat_v3 flavor 需要原始计数,必须在 layer 中
sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor='seurat_v3', layer='counts')

大坑:seurat_v3 flavor 吃原始计数,seurat flavor 吃对数归一化数据。喂错输入会静默运行但产出垃圾 HVG。

2.3 缩放与回归(可选,日益不推荐)

现代 best practice 不建议作为默认做回归掉 total_counts 和 pct_counts_mt——这些协变量与真实的细胞身份和状态混杂(增殖细胞确实有更多 RNA)。用批次整合(Harmony/scVI)处理不需要变异,而不是线性回归。


三、批次整合

技能:bio-single-cell-batch-integration
工具:Harmony (Python/R) / scVI (Python)

import scanpy as sc

# Harmony 整合
sc.external.pp.harmony_integrate(adata, key='sample')   # 以样本为批次键
adata.obsm['X_pca_harmony'] = adata.obsm['X_pca_harmony']

整合 vs 回归:整合(Harmony/scVI)替代线性回归来处理不需要的变异,两者在 DE 分析中的定位不同——整合后的数据不能直接喂给 DE(会双重校正),只能用于聚类和可视化。


四、聚类分析

技能:bio-single-cell-clustering
工具:Scanpy (Python)

4.1 PCA 降维

sc.tl.pca(adata, n_comps=50, svd_solver='arpack')
sc.pl.pca_variance_ratio(adata, n_pcs=50, log=True)   # 肘图选择 n_pcs

关键:n_pcs 对结果的影响远大于 n_neighbors。太少会塌缩真实结构,太多会引入技术噪声。用肘图判断,一般 30-50。

4.2 构建邻居图和聚类

sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
# 固定后端,保证可重复
sc.tl.leiden(adata, resolution=0.5, flavor='igraph', n_iterations=2, directed=False)
sc.tl.umap(adata, min_dist=0.3)
sc.pl.umap(adata, color=['leiden', 'CD3D', 'MS4A1', 'CD14'])

Leiden vs Louvain:
- Leiden 是当前默认(保证连通社区),Louvain 可能返回内部不连通社区(Traag 2019)。
- Scanpy 1.10-1.12 中 sc.tl.leiden 默认后端将迁移,必须显式固定 flavor='igraph'。

4.3 分辨率扫描与验证

关键原则:分辨率不是”真相旋钮”,聚类不是”发现”。过度聚类是默认失败模式——任何均匀的 blob 都能被二分,更高分辨率总能再分。

# 扫描分辨率,观察细胞流动
for res in [0.2, 0.4, 0.6, 0.8, 1.0]:
    sc.tl.leiden(adata, resolution=res, key_added=f'leiden_r{res}',
                 flavor='igraph', n_iterations=2, directed=False)
    print(res, adata.obs[f'leiden_r{res}'].nunique(), 'clusters')

# R中用clustree看跨分辨率细胞流动
# clustree(seurat_obj, prefix = 'RNA_snn_res.')

聚类是假设,不是发现。必须用 marker、稳定性、显著性检验验证。不要在 UMAP 坐标上聚类——UMAP 距离是人为目标下的伪影,不是度量。


五、marker 基因检测与细胞注释

输入:bio-single-cell-markers-annotation
工具:Scanpy (Python) / Seurat (R)

5.1 Marker 检测

核心:marker 检测是描述性排名,不是推断!两种被混淆为”DE”的问题必须区分:
- marker 检测(”cluster X 中哪些基因高?”)→ 排名/注释任务,Wilcoxon 可接受
- 条件 DE(”treated vs control 中哪些基因变?”)→ 必须用 pseudobulk(伪bulk)

import scanpy as sc

# 注意:scanpy 默认是 t-test,必须显式用 wilcoxon
sc.tl.rank_genes_groups(adata, groupby='leiden', method='wilcoxon',
                        pts=True, corr_method='benjamini-hochberg')
markers = sc.get.rank_genes_groups_df(adata, group=None)

# 用效应量和表达比例过滤,不用 p 值
specific = markers[(markers['logfoldchanges'] > 1) &
                   (markers['pct_nz_group'] > 0.5) &
                   (markers['pct_nz_reference'] < 0.25)]
print(specific.groupby('group').head(10)[['group', 'names', 'logfoldchanges', 'pct_nz_group', 'pct_nz_reference']])

为什么不用 p 值:cluster 被定义为最大化组间分离,所以在其上测试 marker 是双重暗示(double-dipping),p 值无效(type-I error 随分辨率升高趋近 1)。用效应量和表达比例过滤。

5.2 基因 signature 打分

t_cell_panel = ['CD3D', 'CD3E', 'CD4', 'CD8A', 'CD8B']
sc.tl.score_genes(adata, gene_list=t_cell_panel,
                  ctrl_size=50, n_bins=25, score_name='T_cell_score')

5.3 手动注释

cluster_labels = {'0': 'CD4 T', '1': 'CD14 Mono', '2': 'B', '3': 'CD8 T',
                  '4': 'NK', '5': 'FCGR3A Mono'}
adata.obs['cell_type'] = adata.obs['leiden'].map(cluster_labels).fillna('Unassigned')

5.4 条件 DE(pseudobulk,正确做法)

# 按 sample x cell_type 聚合原始计数(不是归一化值)
cell_type = adata[adata.obs['cell_type'] == 'CD14 Mono']
pseudobulk = sc.get.aggregate(cell_type, by='sample', func='sum')
counts_df = pseudobulk.layers['sum']
# 之后喂给 DESeq2/edgeR/limma-voom,见 deseq2-basics

关键:条件 DE 必须以样本/供者为重复单位,把细胞当重复是伪重复,虚假阳性增加数量级(Squair 2021)。聚合原始计数(sum),绝不能在归一化/校正数据上做 DE。

5.5 常见 PBMC marker(需在数据集中验证)

细胞类型 Markers
CD4 T CD3D, CD4, IL7R
CD8 T CD3D, CD8A, CD8B
B MS4A1, CD79A, CD19
CD14 Mono CD14, LYZ, S100A8
DC FCER1A, CST3

marker 是条件陈述:血液中的 marker 在肿瘤中可能广泛表达,”vs rest” 依赖 “rest” 是什么。移植的 marker 面板必须重新验证。


六、轨迹推断

输入:bio-single-cell-trajectory-inference
工具:Scanpy + Palantir + CellRank (Python) / Slingshot/Monocle3 (R)

6.1 决定拓扑优先(PAGA)

核心原则:连续谱的存在是生物判断,要在排序之前决定。任何方法都会在离散细胞类型或噪声上输出数字;算法不测试连续谱是否存在。

import scanpy as sc
sc.pp.neighbors(adata, n_neighbors=15, use_rep='X_pca')
sc.tl.leiden(adata, resolution=1.0, flavor='igraph', n_iterations=2, directed=False)
sc.tl.paga(adata, groups='leiden')
sc.pl.paga(adata, threshold=0.03, color='leiden')   # 修剪低连通边
sc.tl.umap(adata, init_pos='paga')                  # 全局拓扑保留

threshold 是关键判断:无存活边的孤立 cluster 是离散细胞类型,不是轨迹分支,不能被强行拉入一个排序。

6.2 方法选择(拓扑优先)

方法 模型/假设 使用场景 失效时
PAGA cluster 图连通性 判断连续谱是否存在(强制第一步) 只给连通性不给定方向
Slingshot 簇中心 MST + 主曲线 已知树/分支拓扑 依赖输入聚类
Monocle3 反向图嵌入主图 树/环形/断连拓扑 在 UMAP 空间学习,继承畸变
DPT 可逆扩散随机游走 线性/简单分支 根敏感
Palantir 有向马尔可夫链 分支命运 根敏感
CellRank 2 不可逆马尔可夫链 + GPCCA 有向命运映射 kernel 质量依赖

拓扑选择原则:线性 → Slingshot/DPT;分叉 → Slingshot/Palantir/CellRank2;树状(>2分支)→ Slingshot/Monocle3;环形 → PAGA;未知/断连 → 先 PAGA。

6.3 伪时间(Diffusion Pseudotime)

sc.tl.diffmap(adata, n_comps=15)
# 根细胞用已知 marker 锚定,不在 UMAP 上用眼睛选
adata.uns['iroot'] = np.flatnonzero(adata.obs['cell_type'] == 'HSC')[0]
sc.tl.dpt(adata, n_dcs=10, n_branchings=0)   # n_branchings=0 -> 纯伪时间

核心原则:伪时间是几何,不是时钟。等距伪时间间隔不表示等距真实时间间隔。根细胞的选择会翻转每个基因的趋势——锚定在正交证据(已知 marker、真实采样时间、速度、干性得分),永远不在 UMAP 上靠眼睛选。

6.4 命运概率(Palantir)

import palantir
dm_res = palantir.utils.run_diffusion_maps(adata, n_components=5)
ms_data = palantir.utils.determine_multiscale_space(dm_res)
pr_res = palantir.core.run_palantir(ms_data, early_cell='HSC_cell_id',
                                    terminal_states=None, num_waypoints=1200)
# pr_res.pseudotime, pr_res.entropy, pr_res.branch_probs

关键:在分叉附近用命运概率,不要硬分配分支标签。多能祖细胞的命运真的未确定,硬分配在生物学上错误。

6.5 CellRank 2 有向命运映射

import cellrank as cr
pk = cr.kernels.PseudotimeKernel(adata, time_key='dpt_pseudotime').compute_transition_matrix()
ck = cr.kernels.ConnectivityKernel(adata).compute_transition_matrix()
combined = 0.8 * pk + 0.2 * ck     # 权重是研究者选择,需扫描

g = cr.estimators.GPCCA(combined)
g.compute_macrostates(n_states=10, cluster_key='leiden')
g.predict_terminal_states(method='stability')
g.predict_initial_states(n_states=1)
g.compute_fate_probabilities()
g.compute_lineage_drivers()

6.6 RNA Velocity(可选,谨慎)

import scvelo as scv
scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=2000)
scv.pp.moments(adata, n_pcs=30, n_neighbors=30)
scv.tl.recover_dynamics(adata)
scv.tl.velocity(adata, mode='dynamical')   # 默认是 'stochastic',显式传 'dynamical'
scv.tl.velocity_graph(adata)
scv.tl.velocity_confidence(adata)   # 先检查置信度

警告:velocity 在成熟/终末/非分裂系统(成年神经元、稳态组织)中不可靠,箭头可能反向;干净 2D 流图可能制造出高维场缺乏的连贯性幻觉。不要直接消费原始箭头,要喂给 CellRank 作为内核,用 marker 验证。


七、细胞通讯分析

输入:bio-single-cell-cell-communication
工具:LIANA (Python) + CellChat (R) + NicheNet (R)

7.1 共识推断(默认)

核心原则:每个配受体输出都是共表达代理,不是信号证据。方法间不一致是因为估计的数量不同,不是噪声。默认用共识(LIANA rank_aggregate)。

import liana as li
import scanpy as sc

adata = sc.read_h5ad('adata_annotated.h5ad')
li.mt.rank_aggregate(adata, groupby='cell_type', resource_name='consensus',
                     expr_prop=0.1, use_raw=False, n_perms=1000, verbose=True)

res = adata.uns['liana_res']
# 输出 magnitude_rank 和 specificity_rank(不是单一 liana_rank)
robust = res[(res['specificity_rank'] < 0.05) & (res['magnitude_rank'] < 0.05)]

7.2 资源敏感性检查

from liana.method import cellphonedb
for resource in ['consensus', 'cellphonedb', 'cellchatdb']:
    cellphonedb(adata, groupby='cell_type', resource_name=resource,
                expr_prop=0.1, use_raw=False, key_added=f'cpdb_{resource}', verbose=False)
# 比较 adata.uns['cpdb_*'] 的 top 对——跨资源存活的才可靠

7.3 CellChat 通路级概率

library(CellChat)
cellchat <- createCellChat(object = seurat_obj, group.by = 'cell_type')
cellchat@DB <- CellChatDB.human
cellchat <- subsetData(cellchat)
cellchat <- identifyOverExpressedGenes(cellchat)
cellchat <- identifyOverExpressedInteractions(cellchat)
cellchat <- computeCommunProb(cellchat, type = 'triMean')   # 保守 trim 均值
cellchat <- filterCommunication(cellchat, min.cells = 10)
cellchat <- computeCommunProbPathway(cellchat)
cellchat <- aggregateNet(cellchat)
cellchat <- netAnalysis_computeCentrality(cellchat, slot.name = 'netP')
# netVisual_aggregate(signaling='WNT'), netVisual_bubble(), netAnalysis_signalingRole_heatmap()

7.4 NicheNet 下游配体活性(R)

library(nichenetr)
geneset_oi <- FindMarkers(seurat_obj, ident.1 = 'activated_T', ident.2 = 'naive_T') %>%
    filter(p_val_adj < 0.05, avg_log2FC > 0.5) %>% rownames()
background <- get_expressed_genes('T_cell', seurat_obj, pct = 0.10)

expressed_ligands <- intersect(unique(lr_network$from),
                               get_expressed_genes(c('Macrophage', 'Dendritic'), seurat_obj, 0.10))
expressed_receptors <- intersect(unique(lr_network$to), background)
potential_ligands <- lr_network %>%
    filter(from %in% expressed_ligands, to %in% expressed_receptors) %>%
    pull(from) %>% unique()

ligand_activities <- predict_ligand_activities(
    geneset = geneset_target, background_expressed_genes = background,
    ligand_target_matrix = ligand_target_matrix, potential_ligands = potential_ligands)

7.5 通讯伪信号(confounds)

混杂因素 如何制造虚假交互 缓解
Ambient RNA 高表达分泌基因泄漏到每个 cluster 先 SoupX 去污染
细胞丰度 大 cluster 给更紧凑 null → 成为网络枢纽 下采样,检查 counts vs 大小
测序深度 深度差异混淆技术与生物 整合计数,先归一化深度
解离应激 FOS/JUN/HSP 是真实配体 打分应激模块

八、条件 DE(pseudobulk)

如果要检验肿瘤 vs 正常之间的差异,正确做法是:
1. 按 sample x cell_type 聚合原始计数(sc.get.aggregate(..., func='sum'))
2. 喂给 DESeq2/edgeR/limma-voom(样本=生物重复单位)
3. 绝不在归一化/校正数据上跑 DE(双重校正)


全流程总结

原始 10x 矩阵
  │
  ├─ Step 1: 数据读取 + 环境RNA去除(SoupX)+ 质控(MAD自适应, 逐样本)
  │
  ├─ Step 2: 归一化(shifted-log)+ 高变基因选择(seurat_v3需raw counts)
  │
  ├─ Step 3: 批次整合(Harmony/scVI,整合后不用于DE)
  │
  ├─ Step 4: 聚类(PCA elbow → kNN图 → Leiden,固定后端,分辨率扫描)
  │
  ├─ Step 5: Marker检测(Wilcoxon, 双态校正感知)+ 手动注释(marker面板)
  │
  ├─ Step 6: 轨迹推断(PAGA拓扑 → Slingshot/DPT/Palantir/CellRank2,伪时间)
  │
  └─ Step 7: 细胞通讯(LIANA共识 + CellChat通路 + NicheNet活性)

涉及技能一览

步骤 技能名 工具 核心作用
1 bio-single-cell-data-io Scanpy 数据读取
1-2 bio-single-cell-preprocessing Scanpy/Seurat 质控、归一化、HVG
3 bio-single-cell-batch-integration Harmony/scVI 批次整合
4 bio-single-cell-clustering Scanpy/Seurat 降维+聚类
5 bio-single-cell-markers-annotation Scanpy/Seurat Marker+注释
6 bio-single-cell-trajectory-inference PAGA/Slingshot/Monocle3/CellRank 轨迹+伪时间
7 bio-single-cell-cell-intercommunication LIANA/CellChat/NicheNet 细胞通讯

20260910 113033 宏基因组微生物分类鉴定全流程实现详解

宏基因组微生物分类鉴定全流程实现详解

基于 bioSkills 技能库(561 个生物信息学技能)的案例 3 实现
涉及技能:5 个 bio-metagenomics- / bio-microbiome- 技能 | 场景:肠道宏基因组 shotgun 测序数据


场景假设

研究者拿到一批人类肠道宏基因组(shotgun 全基因组鸟枪法)测序数据(双端 150bp),目标是:

  1. 回答”样本里有哪些微生物”(分类鉴定,物种水平)
  2. 估计各物种的相对丰度(细胞分数 vs 读段分数)
  3. 解析群落的功能潜能(基因家族与代谢通路)
  4. 比较不同分组(健康 vs 疾病)的群落多样性

数据特征:paired-end reads,含宿主污染(人类 DNA)、接头与低质量碱基,需要先质控再去宿主。


第一步:质控与宿主去除(前置必备)

技能:bio-fastq-quality-control + bio-contamination-screening(或 bio-contamination-controls)
工具:fastp + BBDuk / KneadData

1.1 质控

fastp -i R1.fastq.gz -I R2.fastq.gz \
    -o clean_R1.fastq.gz -O clean_R2.fastq.gz \
    --qualified_quality_phred 20 --length_required 50 \
    --detect_adapter_for_pe --thread 8

关键点:
- --length_required 50 保留至少 50 bp 的读段(Kraken2 对短读分类效果差)
- 去接头是分类的前置条件,接头会制造”宿主 DNA”假信号

1.2 宿主去除

bowtie2 -x human_genome_index -1 clean_R1.fastq.gz -2 clean_R2.fastq.gz \
    --un-conc-gz host_removed_R%.fastq.gz --very-sensitive

关键点:宿主 reads 占低生物量样本的绝大部分,不去除会淹没微生物信号;且 GenBank 中 >200 万条记录带有人类/载体污染序列(Steinegger & Salzberg 2020),必须去宿主。


二、Kraken2 分类 + Bracken 丰度重估

技能:bio-metagenomics-kraken(primary_tool: Kraken2)+ bio-abundance-estimation(Bracken)
工具:Kraken2 + Bracken + KrakenTools

2.1 基础分类命令

kraken2 --db "$KRAKEN_DB" \
    --paired --gzip-compressed --threads 8 \
    --confidence 0.1 \            # 从默认0提高到0.1-0.4,抑制单k-mer假阳性
    --minimum-hit-groups 2 \      # 要求>=2个独立hit区域(临床建议3)
    --report out.kreport \
    --output out.kraken \
    R1.fq.gz R2.fq.gz

核心洞察——Kraken 报告是数据库条件的相似性账本,不是样本清单:
1. 分类 ≠ 存在:--confidence 0 时一个共享 k-mer 就把读分类到数据库最近亲属,即使真实生物不存在
2. 读数 ≠ 丰度:counts 随基因组长度和拷贝数变化,报告百分比是片段分数,不是细胞分数。Bracken 修复的是错误分类层级问题,不修复这个
3. 报告尾部的分类单元是假设,不是发现:单区域 hit、哈希碰撞、污染参考序列会填充长尾

2.2 假阳性控制:独特 minimizer

kraken2 --db "$KRAKEN_DB" --paired --confidence 0.1 \
    --report-minimizer-data \     # 插入2列:总 minimizer + 独特 minimizer
    --report out.krep --output /dev/null \
    R1.fq.gz R2.fq.gz
# 高reads但低独特minimizer的物种 = 假阳性(单一保守区域反复命中)

不要硬编码 “>=1024 unique k-mers”——那是临床数据/特定数据库的经验值,不是常量。用阴性对照标定自己的阈值。

2.3 自建数据库

kraken2-build --download-taxonomy --db custom_db
for lib in bacteria archaea viral human UniVec_Core; do
    kraken2-build --download-library "$lib" --db custom_db
done
kraken2-build --build --db custom_db --threads 16
kraken2-build --clean --db custom_db   # 删除 library/ + taxonomy/ 减小体积
bracken-build -d custom_db -t 16 -k 35 -l 150   # -k 必须等于 Kraken2 的 k(35);-l = 读长

大坑:
- --max-db-size 会随机下采样 k-mers 来凑容量,这就是预建 8GB/16GB 数据库的做法,也是为什么高 confidence 会崩溃——读太少无法通过高阈值
- 数据库版本是最大的实验变量:数据库里没有的分类单元,无论怎么调参都检测不到

2.4 交接给 Bracken

bracken -d "$KRAKEN_DB" -i out.kreport -o out.bracken -w out.bracken.kreport \
    -r 150 \   # 必须匹配 built databaseRLENmers.kmer_distrib 且等于真实读长
    -l S -t 10 # species level;-t 是重分配下限,不是置信度

Bracken 修复的只有”错误层级”问题——它把滞留在 genus 的 reads 按基因组先验重新分配到物种。它不修复基因组大小偏差,也不修复假阳性(甚至可能把不存在物种的 reads 重新分配给最近的近亲来放大它)。所以 先做假阳性控制再跑 Bracken。

2.5 常见错误对照表

错误/症状 原因 解决
成百上千个不可能物种 --confidence 0,无 hit-group 下限 提高 confidence;--minimum-hit-groups >=2
Bracken “kmer_distrib file not found” -r 没有匹配的内置分布 bracken-build -l <readlen>
报告列错位 --report-minimizer-data 插入了2列 用 8 列布局解析
小型数据库近乎零分类 截断数据库 + 高 confidence 用大数据库,或降低 confidence

三、MetaPhlAn 4:标记基因法(互补视角)

技能:bio-metagenomics-metaphlan(primary_tool: MetaPhlAn)
工具:MetaPhlAn 4.1+ / Bowtie2 / minimap2

3.1 基本分类

# paired-end 作为 ONE 逗号分隔参数(MetaPhlAn 按单端处理,不用配对信息)
metaphlan reads_R1.fastq.gz,reads_R2.fastq.gz \
    --input_type fastq \
    --index mpa_vJun23_CHOCOPhlAnSGB_202403 \   # 钉住 index,数据库版本是批次变量
    --nproc 8 \
    --mapout sample.map.bz2 \                    # 缓存 read->marker 映射(4.2 前是 --bowtie2out)
    --output_file profile.txt

核心洞察——MetaPhlAn 百分比是细胞分数,不是读段分数:
- 标记基因测 WHO 在(细胞);k-mer 测 HOW MUCH DNA 在(读段)
- MetaPhlAn 的百分比是基因组大小归一化的分类丰度;Kraken/Bracken 的百分比是 reads 的序列丰度
- 两者不可合并:它们衡量不同量纲的东西(Sun 2021 Nat Methods 18:618),合并成一张表互相比对是常见错误

3.2 SGB 单位

MetaPhlAn 4 的原子分类单位是 SGB(物种级基因组分箱,~95% ANI 簇):
- kSGB 有培养参考基因组、有拉丁名
- uSGB 只由 MAG 定义(需要 >=5 个 MAG),报告为占位 ID 没有名字——不要丢弃 uSGB 行,这是 MetaPhlAn 4 的核心创新,能解释 ~20% 更多肠道读段

3.3 从映射缓存重剖分

metaphlan sample.map.bz2 --input_type mapout \   # 前4.2是 bowtie2out
    --tax_lev s \            # k,p,c,o,f,g,s,t
    --stat_q 0.2 \          # 截断均值:去掉上下20%,平均中间60%
    --output_file profile_species.txt

保存一次 --mapout,之后不同层级/估计器设置无需重新比对——重比对是昂贵步骤,下游全部免费。

3.4 未知分数的缩放效应

  • 未知估计关闭(4.2 前默认):已知 taxa 吸收 100%,数据库缺失群落不可见,高估每个已知分类单元
  • 未知估计开启(4.2 默认):出现 UNCLASSIFIED 行,每个已知丰度按比例缩小
  • 在土壤/海洋/瘤胃中,未知分数可能是样本中最大的”taxon”
  • 4.2 两个 flag 重命名:--bowtie2out → --mapout;--bowtie2db → --db_dir

3.5 MetaPhlAn 常见错误

错误 原因 解决
与 Kraken 百分比合并 分子分数 vs 读段分数,量纲不同 分开放;如需统一用基因组长度换算
低映射率 = QC 失败 只打标记基因,低映射正常 低映射正常;大未知 = 数据库缺失
合并后行不齐 不同 index 构建的 profiles 钉住一个 index 全部重建

四、HUMAnN 3:功能谱分析

技能:bio-metagenomics-functional-profiling(primary_tool: HUMAnN)
工具:HUMAnN 3.6+ / MetaPhlAn / DIAMOND / Bowtie2

4.1 分层搜索算法

HUMAnN 不是对每条读段暴力翻译。三个层级逐层过滤:

  1. 分类学预筛(MetaPhlAn):检测物种,然后只对高于 --prescreen-threshold(默认 0.01%)的物种构建样本特异 ChocoPhlAn 泛基因组
  2. 核苷酸泛基因组搜索(Bowtie2):比对到泛基因组的读段得到 UniRef90 家族 + 高置信物种标签
  3. 翻译蛋白搜索(DIAMOND):未通过的读段 6 框翻译后比对全 UniRef90/50;只有家族,物种是推断的 |unclassified

4.2 运行命令

# 预QC:接头/质量trim + 去宿主(例如 KneadData)。宿主读段膨胀 UNMAPPED 且浪费 DIAMOND 时间
cat sample_R1.fq.gz sample_R2.fq.gz > sample.fq.gz   # 配对读段没有原生配对,拼接成单文件
humann --input sample.fq.gz --output out/ \
    --taxonomic-profile sample_metaphlan.tsv \   # 复用 MetaPhlAn profile;不要 --remove-temp-output
    --threads 8   # 默认:prescreen 0.01, translated-id 80, gap-fill on, minpath on

# 归一化:跨样本统计前先 PER SAMPLE 归一化(RPK 是深度依赖的)
humann_renorm_table -i out/sample_genefamilies.tsv -o out/sample_cpm.tsv -u cpm
humann_join_tables -i out -o merged_pathabundance.tsv --file_name pathabundance
humann_regroup_table -i merged_pathabundance.tsv -g uniref90_ko -o merged_ko.tsv   # 会加 UNGROUPED 行
humann_split_stratified_table -i merged_pathabundance.tsv -o .   # 统计跑在 UNSTRATIFIED 文件上

4.3 不要丢弃 UNMAPPED/UNINTEGRATED

import pandas as pd

df = pd.read_csv('merged_pathabundance_unstratified.tsv', sep='\t', index_col=0)
meta = pd.read_csv('metadata.tsv', sep='\t', index_col=0)
unmapped = df.loc[['UNMAPPED', 'UNINTEGRATED']]     # 分母,检查但不要丢弃
g1 = meta.index[meta['condition'] == 'healthy']
g2 = meta.index[meta['condition'] == 'disease']
unmapped_shift = unmapped[g2].mean(axis=1) - unmapped[g1].mean(axis=1)
print(unmapped_shift.round(1))
# 然后交给 MaAsLin2/ANCOM-BC(对组分与零膨胀建模),而不是裸 t-test/Mann-Whitney

为什么不能丢:丢弃后重新归一化会把已分配的特征重新缩放到和为 1,凭空制造丰度并抹掉数据库覆盖信号。两个样本 UNMAPPED 20% vs 60% 会看起来一模一样。

4.4 功能谱 = 潜力,不是活性

一个基因家族或”完整通路”是该群落编码的能力——从来不是速率、通量或表达证据。RNA(宏转录组)才说表达。叙述”疾病微生物组上调 X”是绝对错误——它只是更丰富地携带了该基因。


五、多样性分析(α 和 β 多样性)

技能:bio-microbiome-diversity-analysis(primary_tool: phyloseq)
工具:QIIME2 / phyloseq / vegan / scikit-bio

5.1 三个”把手”决定答案

一个多样性值不是测量,而是三个选择的输出:
1. 抽平深度(rarefaction depth)
2. 系统发育树(tree)
3. 度量(metric)

--p-sampling-depth 是”披着归一化外衣的样本删除旋钮”:core-metrics 把每个样本稀平到该深度,并静默丢弃总计数低于它的样本——不警告,只是 PCoA 上少几个点。丢弃的通常是最低产出的样本(最低生物量的拭子、病重患者、失败提取),丢失几乎从不随机。

5.2 选择抽样深度

qiime feature-table summarize --i-table table.qza --o-visualization table.qzv  # 深度信息在这
qiime diversity alpha-rarefaction \
    --i-table table.qza --i-phylogeny rooted-tree.qza \
    --p-max-depth 20000 \
    --m-metadata-file metadata.tsv --o-visualization alpha-rarefaction.qza
qiime diversity core-metrics-phylogenetic \
    --i-phylogeny rooted-tree.qza --i-table table.qza \
    --p-sampling-depth 10000 \    # 在 observed-features 平台上;会静默丢弃低深度样本
    --m-metadata-file metadata.tsv --output-dir core-metrics-results

规则:深度要在 observed-features 平台的饱和区;不要用 min(sample_sums)——一个小文库会把所有样本拖到欠饱和噪声;报告丢弃样本列表;在邻近深度确认结论。

5.3 建树(建模选择)

优先 SEPP 片段插入到全长参考树(或 Greengenes2),而不是 de novo MAFFT+FastTree。~250bp 读长的 de novo 树分辨率差、中点根任意,会让 unweighted UniFrac 不稳定(Janssen 2018)。

qiime fragment-insertion sepp \
    --i-representative-sequences rep-seqs.qda \
    --i-reference-database sepp-refs-gg-13-8.qda \
    --p-threads 4 \
    --o-tree insertion-tree.qda --o-placements insertion-placements.qda
qiime fragment-insertion filter-features \
    --i-table table.qda --i-tree insertion-tree.qda \
    --o-filtered-table table-sepp.qda --o-removed-table removed-table.qda

5.3 α 多样性(R)

library(phyloseq); library(vegan)
ps_rare <- rarefy_even_depth(ps, sample.size = chosen_depth, rngseed = 42, replace = FALSE)
alpha <- estimate_richness(ps_rare, measures = c('Observed', 'Shannon', 'InvSimpson'))
alpha$Group <- sample_data(ps_rare)$Group
alpha$Shannon_eff <- exp(alpha$Shannon)   # 有效种数(Hill q=1),跨工具可比
kruskal.test(Shannon ~ Group, data = alpha)

QIIME2 的 Shannon 是 log2(bits),R 是自然对数(nats)——数值差一个常数因子,直接比较看起来像真实效应。报告 exp(H') 可绕过基数问题。

5.4 β 多样性(必须加权 + 非加权都要报告)

wu  <- UniFrac(ps_rare, weighted = TRUE)    # 优势谱系视角
uwu <- UniFrac(ps_rare, weighted = FALSE)   # 稀有谱系 + 拓扑视角
gu  <- as.dist(GUniFrac::GUniFrac(t(as(otu_table(ps_rare), 'matrix')), phy_tree(ps_rare), alpha = 0.5)$unifracs[, , 'd_0.5'])

meta <- data.frame(sample_data(ps_rare))
adonis2(wu ~ Group, data = meta, permutations = 999)   # 显著 = 质心位置差异
permutest(betadisper(wu, meta$Group))                  # 强制:是离散度差异还是位置差异?

如果不做 betadisper,adonis2 的显著可能只是”一组变异更大”而不是”群落组成不同”——尤其不平衡设计(Anderson & Walsh 2013)。unweighted 与 weighted 结论相反时,说明变化在稀有谱系 vs 优势谱系,必须都报告并说明各自暗示的谱系轴。


六、可视化与群落统计

技能:bio-metagenomics-visualization(primary_tool: phyloseq)
工具:phyloseq / vegan / microViz / pandas + scikit-bio / matplotlib

6.1 诚实的组成图

Krona 是”里面有什么”的最诚实答案——完整可钻取层级,而不是预先折叠到 top-10 的柱状图:

krepdeport2krona.py -r kraken.kreport -o krona.txt && ktImportText krona.txt -o krona.html

柱状图规则:把稀有 taxa 折叠成”Other”时必须标注隐藏了多少 taxa 和多少百分比;每 n 展示(绝不叠加一个代表样本);≤12 种色盲安全色;如果有总生物量数据就同时展示绝对负载(相对丰度会隐藏群落 bloom/collapse 的真实幅度)。

6.2 α 多样性:Hill 数 + 丰富度诚实性

alpha <- estimate_richness(ps_counts, measures = c('Shannon', 'InvSimpson'))
hill_q1 <- exp(alpha$Shannon)   # 有效种数 q=1
hill_q2 <- alpha$InvSimpson     # q=2 优势度加权

shotgun richness 是数据库 + 置信度阈值的读出,不是生物学:25-70% 的 shotgun 物种可能是假阳性。Observed/Chao1 只能用在整数 counts(Bracken 可以,MetaPhlAn 百分比不行)。

6.3 β 多样性:组成学正确选择

距离 组成学正确 需要树 何时
Bray-Curtis 否 否 领域默认、直观;标注不协调
Aitchison(CLR 上的欧氏) 是 否 组成学正确选择,需处理零
RPCA (DEICODE) 是 否 稀疏无伪计数
UniFrac(加权/非加权) 部分 是 仅当有树(MetaPhl SGB 树)
library(vegan)
dist_bc <- vegdist(otu_matrix, method = 'bray')
pm <- adonis2(dist_bc ~ group, permutations = 999, by = 'terms')
bd <- betadisper(dist_bc, group); permutest(bd, permutations = 999)  # 必须配对

6.4 Python 组成学排序

import pandas as pd
from skbio.stats.composition import clr, multi_replace
from sklearn.decomposition import PCA

ab = pd.read_csv('merged_abundance.txt', sep='\t', index_col=0)
ab = ab[ab.index.str.contains(r'\|s__') & ~ab.index.str.contains(r'\|t__')]  # 物种行
proportions = (ab.T.values / ab.T.values.sum(axis=1, keepdims=True))
clr_mat = clr(multi_replace(proportions))      # 零替换 + CLR(不是 StandardScaler)
pca = PCA(n_components=2).fit(clr_mat)
coords = pca.transform(clr_mat)

6.5 差异丰度:共识,不是单工具

DA 方法跨数据集中差异巨大(Nearing 2022):报告的显著 taxa 更依赖工具而非生物学。至少跑两个组成学感知的方法,交集 = 高置信,并集 = 探索性,全部报告。ALDEx2 与 ANCOM-II 最保守/一致;LinDA 与 ANCOM-BC FDR 控制好。禁止:未校正的 Wilcoxon/t-test 直接作用于原始相对丰度——一行代码同时错在组成学和多重检验上。

工具 模型 引用
ALDEx2 Dirichlet 蒙特卡洛 → CLR Fernandes 2014 Microbiome
ANCOM-BC log-linear + 抽样分数偏倚校正 Lin & Peddada 2020 Nat Commun
MaAsLin2 一般线性模型 + 协变量 Mallick 2021 PLoS Comput Biol
LinDA CLR 上线性模型 + 偏倚校正 Zhou 2022 Genome Biol

七、全流程总结

原始 shotgun reads(R1+R2)
  │
  ├─ Step 1: fastp 质控(去接头、去低质量)→ Bowtie2 宿主去除
  │
  ├─ Step 2: Kraken2 分类(--confidence 0.1 --minimum-hit-groups 2)
  │          → Bracken 丰度重估计(-r 读长匹配)
  │          → 假阳性控制(--report-minimizer-data / KrakenUniq)
  │
  ├─ Step 3: MetaPhlAn 4 标记基因剖面(细胞分数,uSGB 量化未知 taxa)
  │          (注意:与 Kraken 分数不可合并)
  │
  ├─ Step 4: HUMAnN 3 功能谱(MetaPhl 预筛 → Bowtie2 泛基因组 → DIAMOND)
  │          → 保留 UNMAPPED/UNINTEGRATED → 归一化到 CPM
  │
  ├─ Step 5: 多样性(α: Hill q=0/1/2;β: adonis2 + betadisper 配对)
  │
  └─ Step 6: 可视化(Krona / 堆叠柱 / CLR-PCA / 差异丰度共识)

涉及技能一览

步骤 技能名 工具 核心作用
1 bio-metagenomics-kraken Kraken2 + Bracken k-mer 分类 + 丰度重估计
1-2 bio-metagenomics-metaphlan MetaPhlAn 4 标记基因分类(细胞分数)
3 bio-metagenomics-functional-profiling HUMAnN 3 功能基因 + 通路丰度
4 bio-microbiome-diversity-analysis QIIME2/phyloseq α/β 多样性
5 bio-metagenomics-visualization phyloseq/vegan/microViz 可视化 + 群落统计

三步走总结

  1. “谁在里面”:Kraken2 + Bracken(读段分数)+ MetaPhlAn(细胞分数)双视角
  2. “能做什么”:HUMAnN 功能谱(潜力,不是活性)
  3. “怎么比较”:多样性 + 排序 + 差异丰度共识

20260910 110123 Rna Seq 差异表达分析全流程实现详解

基于 bioSkills 技能库(561 个生物信息学技能)的案例 1 实现
涉及技能:9 个 bio-* 技能 | 场景:肿瘤 vs 正常组织 bulk RNA-seq


场景假设

研究者在肿瘤组织(treat)和正常组织(ctrl)各取 3 个生物学重复样本,进行 bulk RNA-seq 测序(Illumina NovaSeq,PE150),目标是找出两组间的差异表达基因并定位受影响的生物学通路。


第一步:原始数据质控

技能:bio-read-qc-quality-reports + bio-fastq-quality
工具:FastQC + MultiQC + Biopython

1.1 逐文件 QC(FastQC)

# 对6个样本的R1/R2共12个FASTQ文件运行FastQC
fastqc -t 8 -o qc/raw/ raw_data/*.fastq.gz

FastQC 生成每个文件的 HTML 报告,包含 10 个模块(per-base quality、GC content、duplication、adapter content 等)。

关键注意点:

  • FastQC 的 pass/warn/fail 红绿灯是为 WGS DNA 校准的,RNA-seq 数据必然在 per-base content(随机六聚体偏倚)和 duplication(高表达转录本)模块上报红,这是正常的,不是数据问题。
  • NovaSeq/NextSeq 是 2-color chemistry,质量值是分箱的(只有 4 个值),箱线图看起来”方块化”是正常的。

1.2 汇总报告(MultiQC)

multiqc qc/raw/ -o qc/multiqc/ -f

MultiQC 把所有样本的报告聚合到一张总览表,方便横向比较。先读 General Statistics 表,看每个样本的 reads 数、Q30 比例、GC 含量是否有离群值。

1.3 编程级质量分析(Biopython)

from Bio import SeqIO
from collections import defaultdict

# 确认编码格式:2011年后的Illumina数据均为Phred+33('fastq')
# 千万不要猜编码——从仪器元数据确认

# 计算每个read位置的平均质量,识别3'端衰减
position_quals = defaultdict(list)
for record in SeqIO.parse('sample1_R1.fastq', 'fastq'):
    for i, q in enumerate(record.letter_annotations['phred_quality']):
        position_quals[i].append(q)

for pos in sorted(position_quals)[:20]:
    quals = position_quals[pos]
    print(f'Position {pos}: mean={sum(quals)/len(quals):.1f}')

关键判断标准:

Phred 分数 错误率 准确率
Q20 1/100 99%
Q30 1/1000 99.9%
Q40 1/10000 99.99%

Q30 比例是常规质量目标。


第二步:数据预处理

技能:bio-read-qc-fastp-workflow
工具:fastp(一步完成接头修剪 + 质量过滤 + poly-G 去除 + QC 报告)

# NovaSeq RNA-seq 预处理:轻修剪(aligner 会 soft-clip,不要 --dedup)
fastp -i raw_data/sample1_R1.fq.gz -I raw_data/sample1_R2.fq.gz \
      -o clean/sample1_R1.fq.gz -O clean/sample1_R2.fq.gz \
      --detect_adapter_for_pe \
      --trim_poly_g \
      -q 20 -l 50 \
      -w 8 \
      -h clean/sample1.html -j clean/sample1.json

关键参数解释:

  • --detect_adapter_for_pe:fastp 通过 R1/R2 overlap 分析自动检测接头,不需要手动指定接头序列,连只有 1 个碱基的 read-through 也能切掉。
  • --trim_poly_g:NovaSeq 是 2-color chemistry,G 是无信号碱基,poly-G 尾巴质量过滤器看不到但必须去除。
  • -q 20 -l 50:质量低于 Q20 的碱基过滤,过滤后短于 50bp 的 read 丢弃。
  • 绝对不加 --dedup:fastp 的 --dedup 是序列级去重,会删掉高表达转录本的真实 reads,RNA-seq 中绝不能用。

预处理后再 QC

fastqc -t 8 -o qc/trimmed/ clean/*.fastq.gz
multiqc qc/ -o qc/compare/ -f   # 同时对比 raw vs trimmed

第三步:基因组比对

技能:bioinformatics-align-star
工具:STAR(splice-aware aligner)

3.1 构建 STAR 索引(一次性)

# sjdbOverhang = readlength - 1 = 150 - 1 = 149
STAR --runMode genomeGenerate \
     --genomeDir star_index/ \
     --genomeFastaFiles GRCh38.fa \
     --sjdbGTFfile gencode.v44.annotation.gtf \
     --sjdbOverhang 149 \
     --runThreadN 16

关键点:--sjdbOverhang 应设为 read 长度减 1,决定剪接位点侧翼序列长度,直接影响 junction 检测灵敏度。

3.2 比对

for sample in sample1 sample2 sample3 sample4 sample5 sample6; do
  STAR --genomeDir star_index/ \
       --readFilesIn clean/${sample}_R1.fq.gz clean/${sample}_R2.fq.gz \
       --readFilesCommand zcat \
       --outFileNamePrefix aligned/${sample}_ \
       --outSAMtype BAM SortedByCoordinate \
       --quantMode GeneCounts \
       --twopassMode Basic \
       --runThreadN 8
done

关键参数:

  • --outSAMtype BAM SortedByCoordinate:直接输出按坐标排序的 BAM。
  • --quantMode GeneCounts:STAR 顺便生成 per-gene 计数,第 2 列是无链计数、第 3 列正向链、第 4 列反向链——可据此判断 library strandedness。
  • --twopassMode Basic:第一遍比对发现新剪接位点,第二遍用扩展后的 junction 注释重新比对,提高灵敏度。

STAR 特性:唯一比对的 read 获得 MAPQ=255,这在 SAM 规范中意为”MAPQ 不可用”,但对 STAR 而言就是”唯一比对”。对后续 featureCounts 计数无影响,但做 RNA variant calling 时 GATK 可能对 255 有不同处理。


第四步:基因水平定量

技能:featurecounts-counting
工具:featureCounts(Subread)

featureCounts -a gencode.v44.annotation.gtf \
              -o counts.txt \
              -s 2 \
              -p --countReadPairs \
              -T 8 \
              aligned/sample1_Aligned.sortedByCoord.out.bam \
              aligned/sample2_Aligned.sortedByCoord.out.bam \
              aligned/sample3_Aligned.sortedByCoord.out.bam \
              aligned/sample4_Aligned.sortedByCoord.out.bam \
              aligned/sample5_Aligned.sortedByCoord.out.bam \
              aligned/sample6_Aligned.sortedByCoord.out.bam

关键参数:

  • -s 2:反向链特异性。大部分 dUTP/TruSeq 文库是反向链特异性建库。如果链方向选错,计数会减半。不确定时用 RSeQC infer_experiment.py 先验证,或看 STAR GeneCounts 的第 3/4 列哪个数字大。
  • -p --countReadPairs:paired-end 数据按 fragment(read pair)计数,而不是按单条 read 计数(不加 -p 会导致计数翻倍)。

验证输出:

cat counts.txt.summary

正常 poly-A 文库的 Assigned 比例应在 70-90%。如果只有 ~50%,很可能链方向选错了。


第五步:计数矩阵质控

技能:count-matrix-qc
工具:DESeq2 (R) + Python

5.1 加载并检查

library(DESeq2)
library(edgeR)

counts <- read.csv('counts.txt', sep='\t', row.names=1, skip=1)
counts <- counts[, 6:11]  # 去掉前5列元数据列,只保留计数列
coldata <- data.frame(condition = factor(c('ctrl','ctrl','ctrl','treat','treat','treat')),
                      row.names = colnames(counts))

dds <- DESeqDataSetFromMatrix(countData = counts, colData = coldata, design = ~condition)

colSums(counts(dds))        # 每样本总reads
colSums(counts(dds) > 0)    # 每样本检出基因数

5.2 过滤低表达基因

# 用 edgeR 的 filterByExpr 做有原则的过滤,优于硬编码 CPM>1
keep <- filterByExpr(counts(dds), group = dds$condition)
dds <- dds[keep, ]
cat("保留基因数:", sum(keep), ",过滤基因数:", sum(!keep), "\n")

5.3 归一化和变换

# VST(方差稳定变换)用于 QC 可视化,不是用于 DE 检验
# n>30 用 vst(),n<30 用 rlog()——本例 n=6 偏小,但 vst 通常也能用
vsd <- vst(dds, blind = TRUE)   # blind=TRUE 用于无监督QC

关键区分:CPM/TPM 不校正组成偏倚,只按样本内总量缩放,只能用于可视化,不能用于 DE 归一化。DESeq2 用 median-of-ratios 估计 size factor 做归一化,这在 DESeq() 内部自动完成。

5.4 PCA 和样本关系

plotPCA(vsd, intgroup = 'condition')

# 样本距离热图
pheatmap::pheatmap(as.matrix(dist(t(assay(vsd)))), main = 'Sample distance (VST)')

判断标准:

PCA 模式 含义 行动
按 condition 分开 信号强 继续
按批次分开 批次主导 在 design 中加入 batch
某个样本离群 异常或样本错配 查QC、查size factor

5.5 离群值检测

dds <- DESeq(dds)
cooks <- assays(dds)[['cooks']]   # 注意:不是 results(dds)$cooksd
boxplot(log10(cooks), las = 2, main = "Cook's distance per sample")

5.6 批次效应处理

# 已知批次:放入 design 公式,让引擎在原始计数层面校正
design(dds) <- ~ batch + condition   # condition 在最后 = 感兴趣的对比

# 绝不要对 DE 输入数据使用 removeBatchEffect() 或 ComBat 后再喂给 DESeq2
# 那样会双重校正

致命情况:如果 batch 与 condition 完全混杂(比如所有 treat 在 batch A,所有 ctrl 在 batch B),统计上无法补救。


第六步:差异表达检验

技能:deseq2-basics
工具:DESeq2 (R)

library(DESeq2)

# 重新构建(如果上面改了 design)
dds <- DESeqDataSetFromMatrix(countData = counts, colData = coldata, design = ~condition)
dds <- DESeq(dds)

# 提取结果:显式命名对比,alpha=0.05
res <- results(dds, contrast = c('condition', 'treat', 'ctrl'), alpha = 0.05)
summary(res)

# LFC 收缩(apeglm):降噪但不改变 p 值
res_shrunk <- lfcShrink(dds, coef = 'condition_treat_vs_ctrl', type = 'apeglm')

两个关键陷阱:

  1. 默认 results() 静默使用最后一个系数——一定要用 contrast= 显式命名想要的对比,否则可能拿到错误的系数。
  2. 收缩后的 LFC 使用的是未收缩 Wald 检验的 p 值——p 值不变,不要在收缩后的值上做基于阈值的 FDR 声明。LFC 收缩是为了 ranking 和可视化,不是为了重新检验。

Wald vs LRT 选择:

检验 适用场景 特点
Wald 两组对比(默认) 检验单个系数是否为 0
LRT 多水平因子/综合检验 需要 reduced 模型,检验”任何差异”

七步:结果可视化

library(DESeq2)
library(EnhancedVolcano)
library(ggplot2)

7.1 离散度诊断

plotDispEsts(dds)

看基因级离散度(黑点)是否跟随拟合趋势线(红线),收缩后(蓝点)是否被拉向趋势——健康的表现。

7.2 p 值直方图

res_df <- as.data.frame(res)
ggplot(res_df, aes(x = pvalue)) + geom_histogram(bins = 50) + theme_bw()

零假设下应为均匀分布 + 0 附近一个尖峰。如果是 U 形,说明有隐藏批次效应需要加入协变量。

7.3 MA 图

# 未收缩 vs 收缩对比
par(mfrow = c(1, 2))
plotMA(res, ylim = c(-5, 5), main = 'Unshrunken')
plotMA(res_shrunk, ylim = c(-5, 5), main = 'apeglm shrunk')

收缩后低计数基因的噪声被拉向 0,云更紧凑。

7.4 火山图

library(EnhancedVolcano)
EnhancedVolcano(res_shrunk,
    lab = rownames(res_shrunk),
    x = 'log2FoldChange',
    y = 'pvalue',          # 注意:用未收缩的 p 值
    pCutoff = 0.05,
    FCcutoff = 1,
    max.overlaps = Inf)

关键标注:x 轴标注”shrunken log2FC (apeglm)”,y 轴标注”unshrunken Wald p-value”——收缩不重新计算 p 值。

7.5 差异基因热图

# 取 top 50 差异基因
top_genes <- head(order(res$padj), 50)
mat <- assay(vsd)[top_genes, ]
pheatmap::pheatmap(mat, scale = 'row',     # 行z-score:看pattern
                   annotation_col = coldata,
                   show_rownames = FALSE)

7.6 单基因表达图

# 查看某个关键基因的表达模式
plotCounts(dds, gene = 'TP53', intgroup = 'condition')

第八步:GSEA 通路富集

技能:pathway-gsea
工具:clusterProfiler (R)

8.1 构建排序列表

library(clusterProfiler)
library(org.Hs.eg.db)

# 用 DESeq2 的 stat(Wald 统计量)作为排名指标
# 关键:用带符号的、方差校准的统计量,不用原始 p 值
gl <- res$stat
names(gl) <- rownames(res)
gl <- sort(gl, decreasing = TRUE)

# 去掉 NA
gl <- gl[!is.na(gl)]

关键原则:排名指标必须是有符号(signed)且方差校准的统计量(如 DESeq2 的 stat、limma 的 t),绝不能用原始 p 值——p 值没有方向性。

8.2 运行 GSEA

# GO Biological Process
set.seed(42)   # 必须设种子,保证可重复
gse_go <- gseGO(geneList = gl,
                OrgDb = org.Hs.eg.db,
                ont = 'BP',
                minGSSize = 10,
                maxGSSize = 500,
                seed = TRUE)
gse_go <- setReadable(gse_go, OrgDb = org.Hs.eg.db)

# KEGG 通路
gse_kegg <- gseKEGG(geneList = gl,
                    organism = 'hsa',
                    minGSSize = 10,
                    maxGSSize = 500,
                    seed = TRUE)

8.3 查看结果

head(gse_go@result, 10)
列名 含义
enrichmentScore (ES) 富集得分
NES 归一化富集得分(跨基因集可比)
pvalue 原始 p 值
p.adjust FDR 校正后 p 值
leading_edge 核心驱动基因
core_enrichment leading edge 中的基因列表

8.4 可视化

# 气泡图
dotplot(gse_go, showCategory = 20) + ggtitle('GSEA GO BP')

# GSEA 运行得分曲线
library(enrichplot)
gseaplot(gse_go, by = 'count', geneSetID = 1)

全流程总结

原始 FASTQ
  │
  ├─ Step 1: FastQC + MultiQC 质控报告
  │          (RNA-seq 在 per-base content / duplication 报红是正常的)
  │
  ├─ Step 2: fastp 一步预处理
  │          (自动接头修剪 + poly-G去除 + 质量过滤,绝不 --dedup)
  │
  ├─ Step 3: STAR 比对
  │          (sjdbOverhang=149, twopassMode, --quantMode GeneCounts)
  │
  ├─ Step 4: featureCounts 定量
  │          (-s 2 反向链, -p 按fragment计数, 验证Assigned>70%)
  │
  ├─ Step 5: 计数矩阵 QC
  │          (filterByExpr过滤, VST变换, PCA聚类, Cook's离群值, 批次检查)
  │
  ├─ Step 6: DESeq2 差异检验
  │          (Wald检验, alpha=0.05, apeglm LFC收缩, 显式contrast)
  │
  ├─ Step 7: 可视化
  │          (离散度图, p值直方图, MA图, 火山图, 热图, plotCounts)
  │
  └─ Step 8: GSEA 通路富集
             (用 stat 排名, clusterProfiler gseGO/gseKEGG, NES+FDR判定)

涉及技能一览

步骤 技能名 工具 核心作用
1 bio-fastq-quality Biopython 编程级质量分析
1 bio-read-qc-quality-reports FastQC/MultiQC 全局质控报告
2 bio-read-qc-fastp-workflow fastp 一步预处理
3 bio-read-alignment-star-alignment STAR 剪接感知比对
4 bio-rna-quantification-featurecounts-counting featureCounts 基因水平定量
5 bio-rna-quantification-count-matrix-qc DESeq2 矩阵质控+PCA
6 bio-differential-expression-deseq2-basics DESeq2 差异检验+LFC收缩
7 bio-differential-expression-de-visualization DESeq2+ggplot2 诊断与结果图
8 bio-pathway-gsea clusterProfiler GSEA通路富集

20260901 104042 抖音作品采集工具提示词

请开发一个基于 Flask 的抖音作品采集工具,满足以下全部需求:

## 1. 核心功能
- 用户输入抖音**作者主页链接**或**单个作品链接**,系统自动采集该链接下的所有作品(视频/图集)并下载保存到本地文件夹。
- 对每个作品调用 **DeepSeek API** 进行内容分析,生成 120 字以内的内容总结,并将总结结果展示在网页上。
- 提供简单的 Web 操作界面,支持任务提交、状态实时查看、结果展示。

## 2. 技术栈
- Python 3.8+
- Flask(Web 框架)
- requests(HTTP 请求)
- python-dotenv(环境变量管理)
- 不使用数据库,任务状态存储于内存(简单字典即可)
- 抖音解析:使用**第三方解析 API**(需要在代码中预留接口,用户可自行替换为实际可用的服务,例如聚合数据、快解析等,本提示词中默认使用一个虚构的 API `https://api.example.com/douyin/parse`,开发者需自行替换)
- AI 总结:调用 **DeepSeek Chat API**(`https://api.deepseek.com/v1/chat/completions`),模型使用 `deepseek-chat`,API Key 从环境变量 `DEEPSEEK_API_KEY` 读取。

## 3. 项目结构

douyin_collector/
├── app.py # Flask 主程序(路由、任务管理)
├── config.py # 配置文件(下载目录、API Key 默认设置等)
├── requirements.txt # 依赖列表
├── utils/
│ ├── init.py
│ ├── douyin_parser.py # 抖音解析模块
│ ├── downloader.py # 下载器模块
│ └── ai_summarizer.py # DeepSeek 总结模块
├── templates/
│ ├── index.html # 首页:输入链接的表单
│ └── result.html # 结果页:展示任务状态和结果(含 AJAX 轮询)
├── static/
│ └── style.css # 可选,简单样式(可省略)
└── downloads/ # 下载文件保存目录(运行时自动创建)

## 4. 模块详细要求

### 4.1 抖音解析模块 (`utils/douyin_parser.py`)
- 函数 `parse_douyin_url(url: str) -> List[Dict]`,接收用户输入的链接,返回**作品信息列表**(即使只有一个作品也返回列表)。
- 该函数内部调用第三方解析 API(POST 或 GET 均可,示例使用 GET 请求,参数 `url`)。
- 解析 API 返回 JSON 格式,假设包含字段:
  - `is_author`: 布尔值,表示是否为作者主页(若为作者主页,则 `works` 字段是一个包含多个作品信息的列表)
  - 若 `is_author` 为 `True`,则从 `data["works"]` 中提取作品列表;
  - 若为 `False`,则直接返回一个作品对象(包装成列表)。
- 每个作品对象包含以下字段(根据实际 API 可能有所不同,但需在代码中做兼容处理):
  ```python
  {
      "type": "video" 或 "image",
      "author": "作者昵称",
      "title": "作品标题或描述",
      "video_url": "无水印视频直链(type=video时必填)",
      "image_urls": ["图片链接1", "图片链接2", ...] (type=image时必填),
      "cover": "封面图链接(可选)",
      "duration": 15,  # 视频时长(秒,可选)
      "music": "背景音乐名称(可选)",
      "likes": 100,    # 点赞数(可选)
      "comments": 20,  # 评论数(可选)
      "shares": 5,     # 分享数(可选)
      "create_time": "2023-01-01 12:00:00"  # 发布时间(可选)
  }
  ```
- 注意:第三方 API 可能没有所有这些字段,需要给出默认值(例如 `duration` 默认 0,`music` 默认 "无",等等)。
- 如果解析失败(网络错误、API 返回错误等),抛出异常,由上层捕获。

### 4.2 下载器模块 (`utils/downloader.py`)
- 函数 `download_work(work: Dict, save_dir: str) -> str`,接收单个作品信息和保存目录,返回下载后的本地文件路径(视频文件路径或图集文件夹路径)。
- 根据 `work["type"]` 分两种情况:
  - **视频**:从 `work["video_url"]` 下载视频文件,文件名格式建议为 `{作者}_{标题前20字符}.mp4`(需处理非法字符),使用流式下载(requests.get(stream=True))。
  - **图集**:创建子文件夹,文件夹名同样为 `{作者}_{标题前20字符}`,依次下载所有图片,保存为 `image_1.jpg`, `image_2.jpg`, ...。
- 确保 `save_dir` 存在,若不存在则创建。
- 如果下载失败,抛出异常。

### 4.3 AI 总结模块 (`utils/ai_summarizer.py`)
- 函数 `summarize_work(work: Dict) -> str`,接收作品信息,返回内容总结字符串(不超过 120 字)。
- 使用 `requests.post` 调用 DeepSeek API:
  - URL: `https://api.deepseek.com/v1/chat/completions`
  - 请求头:`Content-Type: application/json`,`Authorization: Bearer {DEEPSEEK_API_KEY}`
  - 请求体 JSON:
    ```json
    {
      "model": "deepseek-chat",
      "messages": [
        {"role": "system", "content": "你是一个严谨的内容总结助手。"},
        {"role": "user", "content": "这里是完整的提示词模板(见下)"}
      ],
      "temperature": 0.7,
      "max_tokens": 300,
      "stream": false
    }
    ```
  - API Key 从环境变量 `DEEPSEEK_API_KEY` 读取,若未设置则返回错误信息。
- **完整提示词模板**(用户消息内容,使用 f-string 填充作品信息):
  ```
  你是一个专业的内容分析师,擅长从抖音作品的元数据中提炼核心信息。请根据以下作品信息,生成一段简洁、准确、有吸引力的内容总结(不超过120字),帮助用户快速了解作品主题和亮点。

  【作品信息】
  作者:{author}
  标题/描述:{title}
  类型:{'视频' if type == 'video' else '图集'}
  时长:{duration}秒
  背景音乐:{music}
  点赞数:{likes}
  评论数:{comments}
  分享数:{shares}
  发布时间:{create_time}

  【总结要求】
  1. 概括作品的主要内容或主题;
  2. 突出作品的特色或可能的爆点(如情感、搞笑、知识、颜值等);
  3. 语言流畅,避免冗余,直接输出总结文本,不要任何前缀或解释。
  ```
- 如果 API 调用失败(如网络问题、密钥错误等),捕获异常并返回错误信息字符串,格式如 `"DeepSeek API 调用失败:{错误详情}"`。

### 4.4 Flask 主程序 (`app.py`)
- 定义以下路由:
  - `GET /`:渲染 `index.html`,显示一个表单,包含一个输入框(name="url")和提交按钮。
  - `POST /submit`:接收表单中的 `url`,创建任务 ID(使用 `os.urandom(8).hex()`),将任务信息存入全局字典 `tasks`(结构见下),启动后台线程执行 `process_task(task_id, url)`,然后重定向到 `/task/<task_id>`。
  - `GET /task/<task_id>`:根据任务 ID 从 `tasks` 字典获取任务,渲染 `result.html`,传入任务对象和任务 ID。
  - `GET /api/task/<task_id>`:返回该任务的 JSON 数据(用于前端 AJAX 轮询),若任务不存在返回 404。
- **任务字典结构**:
  ```python
  tasks[task_id] = {
      'status': 'pending',   # pending / parsing / downloading / completed / error
      'message': '任务已提交',
      'results': []           # 最终结果为列表,每个元素为字典,包含 work, local_path, summary
  }
  ```
- `process_task(task_id, url)` 函数逻辑:
  1. 更新状态为 `parsing`,调用 `parse_douyin_url(url)` 获取作品列表。
  2. 如果列表为空或解析失败,设置状态为 `error`,消息为对应错误,返回。
  3. 更新状态为 `downloading`,遍历作品列表,对每个作品:
     - 调用 `download_work(work, config.DOWNLOAD_DIR)` 下载,得到本地路径。
     - 调用 `summarize_work(work)` 生成 AI 总结。
     - 将 `{'work': work, 'local_path': local_path, 'summary': summary}` 追加到 `tasks[task_id]['results']`。
     - 更新消息为 `下载中 {idx+1}/{total}`。
  4. 全部完成后,设置状态为 `completed`,消息为 `处理完成,共 {total} 个作品`。
  5. 若过程中出现异常,设置状态为 `error`,消息包含错误详情。

### 4.5 配置文件 (`config.py`)
- 定义下载目录 `DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), 'downloads')`
- 尝试从 `.env` 文件加载环境变量(使用 `python-dotenv` 的 `load_dotenv()`)
- 不直接写死 API Key,全部从环境变量读取。

### 4.6 前端模板
- `index.html`:简单的 HTML 表单,包含一个输入框和提交按钮,表单 action 指向 `/submit`,method 为 POST。
- `result.html`:显示任务状态(status 和 message),并使用 JavaScript 每 2 秒通过 `fetch('/api/task/{{ task_id }}')` 轮询任务状态,当状态变为 `completed` 或 `error` 时停止轮询,并渲染结果列表(如果有 results)。结果列表中每一项显示:作品标题、作者、本地路径、AI 总结。页面底部提供返回首页的链接。

## 5. 依赖文件 `requirements.txt`

flask==2.3.2
requests==2.31.0
python-dotenv==1.0.0

## 6. 运行说明
- 用户需要在项目根目录创建 `.env` 文件,内容为 `DEEPSEEK_API_KEY=sk-xxxx`(实际密钥)。
- 用户需要将 `utils/douyin_parser.py` 中的第三方解析 API URL 替换为实际可用的服务,并确保返回格式符合上述约定(或自行调整解析逻辑)。
- 运行 `python app.py` 启动服务,默认端口 5000。

## 7. 注意事项
- 代码中要包含适当的注释,特别是关键步骤(如解析、下载、API 调用)。
- 请确保所有模块都能正确导入,路径处理正确(使用 `os.path`)。
- 需要处理文件名中的非法字符(如 `/ \ : * ? " < > |`),替换为下划线。
- 异常处理要完善,避免进程崩溃,所有错误都应反馈到任务状态中。
- 生成的代码应该是完整可运行的,不需要用户再手动补充核心逻辑。

请严格按照以上要求生成完整的项目代码,包括所有目录下的文件内容。