单细胞转录组分析 · 从入门到精通
本教材基于「生信学习库」知识库内容深度整理而成,涵盖:
📖 Single-cell Best Practices 全书 47 章详解
💻 Starlitnightly 18 篇实战教程完整代码
🔧 Scanpy / Bioconductor / omicverse 工具链全参考
🧬 多模态(ATAC/CITE-seq/空间组/TCR)扩展分析
⚙️ 工程化(Snakemake/Biopython/DeepChem/Linux/算法)
全书 33 章,约 13,000 行,可直接作为教科书使用。
第一篇:入门基础
1. 单细胞RNA测序技术原理 1.1 测序技术演进:一代→二代→三代 1.1.1 第一代测序——Sanger测序 第一代测序技术,又称Sanger测序(桑格测序),由Frederick Sanger于1977年发明,是基于双脱氧核苷酸末端终止法的测序技术。其基本原理是:在DNA聚合酶催化的测序反应中,掺入带有荧光标记的ddNTP(双脱氧核苷酸),当ddNTP被掺入到延伸的DNA链中时,由于ddNTP缺乏3’-OH基团,链的延伸被终止。通过毛细管电泳分离不同长度的DNA片段,即可读出DNA序列。
第一代测序的特点:
读长较长(700-1000 bp)
准确率极高(>99.99%)
通量极低,每次只能测定一条DNA序列
成本高昂(人类基因组计划花费约30亿美元)
适用于小规模、高精度需求的验证性测序
在单细胞领域的局限: Sanger测序通量太低,完全无法满足单细胞水平上对数千至数万个细胞进行转录组分析的需求。
1.1.2 第二代测序——高通量测序(NGS) 第二代测序技术(Next-Generation Sequencing, NGS),又称高通量测序,约在2005-2007年间商业化。代表性平台包括Illumina(Solexa)、Roche 454、SOLiD等,其中Illumina平台占据主导地位。
Illumina测序的基本原理(边合成边测序,Sequencing by Synthesis, SBS):
文库构建 :将RNA逆转录为cDNA,片段化后加上接头(adaptor)
桥式PCR扩增 :在flow cell表面进行簇生成(cluster generation)
测序 :每次加入一种带荧光标记的dNTP,合成一个碱基后拍摄荧光信号,确定碱基类型
去荧光 :切除荧光基团,进入下一个循环
第二代测序的特点:
通量极高(一次运行可产生数十亿条reads)
单碱基成本极低(全基因组测序成本降至数百美元)
读长较短(Illumina: 75-300 bp; 已废弃的454: ~700 bp)
需要PCR扩增,可能引入扩增偏差
适用于大规模转录组测序,是单细胞RNA-seq的基础技术平台
在单细胞领域的应用: 当前几乎所有的scRNA-seq协议均基于Illumina平台进行测序。10x Genomics Chromium系统的测序深度通常为每个细胞50,000-100,000 reads。
1.1.3 第三代测序——长读长测序 第三代测序技术(Third-Generation Sequencing, TGS),又称长读长测序或单分子测序,不需要PCR扩增,直接对单条DNA/RNA分子进行测序。代表性平台包括PacBio(SMRT测序)和Oxford Nanopore(纳米孔测序)。
PacBio SMRT测序原理:
使用零模波导孔(Zero-Mode Waveguide, ZMW)技术
单个DNA聚合酶固定在ZMW底部
荧光标记的核苷酸掺入时产生荧光脉冲信号
平均读长10-20 kb,超长模式可达>50 kb
HiFi模式准确率>99.9%
Oxford Nanopore测序原理:
DNA/RNA分子穿过生物纳米孔(蛋白质通道)
不同碱基通过时引起电流变化
通过检测电流信号变化来识别碱基
读长可达数百万碱基
设备便携(MinION仅U盘大小)
第三代测序在单细胞领域的应用:
单细胞全长转录本测序(Iso-Seq),可检测可变剪接和转录本异构体
结合10x Genomics barcode的长读长测序
纳米孔直接RNA测序(无需逆转录,保留RNA修饰信息)
三代测序技术对比:
1.2 scRNA-seq协议分类:全长式 vs 标签式 scRNA-seq协议根据cDNA扩增和测序策略的不同,可分为两大类:全长式(Full-length)和 标签式(Tag-based/UMI-based) 。这一分类是最核心的技术区分,直接决定了下游数据处理方法的选择。
1.2.1 全长式协议(Full-length Protocols) 全长式协议对每个细胞中所有转录本的完整长度进行测序,不使用UMI(Unique Molecular Identifier,唯一分子标识符)。
Smart-seq2
Smart-seq2由Picelli等人于2013年开发,是目前最成熟的全长式scRNA-seq协议。
原理:
单细胞通过FACS(荧光激活细胞分选)分选到96孔或384孔板中
裂解细胞,释放RNA
使用oligo-dT引物进行逆转录,合成第一链cDNA
利用模板切换(Template Switching)机制:当逆转录酶到达mRNA的5’端时,会添加数个非模板的胞嘧啶(C),然后加入含有核糖鸟嘌呤(rG)的模板切换寡核苷酸(TSO),逆转录酶切换到TSO模板继续延伸
通过PCR扩增全长的cDNA
构建测序文库,进行Illumina测序
优势:
覆盖转录本全长,可检测可变剪接(Alternative Splicing)
可检测SNP和等位基因特异性表达
灵敏度高,对低表达基因检测能力更强
适用于转录本异构体分析
劣势:
不使用UMI,无法消除PCR扩增偏差
通量低(通常96-384个细胞/批次)
成本高(每个细胞成本约为10x的10-50倍)
手工操作步骤多,实验周期长
Smart-seq3
Smart-seq3由Hagemann-Jensen等人于2020年开发,是Smart-seq2的升级版本。
关键改进:
引入了5’端的UMI标记
结合了全长式和标签式的优势
通过5’端的UMI实现分子计数,同时保留全长转录本信息
灵敏度进一步提升
原理:
与Smart-seq2类似的模板切换过程
但TSO中包含了UMI序列
因此每个cDNA分子的5’端带有唯一标识
测序时先读取5’端的UMI,再读取转录本序列
CEL-seq / CEL-seq2
CEL-seq(Cell Expression by Linear amplification and Sequencing)由Hashimshony等人于2012年开发,是最早的标签式协议之一。
原理:
使用FACS分选单细胞到孔板中
使用带有细胞barcode和poly-dT的引物进行逆转录
使用体外转录(IVT)进行线性扩增(而非PCR指数扩增)
测序3’端
MARS-seq
MARS-seq(Massively Parallel RNA Single-cell sequencing)由Jaitin等人于2014年开发,同样基于IVT线性扩增和3’端测序。
1.2.2 标签式协议(Tag-based Protocols) 标签式协议仅对转录本的3’端或5’端进行测序,使用UMI和细胞barcode进行分子计数。
10x Genomics Chromium
10x Genomics Chromium是目前最广泛使用的scRNA-seq平台,于2016年推出。
原理:
细胞悬液与含有barcode的凝胶珠(Gel Bead)在微流控芯片中混合
每个凝胶珠表面带有约750,000条寡核苷酸引物,每条引物包含:
Illumina P5接头序列
16 nt的细胞barcode(同一凝胶珠上的所有引物共享相同的barcode)
10 nt的UMI(同一凝胶珠上的不同引物有不同的UMI)
30 nt的poly-dT序列(用于捕获mRNA的poly-A尾)
在微流控芯片中,单个细胞与单个凝胶珠被包裹在同一个液滴(GEM, Gel Bead-in-Emulsion)中
液滴内的细胞被裂解,mRNA被带有barcode和UMI的引物捕获
逆转录产生带有barcode和UMI的cDNA
打破液滴,收集所有cDNA,混合进行PCR扩增和文库构建
测序时读取3’端序列
核心优势:
超高通量:每次可捕获500-10,000个细胞(Chromium X可达数万)
使用UMI消除PCR扩增偏差
自动化程度高,实验流程标准化
可重复性好
成本相对较低
核心劣势:
仅测3’端,无法检测可变剪接和转录本异构体
对低表达基因的灵敏度低于Smart-seq2
Dropout率较高(部分基因在某些细胞中未被检测到)
双细胞(Doublet)问题:两个细胞被包裹在同一个液滴中
Drop-seq
Drop-seq由Macosko等人于2015年开发,是10x Chromium的前驱技术。
原理: 与10x Chromium类似,也是基于液滴微流控技术:
细胞与带有barcode的微珠在微流控装置中共包裹在液滴中
每个微珠上带有约10^8条引物,包含细胞barcode和UMI
液滴内裂解细胞、捕获mRNA、逆转录
与10x的差异:
使用普通微珠(非凝胶珠),细胞裂解和逆转录在液滴内完成
细胞捕获率较低
成本更低但数据质量略逊
是学术界的开源方案
inDrop
inDrop由Klein等人于2015年开发,同样基于液滴微流控技术。
原理与特点:
使用可降解的水凝胶微珠(degradable hydrogel beads)
微珠上的barcode引物可以在液滴内释放
使用IVT(体外转录)进行线性扩增,而非PCR
线性扩增可避免PCR偏差,但扩增效率较低
1.2.3 全长式 vs 标签式详细对比表
1.3 UMI原理与作用 1.3.1 什么是UMI UMI(Unique Molecular Identifier,唯一分子标识符),又称Molecular Barcode,是在逆转录之前附加到每个原始RNA分子上的一段随机核苷酸序列(通常8-12 nt)。它的核心作用是区分来自同一原始分子的多个PCR拷贝和来自不同原始分子的cDNA。
1.3.2 为什么需要UMI 在scRNA-seq实验中,原始RNA分子需要经过以下步骤:
逆转录 (RT):mRNA → cDNA
PCR扩增 :将cDNA扩增到足够的量以进行文库构建和测序
PCR扩增是指数级的,不同的cDNA分子可能被扩增不同次数。假设原始分子A被扩增了100次,原始分子B被扩增了5次——测序reads中A的计数远高于B,但这并不反映原始mRNA的表达水平差异,而是PCR偏差。
没有UMI的情况:
测序reads数 = 原始分子数 × PCR扩增倍数
不同分子的扩增倍数不同,reads数无法直接反映原始分子数
需要依赖统计方法(如移位对数归一化)来校正
有UMI的情况:
每个原始分子在逆转录时被赋予唯一的UMI
PCR扩增后,来自同一原始分子的所有reads共享相同的UMI
去重(deduplication):将相同UMI的reads合并为一个计数
UMI计数 = 原始分子数
1.3.3 UMI去重的数学原理 对于每个基因$g$在细胞$c$中的表达量,UMI计数过程可以建模为:
$$X_{gc} = \text{number of unique UMIs for gene } g \text{ in cell } c$$
而不是简单的reads计数。UMI去重的基本步骤:
对每个细胞中每个基因的所有reads,按照UMI序列分组
相同UMI的reads视为来自同一原始分子,只计为1
不同UMI的reads视为来自不同原始分子
UMI碰撞(Collision)问题: 由于UMI长度有限(如10 nt = 4^10 = 1,048,576种可能),当同一基因在同一细胞中的原始分子数很多时,不同的原始分子可能被分配到相同的UMI,导致计数低估。这被称为UMI碰撞或”生日悖论”问题。
校正公式(基于生日悖论):
$$\hat{N} = -M \ln\left(1 - \frac{n}{M}\right)$$
其中$M$是UMI空间大小($4^L$,$L$为UMI长度),$n$是观测到的不同UMI数,$\hat{N}$是估计的原始分子数。
1.3.4 UMI的优势总结
1.4 液滴式 vs 板式完整对比 1.4.1 液滴式(Droplet-based) 代表平台: 10x Genomics Chromium, Drop-seq, inDrop
原理: 利用微流控技术将单个细胞与单个barcode微珠包裹在水-油乳液的液滴中。每个液滴作为一个独立的反应体系,其中的细胞被裂解,mRNA被带有特异性barcode的引物捕获。
流程(以10x Chromium为例):
制备单细胞悬液(要求细胞活性>80%,无团块)
在Chromium Controller上将细胞悬液与凝胶珠和试剂混合
微流控芯片生成GEM液滴(约100,000个液滴/运行)
GEM内裂解→逆转录→cDNA带barcode+UMI
打破GEM,收集cDNA,PCR扩增
构建Illumina测序文库
优势:
通量极高(每运行可处理500-10,000+细胞)
实验流程高度自动化和标准化
单细胞成本低
适合发现稀有细胞类型
适合构建大规模细胞图谱
劣势:
仅测3’端或5’端
Dropout率较高
双细胞问题(约0.8%-8%的液滴包含两个细胞)
环境RNA(Ambient RNA/Soup)污染
对细胞悬液质量要求高
1.4.2 板式(Plate-based) 代表平台: Smart-seq2/3, CEL-seq2, MARS-seq
原理: 通过FACS(荧光激活细胞分选)或手动操作将单个细胞分选到96孔或384孔板中,每个孔作为独立的反应体系。
流程(以Smart-seq2为例):
制备单细胞悬液
使用FACS将单个细胞分选到含裂解液的96/384孔板中
在孔中进行裂解→逆转录(含模板切换)→PCR扩增
每孔的cDNA单独进行文库构建(或使用indexing PCR合并)
Illumina测序
优势:
全长转录本覆盖(Smart-seq2/3)
可检测可变剪接和转录本异构体
灵敏度高,对低表达基因检测更好
Dropout率相对较低
无双细胞问题(除非FACS分选错误)
可结合FACS表面标记信息进行预筛选
劣势:
通量极低(通常<1000细胞/实验)
实验操作繁琐,手工步骤多
单细胞成本高
不使用UMI(Smart-seq2),PCR偏差无法校正
难以发现稀有细胞类型(通量限制)
1.4.3 液滴式 vs 板式详细对比表
1.5 每种技术的适用场景 1.5.1 场景推荐表
1.5.2 选择决策树 1 2 3 4 5 6 7 8 9 10 你的研究目标是什么? ├── 需要全长转录本信息? │ ├── 是 → Smart-seq2(无UMI需求)/ Smart-seq3(需UMI) │ └── 否 → 继续判断 │ ├── 需要高通量(>1000细胞)? │ │ ├── 是 → 10x Chromium │ │ └── 否 → CEL-seq2 或 MARS-seq │ └── 预算有限? │ ├── 是 → Drop-seq(开源、低成本) │ └── 否 → 10x Chromium(标准化、可重复性好)
1.6 关键参考文献
Sanger测序: Sanger, F., Nicklen, S. & Coulson, A.R. (1977). DNA sequencing with chain-terminating inhibitors. Proceedings of the National Academy of Sciences , 74(12), 5463-5467.
Illumina测序: Bentley, D.R. et al. (2008). Accurate whole human genome sequencing using reversible terminator chemistry. Nature , 456(7218), 53-59.
Smart-seq2: Picelli, S. et al. (2013). Smart-seq2 for sensitive full-length transcriptome profiling in single cells. Nature Methods , 10(11), 1096-1098.
Smart-seq3: Hagemann-Jensen, M. et al. (2020). Single-cell RNA counting at allele and isoform resolution using Smart-seq3. Nature Biotechnology , 38(6), 708-714.
10x Genomics Chromium: Zheng, G.X.Y. et al. (2017). Massively parallel digital transcriptional profiling of single cells. Nature Communications , 8, 14049.
Drop-seq: Macosko, E.Z. et al. (2015). Highly parallel genome-wide expression profiling of individual cells using nanoliter droplets. Cell , 161(5), 1202-1214.
inDrop: Klein, A.M. et al. (2015). Droplet barcoding for single-cell transcriptomics applied to embryonic stem cells. Cell , 161(5), 1187-1201.
CEL-seq: Hashimshony, T. et al. (2012). CEL-seq: single-cell RNA-seq by multiplexed linear amplification. Cell Reports , 2(3), 666-673.
MARS-seq: Jaitin, D.A. et al. (2014). Massively parallel single-cell RNA-seq for marker-free decomposition of tissues into cell types. Science , 343(6172), 776-779.
UMI原理: Kivioja, T. et al. (2011). Counting absolute numbers of molecules using unique molecular identifiers. Nature Methods , 9(1), 72-74.
scRNA-seq综述: Aldridge, S. & Teichmann, S.A. (2020). Single cell transcriptomics comes of age. Nature Communications , 11, 4307.
scRNA-seq技术演进: Wen, L. & Tang, F. (2022). The evolution of single-cell RNA sequencing technology and application. Science China Life Sciences , 65, 838-847.
2. 原始数据处理 2.1 Cell Ranger完整流程 Cell Ranger是10x Genomics官方提供的配套分析软件,用于处理Chromium单细胞RNA-seq数据。它是一个高度集成的分析管线,可以从原始测序数据(BCL格式)直接生成基因表达矩阵和初步的分析结果。
Cell Ranger的核心流程包括三个主要步骤:mkfastq → count → aggr 。
2.1.1 cellranger mkfastq:BCL→FASTQ转换 功能: 将Illumina测序仪产生的原始BCL(Base Call)文件转换为FASTQ格式的测序数据。该步骤借用了Illumina的bcl2fastq工具,但进行了优化以适配10x Genomics的测序结构。
原理:
Illumina测序仪输出的原始数据是BCL格式(每个循环每个cluster的碱基调用)
mkfastq将BCL文件按照样本index(I7/I5)拆分为不同样本的FASTQ文件
对于10x Genomics的测序文库,通常一个样本会产生3个FASTQ文件:
I1 (Index read):包含样本index序列
R1 (Read 1):包含26 bp的cell barcode + UMI序列
R2 (Read 2):包含98 bp的cDNA插入片段序列
命令示例:
1 2 3 4 5 6 7 8 9 10 11 12 cellranger mkfastq \ --run=/path/to/bcl_folder \ --output-dir=/path/to/fastq_output \ --csv=sample_sheet.csv \ --localcores=16 \ --localmem=64
参数说明:
2.1.2 cellranger count:FASTQ→计数矩阵 功能: 对单个样本的FASTQ文件进行比对、过滤、UMI计数和基因表达定量,生成gene-barcode计数矩阵。
完整流程:
比对(Alignment)
使用STAR(Spliced Transcripts Alignment to a Reference)将R2 reads比对到参考基因组
Cell Ranger内置了修改版的STAR,针对10x数据进行了优化
比对时考虑了剪接位点,可以将跨越内含子的reads正确比对
Barcode校正(Barcode Correction)
对R1中读取的26 bp序列进行解析:
前16 bp为cell barcode(与已知的737,000个10x barcode whitelist比对)
后10 bp为UMI
对cell barcode进行容错校正:如果读到的barcode与whitelist中的某个barcode只有1个碱基不同,则校正为whitelist中的barcode
UMI计数(UMI Counting)
对于比对到同一基因、具有相同cell barcode的reads
按UMI分组,相同UMI视为来自同一原始分子
对UMI也进行容错校正(编辑距离≤1的UMI合并)
最终得到每个基因在每个细胞中的UMI计数
细胞呼叫(Cell Calling)
区分真实细胞和空液滴(不含细胞但可能含有环境RNA)
Cell Ranger使用空液滴模型:先假设所有barcode都是空液滴,拟合背景分布
然后识别总UMI数显著高于背景的barcode为真实细胞
默认方法:对每个barcode的总UMI数取log10排序,寻找”拐点”(knee point)
生成输出文件
filtered_feature_bc_matrix/:过滤后的基因表达矩阵(仅包含真实细胞)
raw_feature_bc_matrix/:未过滤的基因表达矩阵(包含所有barcode)
metrics_summary.csv:质控指标汇总
web_summary.html:交互式HTML报告
molecule_info.h5:分子级别信息(用于后续aggr)
命令示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cellranger count \ --id =sample1 \ --transcriptome=/path/to/refdata-gex-GRCh38-2020-A \ --fastqs=/path/to/fastq_output/sample1 \ --sample=sample1 \ --localcores=16 \ --localmem=64 \ --expect-cells=5000
构建参考基因组:
1 2 3 4 5 6 7 8 9 cellranger mkref \ --genome=custom_ref \ --fasta=genome.fa \ --genes=genes.gtf
2.1.3 cellranger aggr:多样本聚合 功能: 将多个cellranger count的输出结果进行整合,生成合并的基因表达矩阵,并进行测序深度的归一化。
为什么需要aggr:
同一样本可能在不同lane上测序
同一组的不同样本需要合并分析
需要消除不同文库之间测序深度的差异
归一化策略:
对每个文库计算一个归一化因子
默认基于映射到转录组的reads数进行归一化
公式:$f_i = \frac{M}{N_i}$,其中$M$是所有文库的中位有效reads数,$N_i$是第$i$个文库的有效reads数
归一化后的UMI计数:$x’{ij} = \lfloor f_i \times x {ij} \rfloor$
命令示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cellranger aggr \ --id =pooled_analysis \ --csv=aggregation.csv \ --normalize=map \ --localcores=32 \ --localmem=128
注意事项:
所有文库必须使用相同版本的参考基因组
chemical列需要注明化学版本(three_prime_v2, three_prime_v3等)
聚合后应重新进行下游分析(归一化、聚类等)
2.2 alevin-fry流程(作为替代方案) 2.2.1 alevin-fry简介 alevin-fry是一个高效、灵活的单细胞测序数据处理工具,由Srivastava等人于2022年发表在Nature Methods上。它是salmon软件生态的一部分,可以作为Cell Ranger的替代方案,具有速度更快、内存占用更少的优势。
核心优势:
处理速度比Cell Ranger快5-10倍
内存占用极低(可处理百万级细胞数据)
支持多种定量模式(pseudoalignment-based)
开源免费,不依赖10x Genomics许可
输出格式与Cell Ranger兼容
2.2.2 alevin-fry完整流程 alevin-fry的处理流程分为三步:salmon alevin → alevin-fry generate-permit-list → alevin-fry quantify
步骤1:使用salmon alevin进行伪比对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 salmon index \ -t transcriptome.fa \ -i salmon_index \ -k 31 salmon alevin \ -l ISR \ -i salmon_index \ -1 R1.fastq.gz \ -2 R2.fastq.gz \ -o alevin_output \ -p 16 \ --chromiumV3
步骤2:生成允许列表(permit list)
1 2 3 4 5 6 7 8 9 10 11 12 alevin-fry generate-permit-list \ -i alevin_output \ -o alevin_fry_output \ --expected-ori 5000 \ --knee-dist
步骤3:定量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 alevin-fry quantify \ -i alevin_fry_output \ -o alevin_fry_output/quant \ -m salmon_index \ --use-mtx \ --rad
输出文件:
alevin_fry_output/quant/alevin/featureDump.txt:质控指标
alevin_fry_output/quant/alevin/quants_mat.mtx:计数矩阵
alevin_fry_output/quant/alevin/quants_mat_rows.txt:细胞barcode
alevin_fry_output/quant/alevin/quants_mat_cols.txt:基因列表
2.2.3 将alevin-fry结果导入Python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import scanpy as scimport pandas as pdfrom scipy.io import mmreadfrom scipy.sparse import csr_matrixmatrix = csr_matrix(mmread("alevin_fry_output/quant/alevin/quants_mat.mtx" )) barcodes = pd.read_csv("alevin_fry_output/quant/alevin/quants_mat_rows.txt" , header=None )[0 ].values genes = pd.read_csv("alevin_fry_output/quant/alevin/quants_mat_cols.txt" , header=None )[0 ].values adata = sc.AnnData( X=matrix.T, obs=pd.DataFrame(index=barcodes), var=pd.DataFrame(index=genes) ) print (adata)
2.3 FASTQ→计数矩阵的完整转化过程 2.3.1 数据流转全景 1 2 3 4 5 6 7 8 9 10 11 原始测序数据 (BCL) ↓ [cellranger mkfastq / bcl2fastq] FASTQ文件 (R1: barcode+UMI, R2: cDNA, I1: sample index) ↓ [cellranger count / salmon alevin] 比对到参考基因组 (SAM/BAM) ↓ [Barcode校正 + UMI去重] 基因-细胞计数矩阵 (Gene × Cell count matrix) ↓ [细胞呼叫/过滤] 过滤后的计数矩阵 (filtered count matrix) ↓ [导入scanpy/Seurat] AnnData / SingleCellExperiment对象
2.3.2 每一步的详细说明 Step 1: BCL → FASTQ
将Illumina原始信号转换为碱基序列
按样本index拆分
输出paired-end FASTQ文件
Step 2: FASTQ → Alignment
将R2 reads比对到参考基因组
Cell Ranger使用STAR(splicing-aware比对)
alevin-fry使用伪比对(pseudoalignment,更快但不提供精确比对坐标)
Step 3: Alignment → Gene Assignment
将比对结果分配到基因
基于GTF注释文件确定read属于哪个基因
对于比对到多个基因的reads,分配给唯一基因或丢弃
Step 4: Barcode Processing + UMI Deduplication
解析R1中的cell barcode和UMI
校正barcode中的测序错误
对同一基因、同一细胞、同一UMI的reads去重
Step 5: Cell Calling
区分真实细胞和空液滴
基于总UMI数的分布,识别显著高于背景的barcode
Step 6: Output Count Matrix
输出稀疏矩阵格式(MTX)
三个文件:矩阵、行索引、列索引
2.4 比对策略:STAR vs Cell Ranger
推荐策略:
一般分析:alevin-fry(速度快、资源消耗少)
需要BAM文件进行下游分析(如变异检测):Cell Ranger
正式发表:Cell Ranger(标准化程度高,审稿人更熟悉)
2.5 多文库聚合策略 2.5.1 为什么需要聚合
一个实验可能包含多个样本
同一样本可能在不同lane上测序
不同样本可能使用不同的化学方案(v2/v3)
需要消除批次效应和测序深度差异
2.5.2 聚合方法 方法1:Cell Ranger aggr(推荐)
在原始UMI计数层面进行聚合
基于映射reads数进行归一化
优点:保留了UMI计数的整数性
缺点:需要molecule_info.h5文件
方法2:下游分析时合并
分别对每个样本运行cellranger count
在Python中使用sc.concat或anndata.concat合并
需要后续进行批次校正(如Harmony、scVI等)
1 2 3 4 5 6 7 8 9 10 11 12 13 import scanpy as scadata1 = sc.read_10x_mtx('sample1/filtered_feature_bc_matrix/' ) adata1.obs['sample' ] = 'sample1' adata2 = sc.read_10x_mtx('sample2/filtered_feature_bc_matrix/' ) adata2.obs['sample' ] = 'sample2' adata = sc.concat([adata1, adata2], join='inner' ) print (adata)
2.6 将Cell Ranger结果导入Python 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import scanpy as scadata = sc.read_10x_mtx( 'path/to/filtered_feature_bc_matrix/' , var_names='gene_symbols' , cache=True ) print (adata)adata = sc.read_10x_h5( 'path/to/filtered_feature_bc_matrix.h5' ) adata_raw = sc.read_10x_mtx( 'path/to/raw_feature_bc_matrix/' , var_names='gene_symbols' ) adata.write_h5ad('raw_counts.h5ad' )
2.7 注意事项
Cell Ranger版本一致性 :同一项目的所有样本应使用相同版本的Cell Ranger,不同版本的结果可能存在细微差异
参考基因组版本 :确保使用与实验设计一致的参考基因组(如GRCh38 vs GRCh37)
内存需求 :Cell Ranger count通常需要32-64 GB内存,建议在服务器上运行
FASTQ文件完整性 :运行前检查FASTQ文件大小和read数量是否正常
细胞悬液质量 :Cell Ranger的细胞呼叫算法依赖于细胞barcode的UMI分布,低质量悬液可能导致错误呼叫
双细胞率 :10x Chromium的双细胞率约为每1000个细胞0.8%,即8000个细胞中约有64个双细胞
环境RNA污染 :死细胞释放的mRNA会污染液滴,导致计数偏差,可通过SoupX校正
alevin-fry的定量模式 :cr-like模式输出与Cell Ranger最接近的结果;full模式提供更精细的UMI去重
多映射reads处理 :Cell Ranger将多映射reads分配给最可能的基因,alevin-fry默认丢弃多映射reads
数据备份 :始终保留原始FASTQ文件和Cell Ranger完整输出
3. 数据结构 3.1 AnnData完整结构 AnnData(Annotated Data)是Python scverse生态系统的核心数据结构,专为存储和操作带注释的多维数据而设计。它是scanpy、scvi-tools、squidpy等单细胞分析工具的基础。
3.1.1 AnnData结构概览 1 2 3 4 5 6 7 8 9 10 AnnData Object ├── X # 核心计数矩阵 (n_obs × n_vars, 通常为稀疏矩阵) ├── obs # 观测(细胞)的一维元数据 (DataFrame, n_obs × k) ├── var # 变量(基因)的一维元数据 (DataFrame, n_vars × k) ├── obsm # 观测的多维注释 (Dict[str, ndarray], n_obs × m) ├── varm # 变量的多维注释 (Dict[str, ndarray], n_vars × m) ├── uns # 非结构化元数据 (Dict, 任意类型) ├── layers # 与X对齐的额外矩阵 (Dict[str, ndarray]) ├── obsp # 观测的成对矩阵 (Dict[str, ndarray], n_obs × n_obs) └── varp # 变量的成对矩阵 (Dict[str, ndarray], n_vars × n_vars)
3.1.2 各属性详解 X — 核心表达矩阵
类型:scipy.sparse.csr_matrix 或 numpy.ndarray
维度:n_obs × n_vars(细胞 × 基因)
通常存储当前分析步骤使用的表达矩阵
可以是原始UMI计数、归一化后的值、缩放后的值等
建议始终在layers中保留原始计数
1 2 3 4 adata.X adata.X[0 :5 , 0 :5 ] adata.X.sum (axis=1 )
obs — 细胞元数据
类型:pandas.DataFrame
行索引:细胞barcode
每列是一个细胞级别的注释信息
1 2 3 4 5 6 7 8 9 10 11 12 adata.obs['cell_type' ] = ['CD4 T cell' , 'B cell' , 'Monocyte' , ...] adata.obs['condition' ] = ['control' , 'treatment' , ...]
var — 基因元数据
类型:pandas.DataFrame
行索引:基因名/Ensembl ID
每列是一个基因级别的注释信息
1 2 3 4 5 6 7 8 9 10 11 12 adata.var['mt' ] = adata.var_names.str .startswith('MT-' ) adata.var['ribo' ] = adata.var_names.str .startswith(('RPS' , 'RPL' ))
obsm — 细胞的多维注释
类型:Dict[str, numpy.ndarray]
每个值的维度:n_obs × m(m为嵌入维度)
通常存储降维结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 adata.obsm['X_pca' ] adata.obsm['X_umap' ] adata.obsm['scaled|original|X_pca' ] adata.obsm['counts|original|X_pca' ]
varm — 基因的多维注释
类型:Dict[str, numpy.ndarray]
每个值的维度:n_vars × m
较少使用,可存储基因的PCA载荷(loadings)等
1 2 adata.varm['PCs' ] = pca_model.components_.T
uns — 非结构化元数据
类型:Dict
可存储任意Python对象
通常存储分析参数、颜色映射、邻域图信息等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 adata.uns['experiment_info' ] = { 'organism' : 'Homo sapiens' , 'tissue' : 'PBMC' , 'platform' : '10x Chromium v3' , 'date' : '2024-01-15' }
layers — 额外表达矩阵
类型:Dict[str, numpy.ndarray] 或 Dict[str, scipy.sparse.matrix]
每个值与X维度相同:n_obs × n_vars
存储不同处理阶段的表达矩阵
1 2 3 4 5 6 7 8 adata.layers['counts' ] = adata.X.copy() adata.layers['log1p_norm' ] = log_normalized adata.layers['scran_norm' ] = scran_normalized adata.layers['pearson_residuals' ] = pearson adata.X = adata.layers['counts' ].copy()
obsp — 细胞成对矩阵
类型:Dict[str, numpy.ndarray] 或 Dict[str, scipy.sparse.matrix]
维度:n_obs × n_obs
存储细胞间的距离矩阵、连接性矩阵等
1 2 3 4 5 6 7 adata.obsp['distances' ] adata.obsp['connectivities' ]
varp — 基因成对矩阵
类型:Dict[str, numpy.ndarray]
维度:n_vars × n_vars
存储基因间的相关矩阵、调控网络等
1 2 adata.varp['gene_correlation' ] = correlation_matrix
3.1.3 AnnData创建、切片、合并的完整代码 创建AnnData对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import numpy as npimport pandas as pdimport anndata as adfrom scipy.sparse import csr_matrixcounts = csr_matrix(np.random.poisson(1 , size=(100 , 2000 )), dtype=np.float32) adata = ad.AnnData(counts) print (adata)n_obs = 1000 n_vars = 5000 counts = csr_matrix(np.random.poisson(1 , size=(n_obs, n_vars)), dtype=np.float32) obs = pd.DataFrame({ 'cell_type' : np.random.choice(['T cell' , 'B cell' , 'Monocyte' ], n_obs), 'batch' : np.random.choice(['batch1' , 'batch2' ], n_obs), 'n_genes' : np.random.randint(500 , 3000 , n_obs), }, index=[f'cell_{i} ' for i in range (n_obs)]) var = pd.DataFrame({ 'gene_id' : [f'ENSG{i:09d} ' for i in range (n_vars)], 'mt' : [gene.startswith('MT-' ) for gene in [f'gene_{i} ' for i in range (n_vars)]], }, index=[f'gene_{i} ' for i in range (n_vars)]) obsm = { 'X_pca' : np.random.randn(n_obs, 50 ).astype(np.float32), 'X_umap' : np.random.randn(n_obs, 2 ).astype(np.float32), } uns = { 'experiment' : 'PBMC_analysis' , 'organism' : 'Homo sapiens' , } layers = { 'counts' : counts.copy(), 'normalized' : csr_matrix(np.log1p(counts.toarray()), dtype=np.float32), } adata = ad.AnnData( X=counts, obs=obs, var=var, obsm=obsm, uns=uns, layers=layers, ) print (adata)
切片(Slicing)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 adata_subset = adata[:500 ] print (adata_subset.shape) mask = adata.obs['cell_type' ] == 'T cell' adata_t_cells = adata[mask] print (adata_t_cells.shape) adata_selected = adata[['cell_0' , 'cell_1' , 'cell_2' ]] adata_genes = adata[:, ['gene_0' , 'gene_1' , 'gene_2' ]] print (adata_genes.shape) mask_hvg = adata.var['highly_variable' ] == True adata_hvg = adata[:, mask_hvg] adata_sub = adata[:500 , :1000 ] print (adata_subset.is_view) adata_copy = adata_subset.copy() print (adata_copy.is_view)
合并(Merging/Concatenating)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import anndata as adadata1 = ad.AnnData(np.random.randn(100 , 2000 )) adata1.obs['batch' ] = 'batch1' adata1.obs_names = [f'cell_b1_{i} ' for i in range (100 )] adata1.var_names = [f'gene_{i} ' for i in range (2000 )] adata2 = ad.AnnData(np.random.randn(150 , 2000 )) adata2.obs['batch' ] = 'batch2' adata2.obs_names = [f'cell_b2_{i} ' for i in range (150 )] adata2.var_names = [f'gene_{i} ' for i in range (2000 )] adata_concat = ad.concat([adata1, adata2], join='inner' ) print (adata_concat.shape) adata_concat_outer = ad.concat([adata1, adata2], join='outer' ) adata_concat = ad.concat( [adata1, adata2], join='inner' , label='batch' , keys=['batch1' , 'batch2' ], merge='same' ) adata_genes1 = ad.AnnData(np.random.randn(100 , 1000 )) adata_genes1.var_names = [f'gene_{i} ' for i in range (1000 )] adata_genes2 = ad.AnnData(np.random.randn(100 , 500 )) adata_genes2.var_names = [f'gene_{1000 +i} ' for i in range (500 )] adata_var_concat = ad.concat( [adata_genes1, adata_genes2], axis=1 , join='inner' ) print (adata_var_concat.shape)
读写AnnData对象
1 2 3 4 5 6 7 8 9 10 11 12 13 adata.write_h5ad('my_data.h5ad' , compression='gzip' ) adata_loaded = ad.read_h5ad('my_data.h5ad' ) adata_backed = ad.read_h5ad('my_data.h5ad' , backed='r' ) adata = sc.read_10x_mtx('path/to/filtered_feature_bc_matrix/' ) adata = sc.read_10x_h5('path/to/filtered_feature_bc_matrix.h5' )
3.2 MuData多模态数据结构 MuData是scverse生态中用于存储多模态单细胞数据的数据结构,建立在AnnData之上。它允许在同一个对象中存储来自不同组学层次的数据(如RNA + ATAC + 蛋白质)。
3.2.1 MuData结构 1 2 3 4 5 6 7 8 9 10 11 12 MuData Object ├── mod # 模态字典 (Dict[str, AnnData]) │ ├── 'rna' # scRNA-seq数据 (AnnData) │ ├── 'atac' # scATAC-seq数据 (AnnData) │ └── 'prot' # CITE-seq蛋白质数据 (AnnData) ├── obs # 全局细胞元数据 (DataFrame) ├── var # 全局变量元数据 (DataFrame) ├── obsm # 全局细胞嵌入 (Dict) ├── varm # 全局变量注释 (Dict) ├── uns # 全局非结构化元数据 (Dict) ├── obsp # 全局细胞成对矩阵 (Dict) └── varp # 全局变量成对矩阵 (Dict)
3.2.2 MuData代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from mudata import MuDataimport anndata as adimport numpy as npimport pandas as pdfrom scipy.sparse import csr_matrixn_obs = 1000 rna_data = ad.AnnData( X=csr_matrix(np.random.poisson(1 , size=(n_obs, 20000 )), dtype=np.float32), obs=pd.DataFrame(index=[f'cell_{i} ' for i in range (n_obs)]), var=pd.DataFrame(index=[f'gene_{i} ' for i in range (20000 )]), ) rna_data.obs['n_genes' ] = np.random.randint(500 , 3000 , n_obs) prot_data = ad.AnnData( X=csr_matrix(np.random.poisson(5 , size=(n_obs, 100 )), dtype=np.float32), obs=pd.DataFrame(index=[f'cell_{i} ' for i in range (n_obs)]), var=pd.DataFrame(index=[f'protein_{i} ' for i in range (100 )]), ) mdata = MuData({'rna' : rna_data, 'prot' : prot_data}) print (mdata)print (mdata['rna' ]) mdata.update() mdata.write('my_mudata.h5mu' ) mdata_loaded = MuData.read('my_mudata.h5mu' ) mdata_backed = MuData.read('my_mudata.h5mu' , backed=True )
3.3 SingleCellExperiment(R)数据结构 SingleCellExperiment(SCE)是R/Bioconductor生态系统中存储单细胞数据的核心数据结构,类似于Python中的AnnData。
3.3.1 SCE结构 1 2 3 4 5 6 7 8 9 10 11 12 13 SingleCellExperiment Object ├── assays # 表达矩阵列表 (List[matrix]) │ ├── 'counts' # 原始计数 │ └── 'logcounts' # 归一化后的值 ├── colData # 细胞元数据 (DataFrame, n_cells × k) ├── rowData # 基因元数据 (DataFrame, n_genes × k) ├── reducedDims # 降维结果 (List[matrix]) │ ├── 'PCA' # PCA结果 │ └── 'UMAP' # UMAP结果 ├── colPairs # 细胞成对数据 (List) ├── rowPairs # 基因成对数据 (List) ├── metadata # 非结构化元数据 (List) └── alternativeExperiments # 替代实验数据
3.3.2 SCE代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 library( SingleCellExperiment) counts <- matrix( rpois( 10000 , lambda = 1 ) , nrow = 200 , ncol = 50 ) rownames( counts) <- paste0( "gene_" , 1 : 200 ) colnames( counts) <- paste0( "cell_" , 1 : 50 ) cell_meta <- DataFrame( cell_type = sample( c ( "T cell" , "B cell" , "Monocyte" ) , 50 , replace = TRUE ) , batch = sample( c ( "batch1" , "batch2" ) , 50 , replace = TRUE ) ) gene_meta <- DataFrame( gene_id = paste0( "ENSG" , sprintf( "%09d" , 1 : 200 ) ) , mt = grepl( "^MT-" , rownames( counts) ) ) sce <- SingleCellExperiment( assays = list ( counts = counts) , colData = cell_meta, rowData = gene_meta ) logcounts( sce) <- log1p( counts) reducedDim( sce, "PCA" ) <- prcomp( t( logcounts( sce) ) ) $ x[ , 1 : 30 ] counts( sce) logcounts( sce) colData( sce) rowData( sce) reducedDim( sce, "PCA" )
3.4 Python/R互操作 在单细胞分析中,Python(scanpy)和R(Seurat/SingleCellExperiment)是最常用的两大分析生态。互操作性允许研究者在两种语言之间灵活切换,利用各自的优势工具。
3.4.1 数据格式映射表
3.4.2 转换工具 方法1:zellkonverter(R → Python)
1 2 3 4 5 6 7 8 9 BiocManager:: install( "zellkonverter" ) library( zellkonverter) writeH5AD( sce, "my_data.h5ad" ) sce <- readH5AD( "my_data.h5ad" )
方法2:anndata2ri(Python → R,通过rpy2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import anndata2rifrom rpy2.robjects import globalenvimport anndata as adanndata2ri.activate() adata = ad.read_h5ad('my_data.h5ad' ) globalenv['adata' ] = adata
方法3:MuDataSeurat(MuData ↔ Seurat)
1 2 3 4 5 6 7 8 9 remotes:: install_github( "zilongstudio/MuDataSeurat" ) library( MuDataSeurat) seu <- ReadH5AD( "my_data.h5ad" ) seu <- ReadH5MU( "my_data.h5mu" )
方法4:sceasy
1 2 3 4 5 6 7 8 9 10 11 import sceasyimport anndata as adadata = ad.read_h5ad('my_data.h5ad' ) sceasy.convert(adata, 'my_data.rds' , output_file_type='anndata2seurat' ) sceasy.convert('my_data.rds' , 'my_data_from_r.h5ad' , output_file_type='seurat2anndata' )
方法5:直接在Python中调用R函数(rpy2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import rpy2.robjects as rofrom rpy2.robjects import pandas2rifrom rpy2.robjects.conversion import localconverterimport anndata2rianndata2ri.activate() adata_pp = adata.copy() sc.pp.normalize_total(adata_pp) sc.pp.log1p(adata_pp) sc.pp.pca(adata_pp) sc.pp.neighbors(adata_pp) data_mat = adata_pp.X.T if hasattr (data_mat, 'toarray' ): data_mat = data_mat.tocsc().toarray() with localconverter(ro.default_converter + pandas2ri.converter): ro.globalenv["data_mat" ] = data_mat ro.r(''' library(scran) size_factors <- sizeFactors( computeSumFactors( SingleCellExperiment(list(counts=data_mat)), min.mean = 0.1 ) ) ''' )size_factors = ro.r('size_factors' )
3.4.3 注意事项
数据类型一致性 :R和Python对稀疏矩阵的支持不同,转换时注意矩阵格式
行/列方向 :R中表达矩阵通常是基因×细胞,Python中是细胞×基因,需要转置
基因名规范 :确保基因名在Python和R中一致(如使用Ensembl ID而非symbol)
版本兼容 :zellkonverter和anndata2ri的版本需要与anndata包版本匹配
内存管理 :大对象在Python和R之间传递时需要双倍内存
大文件处理 :对于超大数据集,建议通过磁盘文件(h5ad/h5mu)进行转换,而非内存传递
3.5 关键参考文献
AnnData: Virshup, I. et al. (2023). The scverse project provides a computational ecosystem for single-cell omics data analysis. Nature Biotechnology , 41(5), 604-606.
Scanpy: Wolf, F.A., Angerer, P. & Theis, F.J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology , 19, 15.
MuData: Danilenko, A. et al. (2022). MuData: multimodal data object for Python. Journal of Open Source Software , 7(73), 4176.
SingleCellExperiment: Amezquita, R.A. et al. (2020). Orchestrating single-cell analysis with Bioconductor. Nature Methods , 17(2), 137-145.
Cell Ranger: 10x Genomics. Cell Ranger - Official Documentation. https://support.10xgenomics.com/single-cell-gene-expression/software/pipelines/latest/what-is-cell-ranger
alevin-fry: Srivastava, A. et al. (2022). Alevin-fry unlocks rapid, accurate and memory-frugal quantification of single-cell RNA-seq data. Nature Methods , 19, 316-322.
STAR: Dobin, A. et al. (2013). STAR: ultrafast universal RNA-seq aligner. Bioinformatics , 29(1), 15-21.
zellkonverter: Zappia, L. & Lun, A. Converting single-cell data structures between Bioconductor and Python. Bioconductor Vignette .
第二篇:预处理全流程
4. 质量控制 4.1 三类数据缺陷 单细胞RNA测序数据由于技术限制,不可避免地存在三类主要缺陷,质量控制的目的就是识别和去除这些有问题的数据。
4.1.1 Drop-out(基因丢失) 定义: Drop-out是指基因在某些细胞中表达量不为零,但由于技术原因(捕获效率低、测序深度不足等)未被检测到,导致计数矩阵中出现大量零值。
成因:
mRNA捕获效率有限(10x Chromium约10-15%)
逆转录效率有限(约30-50%)
测序深度不足
PCR扩增偏差
影响:
计数矩阵稀疏(通常50-80%为零)
下游分析中假阴性率高
差异表达分析中统计功效降低
低表达基因可能被完全遗漏
特征: 在UMI计数数据中,Drop-out率与基因表达水平负相关——高表达基因的Drop-out率低,低表达基因的Drop-out率高。
4.1.2 死亡细胞/低质量细胞 定义: 在样本制备过程中,部分细胞可能已经死亡或正在死亡,这些细胞的转录组特征与正常活细胞不同,会引入噪声。
特征:
细胞膜破裂,胞质mRNA泄漏到环境中
线粒体mRNA相对富集(因为线粒体较大,不易泄漏)
总UMI数低
检测到的基因数少
为什么需要去除:
死亡细胞的转录组不代表真实的生物学状态
环境RNA(来自死亡细胞的mRNA)会污染其他细胞的计数
死亡细胞引入假变异,影响聚类和差异表达分析
识别指标:
高线粒体基因比例(>15-20%通常指示死亡细胞)
低总UMI数
低检测基因数
4.1.3 双细胞(Doublets) 定义: 在液滴式scRNA-seq中,两个或多个细胞可能被包裹在同一个液滴中,被赋予同一个cell barcode,导致测序数据实际上是多个细胞的混合信号。
产生原因:
细胞悬液浓度过高
液滴包裹的随机性
细胞团块未完全解离
双细胞率估算:
10x Chromium约0.8% per 1000 cells
即8000个细胞中约有64个双细胞
公式:$\text{doublet rate} \approx 0.001 \times \frac{N_{\text{cells}}}{1000}$
双细胞类型:
同源双细胞(Homotypic doublet): 两个相同类型的细胞。难以检测,但影响较小(看起来像一个正常的该类型细胞)
异源双细胞(Heterotypic doublet): 两个不同类型的细胞。更容易检测,影响更大(看起来像一个中间态细胞,可能被误认为新的细胞类型)
影响:
可能被误认为新的细胞类型或中间态
影响差异表达分析
影响细胞类型注释
4.2 QC指标详解 质量控制通过计算每个细胞(barcode)的一系列QC指标来评估数据质量,常用的核心指标如下:
4.2.1 nUMIs(total_counts) 定义: 每个细胞中检测到的UMI总数,即该细胞的文库大小(library size)。
计算方法:
$$\text{nUMIs}c = \sum {g=1}^{G} x_{gc}$$
其中$x_{gc}$是细胞$c$中基因$g$的UMI计数。
生物学意义:
反映细胞的总RNA含量
低nUMIs可能指示低质量细胞、死亡细胞或空液滴
高nUMIs可能指示双细胞
不同细胞类型的nUMIs可能自然不同(如大细胞通常有更多RNA)
典型值:
10x Chromium v3:5,000-50,000 UMIs/细胞
低于500-1000 UMIs通常被认为是低质量细胞
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import scanpy as scadata.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('^HB[^(P)]' ) sc.pp.calculate_qc_metrics( adata, qc_vars=['mt' , 'ribo' , 'hb' ], percent_top=None , log1p=False , inplace=True ) print (adata.obs['total_counts' ].describe())
4.2.2 检测基因数(n_genes_by_counts / detected_genes) 定义: 每个细胞中表达量非零的基因数量。
计算方法:
$$\text{n_genes}c = \sum {g=1}^{G} \mathbb{1}(x_{gc} > 0)$$
生物学意义:
反映细胞的转录活性
低n_genes可能指示低质量细胞
高n_genes可能指示双细胞
不同细胞类型的基因表达谱宽度不同
典型值:
10x Chromium v3:1,000-5,000 genes/细胞
低于200-250 genes通常被认为是低质量细胞
4.2.3 线粒体基因比例(pct_counts_mt / mito_perc) 定义: 来自线粒体基因的UMI计数占细胞总UMI计数的比例。
计算方法:
$$\text{pct_counts_mt}c = \frac{\sum {g \in \text{MT}} x_{gc}}{\sum_{g=1}^{G} x_{gc}} \times 100$$
线粒体基因识别(人类): 基因名以”MT-“开头(如MT-ND1, MT-CO1, MT-ATP6等,共13个蛋白编码基因)
线粒体基因识别(小鼠): 基因名以”mt-“开头
生物学原理:
线粒体DNA编码的转录本位于线粒体内部
当细胞死亡时,细胞膜破裂,胞质mRNA泄漏
但线粒体较大且有自己的膜结构,mRNA不易泄漏
因此死亡细胞中线粒体mRNA的相对比例升高
典型阈值:
通常使用5-20%作为过滤阈值
具体阈值取决于组织类型(某些组织如心脏、肌肉天然具有高线粒体基因比例)
注意: 高线粒体基因比例不一定意味着细胞质量差。某些细胞类型(如心肌细胞、运动神经元)由于高能量需求,天然具有高线粒体基因表达。因此,线粒体基因比例应与其他QC指标联合使用。
4.2.4 核糖体基因比例(pct_counts_ribo) 定义: 来自核糖体蛋白基因的UMI计数占细胞总UMI计数的比例。
核糖体基因识别: 基因名以”RPS”(核糖体蛋白小亚基)或”RPL”(核糖体蛋白大亚基)开头。
生物学意义:
核糖体基因是管家基因,在所有活细胞中高度表达
通常占总counts的15-40%
极端高值可能指示细胞应激或技术偏差
可作为辅助QC指标
4.2.5 血红蛋白基因比例(pct_counts_hb) 定义: 来自血红蛋白基因的UMI计数占比。
血红蛋白基因识别: 基因名匹配^HB[^(P)]模式(如HBA1, HBA2, HBB等,排除HBP基因)
适用场景: 主要用于血液相关样本(如外周血单核细胞PBMC),检测红细胞污染。
4.3 MAD(中位数绝对偏差)过滤方法 4.3.1 原理 MAD(Median Absolute Deviation,中位数绝对偏差)是一种基于数据分布的鲁棒统计量,用于衡量数据的离散程度。与标准差相比,MAD对异常值(outlier)更加鲁棒,因为它是基于中位数而非均值计算的。
4.3.2 数学公式 MAD的定义:
$$\text{MAD} = \text{median}(|x_i - \text{median}(x)|)$$
即:先计算所有数据点与中位数的绝对偏差,再取这些绝对偏差的中位数。
基于MAD的异常值检测:
对于下限过滤(如nUMIs过低):
$$\text{lower bound} = \text{median}(x) - n \times \text{MAD}(x)$$
对于上限过滤(如线粒体比例过高):
$$\text{upper bound} = \text{median}(x) + n \times \text{MAD}(x)$$
其中$n$是MAD的倍数,通常取3或5。$n$值越大,过滤越宽松。
MAD与标准差的关系: 对于正态分布,MAD约等于0.6745倍的标准差:
$$\sigma \approx 1.4826 \times \text{MAD}$$
因此,3个MAD约等于2个标准差,5个MAD约等于3.4个标准差。
4.3.3 MAD过滤的优势
4.3.4 MAD过滤代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import numpy as npimport pandas as pddef mad_filter (adata, metrics, nmads=5 , direction='both' ): """ 基于MAD的自动质量控制过滤 参数: adata: AnnData对象 metrics: dict, 格式为 {指标名: 过滤方向} 'both': 双侧过滤 'upper': 仅上限过滤 'lower': 仅下限过滤 nmads: float, MAD的倍数(默认5) direction: 保留未使用的向后兼容参数 返回: 过滤后的AnnData对象 """ mask = pd.Series(True , index=adata.obs_names) for metric, filt_dir in metrics.items(): values = adata.obs[metric] median = np.median(values) mad = np.median(np.abs (values - median)) lower = median - nmads * mad upper = median + nmads * mad if filt_dir == 'lower' or filt_dir == 'both' : mask = mask & (values >= lower) if filt_dir == 'upper' or filt_dir == 'both' : mask = mask & (values <= upper) print (f"{metric} : median={median:.2 f} , MAD={mad:.2 f} , " f"range=[{lower:.2 f} , {upper:.2 f} ], " f"filtered={(~(values >= lower) | ~(values <= upper)).sum ()} " ) return adata[mask].copy() metrics = { 'total_counts' : 'lower' , 'n_genes_by_counts' : 'lower' , 'pct_counts_mt' : 'upper' , } adata_filtered = mad_filter(adata, metrics, nmads=5 )
4.4 Seurat式手动阈值 vs MAD自动阈值对比
Seurat式手动阈值代码:
1 2 3 4 5 6 7 adata = adata[ (adata.obs['total_counts' ] > 500 ) & (adata.obs['n_genes_by_counts' ] > 250 ) & (adata.obs['pct_counts_mt' ] < 15 ), : ].copy()
4.5 SoupX环境RNA校正 4.5.1 原理 在液滴式scRNA-seq中,细胞被裂解后释放的mRNA会溶解在缓冲液中,形成所谓的”汤”(Soup)。这些游离的mRNA(环境RNA,ambient RNA)可能被错误地分配到同一液滴中的细胞,导致背景污染。
SoupX的工作原理:
使用raw_feature_bc_matrix(未过滤的矩阵,包含所有barcode)估计环境RNA的组成
假设空液滴(不含细胞的barcode)中的RNA完全来自环境RNA
通过低表达空液滴的基因表达谱构建环境RNA的轮廓(soup profile)
估计每个细胞中环境RNA的污染比例(soup fraction,$\rho$)
对每个基因在每个细胞中的计数进行校正:
$$x_{gc}^{\text{corrected}} = x_{gc} - \rho_c \cdot s_g \cdot N_c$$
其中$\rho_c$是细胞$c$的污染比例,$s_g$是基因$g$在环境RNA中的比例,$N_c$是细胞$c$的总counts。
4.5.2 SoupX代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 library(SoupX) sc <- load10X(data_dir = "path/to/cellranger_output/" ) sc <- SoupX::load10X( data_dir = "path/to/cellranger_output/" , keep_droplets = FALSE ) sc <- autoEstCont(sc) sc <- adjustCounts(sc) counts_corrected <- getCounts(sc) save10X(sc, "corrected_output/" )
在Python中使用SoupX(通过rpy2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import rpy2.robjects as rofrom rpy2.robjects import numpy2ri, pandas2rifrom rpy2.robjects.conversion import localconverternumpy2ri.activate() pandas2ri.activate() ro.r(''' library(SoupX) # 加载数据 sc <- load10X("path/to/cellranger_output/") # 自动估计污染比例 sc <- autoEstCont(sc) # 校正 sc <- adjustCounts(sc) # 获取校正后的计数矩阵 counts_corrected <- getCounts(sc) ''' )counts_corrected = ro.r('counts_corrected' )
4.5.3 注意事项
需要raw_feature_bc_matrix :SoupX需要未过滤的计数矩阵来估计环境RNA,但该文件通常不公开提供
可能产生非整数计数 :校正后的矩阵可能包含小数值,某些下游工具可能不支持
仅在污染严重时使用 :如果环境RNA污染不严重,不建议使用SoupX,因为校正可能引入新的偏差
污染比例估计 :自动估计不一定准确,建议手动检查并调整
4.6 双细胞检测 4.6.1 Scrublet原理与代码 原理: Scrublet(Single-cell Remover of Doublets)由Wolock等人于2019年开发,是一种基于模拟双细胞的Python双细胞检测工具。
核心思路:
模拟双细胞: 从实际数据中随机选择两个细胞的表达谱,将它们相加,创建模拟双细胞(synthetic doublet)
构建KNN图: 在PCA空间中,对真实细胞和模拟双细胞构建KNN图
计算双细胞评分: 对每个真实细胞,计算其K个最近邻中模拟双细胞的比例,作为双细胞评分(doublet score)
阈值判定: 基于双细胞评分的分布,设定阈值将细胞分类为singlet或doublet
数学细节:
对于细胞$c$,其双细胞评分定义为:
$$\text{score}_c = \frac{\text{number of synthetic doublets among } k \text{ nearest neighbors of } c}{k}$$
预期双细胞率:
$$\text{expected doublet rate} \approx 0.01 \times \frac{N_{\text{cells}}}{1000}$$
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import scrublet as scrimport scipy.iocounts_matrix = scipy.io.mmread('counts.mtx' ).T.tocsr() scrub = scr.Scrublet(counts_matrix, expected_doublet_rate=0.06 ) doublet_scores, predicted_doublets = scrub.scrub_doublets( min_counts=2 , min_cells=3 , min_gene_variability_pctl=85 , n_prin_comps=30 , ) scrub.plot_histogram() adata.obs['doublet_score' ] = doublet_scores adata.obs['predicted_doublet' ] = predicted_doublets adata = adata[~adata.obs['predicted_doublet' ]].copy() print (f"Removed {(predicted_doublets).sum ()} doublets" )print (f"Remaining cells: {adata.n_obs} " )
参数说明:
4.6.2 scDblFinder原理与代码 原理: scDblFinder由Germain等人于2022年开发,是基于R/Bioconductor的双细胞检测工具。与Scrublet不同,scDblFinder使用机器学习分类器(梯度提升树)而非KNN方法。
核心思路:
模拟双细胞: 类似Scrublet,随机选择两个细胞创建模拟双细胞
特征工程: 为每个细胞(真实+模拟)计算一系列特征:
KNN中模拟双细胞的比例
表达矩阵的PCs
聚类信息
nUMIs、n_genes等QC指标
训练分类器: 使用模拟数据训练梯度提升分类器
预测: 对真实细胞进行双细胞概率预测
优势(相比Scrublet):
在基准测试中准确率更高
对异源双细胞检测能力更强
可同时报告双细胞评分和二元分类结果
可检测多细胞(multiplet)
代码示例(R):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 BiocManager:: install( "scDblFinder" ) library( scDblFinder) library( SingleCellExperiment) sce <- SingleCellExperiment( assays = list ( counts = counts_matrix) ) sce <- scDblFinder( sce) table( sce$ scDblFinder.class) head( sce$ scDblFinder.score) sce_filtered <- sce[ , sce$ scDblFinder.class == "singlet" ]
在Python中调用scDblFinder(通过rpy2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import anndata2rianndata2ri.activate() from rpy2.robjects import globalenvglobalenv['adata' ] = adata %%R library(scDblFinder) library(SingleCellExperiment) sce <- SingleCellExperiment(assays = list (counts = t(adata$X))) sce <- scDblFinder(sce, dbr = 0.06 ) doublet_class = %R sce$scDblFinder.class doublet_score = %R sce$scDblFinder.scoreadata.obs['scDblFinder_class' ] = doublet_class adata.obs['scDblFinder_score' ] = doublet_score
4.7 omicverse一站式QC代码 omicverse提供了ov.pp.qc函数,集成了双细胞检测和MAD过滤于一体,是一站式的质量控制工具。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import omicverse as ovimport scanpy as scadata = sc.read_10x_mtx('filtered_feature_bc_matrix/' ) 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('^HB[^(P)]' ) sc.pp.calculate_qc_metrics( adata, qc_vars=['mt' , 'ribo' , 'hb' ], percent_top=None , log1p=False , inplace=True ) adata = ov.pp.qc( adata, mode='seurat' , tresh={ 'mito_perc' : 0.05 , 'nUMIs' : 500 , 'detected_genes' : 250 , }, min_cells=3 , min_genes=200 , ) adata = ov.pp.qc( adata, mode='mads' , nmads=5 , min_cells=3 , min_genes=200 , ) adata.write_h5ad('quality_control.h5ad' )
4.8 完整QC流程代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 import omicverse as ovimport scanpy as scimport numpy as npimport matplotlib.pyplot as pltimport seaborn as snsov.ov_plot_set() adata = sc.read_10x_mtx('filtered_feature_bc_matrix/' , var_names='gene_symbols' ) print (f"原始数据: {adata.shape} " )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('^HB[^(P)]' ) sc.pp.calculate_qc_metrics( adata, qc_vars=['mt' , 'ribo' , 'hb' ], percent_top=None , log1p=False , inplace=True ) print (f"QC指标计算完成:" )print (f" total_counts: {adata.obs['total_counts' ].median():.0 f} (median)" )print (f" n_genes_by_counts: {adata.obs['n_genes_by_counts' ].median():.0 f} (median)" )print (f" pct_counts_mt: {adata.obs['pct_counts_mt' ].median():.1 f} % (median)" )fig, axes = plt.subplots(1 , 3 , figsize=(15 , 4 )) sns.histplot(adata.obs['total_counts' ], bins=100 , kde=False , ax=axes[0 ]) axes[0 ].set_title('Total Counts (nUMIs)' ) axes[0 ].set_xlabel('Total Counts' ) sns.histplot(adata.obs['n_genes_by_counts' ], bins=100 , kde=False , ax=axes[1 ]) axes[1 ].set_title('Detected Genes' ) sns.histplot(adata.obs['pct_counts_mt' ], bins=100 , kde=False , ax=axes[2 ]) axes[2 ].set_title('Mitochondrial Gene %' ) plt.tight_layout() plt.savefig('qc_metrics_distribution.png' , dpi=150 ) plt.show() sc.pp.filter_genes(adata, min_cells=3 ) print (f"过滤低质量基因后: {adata.shape} " )import scrublet as scrscrub = scr.Scrublet(adata.X, expected_doublet_rate=0.06 ) doublet_scores, predicted_doublets = scrub.scrub_doublets( min_counts=2 , min_cells=3 , min_gene_variability_pctl=85 , n_prin_comps=30 , ) adata.obs['doublet_score' ] = doublet_scores adata.obs['predicted_doublet' ] = predicted_doublets print (f"检测到双细胞: {predicted_doublets.sum ()} ({predicted_doublets.mean()*100 :.1 f} %)" )adata = adata[~adata.obs['predicted_doublet' ]].copy() print (f"去除双细胞后: {adata.shape} " )def compute_mad_threshold (values, nmads=5 , direction='lower' ): """计算MAD阈值""" median = np.median(values) mad = np.median(np.abs (values - median)) if direction == 'lower' : threshold = median - nmads * mad elif direction == 'upper' : threshold = median + nmads * mad return threshold, median, mad nmads = 5 lower_nUMIs, med, mad = compute_mad_threshold( adata.obs['total_counts' ], nmads, 'lower' ) print (f"nUMIs: median={med:.0 f} , MAD={mad:.0 f} , threshold={lower_nUMIs:.0 f} " )lower_genes, med, mad = compute_mad_threshold( adata.obs['n_genes_by_counts' ], nmads, 'lower' ) print (f"n_genes: median={med:.0 f} , MAD={mad:.0 f} , threshold={lower_genes:.0 f} " )upper_mt, med, mad = compute_mad_threshold( adata.obs['pct_counts_mt' ], nmads, 'upper' ) print (f"pct_mt: median={med:.1 f} %, MAD={mad:.1 f} %, threshold={upper_mt:.1 f} %" )mask = ( (adata.obs['total_counts' ] >= lower_nUMIs) & (adata.obs['n_genes_by_counts' ] >= lower_genes) & (adata.obs['pct_counts_mt' ] <= upper_mt) ) adata = adata[mask].copy() print (f"MAD过滤后: {adata.shape} " )adata.write_h5ad('quality_control.h5ad' ) print (f"QC完成! 最终数据: {adata.shape} " )
4.9 注意事项和常见陷阱
不要过度过滤 :过滤太严格可能丢失生物学有意义的细胞。例如,某些小型细胞(如血小板)天然具有低nUMIs和低基因数。
线粒体基因比例的陷阱 :高线粒体比例不一定意味着低质量细胞。某些细胞类型(心肌细胞、运动神经元、肾小管细胞)天然具有高线粒体含量。建议结合其他指标联合判断。
MAD倍数选择 :nmads=5是较宽松的阈值(约3.4σ),nmads=3更严格(约2σ)。建议先用5,再根据数据特点调整。
双细胞检测的时机 :应在归一化之前进行双细胞检测,因为归一化可能改变双细胞的特征。
SoupX的使用条件 :需要raw_feature_bc_matrix文件,且仅在环境RNA污染严重时使用。轻度污染不建议使用。
QC指标的相关性 :nUMIs和n_genes高度相关,不要对两者都进行严格的上下限过滤,否则可能过度去除细胞。
批次差异 :不同批次的QC指标分布可能不同,建议每个批次独立设定阈值,或使用MAD方法自动适应。
细胞类型特异性 :过滤前考虑是否了解数据中可能存在的细胞类型及其预期QC特征。
空液滴检测 :对于10x数据,Cell Ranger已经进行了细胞呼叫,但有时会遗漏一些真实细胞。可以检查raw_feature_bc_matrix中的低UMI barcode是否包含被遗漏的细胞。
过滤后重新计算QC指标 :如果对数据进行了大幅过滤,建议重新计算QC指标并检查分布是否合理。
5. 归一化 5.1 为什么需要归一化 5.1.1 技术变异的来源 单细胞RNA测序数据中,细胞间的计数差异并不完全反映真实的生物学差异。由于技术原因,即使同一类型的细胞,其测序计数深度也可能存在巨大差异。这些技术变异的来源包括:
捕获效率差异 :不同细胞的mRNA被捕获的效率不同
逆转录效率差异 :不同细胞的mRNA被逆转录为cDNA的效率不同
PCR扩增偏差 :不同的cDNA分子被扩增的倍数不同
测序深度差异 :不同细胞的reads数量不同
5.1.2 归一化的目标 归一化的核心目标是:消除技术变异(特别是细胞间测序深度的差异),使得细胞间的基因表达差异反映真实的生物学差异,而非技术偏差。
数学上,归一化旨在将”UMI的方差”缩放到指定范围,使得数据满足下游统计模型的假设(如同方差性)。
5.2 伽玛-泊松分布(Gamma-Poisson Distribution) 5.2.1 原理 从理论上和经验上建立的UMI数据模型是伽玛-泊松分布 (Gamma-Poisson Distribution),又称负二项分布 (Negative Binomial Distribution)。
对于基因$g$在细胞$c$中的UMI计数$Y_{gc}$,假设:
$$Y_{gc} | \lambda_{gc} \sim \text{Poisson}(\lambda_{gc})$$
$$\lambda_{gc} \sim \text{Gamma}(\mu_{gc}/\theta, 1/\theta)$$
合并后,$Y_{gc}$服从负二项分布:
$$Y_{gc} \sim \text{NB}(\mu_{gc}, \theta)$$
其方差为:
$$\text{Var}[Y] = \mu + \alpha \mu^2$$
其中:
$\mu$代表UMI平均值(期望表达量)
$\alpha = 1/\theta$是过度离散参数(overdispersion parameter)
$\theta$是形状参数(shape parameter)
5.2.2 参数解释 $\theta$(形状参数):
$\theta$越大,过度离散越小,数据越接近泊松分布
$\theta$越小,过度离散越大,数据方差远大于均值
$\alpha$(过度离散参数):
$\alpha = 0$时,$\text{Var}[Y] = \mu$,数据服从泊松分布
$\alpha > 0$时,数据存在过度离散(overdispersion)
单细胞数据通常具有显著的过度离散($\alpha > 0$)
归一化与伽玛-泊松分布的关系: 不同的归一化方法本质上是对伽玛-泊松分布中不同参数的处理策略,目标是将数据转换到方差稳定的空间。
5.3 移位对数(Shifted Logarithm) 5.3.1 原理 移位对数是最常用的归一化方法,基于delta方法(Delta Method)实现方差稳定化。
Delta方法: 对于非线性函数$f(Y)$,如果$Y$的方差为$\sigma^2$,则$f(Y)$的方差近似为:
$$\text{Var}[f(Y)] \approx [f’(E[Y])]^2 \cdot \text{Var}[Y]$$
对于伽玛-泊松分布,$\text{Var}[Y] = \mu + \alpha\mu^2$。如果我们选择$f(y) = \log(y)$,则:
$$\text{Var}[\log(Y)] \approx \frac{\mu + \alpha\mu^2}{\mu^2} = \frac{1}{\mu} + \alpha$$
当$\mu$足够大时,$\text{Var}[\log(Y)] \approx \alpha$,方差近似恒定。这就是对数变换稳定方差的数学基础。
5.3.2 公式 移位对数的变换公式:
$$f(y) = \log\left(\frac{y}{s} + y_0\right)$$
其中:
$y$是原始UMI计数
$s$是尺寸因子(size factor)
$y_0$是伪计数(pseudo-count),通常为1
尺寸因子$s_c$的计算:
$$s_c = \frac{\sum_g y_{gc}}{L}$$
其中:
$g$遍历所有基因
$L$是目标计数深度(target sum)
不同工具的$L$值选择:
Scanpy默认:$L = \text{median}(\sum_g y_{gc})$,即所有细胞总counts的中位数
Seurat默认:$L = 10^4$
omicverse默认:$L = 50 \times 10^4$
不同$L$值会影响过度离散值$\alpha$,但不影响下游分析结果的本质。
5.3.3 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import scanpy as scimport numpy as npimport seaborn as snsimport matplotlib.pyplot as pltscales_counts = sc.pp.normalize_total(adata, target_sum=None , inplace=False ) adata.layers["log1p_norm" ] = sc.pp.log1p(scales_counts["X" ], copy=True ) sc.pp.normalize_total(adata, target_sum=None ) sc.pp.log1p(adata) fig, axes = plt.subplots(1 , 2 , figsize=(10 , 5 )) p1 = sns.histplot(adata.obs["total_counts" ], bins=100 , kde=False , ax=axes[0 ]) axes[0 ].set_title("Total counts (before normalization)" ) p2 = sns.histplot(adata.layers["log1p_norm" ].sum (1 ), bins=100 , kde=False , ax=axes[1 ]) axes[1 ].set_title("Shifted logarithm (after normalization)" ) plt.tight_layout() plt.show()
5.3.4 适用场景
✅ 大多数scRNA-seq数据分析任务
✅ 降维(PCA)和聚类
✅ 差异表达基因识别
✅ 2023年Nature Methods基准测试中表现最佳的方法之一
⚠️ 对稀有细胞类型的检测不如Pearson残差
5.4 scran反卷积归一化 5.4.1 原理 scran归一化由Lun等人于2016年开发,其核心思想是通过**反卷积(Deconvolution)**方法来估计更准确的尺寸因子。
为什么需要更好的尺寸因子: 简单地将各细胞的总counts归一化到相同深度,假设了所有细胞的RNA含量相同。但实际上,不同细胞类型可能有不同的RNA含量(如大细胞比小细胞有更多RNA)。
scran的方法:
对细胞进行初步聚类(粗粒度),将相似细胞分组
在每组内,将多个细胞的counts求和(pooling),形成”伪细胞”
对伪细胞计算尺寸因子,减少零膨胀的影响
通过线性回归从伪细胞的尺寸因子中解卷积出单个细胞的尺寸因子
数学公式:
假设将$P$个细胞池化,池化后的counts为:
$$Y_{gp}^{\text{pool}} = \sum_{c \in p} Y_{gc}$$
池化后的尺寸因子为:
$$s_p^{\text{pool}} = \sum_{c \in p} s_c$$
通过构建线性方程组,从池化尺寸因子中求解单个细胞的尺寸因子$s_c$。
5.4.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import scanpy as scimport numpy as npfrom scipy.sparse import csr_matrixadata_pp = adata.copy() sc.pp.normalize_total(adata_pp) sc.pp.log1p(adata_pp) sc.pp.pca(adata_pp, n_comps=50 ) sc.pp.neighbors(adata_pp, n_neighbors=10 , n_pcs=50 ) sc.tl.leiden(adata_pp, resolution=0.5 , key_added='groups' ) import rpy2.robjects as rofrom rpy2.robjects import numpy2ri, pandas2rifrom rpy2.robjects.conversion import localconverterdata_mat = adata.X.T if hasattr (data_mat, 'toarray' ): data_mat = data_mat.tocsc().toarray() with localconverter(ro.default_converter + numpy2ri.converter): ro.globalenv["data_mat" ] = data_mat with localconverter(ro.default_converter + pandas2ri.converter): ro.globalenv["input_groups" ] = adata_pp.obs["groups" ] ro.r(''' library(scran) library(BiocParallel) size_factors <- sizeFactors( computeSumFactors( SingleCellExperiment(list(counts=data_mat)), clusters = input_groups, min.mean = 0.1, BPPARAM = MulticoreParam() ) ) ''' )size_factors = ro.r('size_factors' ) adata.obs["size_factors" ] = size_factors scran_normalized = adata.X / adata.obs["size_factors" ].values[:, None ] scran_logged = np.log1p(scran_normalized) adata.layers["scran_normalization" ] = csr_matrix(scran_logged)
5.4.3 适用场景
✅ 批次校正任务(scran归一化考虑了细胞类型特异性的尺寸因子)
✅ 需要更准确尺寸因子的分析
⚠️ 需要R环境
⚠️ 计算速度较慢
⚠️ 对初步聚类的结果有一定依赖
5.5 Pearson残差归一化 5.5.1 原理 Pearson残差归一化(Analytic Pearson Residuals)由Lause等人于2021年提出,基于正则化负二项回归 (Regularized Negative Binomial Regression)。
核心思想: 不再通过尺寸因子和log变换来归一化,而是直接从统计模型中提取残差,将技术变异和生物变异分离。
数学推导:
对于基因$g$在细胞$c$中的计数$Y_{gc}$,假设其服从负二项分布:
$$Y_{gc} \sim \text{NB}(\mu_{gc}, \theta_g)$$
其中均值$\mu_{gc}$由细胞$c$的测序深度决定:
$$\log(\mu_{gc}) = \beta_{g0} + \beta_{g1} \log(s_c)$$
$\beta_{g0}$:基因$g$的截距(baseline表达)
$\beta_{g1}$:测序深度效应(通常$\beta_{g1} \approx 1$)
$s_c$:细胞$c$的测序深度(total counts的某种度量)
Pearson残差定义:
$$Z_{gc} = \frac{Y_{gc} - \hat{\mu}{gc}}{\sqrt{\hat{\mu} {gc} + \hat{\mu}_{gc}^2/\theta_g}}$$
其中:
$\hat{\mu}_{gc}$:模型预测的均值
$\theta_g$:基因$g$的过度离散参数
分母是负二项分布的标准差
Pearson残差的性质:
正值:该基因在该细胞中的表达高于预期
负值:该基因在该细胞中的表达低于预期
接近零:该基因在该细胞中的表达与预期一致
残差的方差近似恒定(方差稳定化)
正则化: 为了避免对低表达基因的$\theta$估计不准确,使用全局的$\theta$-均值关系进行正则化(即从高表达基因估计$\theta$-均值关系,然后外推到低表达基因)。
5.5.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import scanpy as scfrom scipy.sparse import csr_matriximport seaborn as snsimport matplotlib.pyplot as pltanalytic_pearson = sc.experimental.pp.normalize_pearson_residuals(adata, inplace=False ) adata.layers["analytic_pearson_residuals" ] = csr_matrix(analytic_pearson["X" ]) import omicverse as ovadata_pearson = ov.pp.preprocess(adata, mode='pearson|pearson' , n_HVGs=2000 ) fig, axes = plt.subplots(1 , 2 , figsize=(10 , 5 )) sns.histplot(adata.obs["total_counts" ], bins=100 , kde=False , ax=axes[0 ]) axes[0 ].set_title("Total counts" ) sns.histplot(adata.layers["analytic_pearson_residuals" ].sum (1 ), bins=100 , kde=False , ax=axes[1 ]) axes[1 ].set_title("Analytic Pearson residuals" ) plt.tight_layout() plt.show()
5.5.3 适用场景
✅ 识别稀有细胞类型(Pearson残差保留了生物学异质性)
✅ 选择高可变基因(直接基于残差方差选择HVG)
✅ 不需要伪计数和log变换
⚠️ 输出值可正可负,某些工具可能不兼容
⚠️ 不适合直接用于差异表达分析(差异表达通常需要计数矩阵)
5.6 三种方法完整对比
5.7 基于Nature Method基准测试的推荐 2023年4月Nature Methods发表的基准测试(Ahlmann-Eltze & Huber, 2023)比较了22种归一化算法,主要结论:
移位对数在大多数任务中表现良好 ,是最稳健的通用选择
Pearson残差在选择高可变基因方面表现最佳
scran在批次校正任务中有优势
CPM和TPM不推荐 用于scRNA-seq数据(它们假设所有细胞RNA含量相同)
推荐策略:
默认使用移位对数 进行归一化和下游分析
如果需要检测稀有细胞类型,使用Pearson残差
如果需要批次校正,考虑scran
可以同时计算两种归一化结果,存储在不同的layers中
5.8 omicverse一站式归一化代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import omicverse as ovimport scanpy as scadata = sc.read_h5ad('quality_control.h5ad' ) adata.layers['counts' ] = adata.X.copy() adata_log = ov.pp.preprocess( adata.copy(), mode='shiftlog|pearson' , n_HVGs=2000 , ) adata_pearson = ov.pp.preprocess( adata.copy(), mode='pearson|pearson' , n_HVGs=2000 , ) import matplotlib.pyplot as pltimport seaborn as snsfig, axes = plt.subplots(1 , 3 , figsize=(15 , 5 )) sns.histplot(adata.obs['total_counts' ], bins=100 , kde=False , ax=axes[0 ]) axes[0 ].set_title('Total counts (raw)' ) sns.histplot(adata_log.X.sum (1 ), bins=100 , kde=False , ax=axes[1 ]) axes[1 ].set_title('Shifted logarithm' ) sns.histplot(adata_pearson.X.sum (1 ), bins=100 , kde=False , ax=axes[2 ]) axes[2 ].set_title('Analytic Pearson residuals' ) plt.tight_layout() plt.show()
5.9 注意事项
始终保留原始计数 :归一化后,将原始UMI计数保存在adata.layers['counts']中,因为某些下游分析(如差异表达)需要原始计数
选择合适的target_sum :Scanpy默认使用中位数,这在大多数情况下是合理的。避免使用过小或过大的值
归一化方法与下游分析的兼容性 :差异表达分析通常需要原始计数(或归一化计数),而不是log变换或Pearson残差
Pearson残差的输出范围 :Pearson残差可为负值,某些可视化工具可能不兼容
scran的R依赖 :scran需要R环境和BiocParallel,配置可能较复杂
不要对归一化后的数据再次归一化 :每种归一化方法只需应用一次
测序深度极低的细胞 :如果某些细胞的测序深度极低(<500 UMIs),归一化可能不可靠,建议先通过QC过滤
多批次数据 :不同批次的测序深度可能差异很大,建议在归一化前先进行批次校正,或使用批次感知的归一化方法
6. 特征基因选择 6.1 为什么需要特征选择 6.1.1 问题背景 经过上游mapping后,典型的scRNA-seq数据集包含30,000到50,000个基因。然而:
每个典型细胞只表达约2,000-3,000个基因
大量基因在所有细胞中计数为零
许多基因是管家基因(housekeeping genes),在所有细胞中均匀表达,不含细胞类型区分信息
高维数据带来”维度灾难”(Curse of Dimensionality):距离度量失效、计算复杂度指数增长、噪声淹没信号
特征选择的目的: 从30,000+个基因中选择约2,000-4,000个高可变基因(Highly Variable Genes, HVGs) ,这些基因在不同细胞间表达差异最大,最有可能包含细胞类型区分信息。
6.1.2 特征选择的数学直觉 设$X \in \mathbb{R}^{n \times p}$为表达矩阵($n$个细胞,$p$个基因),特征选择即寻找一个子集$S \subset {1, …, p}$,$|S| \approx 2000$,使得$X_S$保留了数据的主要生物学变异。
好的HVGs应该满足:
在不同细胞类型间表达差异大(高方差)
不是技术噪音(排除dropout等引起的假变异)
不受均值-方差关系影响(高表达基因天然方差大,但不一定含有区分信息)
6.2 三种HVG方法 6.2.1 偏离度法(Deviance) 原理:
偏离度法由Townes等人于2019年提出,基于**多项式零模型(Multinomial Null Model)**的偏离度统计量来选择特征基因。
数学公式:
对于基因$g$,假设其在所有细胞中的表达服从多项式分布,零假设为”该基因在所有细胞中表达相同”。
基因$g$的偏离度定义为:
$$D_g = 2 \sum_{c=1}^{n} \left[ y_{gc} \log\left(\frac{y_{gc}}{\hat{\mu}_{gc}}\right) \right]$$
其中:
$y_{gc}$:基因$g$在细胞$c$中的计数
$\hat{\mu}{gc} = \frac{y {g+}}{N_c} \cdot N_c / N_{++}$:零模型下的期望计数
$y_{g+} = \sum_c y_{gc}$:基因$g$在所有细胞中的总计数
$N_c = \sum_g y_{gc}$:细胞$c$的总计数
$N_{++} = \sum_g \sum_c y_{gc}$:所有细胞所有基因的总计数
偏离度越大,说明该基因在不同细胞间的表达越不均匀,越有可能包含生物学信息。
优势:
直接基于原始计数计算,不需要先归一化
基准测试中表现最佳
不受均值-方差关系影响
计算速度快
劣势:
在scanpy中没有原生实现
需要R包scry或scran
代码(使用R的scry包):
1 2 3 4 5 6 7 8 9 10 11 BiocManager:: install( "scry" ) library( scry) deviances <- devianceFeatureSelection( counts_matrix) n_hvgs <- 4000 selected_genes <- names ( sort( deviances, decreasing= TRUE ) ) [ 1 : n_hvgs]
6.2.2 基因方差法(Seurat v3 VST) 原理:
Seurat v3的VST(Variance Stabilizing Transformation)方法由Stuart等人于2019年提出,通过拟合基因均值-方差关系来选择高可变基因。
数学步骤:
对每个基因$g$,计算其均值$\bar{x}_g$和方差$\sigma_g^2$
对均值和方差进行log变换,拟合二阶多项式(局部多项式回归):
$$\log(\sigma_g^2) = a \cdot [\log(\bar{x}_g)]^2 + b \cdot \log(\bar{x}_g) + c$$
对每个基因计算标准化方差(VST后的方差):
$$\text{standardized variance}_g = \frac{\sigma_g^2}{\hat{\sigma}_g^2}$$
其中$\hat{\sigma}_g^2$是从拟合曲线中得到的期望方差。
按标准化方差排序,选择top基因
与偏离度法的关键区别: VST方法试图校正均值-方差关系,而偏离度法直接基于多项式模型计算统计量。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import scanpy as scadata_vst = sc.pp.highly_variable_genes( adata, flavor='seurat_v3' , n_top_genes=2000 , layer='counts' , subset=False , inplace=False , ) print (adata_vst['highly_variable' ].value_counts())
6.2.3 基因离散度法(Seurat Classic) 原理:
传统的Seurat离散度方法,步骤如下:
计算每个基因的均值 (mean)和离散度 (dispersion = variance / mean)
将基因按均值分为20个bin
在每个bin内,对离散度进行z-score标准化
选择标准化离散度最高的基因
z-score标准化的目的: 高表达基因天然具有高方差,简单按方差排序会偏向高表达基因。z-score标准化消除了均值对方差的影响,使得低表达但高变异的基因也能被选中。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import scanpy as scsc.pp.normalize_total(adata) sc.pp.log1p(adata) adata_dis = sc.pp.highly_variable_genes( adata, flavor='seurat' , n_top_genes=2000 , subset=False , inplace=False , ) print (adata_dis['highly_variable' ].value_counts())adata_dis_cut = sc.pp.highly_variable_genes( adata, flavor='seurat' , min_disp=0.5 , min_mean=0.0125 , max_mean=3 , subset=False , inplace=False , ) print (adata_dis_cut['highly_variable' ].value_counts())
6.3 三种方法完整对比
三种方法的重叠度: 在典型数据集中,三种方法选择的2000个HVGs可能只有约600-700个基因是共同的,说明方法选择对下游分析有显著影响。
6.4 推荐方法:偏离度法 + 4000基因 基于基准测试的结果,偏离度法(Deviance)配合4000个基因 是目前推荐的HVG选择策略:
偏离度法在特征选择基准测试中表现最佳
4000个基因比2000个基因保留了更多稀有细胞类型的marker
偏离度法不需要先归一化,避免了归一化方法对HVG选择的影响
实用建议: 由于偏离度法在scanpy中没有原生实现,可以使用Pearson残差法作为替代(两者在概念上相似,都是基于统计模型提取高变异基因)。
1 2 3 4 5 6 7 8 9 10 11 12 13 import scanpy as scsc.pp.highly_variable_genes( adata, flavor='pearson_residuals' , n_top_genes=4000 , layer='counts' , subset=False , ) print (f"选择的HVG数量: {adata.var['highly_variable' ].sum ()} " )
6.5 omicverse代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import omicverse as ovimport scanpy as scadata = sc.read_h5ad('quality_control.h5ad' ) ov.utils.store_layers(adata, layers='counts' ) adata.layers['counts' ] = adata.X.copy() adata = ov.pp.preprocess( adata, mode='shiftlog|pearson' , n_HVGs=2000 , ) adata_pearson = ov.pp.preprocess( adata.copy(), mode='pearson|pearson' , n_HVGs=2000 , ) print (adata.var['highly_variable_features' ].value_counts())
6.6 注意事项
HVG数量选择 :2000-4000是常用范围。2000适合大多数场景,4000适合复杂组织或稀有细胞检测
非HVG基因的保留 :过滤到仅HVG后,非HVG的原始计数会丢失。建议在过滤前将完整数据保存到adata.raw
方法选择的影响 :不同方法选择的HVG可能只有30-40%重叠,方法选择会影响下游聚类结果
避免在归一化后的数据上选择HVG :某些方法(如VST和Pearson残差)需要在原始计数上计算
线粒体/核糖体基因 :通常这些基因不应被选为HVG,除非它们确实包含有意义的生物学变异
批次效应 :如果数据包含多个批次,建议使用批次感知的HVG选择方法(如batch-aware HVG)
7. 降维 7.1 PCA(主成分分析) 7.1.1 原理 PCA(Principal Component Analysis,主成分分析)是最基础的线性降维方法。它通过正交变换将原始高维数据投影到一组新的正交坐标轴(主成分)上,使得第一主成分方向上方差最大,第二主成分方向上方差次之,以此类推。
7.1.2 数学推导 给定表达矩阵$X \in \mathbb{R}^{n \times p}$($n$个细胞,$p$个HVGs),PCA的目标是找到一个低维表示$Z \in \mathbb{R}^{n \times k}$($k \ll p$),使得$Z$尽可能保留$X$中的方差信息。
Step 1:中心化
$$\tilde{X} = X - \bar{X}$$
其中$\bar{X}$是每个基因的均值向量。
Step 2:计算协方差矩阵
$$C = \frac{1}{n-1} \tilde{X}^T \tilde{X} \in \mathbb{R}^{p \times p}$$
Step 3:特征值分解
对协方差矩阵$C$进行特征值分解:
$$C = V \Lambda V^T$$
其中$V$是特征向量矩阵(载荷矩阵),$\Lambda$是对角特征值矩阵。
Step 4:选择前$k$个主成分
将特征值从大到小排序,选择前$k$个特征值对应的特征向量$V_k$,计算低维表示:
$$Z = \tilde{X} V_k$$
方差解释比例:
第$i$个主成分解释的方差比例为:
$$\text{variance ratio}i = \frac{\lambda_i}{\sum {j=1}^{p} \lambda_j}$$
7.1.3 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import scanpy as scimport numpy as npsc.pp.normalize_total(adata) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000 , flavor='seurat_v3' , layer='counts' ) sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 , use_highly_variable=True ) print (adata.obsm['X_pca' ].shape) sc.pl.pca_variance_ratio(adata, n_pcs=50 , log=True ) sc.pl.pca_scatter(adata, color='cell_type' )
7.1.4 选择多少PC 选择合适的主成分数量$k$是关键决策,太少会丢失信息,太多会引入噪音。
肘部法(Elbow Method):
绘制方差解释比例的scree plot
在图中寻找”肘部”——方差解释比例不再显著下降的点
该点对应的PC数即为推荐值
Knee Point方法:
1 2 3 4 5 6 7 8 9 10 11 12 import omicverse as ovadata = ov.pp.preprocess(adata, mode='shiftlog|pearson' , n_HVGs=2000 ) sc.pl.pca_variance_ratio(adata, n_pcs=50 )
Molecular Cross-Validation:
将数据随机分成两半
分别进行PCA
计算两个PCA结果之间的相关性
相关性不再显著增加的PC数即为最佳值
经验值:
通常选择20-50个PC
简单数据集(2-5种主要细胞类型):15-20 PC
复杂数据集(10+种细胞类型):30-50 PC
7.2 t-SNE 7.2.1 原理 t-SNE(t-Distributed Stochastic Neighbor Embedding)由van der Maaten和Hinton于2008年提出,是一种非线性降维方法,特别适合可视化高维数据的局部结构。
核心思想:
在高维空间中,计算每对数据点之间的相似度(条件概率)
在低维空间中,同样计算每对数据点之间的相似度
通过优化,使低维空间中的相似度分布尽可能接近高维空间中的分布
数学公式:
高维空间相似度(高斯分布):
$$p_{j|i} = \frac{\exp(-|x_i - x_j|^2 / 2\sigma_i^2)}{\sum_{k \neq i} \exp(-|x_i - x_k|^2 / 2\sigma_i^2)}$$
对称化:
$$p_{ij} = \frac{p_{j|i} + p_{i|j}}{2n}$$
低维空间相似度(Student-t分布,自由度为1,即Cauchy分布):
$$q_{ij} = \frac{(1 + |y_i - y_j|^2)^{-1}}{\sum_{k \neq l} (1 + |y_k - y_l|^2)^{-1}}$$
目标函数(KL散度):
$$C = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}$$
通过梯度下降最小化$C$,得到低维嵌入$y_i$。
为什么使用t分布而非高斯分布? t分布的重尾特性使得高维空间中的中等距离在低维空间中被映射为更远的距离,从而缓解了”拥挤问题”(crowding problem)。
7.2.2 perplexity参数 perplexity是t-SNE的核心参数,控制每个点的有效近邻数:
$$\text{Perp}(P_i) = 2^{H(P_i)}$$
其中$H(P_i) = -\sum_j p_{j|i} \log_2 p_{j|i}$是信息熵。
perplexity越大 :关注更多的全局结构,聚类更松散
perplexity越小 :关注更多的局部结构,聚类更紧密
推荐范围 :5-50,常用值30
7.2.3 代码 1 2 3 4 5 6 7 import scanpy as scsc.tl.tsne(adata, n_pcs=30 , perplexity=30 , early_exaggeration=12 ) sc.pl.tsne(adata, color=['cell_type' , 'leiden' ], frameon='small' )
7.3 UMAP 7.3.1 原理 UMAP(Uniform Manifold Approximation and Projection)由McInnes等人于2018年提出,基于流形学习和拓扑数据分析。
核心思想:
构建高维数据的模糊拓扑表示(fuzzy simplicial set)
在低维空间中优化一个类似的拓扑结构
通过最小化两个拓扑结构之间的交叉熵来优化低维嵌入
数学步骤:
Step 1:构建KNN图
对每个点找到$k$个最近邻
计算自适应距离:$\rho_i = \min_{j \in N_i} d(x_i, x_j)$(到最近邻的距离)
计算归一化相似度:
$$p_{j|i} = \exp\left(-\frac{d(x_i, x_j) - \rho_i}{\sigma_i}\right)$$
Step 2:对称化 $$p_{ij} = p_{j|i} + p_{i|j} - p_{j|i} \cdot p_{i|j}$$
Step 3:低维嵌入优化
使用交叉熵作为目标函数
近邻点之间吸引,非近邻点之间排斥
7.3.2 min_dist参数 min_dist控制嵌入点之间的最小距离:
min_dist小 (如0.001):聚类更紧密,适合展示细胞类型的分离
min_dist大 (如0.5-1.0):聚类更松散,保留更多全局结构
推荐值 :0.3(scanpy默认)
7.3.3 代码 1 2 3 4 5 6 7 8 9 10 import scanpy as scsc.pp.neighbors(adata, n_neighbors=15 , n_pcs=30 ) sc.tl.umap(adata, min_dist=0.3 , spread=1.0 ) sc.pl.umap(adata, color=['cell_type' , 'leiden' ], frameon='small' )
7.4 扩散映射(Diffusion Map) 7.4.1 原理 扩散映射(Diffusion Map)由Coifman和Lafon于2006年提出,是一种基于扩散过程的非线性降维方法,特别适合捕捉数据中的连续过渡结构(如细胞分化轨迹)。
数学原理:
Step 1:构建相似度矩阵
$$K_{ij} = \exp\left(-\frac{|x_i - x_j|^2}{\epsilon}\right)$$
其中$\epsilon$是核宽度参数。
Step 2:归一化为马尔可夫转移矩阵
$$P = D^{-1} K$$
其中$D$是对角度矩阵,$D_{ii} = \sum_j K_{ij}$。$P_{ij}$表示从状态$i$一步转移到状态$j$的概率。
Step 3:特征值分解
$$P = \Psi \Lambda \Phi^T$$
其中$\Psi$是右特征向量,$\Lambda$是特征值,$\Phi$是左特征向量。
Step 4:扩散坐标
第$t$步扩散后的坐标为:
$$\Psi_t^{(k)} = \lambda_k^t \psi_k$$
其中$\lambda_k$是第$k$个特征值,$\psi_k$是对应的特征向量。
扩散映射的独特优势:
捕捉数据中的连续过渡结构
扩散距离(diffusion distance)反映数据点在流形上的真实距离
适合轨迹推断(pseudotime analysis)
扩散时间$t$控制全局/局部结构的平衡
7.4.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import scanpy as scsc.tl.diffmap(adata, n_comps=15 ) sc.pl.diffmap(adata, color='cell_type' , components=['1,2' , '1,3' ]) root_cell = adata.obs_names[0 ] adata.uns['iroot' ] = np.where(adata.obs_names == root_cell)[0 ][0 ] sc.tl.dpt(adata, n_dcs=10 ) sc.pl.diffmap(adata, color='dpt_pseudotime' )
7.5 MDE(Minimum Distortion Embedding) 7.5.1 原理 MDE(Minimum Distortion Embedding)是一种基于PyMDE库的降维方法,可以看作是UMAP/t-SNE的通用框架。它通过最小化”失真”(distortion)来优化低维嵌入。
优势:
GPU加速,速度比UMAP快
内置邻域计算
支持负采样(negative sampling),可控制全局结构
7.5.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import omicverse as ovadata.obsm["X_mde_scaled" ] = ov.utils.mde(adata.obsm["scaled|original|X_pca" ]) adata.obsm["X_mde_counts" ] = ov.utils.mde(adata.obsm["counts|original|X_pca" ]) ov.utils.embedding( adata, basis='X_mde_scaled' , color=['cell_type' , 'MS4A1' ], frameon='small' )
7.6 各方法对比与适用场景
7.7 完整降维代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import omicverse as ovimport scanpy as scimport numpy as npimport matplotlib.pyplot as pltov.ov_plot_set() adata = sc.read_h5ad('preprocessed.h5ad' ) sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 , use_highly_variable=True ) sc.pl.pca_variance_ratio(adata, n_pcs=50 , log=True ) sc.pp.neighbors(adata, n_neighbors=15 , n_pcs=30 ) sc.tl.umap(adata, min_dist=0.3 ) sc.tl.tsne(adata, n_pcs=30 , perplexity=30 ) sc.tl.diffmap(adata, n_comps=15 ) adata.obsm["X_mde" ] = ov.utils.mde(adata.obsm["scaled|original|X_pca" ]) fig, axes = plt.subplots(2 , 2 , figsize=(12 , 12 )) sc.pl.pca_scatter(adata, color='cell_type' , ax=axes[0 , 0 ], show=False , title='PCA' ) sc.pl.umap(adata, color='cell_type' , ax=axes[0 , 1 ], show=False , title='UMAP' ) sc.pl.tsne(adata, color='cell_type' , ax=axes[1 , 0 ], show=False , title='t-SNE' ) ov.utils.embedding(adata, basis='X_mde' , color='cell_type' , ax=axes[1 , 1 ], show=False , title='MDE' ) plt.tight_layout() plt.savefig('dimensionality_reduction_comparison.png' , dpi=150 ) plt.show() adata.write_h5ad('dimensionality_reduction.h5ad' )
7.8 注意事项
PCA是必需步骤 :t-SNE、UMAP等非线性方法都需要先进行PCA降维,以减少噪声和计算量
PC数量选择 :过多PC引入噪声,过少PC丢失信息。20-50是常用范围
UMAP/t-SNE距离不可解释 :低维嵌入中的距离不反映真实的高维距离,不要基于UMAP距离做定量比较
t-SNE的随机性 :t-SNE每次运行结果可能不同,需要设置随机种子保证可重复性
perplexity/min_dist的调整 :不同参数可能展示不同的数据结构,建议尝试多个参数值
扩散映射的适用性 :仅当数据包含连续过渡结构时使用,否则PCA/UMAP更合适
MDE的速度优势 :对于超过100,000个细胞的大数据集,MDE比UMAP快得多
缩放对PCA的影响 :缩放(Z-score标准化)会去除基因间的表达量差异,使得低表达基因和高表达基因的贡献更均等
8. 聚类 8.1 KNN图构建 8.1.1 原理 KNN(K-Nearest Neighbors,K最近邻)图是聚类的基础。在单细胞分析中,KNN图的构建步骤如下:
在PCA降维后的空间中,计算所有细胞之间的欧氏距离
对每个细胞,找到其$k$个最相似的细胞(最近邻)
将每个细胞与其$k$个最近邻用边连接,形成图
KNN图的性质:
节点 = 细胞
边 = 近邻关系
表达空间中的密集区域 → 图中的密集连接区域
反映了数据的潜在拓扑结构
8.1.2 K值选择
推荐值: scanpy默认n_neighbors=15,适用于大多数数据集。
K值对聚类的影响:
K太小:KNN图过于稀疏,聚类过于细碎
K太大:KNN图过于稠密,聚类过于粗糙,难以区分相似细胞类型
8.1.3 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import scanpy as scsc.pp.neighbors( adata, n_neighbors=15 , n_pcs=30 , use_rep='X_pca' , method='umap' , metric='euclidean' , ) print (f"KNN图节点数: {adata.n_obs} " )print (f"距离矩阵形状: {adata.obsp['distances' ].shape} " )print (f"连接性矩阵形状: {adata.obsp['connectivities' ].shape} " )
注意: KNN图不一定要基于PCA构建。也可以使用其他低维表示,如scVI的潜在空间(X_scVI)或GLUE的嵌入(X_glue):
1 2 sc.pp.neighbors(adata, use_rep='X_scVI' , n_neighbors=15 )
8.2 Leiden算法 8.2.1 原理 Leiden算法由Traag等人于2019年提出,是Louvain算法的改进版本,用于社区检测(community detection),即在图中寻找密集连接的子图(社区/聚类)。
核心概念——模块度(Modularity):
模块度衡量的是社区内部连接的密度相对于随机图期望的密度:
$$Q = \frac{1}{2m} \sum_{ij} \left[ A_{ij} - \frac{k_i k_j}{2m} \right] \delta(c_i, c_j)$$
其中:
$A_{ij}$:邻接矩阵的元素
$k_i = \sum_j A_{ij}$:节点$i$的度
$m = \frac{1}{2}\sum_{ij} A_{ij}$:总边数
$c_i$:节点$i$所属的社区
$\delta(c_i, c_j)$:Kronecker delta函数($c_i = c_j$时为1,否则为0)
模块度的直观理解:
$Q > 0$:社区内部连接比随机期望更多,存在有意义的社区结构
$Q$越大,社区结构越明显
最大值$Q = 1$(所有社区内部完全连接,社区间无连接)
Leiden算法的步骤:
局部移动(Local Moving):
从每个节点自成一群开始
遍历所有节点,将每个节点移到使模块度增益最大的相邻社区
模块度增益:$\Delta Q = Q_{\text{after}} - Q_{\text{before}}$
细化(Refinement):
对第一步得到的分区进行细化
确保每个社区内部是”良连通的”(well-connected)
这是Leiden相比Louvain的关键改进
聚合(Aggregation):
基于细化后的分区,将同一社区的节点聚合为一个超级节点
构建聚合网络
迭代:
8.2.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import scanpy as scsc.tl.leiden( adata, resolution=1.0 , key_added='leiden' , flavor='leidenalg' , ) print (adata.obs['leiden' ].value_counts())sc.pl.umap(adata, color='leiden' , frameon='small' , title='Leiden Clustering' )
8.3 Louvain算法 8.3.1 原理 Louvain算法由Blondel等人于2008年提出,是最早的图聚类社区检测算法之一。其步骤与Leiden类似,但缺少细化步骤 。
Louvain vs Leiden的关键区别:
为什么不推荐Louvain:
Louvain可能产生非良连通的社区 (即社区内部的某些节点之间没有路径连接),这在生物学上是不可解释的
Leiden算法解决了这个问题,通过细化步骤保证社区内部是良连通的
Louvain的原始实现已停止维护
8.4 分辨率参数 8.4.1 含义 分辨率参数(resolution parameter, $\gamma$)是Leiden算法的核心参数,控制聚类的粗细程度。
在模块度公式中引入分辨率参数:
$$Q_{\gamma} = \frac{1}{2m} \sum_{ij} \left[ A_{ij} - \gamma \frac{k_i k_j}{2m} \right] \delta(c_i, c_j)$$
$\gamma = 1$:标准模块度
$\gamma > 1$:倾向于产生更多、更小的社区(细粒度聚类)
$\gamma < 1$:倾向于产生更少、更大的社区(粗粒度聚类)
8.4.2 多分辨率聚类策略 由于不知道数据集中存在多少种细胞类型,推荐使用多分辨率聚类 策略:尝试多个分辨率参数,选择最合理的聚类结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import omicverse as ovimport scanpy as scresolutions = [0.25 , 0.5 , 1.0 , 2.0 ] for res in resolutions: sc.tl.leiden( adata, resolution=res, key_added=f'leiden_res{res} ' , ) n_clusters = adata.obs[f'leiden_res{res} ' ].nunique() print (f"Resolution {res} : {n_clusters} clusters" ) adata.obsm["X_mde" ] = ov.utils.mde(adata.obsm["scaled|original|X_pca" ]) ov.utils.embedding( adata, basis='X_mde' , color=[f"leiden_res{r} " for r in resolutions], title=[f'Resolution: {r} ' for r in resolutions], ncols=2 , frameon='small' , )
分辨率选择指南:
8.5 聚类评估方法 8.5.1 内部评估指标 轮廓系数(Silhouette Score):
$$s(i) = \frac{b(i) - a(i)}{\max(a(i), b(i))}$$
其中$a(i)$是细胞$i$到同聚类内其他细胞的平均距离,$b(i)$是细胞$i$到最近的其他聚类中细胞的平均距离。
$s(i) \in [-1, 1]$
接近1:聚类效果好
接近0:聚类边界模糊
接近-1:细胞可能被分到了错误的聚类
1 2 3 4 5 6 7 8 from sklearn.metrics import silhouette_scorescore = silhouette_score( adata.obsm['X_pca' ][:, :30 ], adata.obs['leiden' ].astype(int ) ) print (f"Silhouette Score: {score:.3 f} " )
8.5.2 模块度 1 2 3 4 5 6 7 8 9 10 11 import igraph as igimport leidenalgg = sc._utils.get_igraph_from_adjacency(adata.obsp['connectivities' ]) partition = leidenalg.find_partition(g, leidenalg.RBConfigurationVertexPartition, resolution_parameter=1.0 ) modularity = partition.modularity print (f"Modularity: {modularity:.3 f} " )
8.5.3 聚类稳定性评估 通过子采样评估聚类的稳定性:对数据多次子采样,分别聚类,比较结果的一致性。
8.6 完整聚类代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import omicverse as ovimport scanpy as scimport numpy as npov.ov_plot_set() adata = sc.read_h5ad('dimensionality_reduction.h5ad' ) print (f"数据维度: {adata.shape} " )sc.pp.neighbors( adata, n_neighbors=15 , n_pcs=30 , use_rep='scaled|original|X_pca' , ) resolutions = [0.25 , 0.5 , 1.0 , 2.0 ] for res in resolutions: sc.tl.leiden( adata, resolution=res, key_added=f'leiden_res{res} ' , ) n_clusters = adata.obs[f'leiden_res{res} ' ].nunique() print (f"Resolution {res} : {n_clusters} clusters" ) adata.obsm["X_mde" ] = ov.utils.mde(adata.obsm["scaled|original|X_pca" ]) ov.utils.embedding( adata, basis='X_mde' , color=[f"leiden_res{r} " for r in resolutions], title=[f'Resolution: {r} ' for r in resolutions], ncols=2 , frameon='small' , ) best_resolution = 0.5 adata.obs['leiden_final' ] = adata.obs[f'leiden_res{best_resolution} ' ] from sklearn.metrics import silhouette_scorescore = silhouette_score( adata.obsm['scaled|original|X_pca' ][:, :30 ], adata.obs['leiden_final' ].astype('category' ).cat.codes ) print (f"Silhouette Score: {score:.3 f} " )adata.write_h5ad('clustering_results.h5ad' ) print ("聚类完成!" )
8.7 注意事项
UMAP距离不可解释 :UMAP/t-SNE嵌入中的距离不反映真实的高维距离。不要基于UMAP图上的距离来判断细胞类型的相似性,应使用PCA空间中的距离。
分辨率选择没有标准答案 :需要结合生物学知识和数据特点来选择合适的分辨率。建议从0.5-1.0开始,根据聚类结果调整。
子聚类的风险 :对某个聚类进行子聚类(subclustering)可以发现更细的细胞状态,但也可能过度拟合噪声。建议子聚类时降低分辨率。
KNN图的影响 :K值选择会影响聚类结果。K太小会导致聚类过于细碎,K太大会导致聚类过于粗糙。
聚类结果的命名 :Leiden聚类的标签(0, 1, 2, …)是随机的,不代表任何生物学顺序。需要通过标记基因(marker genes)进行注释。
避免基于单一分辨率下结论 :建议检查多个分辨率的结果,确保结论不依赖于特定分辨率。
批次效应的影响 :如果数据包含批次效应,聚类可能反映的是批次差异而非生物学差异。建议在聚类前进行批次校正。
Leiden vs Louvain :始终使用Leiden算法,Louvain算法已不推荐使用。
大数据集的加速 :对于超过50,000个细胞的数据集,可以使用sc.pp.neighbors的method='rapids'(GPU加速)或使用rapids-singlecell包。
聚类不是目的,而是手段 :聚类的目的是为了后续的细胞类型注释和差异表达分析。聚类结果本身不具备生物学意义,需要通过marker基因赋予生物学解释。
8.8 关键参考文献
Leiden算法: Traag, V.A., Waltman, L. & van Eck, N.J. (2019). From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports , 9, 5233.
Louvain算法: Blondel, V.D. et al. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics , P10008.
模块度: Newman, M.E. (2006). Modularity and community structure in networks. Proceedings of the National Academy of Sciences , 103(23), 8577-8582.
PCA: Jolliffe, I.T. & Cadima, J. (2016). Principal component analysis: a review and recent developments. Philosophical Transactions of the Royal Society A , 374, 20150202.
t-SNE: van der Maaten, L. & Hinton, G. (2008). Visualizing data using t-SNE. Journal of Machine Learning Research , 9, 2579-2605.
UMAP: McInnes, L., Healy, J. & Melville, J. (2020). UMAP: Uniform Manifold Approximation and Projection for dimension reduction. arXiv preprint arXiv:1802.03426 .
扩散映射: Coifman, R.R. & Lafon, S. (2006). Diffusion maps. Applied and Computational Harmonic Analysis , 21(1), 5-30.
Scrublet: Wolock, S.L., Lopez, R. & Klein, A.M. (2019). Scrublet: computational identification of cell doublets in single-cell transcriptomic data. Cell Systems , 8(4), 281-291.
scDblFinder: McGinnis, C.S. et al. (2019). DoubletFinder: doublet detection in single-cell RNA sequencing data using artificial nearest neighbors. Cell Systems , 8(4), 329-337.
SoupX: Young, M.D. & Behjati, S. (2020). SoupX removes ambient RNA contamination from droplet-based single-cell RNA sequencing data. GigaScience , 9(12), giaa151.
scran归一化: Lun, A.T.L., Bach, K. & Marioni, J.C. (2016). Pooling across cells to normalize single-cell RNA sequencing data with many zero counts. Genome Biology , 17, 75.
Pearson残差: Lause, J. et al. (2021). Analytic Pearson residuals for normalization of single-cell RNA-seq UMI data. Genome Biology , 22, 258.
偏离度法: Townes, F.W. et al. (2019). Feature selection and dimension reduction for single-cell RNA-seq based on a multinomial model. Genome Biology , 20, 295.
归一化基准测试: Ahlmann-Eltze, C. & Huber, W. (2023). Comparison of transformations for single-cell RNA-seq data. Nature Methods , 20, 665-672.
Single-cell best practices: Heumos, L. et al. (2023). Best practices for single-cell analysis across modalities. Nature Reviews Genetics , 24, 550-572.
omicverse: Xu, Z. et al. (2024). OmicVerse: a single cell multi-omics framework. bioRxiv .
scanpy: Wolf, F.A., Angerer, P. & Theis, F.J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology , 19, 15.
MDE: Agrawal, A. et al. PyMDE: Minimum-Distortion Embedding. arXiv preprint .
Seurat v3 VST: Stuart, T. et al. (2019). Comprehensive integration of single-cell data. Cell , 177(7), 1888-1902.
Cell Ranger: 10x Genomics. Cell Ranger Documentation. https://support.10xgenomics.com/
附录:预处理完整流程总结 A.1 预处理流程概览图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 原始FASTQ文件 ↓ [Cell Ranger mkfastq] 拆分后的FASTQ (R1: barcode+UMI, R2: cDNA) ↓ [Cell Ranger count / alevin-fry] 基因-细胞计数矩阵 (raw + filtered) ↓ [导入Python/scanpy] AnnData对象 (adata.X = 原始UMI计数) ↓ ==== 质量控制 (QC) ==== ├── 计算QC指标 (nUMIs, n_genes, pct_mt, pct_ribo) ├── 环境RNA校正 (SoupX, 可选) ├── 双细胞检测 (Scrublet / scDblFinder) ├── 过滤低质量细胞 (MAD方法 / 手动阈值) └── 过滤低质量基因 (min_cells=3) ↓ QC后的AnnData对象 ↓ ==== 归一化 (Normalization) ==== ├── 保存原始计数到 adata.layers['counts'] ├── 移位对数归一化 (默认推荐) │ ├── sc.pp.normalize_total(adata, target_sum=None) │ └── sc.pp.log1p(adata) ├── scran反卷积归一化 (批次校正场景) └── Pearson残差归一化 (稀有细胞检测场景) ↓ 归一化后的AnnData对象 ↓ ==== 特征基因选择 (HVG Selection) ==== ├── 偏离度法 (推荐, 需R/omicverse) ├── Pearson残差法 (推荐, scanpy原生) ├── Seurat VST (通用) └── 选择2000-4000个高可变基因 ↓ HVG选择后的AnnData对象 ↓ ==== 降维 (Dimensionality Reduction) ==== ├── Z-score缩放 (sc.pp.scale) ├── PCA降维 (sc.tl.pca, n_comps=50) ├── 选择PC数量 (肘部法/Knee Point, 推荐20-50) ├── 构建KNN邻域图 (sc.pp.neighbors, k=15) ├── UMAP可视化 (sc.tl.umap) ├── t-SNE可视化 (sc.tl.tsne, 可选) ├── 扩散映射 (sc.tl.diffmap, 轨迹推断场景) └── MDE可视化 (ov.utils.mde, 快速) ↓ 降维后的AnnData对象 ↓ ==== 聚类 (Clustering) ==== ├── Leiden聚类 (推荐) │ ├── sc.tl.leiden(adata, resolution=0.5-1.0) │ └── 多分辨率聚类策略 ├── 聚类评估 (轮廓系数/模块度) └── 聚类结果可视化 (UMAP/MDE) ↓ 预处理完成的AnnData对象 → 下游分析
A.2 一站式预处理代码(omicverse) 以下代码展示了使用omicverse进行一站式预处理的完整流程,适合快速上手:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import omicverse as ovimport scanpy as scimport numpy as npov.ov_plot_set() adata = sc.read_10x_mtx('filtered_feature_bc_matrix/' , var_names='gene_symbols' ) print (f"原始数据: {adata.shape} " )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('^HB[^(P)]' ) sc.pp.calculate_qc_metrics( adata, qc_vars=['mt' , 'ribo' , 'hb' ], percent_top=None , log1p=False , inplace=True ) adata = ov.pp.qc( adata, mode='seurat' , tresh={'mito_perc' : 0.05 , 'nUMIs' : 500 , 'detected_genes' : 250 }, min_cells=3 , min_genes=200 , ) print (f"QC后: {adata.shape} " )adata.layers['counts' ] = adata.X.copy() adata = ov.pp.preprocess( adata, mode='shiftlog|pearson' , n_HVGs=2000 , ) print (f"预处理后: {adata.shape} " )sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 , use_highly_variable=True ) sc.pp.neighbors(adata, n_neighbors=15 , n_pcs=30 ) sc.tl.umap(adata, min_dist=0.3 ) adata.obsm["X_mde" ] = ov.utils.mde(adata.obsm["scaled|original|X_pca" ]) for res in [0.25 , 0.5 , 1.0 , 2.0 ]: sc.tl.leiden(adata, resolution=res, key_added=f'leiden_res{res} ' ) adata.obs['leiden_final' ] = adata.obs['leiden_res0.5' ] ov.utils.embedding( adata, basis='X_mde' , color=['leiden_res0.5' , 'leiden_res1.0' ], frameon='small' , ) adata.write_h5ad('preprocessed_complete.h5ad' ) print (f"预处理完成! 最终数据: {adata.shape} " )
A.3 常见问题与故障排除 Q1: Cell Ranger运行报错”insufficient memory” 解决方案:
增加服务器内存(建议≥64 GB)
减少并行线程数(--localcores)
使用alevin-fry替代(内存需求更低)
Q2: 过滤后细胞数太少 可能原因:
样本质量差(细胞活性低)
过滤阈值过严
MAD倍数过小
解决方案:
放宽阈值(nmads=5→7)
检查样本质量
查看QC指标分布,判断是否存在合理的低质量细胞
Q3: 归一化后数据分布异常 可能原因:
target_sum设置不当
数据中存在极端的测序深度差异
解决方案:
使用默认的target_sum=None
先过滤极端的细胞
尝试scran归一化
Q4: UMAP/t-SNE结果看起来像”一团” 可能原因:
PC数量选择不当
HVG数量不当
数据本身缺少明显的细胞类型区分
解决方案:
调整PC数量(增加或减少)
增加HVG数量到3000-4000
检查数据质量和生物学假设
Q5: 聚类结果不稳定(每次运行结果不同) 可能原因:
解决方案:
设置随机种子:sc.tl.leiden(adata, random_state=0)
增加n_neighbors值使图更稳定
Q6: scrone归一化报错 可能原因:
解决方案:
确保R和BiocManager已安装
BiocManager::install("scran")
考虑使用移位对数替代
Q7: 双细胞检测去除过多细胞 可能原因:
expected_doublet_rate设置过高
数据中存在具有中间态特征的细胞
解决方案:
降低expected_doublet_rate
检查被去除细胞的特征,判断是否为真实的中间态细胞
使用更宽松的双细胞评分阈值
Q8: 线粒体基因比例普遍偏高 可能原因:
组织特性(心脏、肌肉等高能量组织)
样本制备不当导致大量细胞死亡
解决方案:
结合其他QC指标判断
适当放宽线粒体基因比例阈值
检查样本制备流程
A.4 关键软件版本与安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 conda create -n scenv python=3.10 conda activate scenv pip install scanpy pip install anndata pip install omicverse pip install scrublet pip install scvi-tools pip install leidenalg pip install rpy2 pip install anndata2ri
推荐版本(截至2024年):
A.5 数据存放规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 adata
以上便是单细胞RNA测序分析从入门基础到预处理全流程的完整详尽教材。每一章节都包含了理论基础、数学公式、完整代码和注意事项,读者可以按顺序学习,也可以根据需要直接查阅特定章节。建议初学者先通读全文建立整体概念,再结合实际数据逐步实践每个步骤。
A.6 预处理参数速查表 以下是预处理过程中最常用的参数及其推荐值的快速参考表,供读者在实际分析中随时查阅。
A.7 预处理流程决策指南 在实际分析中,研究者经常面临多个选择。以下决策指南帮助读者在不同场景下做出合理的选择。
场景一:标准PBMC分析
QC:MAD过滤 + Scrublet双细胞检测
归一化:移位对数
HVG:2000个,Pearson残差法
降维:PCA(30PC) + UMAP
聚类:Leiden, resolution=0.5-1.0
场景二:复杂组织(如肿瘤)分析
QC:MAD过滤 + Scrublet双细胞检测 + SoupX环境RNA校正
归一化:移位对数(主分析)+ Pearson残差(稀有细胞检测)
HVG:4000个,Pearson残差法
降维:PCA(50PC) + UMAP + Diffusion Map
聚类:多分辨率Leiden, resolution=0.25-2.0
场景三:多批次数据整合
QC:每个批次独立QC
归一化:移位对数 + 批次校正(Harmony/scVI)
HVG:批次感知的HVG选择
降维:校正后的PCA + UMAP
聚类:基于校正后的嵌入进行Leiden聚类
场景四:发育轨迹分析
QC:MAD过滤(注意保留过渡态细胞)
归一化:移位对数 或 Pearson残差
HVG:3000-4000个
降维:PCA + Diffusion Map
聚类:Leiden, 较低分辨率(0.25-0.5)
场景五:大规模细胞图谱
QC:MAD过滤 + Scrublet + 严格阈值
归一化:移位对数
HVG:3000个,Pearson残差法
降维:PCA + MDE(快速可视化)
聚类:多分辨率Leiden + 层次化注释
A.8 从原始数据到发表级结果的检查清单 在完成全部预处理流程后,建议按照以下检查清单逐项验证分析质量,确保结果可靠、可重复。
数据质量检查:
质量控制检查:
归一化检查:
特征选择检查:
降维检查:
聚类检查:
可重复性检查:
完成以上所有检查后,预处理阶段的输出即可用于下游的细胞类型注释、差异表达分析等高级分析任务。
Part 2:细胞识别与下游分析
第三篇:细胞识别
第九章:细胞类型注释
基于Ch12 + 教程六,详细展开
9.1 细胞类型概念与分类层次 在单细胞转录组数据分析中,细胞类型注释(Cell Type Annotation)是最关键的分析步骤之一。它决定了后续所有生物学解读的方向和可靠性。细胞类型注释的核心目标是:将基于无监督聚类得到的每个细胞簇(cluster),赋予有生物学意义的标签。
细胞类型的定义层次
细胞类型的定义并非一个简单的二元问题,而是存在多层次的分类体系:
细胞大类(Cell Class) :如免疫细胞(Immune cells)、上皮细胞(Epithelial cells)、基质细胞(Stromal cells)、内皮细胞(Endothelial cells)等。这是最粗粒度的分类。
细胞类型(Cell Type) :在细胞大类之下进一步细分,如免疫细胞可细分为T细胞(T cells)、B细胞(B cells)、巨噬细胞(Macrophages)、树突状细胞(Dendritic cells)等。
细胞亚型(Cell Subtype) :在同一细胞类型内进一步区分功能状态,如T细胞可细分为CD4+ T细胞、CD8+ T细胞、调节性T细胞(Treg)、γδ T细胞等。
细胞状态(Cell State) :同一细胞亚型在不同微环境或刺激下的功能状态,如效应T细胞(Effector T cells)、耗竭T细胞(Exhausted T cells)、记忆T细胞(Memory T cells)等。
注释的哲学问题 :需要注意的是,”细胞类型”的边界是人为定义的。在连续的细胞分化轨迹中,细胞类型的划分往往是模糊的。因此,注释时应遵循”宁可保守,不过度解读”的原则,在标记基因证据不足时,使用”类X细胞”(X-like cells)或”可能的X细胞”(Probable X cells)等表述。
9.2 手动注释 手动注释(Manual Annotation)是细胞类型注释的金标准方法。它依赖于领域知识,通过检查每个细胞簇的标记基因表达模式来推断细胞身份。
9.2.1 标记基因组合 标记基因(Marker Genes)是在特定细胞类型中特异性高表达的基因。注释时需要组合使用多个标记基因,而非依赖单一基因,因为:
单一标记基因可能跨细胞类型表达(如CD3D在所有T细胞亚型中均表达)
部分基因存在转录本剪接变异或同源基因干扰
不同细胞类型可能共享部分标记基因(如CD4在巨噬细胞和CD4+ T细胞中均表达)
常见细胞类型标记基因参考表:
9.2.2 rank_genes_groups:差异基因鉴定 Scanpy提供的rank_genes_groups函数是鉴定每个簇标记基因的核心工具。它通过对每个簇与其他所有簇进行比较,鉴定在该簇中特异性高表达的基因。
方法选择 :
method='wilcoxon':Wilcoxon秩和检验,最常用,稳健性好
method='t-test':t检验,速度快但对异常值敏感
method='logreg':逻辑回归,适用于多分类问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad("clustered_data.h5ad" ) sc.tl.rank_genes_groups( adata, groupby='leiden' , method='wilcoxon' , corr_method='benjamini-hochberg' , key_added='wilcoxon' , use_raw=True , pts=True , ) sc.pl.rank_genes_groups( adata, n_genes=15 , sharey=False , key='wilcoxon' , fontsize=12 , ) marker_df = sc.get.rank_genes_groups_df( adata, group=None , key='wilcoxon' , ) marker_df_filtered = marker_df[ (marker_df['pvals_adj' ] < 0.05 ) & (marker_df['logfoldchanges' ] > 1.0 ) & (marker_df['pct_nz_group' ] > 0.5 ) ] print (f"过滤前标记基因数: {len (marker_df)} " )print (f"过滤后标记基因数: {len (marker_df_filtered)} " )cluster_0_markers = sc.get.rank_genes_groups_df( adata, group='0' , key='wilcoxon' , ) print (cluster_0_markers.head(10 ))
输出说明 :
names:基因名
scores:统计检验得分
pvals:原始p值
pvals_adj:校正后p值
logfoldchanges:log2倍数变化
pct_nz_group:目标簇中表达该基因的细胞比例
pct_nz_reference:参考簇中表达该基因的细胞比例
9.2.3 UMAP可视化标记基因 UMAP可视化是手动注释中直观确认标记基因表达模式的关键步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import scanpy as scimport matplotlib.pyplot as pltfig, ax = plt.subplots(1 , 1 , figsize=(8 , 6 )) sc.pl.umap( adata, color='leiden' , ax=ax, title='Leiden Clustering' , legend_loc='on data' , legend_fontsize=10 , frameon=False , ) plt.savefig("umap_leiden.png" , dpi=300 , bbox_inches='tight' ) plt.show() marker_genes = { 'T cells' : ['CD3D' , 'CD3E' , 'CD2' ], 'B cells' : ['CD79A' , 'MS4A1' , 'CD19' ], 'Monocytes' : ['CD14' , 'S100A8' , 'S100A9' ], 'NK cells' : ['NCAM1' , 'NKG7' , 'GNLY' ], 'Epithelial' : ['EPCAM' , 'KRT8' , 'KRT18' ], 'Endothelial' : ['PECAM1' , 'VWF' , 'CDH5' ], 'Fibroblasts' : ['COL1A1' , 'DCN' , 'LUM' ], } sc.pl.umap( adata, color=[gene for genes in marker_genes.values() for gene in genes], gene_symbols='gene_name' , cmap='Reds' , vmin=0 , vmax='p99' , frameon=False , ncols=3 , save='_marker_genes.png' , ) sc.pl.violin( adata, ['CD3D' , 'CD79A' , 'CD14' , 'NCAM1' ], groupby='leiden' , stripplot=False , multi_panel=True , rotation=45 , save='_markers_violin.png' , ) sc.pl.dotplot( adata, marker_genes, groupby='leiden' , dendrogram=True , standard_scale='var' , save='_marker_dotplot.png' , ) sc.pl.stacked_violin( adata, marker_genes, groupby='leiden' , dendrogram=True , swap_axes=True , save='_marker_stacked_violin.png' , ) sc.pl.matrixplot( adata, marker_genes, groupby='leiden' , dendrogram=True , standard_scale='var' , cmap='Blues' , save='_marker_matrixplot.png' , )
9.2.4 手动注释完整流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad("clustered_data.h5ad" ) print (f"细胞数: {adata.n_obs} , 基因数: {adata.n_vars} " )print (f"聚类数: {adata.obs['leiden' ].nunique()} " )sc.tl.rank_genes_groups( adata, groupby='leiden' , method='wilcoxon' , corr_method='benjamini-hochberg' , pts=True , ) all_markers = sc.get.rank_genes_groups_df(adata, group=None ) all_markers.to_csv("cluster_markers.csv" , index=False ) marker_genes_dict = { 'T cells' : ['CD3D' , 'CD3E' , 'CD2' ], 'CD4+ T cells' : ['CD4' , 'IL7R' ], 'CD8+ T cells' : ['CD8A' , 'CD8B' ], 'Treg' : ['FOXP3' , 'IL2RA' ], 'B cells' : ['CD79A' , 'MS4A1' , 'CD19' ], 'Plasma cells' : ['IGHG1' , 'JCHAIN' , 'MZB1' ], 'NK cells' : ['NCAM1' , 'KLRD1' , 'NKG7' ], 'Monocytes' : ['CD14' , 'S100A8' , 'LYZ' ], 'Macrophages' : ['CD68' , 'CD163' , 'MARCO' ], 'Dendritic cells' : ['CD1C' , 'FCER1A' ], 'pDC' : ['LILRA4' , 'IRF7' ], 'Epithelial' : ['EPCAM' , 'KRT8' , 'KRT18' ], 'Endothelial' : ['PECAM1' , 'VWF' , 'CDH5' ], 'Fibroblasts' : ['COL1A1' , 'DCN' , 'LUM' ], } sc.pl.dotplot( adata, marker_genes_dict, groupby='leiden' , dendrogram=True , standard_scale='var' , figsize=(14 , 6 ), save='_annotation_dotplot.png' , ) cluster_annotation = { '0' : 'CD4+ T cells' , '1' : 'Monocytes' , '2' : 'B cells' , '3' : 'CD8+ T cells' , '4' : 'NK cells' , '5' : 'Fibroblasts' , '6' : 'Epithelial' , '7' : 'Endothelial' , '8' : 'Dendritic cells' , '9' : 'Plasma cells' , '10' : 'Treg' , '11' : 'pDC' , '12' : 'Macrophages' , } adata.obs['cell_type' ] = adata.obs['leiden' ].map (cluster_annotation) print (adata.obs['cell_type' ].value_counts())sc.pl.umap( adata, color='cell_type' , legend_loc='right margin' , legend_fontsize=8 , frameon=False , title='Manual Cell Type Annotation' , save='_annotated.png' , ) adata.write_h5ad("annotated_data.h5ad" ) print ("注释完成!数据已保存。" )
9.3 自动注释方法详解 自动注释方法利用已标注的参考数据集,通过机器学习或统计模型预测新数据中细胞的类型。这些方法大大提高了注释效率,但需要与手动注释结合使用。
9.3.1 CellTypist 原理 :
CellTypist是一个基于逻辑回归(Logistic Regression)的自动细胞类型注释工具,由Teichmann实验室开发。其核心思路是:
从大规模已标注的参考数据集中训练多分类逻辑回归模型
使用正则化(L1/L2)防止过拟合
对新数据集进行概率预测,输出每个细胞属于各细胞类型的概率
逻辑回归模型的形式为:
$$P(y=k \mid \mathbf{x}) = \frac{\exp(\mathbf{w}k^T \mathbf{x} + b_k)}{\sum {j=1}^{K} \exp(\mathbf{w}_j^T \mathbf{x} + b_j)}$$
其中 $\mathbf{x}$ 是细胞的基因表达向量,$\mathbf{w}_k$ 和 $b_k$ 是第 $k$ 类的权重和偏置,$K$ 是细胞类型总数。
Majority Voting(多数投票) :CellTypist的一个关键创新是majority voting机制。在over-clustering(过度聚类)后,对同一簇内的所有细胞进行投票,选择多数细胞被预测的类型作为该簇的最终标签。这有效降低了单个细胞预测的噪声。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 import celltypistfrom celltypist import modelsimport scanpy as scmodels.download_models(force_update=True ) model_list = models.models_description() print (model_list)model_name = "Immune_All_Low.pkl" models.download_models(model=[model_name]) adata = sc.read_h5ad("preprocessed_data.h5ad" ) print (f"数据维度: {adata.shape} " )print (f"X矩阵类型: {type (adata.X)} " )predictions = celltypist.annotate( adata, model=model_name, majority_voting=False , ) print (predictions.predicted_labels.head(20 ))adata = predictions.to_adata() predictions_voting = celltypist.annotate( adata, model=model_name, majority_voting=True , ) adata_voting = predictions_voting.to_adata() adata_voting.obs['celltypist_label' ] = adata_voting.obs['majority_voting' ] sc.pl.umap( adata_voting, color=['celltypist_label' , 'cell_type_fine' , 'leiden' ], ncols=2 , frameon=False , save='_celltypist_comparison.png' , ) sc.pl.umap( adata_voting, color='cell_type_fine_prob' , cmap='RdYlGn' , frameon=False , title='CellTypist Prediction Probability' , save='_celltypist_probability.png' , ) train_adata = sc.read_h5ad("annotated_reference.h5ad" ) model = celltypist.train( train_adata, labels='cell_type' , n_jobs=10 , feature_selection=True , use_SGD=True , ) model.write("my_custom_model.pkl" ) predictions_custom = celltypist.annotate( adata, model="my_custom_model.pkl" , majority_voting=True , ) adata = predictions_custom.to_adata() print ("CellTypist注释完成!" )
注意事项 :
模型选择应与数据类型匹配(如免疫细胞数据应选择Immune模型)
Majority voting通常能提高注释准确性,但可能导致小亚群被忽略
低概率预测(如 < 0.5)的细胞可能需要手动验证
不同器官/组织的细胞类型组成差异可能导致模型预测偏差
9.3.2 CellAssign 原理 :
CellAssign是一个基于概率模型的细胞类型注释工具,由Irizarry实验室开发。与CellTypist不同的是,CellAssign不需要完整的参考数据集,而是利用先验定义的标记基因-细胞类型对应关系进行推断。
CellAssign的核心是一个概率生成模型:
$$P(y_i = k \mid \mathbf{x}_i, \Theta) \propto P(\mathbf{x}_i \mid y_i = k, \Theta) \cdot \pi_k$$
其中 $\mathbf{x}_i$ 是细胞 $i$ 的基因表达,$y_i$ 是其细胞类型标签,$\pi_k$ 是细胞类型 $k$ 的先验比例,$\Theta$ 是模型参数。
模型假设标记基因的表达服从:
$$x_{ig} \mid y_i = k \sim \text{NB}(\mu_{gk}, \phi_g)$$
其中 $\mu_{gk}$ 取决于基因 $g$ 是否为细胞类型 $k$ 的标记基因,$\phi_g$ 是基因特异的dispersion参数。对于标记基因:
$$\mu_{gk} = s_i \cdot \exp(\beta_g + \delta_{gk})$$
其中 $s_i$ 是细胞特异的size factor,$\beta_g$ 是基因基线表达,$\delta_{gk}$ 是标记基因的增量表达。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import scanpy as scimport pandas as pdimport numpy as npfrom cellassign import CellAssignmarker_gene_mat = pd.DataFrame( { 'T cells' : [1 , 1 , 0 , 0 , 0 , 0 , 0 ], 'B cells' : [0 , 0 , 1 , 1 , 0 , 0 , 0 ], 'Monocytes' : [0 , 0 , 0 , 0 , 1 , 1 , 0 ], 'NK cells' : [0 , 0 , 0 , 0 , 0 , 0 , 1 ], }, index=['CD3D' , 'CD3E' , 'CD79A' , 'MS4A1' , 'CD14' , 'LYZ' , 'NKG7' ] ) adata = sc.read_h5ad("preprocessed_data.h5ad" ) if adata.raw is not None : adata_cellassign = adata.raw.to_adata() else : adata_cellassign = adata.copy() ca = CellAssign( adata=adata_cellassign, marker_gene_matrix=marker_gene_mat, ) ca.fit() predictions = ca.predict() print (predictions.head())adata.obs['cellassign_label' ] = predictions['cell_type' ] adata.obs['cellassign_prob' ] = predictions['max_prob' ] sc.pl.umap( adata, color=['cellassign_label' , 'cellassign_prob' ], frameon=False , save='_cellassign.png' , ) print ("CellAssign注释完成!" )
注意事项 :
标记基因矩阵的质量直接影响注释结果
CellAssign对标记基因的选择非常敏感,应使用经过验证的经典标记基因
模型假设存在”未分配”(unassigned)细胞类型,用于处理无法匹配的细胞
CellAssign的计算开销较大,对大数据集需要较长时间
9.3.3 Garnett 原理 :
Garnett是由Trapnell实验室开发的层级式细胞类型注释工具。其核心创新是使用一种专门的标记语言(Marker Language)来定义细胞类型的层级关系和标记基因组合。
Garnett的标记语言格式示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 > cluster T_cell expressed: CD3D, CD3E, CD2 not expressed: CD79A, MS4A1 > cluster CD4_T_cell expressed: CD4, IL7R not expressed: CD8A > cluster CD8_T_cell expressed: CD8A, CD8B not expressed: CD4 > cluster B_cell expressed: CD79A, MS4A1 not expressed: CD3D, CD3E
Garnett训练一个基于弹性网络正则化的逻辑回归模型:
$$\hat{\mathbf{w}} = \arg\min_{\mathbf{w}} \left[ -\ell(\mathbf{w}) + \lambda_1 |\mathbf{w}|_1 + \lambda_2 |\mathbf{w}|_2^2 \right]$$
其中 $\ell(\mathbf{w})$ 是对数似然,$\lambda_1$ 和 $\lambda_2$ 分别是L1和L2正则化系数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 import scanpy as scimport pandas as pdfrom sklearn.linear_model import LogisticRegressionfrom sklearn.preprocessing import StandardScalerimport numpy as npmarker_genes = ['CD3D' , 'CD3E' , 'CD2' , 'CD79A' , 'MS4A1' , 'CD4' , 'IL7R' , 'CD8A' , 'CD8B' , 'CD14' , 'LYZ' ] adata = sc.read_h5ad("annotated_data.h5ad" ) marker_idx = [list (adata.var_names).index(g) for g in marker_genes if g in adata.var_names] X_markers = adata.raw[:, marker_genes].X if adata.raw else adata[:, marker_genes].X scaler = StandardScaler(with_mean=False ) X_scaled = scaler.fit_transform(X_markers) y = adata.obs['cell_type' ].values clf_l1 = LogisticRegression( penalty='elasticnet' , solver='saga' , l1_ratio=0.5 , max_iter=10000 , multi_class='multinomial' , ) clf_l1.fit(X_scaled, y) y_pred = clf_l1.predict(X_scaled) adata.obs['garnett_like_label' ] = y_pred print ("Garnett风格注释完成!" )
9.3.4 SCSA(教程八) 原理 :
SCSA(Single-Cell Segregation Analyzer)是一种基于标记基因富集分析的自动注释方法。其核心思路是:
对每个簇鉴定差异表达基因(DEG)
将DEG列表与已知的细胞类型标记基因数据库进行富集分析
根据富集得分和统计显著性为每个簇分配最可能的细胞类型
SCSA使用的统计检验基于超几何分布(Hypergeometric Distribution):
$$P(X \geq k) = 1 - \sum_{i=0}^{k-1} \frac{\binom{M}{i}\binom{N-M}{n-i}}{\binom{N}{n}}$$
其中 $N$ 是数据库中总基因数,$M$ 是与某细胞类型相关的基因数,$n$ 是输入的DEG数,$k$ 是DEG中与该细胞类型相关的基因数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 import scanpy as scimport pandas as pdimport numpy as npfrom scipy.stats import hypergeomadata = sc.read_h5ad("clustered_data.h5ad" ) sc.tl.rank_genes_groups( adata, groupby='leiden' , method='wilcoxon' , corr_method='benjamini-hochberg' , pts=True , ) marker_df = sc.get.rank_genes_groups_df(adata, group=None ) marker_out = [] for cluster in adata.obs['leiden' ].unique(): cluster_markers = sc.get.rank_genes_groups_df(adata, group=cluster) cluster_markers = cluster_markers[ (cluster_markers['pvals_adj' ] < 0.05 ) & (cluster_markers['logfoldchanges' ] > 0.5 ) ] for _, row in cluster_markers.iterrows(): marker_out.append({ 'cluster' : cluster, 'gene' : row['names' ], 'score' : row['scores' ], 'pval' : row['pvals_adj' ], }) marker_df_scsa = pd.DataFrame(marker_out) marker_df_scsa.to_csv("scsa_input.csv" , index=False , sep='\t' ) def scsa_annotation (adata, groupby='leiden' , db='cellmarker' , top_n=100 ): """ 简化版SCSA注释:基于超几何检验的标记基因富集 """ cellmarker_db = { 'T cells' : {'CD3D' , 'CD3E' , 'CD2' , 'IL7R' , 'CD4' , 'CD8A' }, 'B cells' : {'CD79A' , 'MS4A1' , 'CD19' , 'CD79B' }, 'Monocytes' : {'CD14' , 'S100A8' , 'S100A9' , 'LYZ' }, 'NK cells' : {'NCAM1' , 'NKG7' , 'GNLY' , 'KLRD1' }, 'Epithelial' : {'EPCAM' , 'KRT8' , 'KRT18' , 'KRT19' }, 'Endothelial' : {'PECAM1' , 'VWF' , 'CDH5' }, 'Fibroblasts' : {'COL1A1' , 'DCN' , 'LUM' , 'COL1A2' }, } all_db_genes = set () for genes in cellmarker_db.values(): all_db_genes.update(genes) N = len (all_db_genes) annotations = {} for cluster in adata.obs[groupby].unique(): markers = sc.get.rank_genes_groups_df(adata, group=cluster) markers = markers[ (markers['pvals_adj' ] < 0.05 ) & (markers['logfoldchanges' ] > 0.5 ) ].head(top_n) deg_set = set (markers['names' ].values) & all_db_genes n = len (deg_set) best_type = None best_pval = 1.0 best_score = 0 for cell_type, type_genes in cellmarker_db.items(): M = len (type_genes) k = len (deg_set & type_genes) if k == 0 : continue pval = hypergeom.sf(k - 1 , N, M, n) score = -np.log10(pval) * (k / M) if score > best_score: best_score = score best_pval = pval best_type = cell_type annotations[cluster] = { 'cell_type' : best_type, 'p_value' : best_pval, 'score' : best_score, } print (f"Cluster {cluster} : {best_type} (p={best_pval:.2 e} , score={best_score:.2 f} )" ) return annotations annotations = scsa_annotation(adata) annotation_map = {k: v['cell_type' ] for k, v in annotations.items()} adata.obs['scsa_label' ] = adata.obs['leiden' ].map (annotation_map) sc.pl.umap(adata, color='scsa_label' , frameon=False , save='_scsa.png' ) print ("SCSA注释完成!" )
原理 :
MetaTiME是一种整合了单细胞转录组数据和TME(Tumor Microenvironment)知识的细胞类型注释方法。它结合了CellTypist的自动预测能力和TiME(Tumor Immune Microenvironment)的领域知识,特别适用于肿瘤免疫微环境的注释。
MetaTiME的核心步骤:
从大规模肿瘤微环境数据中学习元组件(Meta-components)
每个元组件对应一种细胞状态或功能程序
通过元组件的活性推断细胞类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import scanpy as scimport pandas as pdimport numpy as npimport MetaTiME as mtadata = sc.read_h5ad("tumor_tme_data.h5ad" ) me_model = mt.get_me_model() adata = mt.score_me(adata, me_model) print (adata.obsm.keys()) mt.plot_me_score( adata, me_model, n_top=10 , figsize=(12 , 8 ), ) import celltypistfrom celltypist import modelsmodels.download_models(model=["Immune_All_Low.pkl" ]) predictions = celltypist.annotate( adata, model="Immune_All_Low.pkl" , majority_voting=True , ) adata = predictions.to_adata() sc.pl.umap( adata, color=['majority_voting' , 'leiden' ], frameon=False , ncols=2 , save='_celltypist_metatime.png' , ) print ("CellTypist+MetaTiME注释完成!" )
9.3.6 TOSICA(教程十) 原理 :
TOSICA(Transformer-based One-stop Annotation)是一种基于Transformer架构的迁移学习细胞类型注释方法。其核心创新在于:
Transformer注意力机制 :使用自注意力(Self-Attention)机制捕获基因间的长程依赖关系
迁移学习 :在大规模参考数据上预训练,然后迁移到新数据集上
One-stop :同时完成细胞类型注释和跨数据集整合
Transformer的核心计算是Scaled Dot-Product Attention:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$
其中 $Q$(Query)、$K$(Key)、$V$(Value)是输入的线性变换,$d_k$ 是Key的维度。
多头注意力(Multi-Head Attention)进一步扩展:
$$\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, …, \text{head}_h)W^O$$
其中 $\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import scanpy as scimport pandas as pdimport numpy as npimport TOSICA as tosref_adata = sc.read_h5ad("annotated_reference.h5ad" ) print (f"参考数据集: {ref_adata.n_obs} cells, {ref_adata.n_vars} genes" )query_adata = sc.read_h5ad("query_data.h5ad" ) print (f"查询数据集: {query_adata.n_obs} cells, {query_adata.n_vars} genes" )model = tos.train( ref_adata, label_key='cell_type' , gff_path='gene_gff.txt' , batch_size=64 , epochs=50 , lr=1e-4 , n_heads=4 , n_layers=2 , hidden_dim=256 , dropout=0.1 , save_dir='./tosica_model' , ) predictions = tos.predict( query_adata, model_path='./tosica_model' , batch_size=256 , ) query_adata.obs['tosica_label' ] = predictions['predicted_label' ] query_adata.obs['tosica_prob' ] = predictions['probability' ] sc.pl.umap( query_adata, color=['tosica_label' , 'tosica_prob' ], frameon=False , ncols=2 , save='_tosica.png' , ) if 'cell_type' in query_adata.obs.columns: from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score ari = adjusted_rand_score( query_adata.obs['cell_type' ], query_adata.obs['tosica_label' ] ) nmi = normalized_mutual_info_score( query_adata.obs['cell_type' ], query_adata.obs['tosica_label' ] ) print (f"TOSICA ARI: {ari:.4 f} " ) print (f"TOSICA NMI: {nmi:.4 f} " ) print ("TOSICA注释完成!" )
TOSICA注意事项 :
需要足够大的参考数据集进行训练
基因特征文件(gff)的准备是关键步骤
GPU加速可显著提高训练速度
对于与参考数据差异较大的查询数据(如不同物种),迁移效果可能下降
9.4 手动+自动结合策略 最佳实践是结合手动注释和自动注释的优势:
先自动后手动 :先使用自动注释获得初步标签,再手动验证和修正
交叉验证 :使用多种自动注释方法,一致性高的注释更可信
标记基因确认 :对自动注释的每个细胞类型,检查标记基因表达
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import scanpy as scimport pandas as pdimport numpy as npfrom sklearn.metrics import adjusted_rand_scoreadata = sc.read_h5ad("preprocessed_data.h5ad" ) import celltypistfrom celltypist import modelsmodels.download_models(model=["Immune_All_Low.pkl" ]) ct_pred = celltypist.annotate(adata, model="Immune_All_Low.pkl" , majority_voting=True ) adata.obs['celltypist_label' ] = ct_pred.to_adata().obs['majority_voting' ] if 'manual_label' in adata.obs.columns: agreement = (adata.obs['celltypist_label' ] == adata.obs['manual_label' ]).mean() print (f"CellTypist与手动注释一致率: {agreement:.2 %} " ) adata.obs['annotation_agreement' ] = ( adata.obs['celltypist_label' ] == adata.obs.get('manual_label' , adata.obs['celltypist_label' ]) ) adata.obs['needs_review' ] = ~adata.obs['annotation_agreement' ] review_cells = adata[adata.obs['needs_review' ]].copy() print (f"需要手动验证的细胞数: {review_cells.n_obs} " )adata.obs['final_label' ] = adata.obs['celltypist_label' ].copy() sc.pl.umap(adata, color='final_label' , frameon=False , save='_final_annotation.png' ) adata.write_h5ad("fully_annotated_data.h5ad" )
9.5 标记基因数据库 9.5.1 CellMarker CellMarker( http://bio-bigdata.hrbmu.edu.cn/CellMarker/)是一个手动整理的细胞标记基因数据库,包含:
超过13,000个细胞标记基因
覆盖467种细胞类型
来源包括文献、数据库和单细胞数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import pandas as pdcellmarker_url = "http://bio-bigdata.hrbmu.edu.cn/CellMarker/download/all_cell_markers.txt"
9.5.2 PanglaoDB PanglaoDB( https://panglaodb.se/)是一个单细胞RNA测序数据的标记基因数据库,特点:
包含人和小鼠的细胞类型标记基因
基于单细胞数据质量评估标记基因特异性
提供标记基因的特异性和敏感性评分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import pandas as pdpanglao_url = "https://panglaodb.se/markers/PanglaoDB_markers_27_Mar_2020.tsv"
9.6 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import scanpy as scimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad("after_clustering.h5ad" ) print (f"数据维度: {adata.shape} " )print (f"聚类数: {adata.obs['leiden' ].nunique()} " )sc.tl.rank_genes_groups( adata, groupby='leiden' , method='wilcoxon' , corr_method='benjamini-hochberg' , pts=True , ) all_markers = sc.get.rank_genes_groups_df(adata, group=None ) all_markers.to_csv("all_cluster_markers.csv" , index=False ) import celltypistfrom celltypist import modelsmodels.download_models(model=["Immune_All_Low.pkl" ]) predictions = celltypist.annotate( adata, model="Immune_All_Low.pkl" , majority_voting=True , ) adata = predictions.to_adata() adata.obs['auto_label' ] = adata.obs['majority_voting' ] marker_genes_dict = { 'T cells' : ['CD3D' , 'CD3E' , 'CD2' ], 'B cells' : ['CD79A' , 'MS4A1' ], 'Monocytes' : ['CD14' , 'LYZ' ], 'NK cells' : ['NKG7' , 'GNLY' ], 'Epithelial' : ['EPCAM' , 'KRT8' ], 'Endothelial' : ['PECAM1' , 'VWF' ], 'Fibroblasts' : ['COL1A1' , 'DCN' ], } sc.pl.dotplot( adata, marker_genes_dict, groupby='leiden' , dendrogram=True , standard_scale='var' , save='_verification_dotplot.png' , ) final_annotation = {} for cluster in adata.obs['leiden' ].unique(): auto_labels = adata[adata.obs['leiden' ] == cluster].obs['auto_label' ] most_common_auto = auto_labels.mode()[0 ] final_annotation[cluster] = most_common_auto adata.obs['cell_type' ] = adata.obs['leiden' ].map (final_annotation) sc.pl.umap( adata, color=['cell_type' , 'auto_label' , 'leiden' ], frameon=False , ncols=2 , save='_final_annotations.png' , ) adata.write_h5ad("annotated_data.h5ad" ) print ("细胞类型注释完成!" )
9.7 注意事项
不要过度依赖自动注释 :自动注释工具可能产生错误,特别是对于稀有细胞类型或参考数据集中未包含的细胞类型。
标记基因的物种特异性 :人和小鼠的同源基因可能有不同的表达模式,跨物种注释需要格外谨慎。
细胞状态vs细胞类型 :注意区分细胞类型和细胞状态的注释。同一细胞类型在不同状态下可能表达不同的标记基因。
聚类分辨率的影响 :注释结果依赖于聚类分辨率。分辨率过低会合并不同细胞类型,过高会将同一类型拆分为多个簇。
doublet的影响 :未去除的doublet可能同时表达多种细胞类型的标记基因,干扰注释。
数据库更新 :标记基因数据库需要定期更新,以纳入最新发现的标记基因。
命名规范 :使用标准化的细胞类型命名,推荐参考Cell Ontology( https://cellontology.org/)。
可重复性 :记录注释过程中的所有决策和依据,确保分析可重复。
关键参考文献 :
Domínguez Conde et al. (2022). Cross-tissue immune cell analysis reveals tissue-specific features in humans. Science . (CellTypist)
Zhang et al. (2019). CellAssign: probabilistic cell-type assignment of single-cell RNA-seq data. Nature Biotechnology .
Pliner et al. (2019). Supervised classification enables rapid annotation of cell atlases. Nature Methods . (Garnett)
Cao et al. (2020). A human cell landscape at single-cell level. Nature . (SCSA相关)
Shao et al. (2022). TOSICA: A Transformer-based One-stop Annotation for single-cell RNA-seq data. Bioinformatics .
Jin et al. (2022). MetaTiME: Translating Tumor Microenvironment Knowledge to Single-cell Analysis. Nature Communications .
第十章:数据整合与批次效应校正
基于Ch13 + 教程七,详细展开
10.1 批次效应来源:技术vs生物 在单细胞转录组研究中,批次效应(Batch Effect)是不可避免的系统性变异。理解批次效应的来源对于选择合适的整合方法至关重要。
技术批次效应 :
技术批次效应来源于实验过程中的系统性差异:
样本制备差异 :不同批次的组织消化时间、酶浓度、温度等条件的微小差异会导致细胞转录组的系统性偏移。
测序平台差异 :10x Chromium v2 vs v3、Smart-seq2 vs 10x、不同测序深度等平台间存在固有差异。
建库策略差异 :3’端测序 vs 5’端测序、全长测序 vs 3’端富集等建库方法的不同。
操作人员差异 :不同实验人员的操作习惯会导致系统性的技术变异。
实验室环境差异 :不同实验室的仪器、试剂批次、环境条件等。
生物批次效应 :
生物变异是我们希望保留的信号,但有时会被误认为是批次效应:
个体差异 :不同供体的遗传背景、年龄、性别等导致的真实生物学差异。
组织微环境差异 :同一器官不同区域、原发灶vs转移灶等空间异质性。
疾病状态差异 :健康vs疾病、不同分期/分级间的生物学差异。
时间动态差异 :发育或疾病进展过程中的真实时间变化。
区分原则 :整合方法的目标是消除技术批次效应,同时保留真实的生物学变异。过度整合会抹除真实的生物学信号,整合不足则遗留技术噪声。
10.2 整合方法详解 10.2.1 scVI:变分自编码器 原理 :
scVI(single-cell Variational Inference)是基于变分自编码器(Variational Autoencoder, VAE)的单细胞数据整合方法,由Yosef实验室开发。
scVI的核心模型假设细胞的基因表达计数矩阵服从零膨胀负二项分布(Zero-Inflated Negative Binomial, ZINB):
$$P(x_{ng} \mid \cdot) = (1 - h_{ng}) \cdot \delta_0(x_{ng}) + h_{ng} \cdot \text{NB}(x_{ng} \mid \mu_{ng}, \theta_g)$$
其中:
$x_{ng}$ 是细胞 $n$ 中基因 $g$ 的观测计数
$h_{ng}$ 是dropout概率(零膨胀部分)
$\mu_{ng}$ 是负二项分布的均值
$\theta_g$ 是基因特异的dispersion参数
模型通过编码器(Encoder)将高维基因表达数据映射到低维潜在空间:
$$q_\phi(\mathbf{z}_n \mid \mathbf{x}_n, s_n, b_n) = \mathcal{N}(\mathbf{z}n \mid \boldsymbol{\mu} \phi(\mathbf{x}n, s_n), \text{diag}(\boldsymbol{\sigma} \phi^2(\mathbf{x}_n, s_n)))$$
其中 $s_n$ 是size factor,$b_n$ 是批次标签。
解码器(Decoder)从潜在表示重建基因表达:
$$\mu_{ng} = w_n \cdot \exp(f_\gamma(\mathbf{z}_n, b_n)_g)$$
其中 $w_n$ 是细胞特异的scaling因子,$f_\gamma$ 是解码器网络,$b_n$ 是批次标签(用于建模批次效应)。
训练目标为最大化证据下界(ELBO):
$$\mathcal{L}(\phi, \gamma) = \mathbb{E}{q \phi(\mathbf{z} \mid \mathbf{x})}[\log p_\gamma(\mathbf{x} \mid \mathbf{z}, s, b)] - D_{\text{KL}}(q_\phi(\mathbf{z} \mid \mathbf{x}, s) | p(\mathbf{z}))$$
关键设计:批次标签 $b_n$ 作为解码器的条件输入,使得潜在空间 $\mathbf{z}$ 不包含批次信息,从而实现批次效应校正。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import scanpy as scimport scviimport pandas as pdimport numpy as npadata = sc.read_h5ad("multi_batch_data.h5ad" ) print (f"数据维度: {adata.shape} " )print (f"批次信息: {adata.obs['batch' ].value_counts()} " )scvi.model.SCVI.setup_anndata( adata, layer="counts" , batch_key="batch" , categorical_covariate_keys=["sample" ], continuous_covariate_keys=["percent_mito" ], ) model = scvi.model.SCVI( adata, n_latent=30 , n_layers=2 , n_hidden=128 , gene_likelihood="zinb" , dispersion="gene-batch" , use_observed_lib_size=False , ) model.train( max_epochs=400 , early_stopping=True , early_stopping_patience=15 , batch_size=512 , plan_kwargs={'lr' : 1e-3 }, ) ax = model.history['train_loss_epoch' ].plot() model.history['validation_loss_epoch' ].plot(ax=ax) ax.set_xlabel('Epoch' ) ax.set_ylabel('Loss' ) ax.set_title('scVI Training Loss' ) plt.savefig("scvi_training_loss.png" , dpi=300 , bbox_inches='tight' ) plt.show() adata.obsm["X_scVI" ] = model.get_latent_representation() adata.layers["scvi_normalized" ] = model.get_normalized_expression() sc.pp.neighbors(adata, use_rep="X_scVI" ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap( adata, color=['batch' , 'leiden' ], frameon=False , ncols=2 , save='_scvi_integration.png' , ) model.save("scvi_model/" , overwrite=True ) adata.write_h5ad("scvi_integrated_data.h5ad" ) print ("scVI整合完成!" )
10.2.2 scANVI:半监督整合+注释 原理 :
scANVI(single-cell Annotation using Variational Inference)是scVI的扩展,在整合批次效应的同时进行细胞类型注释。它利用部分已标注的细胞信息,以半监督方式训练模型。
scANVI的核心思想是在scVI的VAE框架中加入细胞类型标签的建模:
$$P(\mathbf{x}_n, y_n \mid s_n, b_n) = \int P(\mathbf{x}_n \mid \mathbf{z}_n, s_n, b_n) P(\mathbf{z}_n \mid y_n) P(y_n) d\mathbf{z}_n$$
对于已标注细胞,$y_n$ 是已知的;对于未标注细胞,模型推断 $y_n$ 的后验分布:
$$q_\phi(y_n \mid \mathbf{x}n) = \text{softmax}(g \phi(\mathbf{x}_n))$$
其中 $g_\phi$ 是一个分类网络。
scANVI的训练过程:
先用scVI预训练(无标签信息)
然后用scANVI微调(加入标签信息)
这种方法的优势:同时完成批次效应校正和细胞类型注释,且注释结果已经考虑了批次效应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import scanpy as scimport scviimport pandas as pdimport numpy as npadata = sc.read_h5ad("multi_batch_data.h5ad" ) print (f"已知标签细胞数: {(adata.obs['cell_type' ] != 'Unknown' ).sum ()} " )print (f"未知标签细胞数: {(adata.obs['cell_type' ] == 'Unknown' ).sum ()} " )scvi.model.SCVI.setup_anndata( adata, layer="counts" , batch_key="batch" , ) scvi_model = scvi.model.SCVI(adata, n_latent=30 ) scvi_model.train(max_epochs=200 , early_stopping=True ) scvi.model.SCANVI.setup_anndata( adata, layer="counts" , batch_key="batch" , labels_key="cell_type" , unlabeled_category="Unknown" , ) scanvi_model = scvi.model.SCANVI.from_scvi_model( scvi_model, unlabeled_category="Unknown" , ) scanvi_model.train(max_epochs=200 , early_stopping=True ) adata.obs['scanvi_label' ] = scanvi_model.predict() adata.obsm["X_scANVI" ] = scanvi_model.get_latent_representation() adata.obs['scanvi_prob' ] = scanvi_model.predict(soft=True ).max (axis=1 ) sc.pp.neighbors(adata, use_rep="X_scANVI" ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap( adata, color=['batch' , 'scanvi_label' , 'scanvi_prob' ], frameon=False , ncols=2 , save='_scanvi_integration.png' , ) import matplotlib.pyplot as pltplt.figure(figsize=(8 , 4 )) plt.hist(adata.obs['scanvi_prob' ], bins=50 , edgecolor='black' ) plt.xlabel('Prediction Probability' ) plt.ylabel('Number of Cells' ) plt.title('scANVI Prediction Probability Distribution' ) plt.savefig('scanvi_prob_dist.png' , dpi=300 , bbox_inches='tight' ) plt.show() scanvi_model.save("scanvi_model/" , overwrite=True ) adata.write_h5ad("scanvi_integrated_data.h5ad" ) print ("scANVI整合+注释完成!" )
10.2.3 scVI vs scANVI选择策略
推荐策略 :
如果只需要整合:使用scVI
如果需要同时整合和注释:使用scANVI
如果标签质量不确定:先用scVI整合,手动验证后再用scANVI
如果有高质量标签:直接使用scANVI
10.2.4 Harmony 原理 :
Harmony是一种基于迭代聚类的数据整合方法,由Regev实验室开发。其核心思想是在降维后的PCA空间中,通过迭代软聚类和线性校正来消除批次效应。
Harmony的算法步骤:
初始化 :对合并的数据进行PCA降维,得到 $\mathbf{Z}^{(0)} \in \mathbb{R}^{d \times N}$
迭代更新 :
a. 软聚类 :将每个细胞分配到多个聚类,计算隶属度矩阵 $\mathbf{R}$:
$$R_{ki} = \frac{\exp(-|\mathbf{z}_i - \mathbf{y}k|^2 / \sigma)}{\sum {k’} \exp(-|\mathbf{z}i - \mathbf{y} {k’}|^2 / \sigma)}$$
其中 $\mathbf{y}_k$ 是聚类中心,$\sigma$ 是带宽参数。
b. 批次校正 :计算每个聚类中各批次的校正向量:
$$\mathbf{W}_k = \text{diag}(\mathbf{R}_k) \cdot \mathbf{P}_b - \mathbf{P}_b$$
其中 $\mathbf{P}_b$ 是批次编码矩阵。
c. 线性变换 :应用校正变换:
$$\mathbf{Z}^{(t+1)} = \mathbf{Z}^{(t)} - \mathbf{W} \cdot \mathbf{R}^T$$
收敛判断 :当 $|\mathbf{Z}^{(t+1)} - \mathbf{Z}^{(t)}| < \epsilon$ 时停止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import scanpy as scimport harmonypy as hmimport pandas as pdimport numpy as npadata = sc.read_h5ad("multi_batch_data.h5ad" ) sc.pp.pca(adata, n_comps=50 ) harmony_out = hm.run_harmony( adata.obsm['X_pca' ], adata.obs, 'batch' , max_iter_harmony=20 , epsilon_harmony=1e-5 , sigma=0.1 , theta=2.0 , ) adata.obsm['X_harmony' ] = harmony_out.Z_corr.T sc.pp.neighbors(adata, use_rep='X_harmony' ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap( adata, color=['batch' , 'leiden' ], frameon=False , ncols=2 , save='_harmony_integration.png' , ) adata.write_h5ad("harmony_integrated_data.h5ad" ) print ("Harmony整合完成!" )
Harmony参数调优 :
theta:批次校正强度,默认2.0。增大则更强的批次校正,但可能过度整合。
sigma:聚类带宽,默认0.1。影响软聚类的模糊程度。
max_iter_harmony:最大迭代次数,默认20。通常10-20次迭代即可收敛。
10.2.5 BBKNN 原理 :
BBKNN(Batch-Balanced K-Nearest Neighbors)是一种在图构建层面进行批次校正的方法。其核心思想是:在构建KNN图时,确保每个细胞的邻居来自各个批次而非仅来自同一批次。
算法步骤:
对每个细胞,在每个批次中分别寻找 $k/b$ 个最近邻($b$ 为批次数)
合并所有批次的邻居,构建批次平衡的KNN图
基于该图进行后续的UMAP和聚类分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import scanpy as scimport bbknnadata = sc.read_h5ad("multi_batch_data.h5ad" ) sc.pp.pca(adata, n_comps=50 ) bbknn.bbknn( adata, batch_key='batch' , neighbors_within_batch=15 , n_pcs=50 , trim=None , ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap( adata, color=['batch' , 'leiden' ], frameon=False , ncols=2 , save='_bbknn_integration.png' , ) adata.write_h5ad("bbknn_integrated_data.h5ad" ) print ("BBKNN整合完成!" )
10.2.6 ComBat 原理 :
ComBat是一种经典的批次效应校正方法,最初用于微阵列数据,后来被应用于单细胞数据。它基于经验贝叶斯(Empirical Bayes)框架,估计并校正批次特异的位置和尺度参数。
ComBat的模型:
$$Y_{ijg} = \alpha_g + \mathbf{X}\boldsymbol{\beta}g + \gamma {bg} + \delta_{bg} \epsilon_{ijg}$$
其中:
$Y_{ijg}$ 是批次 $b$ 中样本 $j$ 基因 $g$ 的表达值
$\alpha_g$ 是基因 $g$ 的总体均值
$\mathbf{X}\boldsymbol{\beta}_g$ 是协变量效应
$\gamma_{bg}$ 是批次 $b$ 对基因 $g$ 的位置效应(加法偏移)
$\delta_{bg}$ 是批次 $b$ 对基因 $g$ 的尺度效应(乘法偏移)
$\epsilon_{ijg}$ 是误差项
经验贝叶斯估计:
$$\hat{\gamma}_{bg}^{EB} = \frac{\hat{\tau}g^2 \hat{\gamma} {bg} + \hat{\lambda}_g \hat{\sigma}_g^2}{\hat{\tau}_g^2 + \hat{\sigma}_g^2}$$
$$\hat{\delta}_{bg}^{EB} = \frac{\hat{\rho}g \hat{\delta} {bg} + \hat{\eta}_g \hat{\sigma}_g^2}{\hat{\rho}_g + \hat{\sigma}_g^2}$$
校正后的表达值:
$$Y_{ijg}^{*} = \frac{\hat{\sigma}g}{\hat{\delta} {bg}^{EB}} (Y_{ijg} - \hat{\gamma}_{bg}^{EB}) + \alpha_g + \mathbf{X}\hat{\boldsymbol{\beta}}_g$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad("multi_batch_data.h5ad" ) sc.pp.combat( adata, key='batch' , ) sc.pp.pca(adata, n_comps=50 ) sc.pp.neighbors(adata) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap( adata, color=['batch' , 'leiden' ], frameon=False , ncols=2 , save='_combat_integration.png' , ) adata.write_h5ad("combat_integrated_data.h5ad" ) print ("ComBat校正完成!" )
ComBat注意事项 :
ComBat假设数据近似正态分布,对log-normalized数据效果更好
不适用于count数据
对于少量样本的批次,经验贝叶斯估计可能不稳定
ComBat是线性校正方法,无法处理非线性的批次效应
10.3 整合效果评估:scIB指标 scIB(single-cell Integration Benchmarking)是一个系统评估单细胞数据整合效果的工具,包含多个评估指标。
评估维度 :
批次效应去除(Batch Correction) :
graph connectivity (gc) :评估批次内KNN图的连通性,值越高表示同一细胞类型的细胞越紧密连接
silhouette score (asw_label) :基于细胞类型标签的轮廓系数,评估聚类质量
iLISI :局部逆Simpson指数,评估批次混合程度
生物学变异保留(Bio Conservation) :
NMI(Normalized Mutual Information) :评估聚类与已知标签的一致性
ARI(Adjusted Rand Index) :评估聚类与已知标签的调整后一致性
cell type ASW :基于细胞类型的轮廓系数
isolated label F1 :对稀有细胞类型的F1分数
cLISI :细胞类型局部逆Simpson指数,评估细胞类型的纯度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 import scanpy as scimport pandas as pdimport numpy as npimport scibadata = sc.read_h5ad("integrated_data.h5ad" ) metrics = scib.metrics.metrics( adata, adata_int=adata, batch_key='batch' , label_key='cell_type' , embed='X_pca' , cluster_key='leiden' , nmi_ari_cluster_labels=True , silhouette_=True , graph_conn_=True , pcr_=True , isolated_labels_=True , lisi_graph_=True , ) print (metrics)gc = scib.me.graph_connectivity(adata, label_key='cell_type' ) print (f"Graph Connectivity: {gc:.4 f} " )sil = scib.me.silhouette(adata, label_key='cell_type' , embed='X_pca' ) print (f"Silhouette Score: {sil:.4 f} " )nmi = scib.me.nmi(adata, label_key='cell_type' , cluster_key='leiden' ) print (f"NMI: {nmi:.4 f} " )ari = scib.me.ari(adata, label_key='cell_type' , cluster_key='leiden' ) print (f"ARI: {ari:.4 f} " )ilisi = scib.me.ilisi_graph(adata, batch_key='batch' ) print (f"iLISI: {ilisi:.4 f} " )clisi = scib.me.clisi_graph(adata, batch_key='batch' , label_key='cell_type' ) print (f"cLISI: {clisi:.4 f} " )results = [] for method in ['scVI' , 'Harmony' , 'BBKNN' , 'ComBat' ]: adata_method = sc.read_h5ad(f"{method.lower()} _integrated_data.h5ad" ) gc = scib.me.graph_connectivity(adata_method, label_key='cell_type' ) sil = scib.me.silhouette(adata_method, label_key='cell_type' , embed='X_pca' ) nmi = scib.me.nmi(adata_method, label_key='cell_type' , cluster_key='leiden' ) ari = scib.me.ari(adata_method, label_key='cell_type' , cluster_key='leiden' ) results.append({ 'Method' : method, 'Graph_Connectivity' : gc, 'Silhouette' : sil, 'NMI' : nmi, 'ARI' : ari, }) results_df = pd.DataFrame(results) print (results_df)import matplotlib.pyplot as pltfig, axes = plt.subplots(2 , 2 , figsize=(12 , 10 )) for ax, metric in zip (axes.flat, ['Graph_Connectivity' , 'Silhouette' , 'NMI' , 'ARI' ]): results_df.plot.bar(x='Method' , y=metric, ax=ax, legend=False ) ax.set_title(metric) ax.set_ylim(0 , 1 ) plt.tight_layout() plt.savefig("integration_benchmark.png" , dpi=300 , bbox_inches='tight' ) plt.show()
10.4 omicverse一站式整合代码 omicverse提供了一个统一的接口来调用多种整合方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import omicverse as ovimport scanpy as scadata = sc.read_h5ad("multi_batch_data.h5ad" ) adata_integrated = ov.single.batch_correction( adata, batch_key='batch' , method='scvi' , n_pcs=50 , n_neighbors=15 , n_latent=30 , max_epochs=400 , ) adata_scvi = ov.single.scvi_integrate( adata, batch_key='batch' , n_latent=30 , max_epochs=400 , ) adata_harmony = ov.single.harmony_integrate( adata, batch_key='batch' , theta=2.0 , ) sc.pp.neighbors(adata_integrated, use_rep='X_scVI' ) sc.tl.umap(adata_integrated) sc.tl.leiden(adata_integrated, resolution=0.8 ) sc.pl.umap( adata_integrated, color=['batch' , 'leiden' ], frameon=False , ncols=2 , save='_omicverse_integration.png' , ) print ("omicverse一站式整合完成!" )
10.5 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 import scanpy as scimport scviimport harmonypy as hmimport bbknnimport scibimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltbatch1 = sc.read_h5ad("batch1.h5ad" ) batch2 = sc.read_h5ad("batch2.h5ad" ) batch3 = sc.read_h5ad("batch3.h5ad" ) adata = batch1.concatenate(batch2, batch3, batch_key='batch' , batch_categories=['B1' , 'B2' , 'B3' ]) print (f"总细胞数: {adata.n_obs} " )print (f"各批次细胞数: {adata.obs['batch' ].value_counts()} " )sc.pp.filter_cells(adata, min_genes=200 ) sc.pp.filter_genes(adata, min_cells=3 ) adata.var['mt' ] = adata.var_names.str .startswith('MT-' ) sc.pp.calculate_qc_metrics(adata, qc_vars=['mt' ], inplace=True ) adata = adata[adata.obs['pct_counts_mt' ] < 20 ] adata.layers['counts' ] = adata.X.copy() sc.pp.normalize_total(adata, target_sum=1e4 ) sc.pp.log1p(adata) adata.raw = adata sc.pp.highly_variable_genes( adata, n_top_genes=2000 , batch_key='batch' , flavor='seurat_v3' , subset=False , ) adata_hvg = adata[:, adata.var['highly_variable' ]].copy() sc.pp.pca(adata_hvg, n_comps=50 ) sc.pp.neighbors(adata_hvg) sc.tl.umap(adata_hvg) sc.pl.umap(adata_hvg, color='batch' , title='Before Integration' , save='_before_integration.png' ) scvi.model.SCVI.setup_anndata(adata, layer='counts' , batch_key='batch' ) model_scvi = scvi.model.SCVI(adata, n_latent=30 ) model_scvi.train(max_epochs=400 , early_stopping=True ) adata.obsm['X_scVI' ] = model_scvi.get_latent_representation() sc.pp.pca(adata, n_comps=50 ) harmony_out = hm.run_harmony(adata.obsm['X_pca' ], adata.obs, 'batch' ) adata.obsm['X_harmony' ] = harmony_out.Z_corr.T adata_bbknn = adata.copy() bbknn.bbknn(adata_bbknn, batch_key='batch' , neighbors_within_batch=15 ) sc.pp.neighbors(adata, use_rep='X_scVI' ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.8 ) sc.pl.umap(adata, color=['batch' , 'leiden' ], ncols=2 , save='_scvi_integrated.png' ) sc.pp.neighbors(adata, use_rep='X_harmony' ) sc.tl.umap(adata) sc.pl.umap(adata, color=['batch' , 'leiden' ], ncols=2 , save='_harmony_integrated.png' ) adata.write_h5ad("fully_integrated_data.h5ad" ) model_scvi.save("final_scvi_model/" , overwrite=True ) print ("数据整合完成!" )
10.6 注意事项:过度整合风险 过度整合(Over-integration)是数据整合中最常见的陷阱之一,其表现为:
稀有细胞类型丢失 :过度校正可能将稀有细胞类型”整合”到相近但不同的常见细胞类型中。
真实的生物学差异被抹除 :不同条件下的同一细胞类型可能存在真实的转录组差异(如激活vs静息T细胞),过度整合会将这些差异消除。
信号平滑化 :过度整合使所有细胞看起来过于相似,失去了数据的异质性信息。
避免过度整合的策略 :
调整整合强度 :
Harmony的theta参数:减小theta值降低整合强度
scVI的模型容量:减少n_latent或n_layers可能减少过度整合
BBKNN的neighbors_within_batch:减小此值降低整合强度
保留已知的生物协变量 :
在ComBat中将已知条件作为协变量保留
在scVI中使用categorical_covariate_keys显式建模生物协变量
评估稀有细胞类型 :整合后检查稀有细胞类型是否仍然存在。
分层整合 :先在大类层面整合,再在亚类层面整合,避免跨大类过度整合。
多方法比较 :使用多种整合方法,比较结果的一致性。
关键参考文献 :
Lopez et al. (2018). Deep generative modeling for single-cell transcriptomics. Nature Methods . (scVI)
Xu et al. (2021). Probabilistic harmonization and annotation of single-cell transcriptomics data with deep generative models. Molecular Systems Biology . (scANVI)
Korsunsky et al. (2019). Fast, sensitive and accurate integration of single-cell data with Harmony. Nature Methods .
Polański et al. (2020). BBKNN: fast batch alignment of single cell transcriptomes. Bioinformatics .
Johnson et al. (2007). Adjusting batch effects in microarray expression data using empirical Bayes methods. Biostatistics . (ComBat)
Luecken et al. (2022). Benchmarking atlas-level data integration in single-cell genomics. Nature Methods . (scIB)
第四篇:下游分析
第十一章:差异表达基因分析
基于Ch17 + 教程十一,详细展开
11.1 差异表达的三种统计框架 差异表达基因(Differentially Expressed Genes, DEG)分析是单细胞转录组分析中的核心下游步骤。与bulk RNA-seq不同,单细胞数据的DEG分析面临独特的统计挑战。
11.1.1 Wilcoxon秩和检验 原理 :
Wilcoxon秩和检验(也称Mann-Whitney U检验)是最常用的单细胞DEG检验方法。它是一种非参数检验,不假设数据的分布形式,通过比较两组样本的秩(rank)来判断差异。
检验统计量:
$$U = \sum_{i=1}^{n_1} \sum_{j=1}^{n_2} S(X_i, Y_j)$$
其中 $S(X_i, Y_j) = 1$ 如果 $X_i > Y_j$,$S(X_i, Y_j) = 0.5$ 如果 $X_i = Y_j$,否则为0。
在样本量较大时,$U$ 近似正态分布:
$$Z = \frac{U - \mu_U}{\sigma_U}$$
其中 $\mu_U = n_1 n_2 / 2$,$\sigma_U = \sqrt{n_1 n_2 (n_1 + n_2 + 1) / 12}$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad("annotated_data.h5ad" ) sc.tl.rank_genes_groups( adata, groupby='cell_type' , method='wilcoxon' , corr_method='benjamini-hochberg' , pts=True , use_raw=True , ) sc.pl.rank_genes_groups(adata, n_genes=15 , sharey=False ) degs = sc.get.rank_genes_groups_df(adata, group=None ) degs.to_csv("wilcoxon_degs.csv" , index=False ) t_cells = adata[adata.obs['cell_type' ] == 'CD4+ T cells' ].copy() sc.tl.rank_genes_groups( t_cells, groupby='condition' , method='wilcoxon' , corr_method='benjamini-hochberg' , pts=True , ) tcell_degs = sc.get.rank_genes_groups_df(t_cells, group=None ) tcell_degs.to_csv("tcell_wilcoxon_degs.csv" , index=False ) sig_degs = tcell_degs[ (tcell_degs['pvals_adj' ] < 0.05 ) & (tcell_degs['logfoldchanges' ].abs () > 0.5 ) ] print (f"显著DEG数: {len (sig_degs)} " )sc.pl.rank_genes_groups_violin(t_cells, n_genes=8 , save='_wilcoxon_violin.png' ) sc.pl.rank_genes_groups_heatmap(t_cells, n_genes=10 , save='_wilcoxon_heatmap.png' )
局限性 :
伪重复问题(Pseudoreplication) :Wilcoxon检验将每个细胞视为独立样本,但来自同一个体的细胞之间存在相关性。这会严重膨胀统计量,导致假阳性率升高。
对大量细胞敏感 :当细胞数很多时(如 >10,000),即使非常小的差异也可能被检测为”显著”。
无法处理混杂变量 :简单的Wilcoxon检验无法控制年龄、性别等协变量的影响。
11.1.2 MAST 原理 :
MAST(Model-based Analysis of Single-cell Transcriptomics)是一种专门为单细胞数据设计的差异表达分析方法。它使用条件统一方差分析(Conditional Unified Analysis,即Hurdle模型),同时建模基因的表达概率和表达量。
MAST的核心模型是一个两部分的Hurdle模型:
离散部分(表达/不表达) :
$$\text{logit}(P(C_{ig} = 1)) = \beta_{0g}^{(c)} + \beta_{1g}^{(c)} X_i + \boldsymbol{\gamma}_g^{(c)} \mathbf{Z}_i$$
其中 $C_{ig} = 1$ 表示细胞 $i$ 中基因 $g$ 被检测到(表达),$X_i$ 是条件变量,$\mathbf{Z}_i$ 是协变量。
连续部分(条件表达量) :
$$Y_{ig} \mid C_{ig} = 1 \sim \mathcal{N}(\beta_{0g}^{(y)} + \beta_{1g}^{(y)} X_i + \boldsymbol{\gamma}_g^{(y)} \mathbf{Z}_i, \sigma_g^2)$$
最终检验统计量结合两部分:
$$\chi^2 = \frac{(\hat{\beta}{1g}^{(c)})^2}{\text{Var}(\hat{\beta} {1g}^{(c)})} + \frac{(\hat{\beta}{1g}^{(y)})^2}{\text{Var}(\hat{\beta} {1g}^{(y)})}$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 import scanpy as scimport pandas as pdimport numpy as npimport rpy2.robjects as rofrom rpy2.robjects import pandas2rifrom rpy2.robjects.packages import importrpandas2ri.activate() mast = importr('MAST' ) adata = sc.read_h5ad("annotated_data.h5ad" ) t_cells = adata[adata.obs['cell_type' ] == 'CD4+ T cells' ].copy() from statsmodels.api import Logit, OLSfrom scipy.stats import chi2def mast_like_deg (adata, groupby, condition, covariates=None ): """ 简化版MAST差异表达分析 """ from statsmodels.api import Logit, OLS from statsmodels.tools import add_constant groups = adata.obs[groupby].unique() results = [] for gene in adata.var_names[:100 ]: for group in groups: subset = adata[adata.obs[groupby] == group] expr = subset[:, gene].X.toarray().flatten() condition_vals = (subset.obs[condition] == subset.obs[condition].unique()[1 ]).astype(float ) detected = (expr > 0 ).astype(float ) if detected.sum () == 0 or detected.sum () == len (detected): continue try : X_c = add_constant(condition_vals) model_c = Logit(detected, X_c).fit(disp=0 ) beta_c = model_c.params[1 ] se_c = model_c.bse[1 ] chi_c = (beta_c / se_c) ** 2 except : chi_c = 0 expressed = expr[expr > 0 ] condition_expressed = condition_vals[expr > 0 ] if len (expressed) < 10 : continue try : X_y = add_constant(condition_expressed) model_y = OLS(expressed, X_y).fit() beta_y = model_y.params[1 ] se_y = model_y.bse[1 ] chi_y = (beta_y / se_y) ** 2 except : chi_y = 0 chi_combined = chi_c + chi_y pval = 1 - chi2.cdf(chi_combined, df=2 ) results.append({ 'gene' : gene, 'group' : group, 'chi_discrete' : chi_c, 'chi_continuous' : chi_y, 'chi_combined' : chi_combined, 'pval' : pval, }) result_df = pd.DataFrame(results) from statsmodels.stats.multitest import multipletests result_df['pval_adj' ] = multipletests(result_df['pval' ], method='fdr_bh' )[1 ] return result_df
11.1.3 Pseudobulk方法 原理 :
Pseudobulk(伪批量)方法是当前最推荐的差异表达分析方法。其核心思想是:将来自同一个体的同一细胞类型的所有细胞的表达值求和或取平均,生成”伪bulk”样本,然后使用成熟的bulk RNA-seq差异表达工具进行分析。
为什么推荐Pseudobulk :
关键问题在于伪重复(Pseudoreplication) 。在单细胞数据中,来自同一个体的细胞不是独立的——它们共享同一个体的遗传背景、微环境等。将每个细胞视为独立样本的统计方法(如Wilcoxon、MAST)违反了独立性假设,导致:
假阳性率膨胀 :因为同一个体的多个细胞提供的是”伪重复”而非真正的生物学重复
p值不可靠 :统计检验假设样本独立,违反此假设导致p值不可信
效应量估计偏差 :伪重复放大了表观效应量
Pseudobulk方法的数学表达:
对于个体 $s$ 的细胞类型 $k$,伪bulk表达为:
$$Y_{gsk} = \sum_{i \in \text{cells}(s,k)} x_{gi}$$
或取CPM/TPM标准化后取平均:
$$\bar{Y}{gsk} = \frac{1}{n {sk}} \sum_{i \in \text{cells}(s,k)} \text{CPM}_{gi}$$
然后使用bulk RNA-seq的统计模型(如edgeR的负二项模型、DESeq2的负二项模型)进行差异分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import scanpy as scimport pandas as pdimport numpy as npimport decoupler as dc adata = sc.read_h5ad("annotated_data.h5ad" ) pseudobulk = dc.get_pseudobulk( adata, sample_col='sample' , groups_col='cell_type' , layer='counts' , mode='sum' , min_cells=10 , min_counts=1000 , ) print (f"Pseudobulk数据维度: {pseudobulk.shape} " )print (f"样本数: {pseudobulk.n_obs} " )from pydeseq2.DeseqDataSet import DeseqDataSetfrom pydeseq2.DeseqStats import DeseqStatscell_type = 'CD4+ T cells' pb_ct = pseudobulk[pseudobulk.obs['cell_type' ] == cell_type].copy() sc.pp.filter_genes(pb_ct, min_cells=3 ) dds = DeseqDataSet( counts=pb_ct.X.astype(int ), metadata=pb_ct.obs[['condition' , 'sample' ]], design_factors='condition' , ) dds.deseq2() stat_res = DeseqStats(dds, contrast=['condition' , 'disease' , 'healthy' ]) stat_res.summary() deseq_results = stat_res.results_df deseq_results = deseq_results.sort_values('pvalue' ) print (deseq_results.head(20 ))sig_degs = deseq_results[ (deseq_results['padj' ] < 0.05 ) & (deseq_results['log2FoldChange' ].abs () > 1.0 ) ] print (f"显著DEG数: {len (sig_degs)} " )import matplotlib.pyplot as pltplt.figure(figsize=(10 , 8 )) plt.scatter( deseq_results['log2FoldChange' ], -np.log10(deseq_results['pvalue' ]), c='gray' , alpha=0.5 , s=10 , ) sig_up = deseq_results[(deseq_results['padj' ] < 0.05 ) & (deseq_results['log2FoldChange' ] > 1 )] sig_down = deseq_results[(deseq_results['padj' ] < 0.05 ) & (deseq_results['log2FoldChange' ] < -1 )] plt.scatter(sig_up['log2FoldChange' ], -np.log10(sig_up['pvalue' ]), c='red' , s=10 , label='Up' ) plt.scatter(sig_down['log2FoldChange' ], -np.log10(sig_down['pvalue' ]), c='blue' , s=10 , label='Down' ) plt.xlabel('log2 Fold Change' ) plt.ylabel('-log10(p-value)' ) plt.title(f'Volcano Plot - {cell_type} ' ) plt.legend() plt.savefig(f"volcano_{cell_type.replace(' ' , '_' )} .png" , dpi=300 , bbox_inches='tight' ) plt.show()
11.1.4 DESeq2在单细胞中的应用 DESeq2是bulk RNA-seq差异表达分析的金标准工具。其核心模型假设基因计数服从负二项分布:
$$K_{ij} \sim \text{NB}(\mu_{ij}, \alpha_i)$$
其中 $\mu_{ij} = s_j q_{ij}$,$s_j$ 是size factor,$q_{ij}$ 是归一化后的真实表达量,$\alpha_i$ 是基因 $i$ 的dispersion参数。
DESeq2的Wald检验:
$$H_0: \beta_{ir} = 0$$
$$W = \frac{\hat{\beta}{ir}}{\text{SE}(\hat{\beta} {ir})}$$
其中 $\hat{\beta}_{ir}$ 是条件 $r$ 对基因 $i$ 的log2倍数变化估计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 from pydeseq2.DeseqDataSet import DeseqDataSetfrom pydeseq2.DeseqStats import DeseqStatsimport scanpy as scimport pandas as pdimport numpy as npimport decoupler as dcadata = sc.read_h5ad("annotated_data.h5ad" ) pseudobulk = dc.get_pseudobulk( adata, sample_col='sample' , groups_col='cell_type' , layer='counts' , mode='sum' , min_cells=10 , ) all_results = {} for cell_type in pseudobulk.obs['cell_type' ].unique(): pb_ct = pseudobulk[pseudobulk.obs['cell_type' ] == cell_type].copy() genes_to_keep = (pb_ct.X > 0 ).sum (axis=0 ) >= 3 if isinstance (genes_to_keep, np.matrix): genes_to_keep = np.array(genes_to_keep).flatten() pb_ct = pb_ct[:, genes_to_keep] n_samples = pb_ct.n_obs if n_samples < 4 : print (f"{cell_type} : 样本数不足({n_samples} ),跳过" ) continue try : dds = DeseqDataSet( counts=pb_ct.X.astype(int ), metadata=pb_ct.obs, design_factors='condition' , ) dds.deseq2() stat_res = DeseqStats(dds, contrast=['condition' , 'disease' , 'healthy' ]) stat_res.summary() results = stat_res.results_df all_results[cell_type] = results sig = results[(results['padj' ] < 0.05 ) & (results['log2FoldChange' ].abs () > 1 )] print (f"{cell_type} : {len (sig)} significant DEGs" ) except Exception as e: print (f"{cell_type} : Error - {e} " ) with pd.ExcelWriter("all_celltype_degs.xlsx" ) as writer: for ct, res in all_results.items(): res.to_excel(writer, sheet_name=ct[:31 ]) print ("DESeq2分析完成!" )
11.1.5 SEACells元细胞方法 原理 :
SEACells(Single-cell Aggregation via Elliptical Atoms)是一种元细胞(Metacell)方法,通过将相似的细胞聚合为”元细胞”,减少数据噪声同时保留生物学信号。
SEACells的算法核心是使用非负矩阵分解(NMF)和贪心算法来寻找元细胞:
计算细胞间的核相似度矩阵:$K_{ij} = \exp(-|\mathbf{x}_i - \mathbf{x}_j|^2 / 2\sigma^2)$
使用NMF学习元细胞权重矩阵 $G \in \mathbb{R}^{N \times M}$($M$ 为元细胞数,$N$ 为细胞数)
约束 $G$ 为非负且每行和为1(每个细胞可被分配到多个元细胞)
元细胞 $m$ 的表达为:
$$\mathbf{y}m = \frac{\sum {i=1}^{N} G_{im} \mathbf{x}i}{\sum {i=1}^{N} G_{im}}$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import scanpy as scimport pandas as pdimport numpy as npimport SEACellsadata = sc.read_h5ad("annotated_data.h5ad" ) n_metacells = max (adata.n_obs // 100 , 10 ) print (f"建议元细胞数: {n_metacells} " )model = SEACells.core.SEACells( adata, n_cells=n_metacells, build_kernel_on='X_pca' , n_waypoint_eigs=10 , convergence_epsilon=1e-5 , max_iter=100 , ) model.fit() adata.obs['metacell' ] = model.get_assignments() metacell_adata = SEACells.core.summarize_by_soft_assignment( adata, model, summarize_layer='counts' , ) print (f"元细胞数: {metacell_adata.n_obs} " )sc.pl.umap( adata, color='metacell' , frameon=False , save='_metacells.png' , )
11.1.6 三种方法对比与推荐
推荐策略 :
正式发表的DEG分析 :必须使用Pseudobulk方法
初步探索 :可以使用Wilcoxon快速筛选
需要控制协变量 :MAST或Pseudobulk
样本数少 :Pseudobulk可能统计功效不足,可结合Wilcoxon结果
11.1.7 多重检验校正 在DEG分析中,需要同时检验数千个基因,因此必须进行多重检验校正以控制假阳性率。
常用方法 :
Benjamini-Hochberg (BH) FDR :控制错误发现率(False Discovery Rate),最常用
$$\text{FDR} = \frac{V}{\max(R, 1)}$$
其中 $V$ 是假阳性数,$R$ 是总阳性数。BH方法保证 $\text{FDR} \leq \alpha$。
Bonferroni校正 :控制家族错误率(Family-Wise Error Rate),最保守
$$p_{\text{adj}} = \min(p \times m, 1)$$
其中 $m$ 是检验总数。过于保守,可能导致假阴性增加。
Benjamini-Yekutieli :适用于检验间存在相关性时
1 2 3 4 5 6 7 from statsmodels.stats.multitest import multipletestspvals_adj_bh = multipletests(pvals, alpha=0.05 , method='fdr_bh' )[1 ] pvals_adj_bonf = multipletests(pvals, alpha=0.05 , method='bonferroni' )[1 ]
11.2 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 import scanpy as scimport pandas as pdimport numpy as npimport decoupler as dcimport matplotlib.pyplot as pltadata = sc.read_h5ad("annotated_data.h5ad" ) print (f"细胞数: {adata.n_obs} " )print (f"细胞类型: {adata.obs['cell_type' ].unique()} " )print (f"条件: {adata.obs['condition' ].unique()} " )for ct in adata.obs['cell_type' ].unique(): ct_data = adata[adata.obs['cell_type' ] == ct] sc.tl.rank_genes_groups( ct_data, groupby='condition' , method='wilcoxon' , key_added=f'wilcoxon_{ct} ' , ) pseudobulk = dc.get_pseudobulk( adata, sample_col='sample' , groups_col='cell_type' , layer='counts' , mode='sum' , min_cells=10 , ) from pydeseq2.DeseqDataSet import DeseqDataSetfrom pydeseq2.DeseqStats import DeseqStatsall_degs = {} for ct in pseudobulk.obs['cell_type' ].unique(): pb_ct = pseudobulk[pseudobulk.obs['cell_type' ] == ct].copy() sc.pp.filter_genes(pb_ct, min_cells=3 ) if pb_ct.n_obs < 4 : print (f"{ct} : 样本数不足,跳过" ) continue try : dds = DeseqDataSet( counts=pb_ct.X.astype(int ), metadata=pb_ct.obs, design_factors='condition' , ) dds.deseq2() stat_res = DeseqStats(dds, contrast=['condition' , 'disease' , 'healthy' ]) stat_res.summary() all_degs[ct] = stat_res.results_df sig = stat_res.results_df[ (stat_res.results_df['padj' ] < 0.05 ) & (stat_res.results_df['log2FoldChange' ].abs () > 1 ) ] print (f"{ct} : {len (sig)} DEGs" ) except Exception as e: print (f"{ct} : Error - {e} " ) for ct, res in all_degs.items(): fig, ax = plt.subplots(figsize=(10 , 8 )) sig = res['padj' ] < 0.05 up = sig & (res['log2FoldChange' ] > 1 ) down = sig & (res['log2FoldChange' ] < -1 ) ax.scatter(res['log2FoldChange' ], -np.log10(res['pvalue' ]), c='gray' , alpha=0.3 , s=5 ) ax.scatter(res.loc[up, 'log2FoldChange' ], -np.log10(res.loc[up, 'pvalue' ]), c='red' , s=10 , label=f'Up ({up.sum ()} )' ) ax.scatter(res.loc[down, 'log2FoldChange' ], -np.log10(res.loc[down, 'pvalue' ]), c='blue' , s=10 , label=f'Down ({down.sum ()} )' ) ax.set_xlabel('log2 Fold Change' ) ax.set_ylabel('-log10(p-value)' ) ax.set_title(f'Volcano Plot - {ct} ' ) ax.legend() plt.savefig(f"volcano_{ct.replace(' ' , '_' )} .png" , dpi=300 , bbox_inches='tight' ) plt.close() with pd.ExcelWriter("deg_results.xlsx" ) as writer: for ct, res in all_degs.items(): res.to_excel(writer, sheet_name=ct[:31 ]) print ("DEG分析完成!" )
11.3 注意事项
必须使用Pseudobulk :对于需要正式发表的DEG分析,Pseudobulk是唯一能正确处理伪重复问题的方法。
最小样本数 :Pseudobulk方法需要足够的生物学重复(建议 ≥ 3 per condition per cell type)。
低表达基因过滤 :DESeq2等工具对低表达基因敏感,应在分析前过滤。
效应量与统计显著性 :不仅要看p值,还要看log2FC。大量细胞可能使微小差异变得”显著”。
多重检验校正 :始终使用FDR校正,不要仅依赖原始p值。
协变量控制 :在Pseudobulk模型中应纳入年龄、性别等混杂变量。
关键参考文献 :
Squair et al. (2021). Confronting false discoveries in single-cell differential expression. Nature Communications . (Pseudobulk推荐)
Finak et al. (2015). MAST: a flexible statistical framework for assessing transcriptional changes and characterizing heterogeneity in single-cell RNA sequencing data. Genome Biology .
Love et al. (2014). Moderated estimation of fold change and dispersion for RNA-seq data with DESeq2. Genome Biology .
Persad et al. (2023). SEACells infers transcriptional and epigenomic cellular states from single-cell genomics data. Nature Biotechnology .
Zimmerman et al. (2021). A practical guide to single-cell RNA-sequencing for biomedical research and clinical applications. Genome Medicine .
第十二章:细胞比例分析
基于Ch18 + 教程十二,详细展开
12.1 成分数据的统计挑战 细胞比例分析(Compositional Data Analysis)研究不同条件下细胞类型比例的变化。这类数据面临独特的统计挑战:
成分数据约束 :细胞比例之和恒为1($\sum_k p_k = 1$),这使得:
比例间存在人为的负相关(一个增加必然导致其他减少)
不能直接使用标准统计检验(如t检验、卡方检验)
需要专门的统计模型
零膨胀问题 :某些细胞类型在部分样本中可能完全不存在(比例为0),标准统计模型无法处理。
小样本量 :单细胞研究中生物学重复通常较少(3-10个/组),限制了统计功效。
12.2 scCODA 原理 :
scCODA(single-cell Compositional Data Analysis)是一种基于Dirichlet-multinomial模型的细胞比例差异分析方法,专门为单细胞数据设计。
Dirichlet-multinomial模型 :
假设样本 $i$ 的细胞类型计数 $\mathbf{y}i = (y {i1}, …, y_{iK})$ 服从多项分布:
$$\mathbf{y}_i \mid \boldsymbol{\pi}_i \sim \text{Multinomial}(N_i, \boldsymbol{\pi}_i)$$
而比例向量 $\boldsymbol{\pi}_i$ 服从Dirichlet分布:
$$\boldsymbol{\pi}_i \sim \text{Dirichlet}(\boldsymbol{\alpha})$$
其中 $\boldsymbol{\alpha} = (\alpha_1, …, \alpha_K)$ 是Dirichlet参数。
scCODA的关键创新是引入参考细胞类型(Reference Cell Type) ,其比例假设在条件间不变。其他细胞类型的变化是相对于参考类型来衡量的。
scCODA使用贝叶斯框架,对log转化后的Dirichlet参数建模:
$$\log(\alpha_k) = \beta_{k0} + \beta_{k1} X$$
其中 $X$ 是条件变量,$\beta_{k1}$ 是条件效应。使用spike-and-slab先验进行变量选择:
$$\beta_{k1} \sim (1 - \gamma_k) \cdot \delta_0 + \gamma_k \cdot \mathcal{N}(0, \sigma^2)$$
其中 $\gamma_k \in {0, 1}$ 是指示变量,表示细胞类型 $k$ 的比例是否在条件间有显著变化。
统计显著性判断 :如果 $\gamma_k$ 的后验概率 $P(\gamma_k = 1 \mid \text{data}) > 0.95$(默认阈值),则认为细胞类型 $k$ 的比例有显著变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import scanpy as scimport pandas as pdimport numpy as npfrom sccoda.util import comp_ana as modfrom sccoda.util import cell_composition_data as datfrom sccoda.model import other_models as omadata = sc.read_h5ad("annotated_data.h5ad" ) data_sc = dat.from_scanpy( adata, cell_type_identifier='cell_type' , sample_identifier='sample' , covariate_obs=['condition' ], ) print (data_sc)print (f"样本数: {data_sc.n_obs} " )print (f"细胞类型数: {data_sc.n_vars} " )cell_counts = pd.crosstab(adata.obs['sample' ], adata.obs['cell_type' ]) cell_props = cell_counts.div(cell_counts.sum (axis=1 ), axis=0 ) import matplotlib.pyplot as pltfig, ax = plt.subplots(figsize=(12 , 6 )) cell_props.plot(kind='bar' , stacked=True , ax=ax) ax.set_ylabel('Proportion' ) ax.set_title('Cell Type Proportions by Sample' ) plt.tight_layout() plt.savefig("cell_proportions.png" , dpi=300 ) plt.show() model_sc = mod.CompositionalAnalysis( data_sc, formula="condition" , reference_cell_type="CD4+ T cells" , ) samples = model_sc.sample_hmc( num_samples=10000 , num_warmup=5000 , num_chains=4 , rng_key=42 , ) results = mod.summary(samples) print (results)fig, ax = plt.subplots(figsize=(10 , 6 )) mod.plot_effects(samples, data_sc, ax=ax) plt.savefig("sccoda_effects.png" , dpi=300 , bbox_inches='tight' ) plt.show() fig, ax = plt.subplots(figsize=(10 , 6 )) mod.plot_credible_effects(samples, data_sc, ax=ax) plt.savefig("sccoda_credible.png" , dpi=300 , bbox_inches='tight' ) plt.show() significant = results[results['Infinite probability' ] > 0.95 ] print (f"显著变化的细胞类型: {significant.index.tolist()} " )ref_types = dat.auto_reference(data_sc) print (f"推荐的参考细胞类型: {ref_types} " )
12.3 tascCODA 原理 :
tascCODA(tree-aggregated scCODA)是scCODA的扩展,利用细胞类型之间的层级关系(树结构)来提高检测能力和可解释性。
tascCODA的核心思想是:
构建细胞类型的层级树(如从细胞大类到细胞亚型)
在树的节点上建模比例变化,而非仅在叶节点
通过树结构的稀疏先验,同时检测不同粒度上的比例变化
tascCODA的先验使用树结构的spike-and-slab:
$$\beta_n \sim (1 - \gamma_n) \cdot \delta_0 + \gamma_n \cdot \mathcal{N}(0, \sigma^2)$$
其中节点 $n$ 的inclusion概率 $\gamma_n$ 依赖于其父节点:
$$P(\gamma_n = 1 \mid \gamma_{\text{parent}(n)}) = \begin{cases} \pi_1 & \text{if } \gamma_{\text{parent}(n)} = 1 \ \pi_0 & \text{if } \gamma_{\text{parent}(n)} = 0 \end{cases}$$
这确保了如果父节点(如T细胞大类)有显著变化,子节点(如CD4+ T细胞)也更有可能被检测到变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import scanpy as scimport pandas as pdimport numpy as npfrom sccoda.util import comp_ana as modfrom sccoda.util import cell_composition_data as datfrom sccoda.model import other_models as omimport sccodaadata = sc.read_h5ad("annotated_data.h5ad" ) data_sc = dat.from_scanpy( adata, cell_type_identifier='cell_type' , sample_identifier='sample' , covariate_obs=['condition' ], ) tree_str = "((CD4_T_cells, CD8_T_cells, Treg)T_cells, B_cells, (Monocytes, Macrophages, DCs)Myeloid)Immune, Epithelial, (Fibroblasts, Endothelial)Stroma" data_tasccoda = dat.from_scanpy( adata, cell_type_identifier='cell_type' , sample_identifier='sample' , covariate_obs=['condition' ], tree=tree_str, ) model_tasc = mod.CompositionalAnalysisTree( data_tasccoda, formula="condition" , reference_cell_type="CD4_T_cells" , reg="scaled_by_tree" , pen_args=dict (alpha=0.5 ), ) samples_tasc = model_tasc.sample_hmc( num_samples=10000 , num_warmup=5000 , ) results_tasc = mod.summary(samples_tasc) print (results_tasc)mod.plot_tree_effects( samples_tasc, data_tasccoda, figsize=(14 , 8 ), ) plt.savefig("tasccoda_tree_effects.png" , dpi=300 , bbox_inches='tight' ) plt.show() significant_nodes = results_tasc[results_tasc['Infinite probability' ] > 0.95 ] print (f"显著变化的节点: {significant_nodes.index.tolist()} " )
12.4 统计显著性判断 scCODA和tascCODA的显著性判断基于贝叶斯框架中的inclusion概率(Inclusion Probability) :
$P(\gamma_k = 1 \mid \text{data}) > 0.95$:细胞类型 $k$ 的比例在条件间有显著变化
$P(\gamma_k = 1 \mid \text{data}) < 0.05$:细胞类型 $k$ 的比例在条件间无显著变化
$0.05 \leq P(\gamma_k = 1 \mid \text{data}) \leq 0.95$:不确定,需要更多数据
注意 :inclusion概率 ≠ p值。它衡量的是模型对效应存在性的确信程度。
12.5 参考细胞类型选择 参考细胞类型的选择是scCODA分析的关键步骤:
选择原则 :
选择在条件间比例最稳定的细胞类型
优先选择比例较大的细胞类型(减少统计不确定性)
不应选择预期会发生变化的细胞类型
选择方法 :
可视化评估 :查看各细胞类型在不同条件下的比例分布
scCODA自动选择 :使用auto_reference()函数
敏感性分析 :尝试不同的参考类型,检查结论是否一致
12.6 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 import scanpy as scimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom sccoda.util import comp_ana as modfrom sccoda.util import cell_composition_data as datadata = sc.read_h5ad("annotated_data.h5ad" ) cell_counts = pd.crosstab(adata.obs['sample' ], adata.obs['cell_type' ]) cell_props = cell_counts.div(cell_counts.sum (axis=1 ), axis=0 ) fig, axes = plt.subplots(1 , 2 , figsize=(16 , 6 )) cell_props.plot(kind='bar' , stacked=True , ax=axes[0 ]) axes[0 ].set_ylabel('Proportion' ) axes[0 ].set_title('Cell Type Proportions' ) axes[0 ].legend(bbox_to_anchor=(1.05 , 1 )) for cond in adata.obs['condition' ].unique(): cond_samples = adata[adata.obs['condition' ] == cond].obs['sample' ].unique() cond_props = cell_props.loc[cell_props.index.isin(cond_samples)] cond_props.mean().plot(kind='bar' , ax=axes[1 ] if cond == adata.obs['condition' ].unique()[0 ] else axes[1 ], alpha=0.7 , label=cond) axes[1 ].set_ylabel('Mean Proportion' ) axes[1 ].set_title('Mean Proportions by Condition' ) axes[1 ].legend() plt.tight_layout() plt.savefig("cell_composition_overview.png" , dpi=300 ) plt.show() data_sc = dat.from_scanpy( adata, cell_type_identifier='cell_type' , sample_identifier='sample' , covariate_obs=['condition' ], ) ref_suggestions = dat.auto_reference(data_sc) print (f"推荐的参考细胞类型: {ref_suggestions} " )ref_type = ref_suggestions[0 ] if ref_suggestions else adata.obs['cell_type' ].value_counts().index[0 ] model_sc = mod.CompositionalAnalysis( data_sc, formula="condition" , reference_cell_type=ref_type, ) samples = model_sc.sample_hmc( num_samples=10000 , num_warmup=5000 , ) results = mod.summary(samples) print (results)mod.plot_credible_effects(samples, data_sc) plt.savefig("sccoda_results.png" , dpi=300 , bbox_inches='tight' ) plt.show() significant = results[results['Infinite probability' ] > 0.95 ] print (f"\n显著变化的细胞类型:" )for ct in significant.index: effect = significant.loc[ct, 'Final Parameter' ] direction = "增加" if effect > 0 else "减少" prob = significant.loc[ct, 'Infinite probability' ] print (f" {ct} : {direction} (效应={effect:.3 f} , 后验概率={prob:.3 f} )" ) results.to_csv("sccoda_results.csv" ) print ("细胞比例分析完成!" )
12.7 注意事项
参考类型的选择至关重要 :不同的参考类型可能导致不同的结论。建议进行敏感性分析。
样本量要求 :scCODA需要足够的生物学重复。建议每组至少3个样本。
零比例的处理 :如果某细胞类型在某些样本中完全缺失,可能需要特殊处理。
协变量控制 :可以在模型公式中纳入协变量(如年龄、性别)。
MCMC收敛性 :检查MCMC链的收敛性,可通过trace plot和$\hat{R}$统计量判断。
比例vs绝对丰度 :scCODA分析的是相对比例变化,不能直接得出绝对丰度的结论。如需分析绝对丰度,需要额外的信息。
关键参考文献 :
Büttner et al. (2021). scCODA is a Bayesian model for compositional single-cell data. Nature Methods .
Ostner et al. (2023). tascCODA: Bayesian tree-aggregated compositional data analysis for single-cell data. Bioinformatics .
第十三章:基因集富集分析
基于Ch19,详细展开
13.1 ORA(Over-Representation Analysis) 原理 :
ORA是最简单和最广泛使用的富集分析方法。其核心思想是:检验差异表达基因列表中,属于某个基因集的基因数量是否显著超过随机预期。
ORA使用超几何检验(Fisher精确检验):
$$P(X \geq k) = \sum_{i=k}^{\min(m, M)} \frac{\binom{M}{i}\binom{N-M}{m-i}}{\binom{N}{m}}$$
其中:
$N$:背景基因总数(通常是所有被检测的基因)
$M$:属于某基因集的基因数
$m$:输入的差异表达基因总数
$k$:输入基因中属于某基因集的基因数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import scanpy as scimport pandas as pdimport numpy as npimport gseapy as gpadata = sc.read_h5ad("annotated_data.h5ad" ) sc.tl.rank_genes_groups(adata, groupby='condition' , method='wilcoxon' ) degs = sc.get.rank_genes_groups_df(adata, group='disease' ) sig_degs = degs[ (degs['pvals_adj' ] < 0.05 ) & (degs['logfoldchanges' ] > 0.5 ) ] gene_list = sig_degs['names' ].tolist() print (f"显著DEG数: {len (gene_list)} " )enr = gp.enrichr( gene_list=gene_list, gene_sets='MSigDB_Hallmark_2020' , organism='Human' , outdir='enrichr_results' , cutoff=0.05 , ) enr_multi = gp.enrichr( gene_list=gene_list, gene_sets=[ 'MSigDB_Hallmark_2020' , 'KEGG_2021_Human' , 'GO_Biological_Process_2023' , 'Reactome_2022' , ], organism='Human' , outdir='enrichr_multi_results' , cutoff=0.05 , ) print (enr.results.head(20 ))from gseapy.plot import barplot, dotplotbarplot(enr.results, title='ORA Results' , figsize=(8 , 6 )) plt.savefig("ora_barplot.png" , dpi=300 , bbox_inches='tight' ) plt.show() dotplot(enr.results, title='ORA Results' , figsize=(8 , 6 )) plt.savefig("ora_dotplot.png" , dpi=300 , bbox_inches='tight' ) plt.show() enr_custom = gp.enrichr( gene_list=gene_list, gene_sets='custom_genesets.gmt' , organism='Human' , outdir='enrichr_custom_results' , )
13.2 GSEA(Gene Set Enrichment Analysis) 原理 :
GSEA由Subramanian等人于2005年提出,与ORA不同,GSEA不需要预先设定差异表达的阈值,而是使用所有基因的排序信息。
GSEA算法步骤 :
排序 :按某种相关性度量(如log2FC或t统计量)对基因排序
计算富集得分(Enrichment Score, ES) :
$$ES(S) = \max_{1 \leq j \leq N} \left| \sum_{i=1}^{j} \delta_i \right|$$
其中:
$$\delta_i = \begin{cases} \frac{|r_i|^p}{N_R} & \text{if } g_i \in S \ -\frac{1}{N - N_H} & \text{if } g_i \notin S \end{cases}$$
$$N_R = \sum_{g_j \in S} |r_j|^p$$
其中 $S$ 是目标基因集,$r_i$ 是基因 $i$ 的相关性得分,$N_H$ 是基因集 $S$ 中的基因数,$p$ 是权重参数(默认1)。
归一化ES :$NES = ES / \mu_{ES_{null}}$
显著性评估 :通过排列检验估计NES的显著性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import scanpy as scimport pandas as pdimport numpy as npimport gseapy as gpadata = sc.read_h5ad("annotated_data.h5ad" ) sc.tl.rank_genes_groups(adata, groupby='condition' , method='wilcoxon' ) degs = sc.get.rank_genes_groups_df(adata, group='disease' ) rnk = degs[['names' , 'scores' ]].copy() rnk = rnk.drop_duplicates(subset='names' ) rnk = rnk.sort_values('scores' , ascending=False ) rnk_dict = dict (zip (rnk['names' ], rnk['scores' ])) pre_res = gp.prerank( rnk=rnk_dict, gene_sets='MSigDB_Hallmark_2020' , processes=4 , permutation_num=1000 , outdir='gsea_results' , format ='png' , seed=42 , min_size=15 , max_size=500 , ) print (pre_res.res2d.head(20 ))from gseapy.plot import gseaplotfor term in pre_res.res2d.index[:5 ]: gseaplot( rank_metric=pre_res.ranking, term=term, **pre_res.results[term], ofname=f'gsea_{term} .png' , )
13.3 AUCell 原理 :
AUCell是一种基于基因集活性评分的方法,通过计算每个细胞中基因集内基因在表达排序中的曲线下面积(Area Under the Curve, AUC)来评估基因集活性。
算法步骤 :
基因表达排序 :对每个细胞,将所有基因按表达量从高到低排序
计算AUC :对目标基因集,绘制其在排序列表中的位置作为阶梯函数,计算AUC
$$AUC_i = \frac{1}{N_H \cdot (N - N_H)} \sum_{g \in S} (\text{rank}_i(g) - 1)$$
其中 $N$ 是总基因数,$N_H$ 是基因集大小,$\text{rank}_i(g)$ 是基因 $g$ 在细胞 $i$ 中的排名。
归一化 :AUC值除以最大可能AUC
AUCell的优势在于它为每个细胞计算基因集活性得分,因此可以与UMAP可视化结合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import scanpy as scimport pandas as pdimport numpy as npimport decoupler as dcadata = sc.read_h5ad("annotated_data.h5ad" ) gene_sets = { 'Inflammatory_Response' : ['IL6' , 'TNF' , 'IL1B' , 'CXCL8' , 'PTGS2' , 'NFKBIA' , 'RELA' ], 'Interferon_Response' : ['IFNG' , 'STAT1' , 'IRF1' , 'CXCL10' , 'IDO1' , 'GBP1' ], 'T_cell_activation' : ['CD3D' , 'CD3E' , 'CD28' , 'IL2RA' , 'ICOS' , 'CTLA4' ], 'Proliferation' : ['MKI67' , 'TOP2A' , 'PCNA' , 'MCM2' , 'MCM5' , 'CDC20' ], 'EMT' : ['VIM' , 'SNAI1' , 'SNAI2' , 'ZEB1' , 'CDH2' , 'FN1' ], } gene_set_df = pd.DataFrame([ {'source' : name, 'target' : gene} for name, genes in gene_sets.items() for gene in genes ]) dc.run_aucell( adata, net=gene_set_df, source='source' , target='target' , use_raw=False , ) print (adata.obsm.keys()) aucell_scores = dc.get_acts(adata, obsm_key='aucell_estimate' ) for gs_name in gene_sets.keys(): adata.obs[f'AUC_{gs_name} ' ] = aucell_scores[:, gs_name].X.flatten() sc.pl.umap( adata, color=[f'AUC_{gs} ' for gs in gene_sets.keys()], cmap='RdYlBu_r' , frameon=False , ncols=3 , save='_aucell_scores.png' , ) sc.pl.violin( adata, [f'AUC_{gs} ' for gs in gene_sets.keys()], groupby='cell_type' , rotation=45 , save='_aucell_violin.png' , )
13.4 decoupler:统一框架 原理 :
decoupler是一个统一的基因集富集和调控网络推断框架,整合了多种方法:
ORA :超几何检验
GSEA :基于排序的富集分析
GSVA :基因集变异分析
AUCell :曲线下面积
Viper :调控子活性推断
ulm :单样本线性模型
mlm :多元线性模型
wmean :加权均值
wsum :加权求和
decoupler的核心设计是:所有方法共享相同的输入格式(AnnData + 基因集网络),方便方法比较。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import scanpy as scimport pandas as pdimport numpy as npimport decoupler as dcadata = sc.read_h5ad("annotated_data.h5ad" ) msigdb = dc.get_resource('MSigDB' ) print (f"MSigDB基因集数: {msigdb['geneset' ].nunique()} " )hallmark = msigdb[msigdb['collection' ] == 'hallmark' ] print (f"Hallmark基因集数: {hallmark['geneset' ].nunique()} " )hallmark = hallmark[hallmark['organism' ] == 'human' ] hallmark = hallmark.groupby('geneset' ).filter (lambda x: 15 <= len (x) <= 500 ) dc.run_ora( adata, net=hallmark, source='geneset' , target='genesymbol' , use_raw=False , ) ora_scores = dc.get_acts(adata, obsm_key='ora_estimate' ) dc.run_gsva( adata, net=hallmark, source='geneset' , target='genesymbol' , use_raw=False , ) gsva_scores = dc.get_acts(adata, obsm_key='gsva_estimate' ) dc.run_aucell( adata, net=hallmark, source='geneset' , target='genesymbol' , use_raw=False , ) aucell_scores = dc.get_acts(adata, obsm_key='aucell_estimate' ) dc.run_ulm( adata, net=hallmark, source='geneset' , target='genesymbol' , use_raw=False , ) ulm_scores = dc.get_acts(adata, obsm_key='ulm_estimate' ) gs_name = 'HALLMARK_INFLAMMATORY_RESPONSE' scores_compare = pd.DataFrame({ 'ORA' : ora_scores[:, gs_name].X.flatten(), 'GSVA' : gsva_scores[:, gs_name].X.flatten(), 'AUCell' : aucell_scores[:, gs_name].X.flatten(), 'ULM' : ulm_scores[:, gs_name].X.flatten(), }, index=adata.obs_names) print (scores_compare.corr())adata.obs[f'GSVA_{gs_name} ' ] = gsva_scores[:, gs_name].X.flatten() sc.pl.umap(adata, color=f'GSVA_{gs_name} ' , cmap='RdYlBu_r' , save='_gsva_inflammatory.png' ) top_pathways = gsva_scores.var_names[:20 ] dc.plot_barplot( gsva_scores, groupby='cell_type' , top=10 , vertical=True , save='pathway_activity_barplot.png' , )
13.5 基因集数据库
GO(Gene Ontology) :生物过程(BP)、分子功能(MF)、细胞组分(CC)
KEGG :代谢和信号通路
MSigDB :包含多个子集
Hallmark(50个核心通路)
C2( curated gene sets,包含KEGG、Reactome等)
C5(GO gene sets)
C7(免疫相关基因集)
H(Hallmark)
Reactome :生物学通路数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 go = dc.get_resource('GO' ) print (f"GO基因集数: {go['geneset' ].nunique()} " )kegg = dc.get_resource('KEGG' ) print (f"KEGG通路数: {kegg['geneset' ].nunique()} " )reactome = dc.get_resource('Reactome' ) print (f"Reactome通路数: {reactome['geneset' ].nunique()} " )msigdb = dc.get_resource('MSigDB' ) collections = msigdb['collection' ].unique() print (f"MSigDB集合: {collections} " )
13.6 各方法对比
13.7 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import scanpy as scimport pandas as pdimport numpy as npimport decoupler as dcimport gseapy as gpimport matplotlib.pyplot as pltadata = sc.read_h5ad("annotated_data.h5ad" ) sc.tl.rank_genes_groups(adata, groupby='condition' , method='wilcoxon' ) degs = sc.get.rank_genes_groups_df(adata, group='disease' ) sig_degs = degs[(degs['pvals_adj' ] < 0.05 ) & (degs['logfoldchanges' ] > 0.5 )] gene_list_up = sig_degs['names' ].tolist() enr = gp.enrichr( gene_list=gene_list_up, gene_sets=['MSigDB_Hallmark_2020' , 'KEGG_2021_Human' ], organism='Human' , outdir='enrichr_results' , cutoff=0.05 , ) rnk = dict (zip (degs['names' ], degs['scores' ])) gsea_res = gp.prerank( rnk=rnk, gene_sets='MSigDB_Hallmark_2020' , permutation_num=1000 , outdir='gsea_results' , seed=42 , ) msigdb = dc.get_resource('MSigDB' ) hallmark = msigdb[msigdb['collection' ] == 'hallmark' ] hallmark = hallmark[hallmark['organism' ] == 'human' ] hallmark = hallmark.groupby('geneset' ).filter (lambda x: 15 <= len (x) <= 500 ) dc.run_ulm(adata, net=hallmark, source='geneset' , target='genesymbol' , use_raw=False ) ulm_scores = dc.get_acts(adata, obsm_key='ulm_estimate' ) for gs in ulm_scores.var_names[:10 ]: adata.obs[f'ULM_{gs} ' ] = ulm_scores[:, gs].X.flatten() sc.pl.umap(adata, color=[f'ULM_{gs} ' for gs in ulm_scores.var_names[:6 ]], cmap='RdYlBu_r' , frameon=False , ncols=3 , save='_pathway_activity.png' ) print ("基因集富集分析完成!" )
关键参考文献 :
Subramanian et al. (2005). Gene set enrichment analysis: A knowledge-based approach for interpreting genome-wide expression profiles. PNAS . (GSEA)
Aibar et al. (2017). SCENIC: single-cell regulatory network inference and clustering. Nature Methods . (AUCell)
Hänzelmann et al. (2013). GSVA: gene set variation analysis for microarray and RNA-seq data. BMC Bioinformatics .
Badia-i-Mompel et al. (2022). decoupler: ensemble of computational methods to infer biological activities from omics data. Bioinformatics Advances .
第十四章:扰动建模
基于Ch20 + 教程十三,详细展开
14.1 Augur:排序扰动响应细胞 原理 :
Augur是一种基于机器学习的扰动响应分析方法,其核心思想是:对扰动响应越强的细胞类型,越容易被机器学习模型区分条件标签 。
Augur的算法流程:
特征选择 :对每种细胞类型,使用随机森林或逻辑回归区分条件标签
交叉验证 :通过多次交叉验证评估分类准确率
重要性排序 :根据分类准确率对细胞类型排序,准确率越高表示该细胞类型对扰动响应越强
置换检验 :通过置换标签计算统计显著性
Augur的评分:
$$\text{Augur Score}_k = \text{AUC}_k(\text{CV})$$
其中 $\text{AUC}_k(\text{CV})$ 是细胞类型 $k$ 在交叉验证中的平均AUC(曲线下面积)。
置换检验的p值:
$$p_k = \frac{1}{B} \sum_{b=1}^{B} \mathbb{I}(\text{AUC}_k^{(b)} \geq \text{AUC}_k^{\text{obs}})$$
其中 $B$ 是置换次数,$\text{AUC}_k^{(b)}$ 是第 $b$ 次置换后的AUC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import scanpy as scimport pandas as pdimport numpy as npimport augurpy as agadata = sc.read_h5ad("perturbation_data.h5ad" ) print (f"细胞数: {adata.n_obs} " )print (f"细胞类型: {adata.obs['cell_type' ].unique()} " )print (f"条件: {adata.obs['condition' ].unique()} " )augur_results = ag.create_augur_app() auc_df, adata_augur = ag.train( adata, label='condition' , cell_type_key='cell_type' , n_subsamples=50 , n_threads=4 , random_state=42 , classifier='random_forest' , select_features=True , ) print (auc_df.sort_values('augur_score' , ascending=False ))pvals_df = ag.predict( adata_augur, n_permutations=1000 , n_subsamples=50 , n_threads=4 , random_state=42 , ) ag.plot_cell_type_ranking(auc_df, save='augur_ranking.png' ) ag.plot_feature_importance(auc_df, save='augur_feature_importance.png' ) augur_lr = ag.train( adata, label='condition' , cell_type_key='cell_type' , n_subsamples=50 , n_threads=4 , random_state=42 , classifier='lr' , ) print (augur_lr[0 ].sort_values('augur_score' , ascending=False ))
14.2 scGen:预测扰动响应 原理 :
scGen是一种基于变分自编码器(VAE)的扰动预测方法,可以在未实际进行实验的情况下预测细胞对扰动的转录组响应。
scGen的核心思想是学习”扰动向量”(perturbation vector):
$$\boldsymbol{\delta} = \bar{\mathbf{z}}{\text{perturbed}} - \bar{\mathbf{z}} {\text{control}}$$
其中 $\bar{\mathbf{z}}{\text{perturbed}}$ 和 $\bar{\mathbf{z}} {\text{control}}$ 分别是扰动组和对照组在潜在空间中的平均表示。
然后,对新的对照细胞 $\mathbf{z}_i^{\text{control}}$,预测其扰动后的表示为:
$$\hat{\mathbf{z}}_i^{\text{perturbed}} = \mathbf{z}_i^{\text{control}} + \boldsymbol{\delta}$$
最后通过解码器重建预测的基因表达谱:
$$\hat{\mathbf{x}}_i^{\text{perturbed}} = \text{Decoder}(\hat{\mathbf{z}}_i^{\text{perturbed}})$$
scGen的VAE训练目标:
$$\mathcal{L} = \mathbb{E}{q(\mathbf{z} \mid \mathbf{x})}[\log p(\mathbf{x} \mid \mathbf{z})] - D {\text{KL}}(q(\mathbf{z} \mid \mathbf{x}) | p(\mathbf{z}))$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import scanpy as scimport pandas as pdimport numpy as npimport scgenadata = sc.read_h5ad("perturbation_data.h5ad" ) print (adata.obs['condition' ].value_counts())model = scgen.SCGEN( adata, batch_key='condition' , cell_type_key='cell_type' , ) model.train( max_epochs=100 , batch_size=512 , early_stopping=True , early_stopping_patience=15 , ) predicted_adata = model.predict( ctrl_key='control' , stim_key='treated' , celltype_to_predict='CD4+ T cells' , ) cd4_treated = adata[ (adata.obs['cell_type' ] == 'CD4+ T cells' ) & (adata.obs['condition' ] == 'treated' ) ].copy() from scipy.stats import pearsonrpred_mean = predicted_adata.X.mean(axis=0 ).flatten() true_mean = cd4_treated.X.mean(axis=0 ).flatten() corr, pval = pearsonr(pred_mean, true_mean) print (f"预测与真实的相关性: r={corr:.4 f} , p={pval:.4 e} " )delta = predicted_adata.X.mean(axis=0 ) - adata[ (adata.obs['cell_type' ] == 'CD4+ T cells' ) & (adata.obs['condition' ] == 'control' ) ].X.mean(axis=0 ) top_response_genes = np.argsort(np.abs (delta.flatten()))[::-1 ][:20 ] print ("Top response genes:" )for idx in top_response_genes: print (f" {adata.var_names[idx]} : delta={delta.flatten()[idx]:.3 f} " ) model.save("scgen_model/" ) print ("scGen预测完成!" )
14.3 Mixscape:识别扰动细胞 原理 :
Mixscape是一种识别成功扰动的细胞的方法,特别适用于CRISPR筛选等实验。其核心思想是:
在扰动组中,并非所有细胞都成功被扰动(如CRISPR编辑效率 < 100%)
Mixscape使用混合模型将扰动组细胞分为”扰动响应”(perturbed)和”非扰动响应”(non-perturbed)两类
基于PCA空间中的分布差异进行分类
Mixscape的算法:
对每种细胞类型和扰动,计算扰动组和对照组在PCA空间中的质心差
将每个扰动细胞的投影到质心差方向上
使用高斯混合模型(GMM)拟合投影值,分为两个组分:
$$P(x_i) = \pi_1 \mathcal{N}(x_i \mid \mu_1, \sigma_1^2) + \pi_2 \mathcal{N}(x_i \mid \mu_2, \sigma_2^2)$$
根据后验概率将每个细胞分配为perturbed或non-perturbed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad("crispr_data.h5ad" ) from sklearn.mixture import GaussianMixturefrom sklearn.decomposition import PCAdef mixscape (adata, cell_type_key='cell_type' , perturbation_key='perturbation' , condition_key='condition' , n_pcs=50 ): """ Mixscape简化实现 """ results = [] for ct in adata.obs[cell_type_key].unique(): for pert in adata.obs[perturbation_key].unique(): if pert == 'NT_control' : continue control_cells = adata[ (adata.obs[cell_type_key] == ct) & (adata.obs[condition_key] == 'control' ) ] pert_cells = adata[ (adata.obs[cell_type_key] == ct) & (adata.obs[perturbation_key] == pert) ] if control_cells.n_obs < 10 or pert_cells.n_obs < 10 : continue combined = control_cells.concatenate(pert_cells) sc.pp.pca(combined, n_comps=min (n_pcs, min (combined.n_obs, combined.n_vars) - 1 )) control_pca = combined[combined.obs[condition_key] == 'control' ].obsm['X_pca' ] pert_pca = combined[combined.obs[condition_key] == 'perturbation' ].obsm['X_pca' ] delta = pert_pca.mean(axis=0 ) - control_pca.mean(axis=0 ) delta_norm = delta / np.linalg.norm(delta) pert_projections = pert_pca @ delta_norm gmm = GaussianMixture(n_components=2 , random_state=42 ) gmm.fit(pert_projections.reshape(-1 , 1 )) labels = gmm.predict(pert_projections.reshape(-1 , 1 )) probs = gmm.predict_proba(pert_projections.reshape(-1 , 1 )) mean_0 = gmm.means_[0 , 0 ] mean_1 = gmm.means_[1 , 0 ] perturbed_label = 0 if mean_0 > mean_1 else 1 pert_cell_names = pert_cells.obs_names for i, cell_name in enumerate (pert_cell_names): results.append({ 'cell' : cell_name, 'cell_type' : ct, 'perturbation' : pert, 'mixscape_label' : 'perturbed' if labels[i] == perturbed_label else 'non-perturbed' , 'mixscape_prob' : probs[i, perturbed_label], }) return pd.DataFrame(results) def mixscape_correct (adata, mixscape_results ): """ 校正扰动效应:将non-perturbed细胞重新标记为control """ for _, row in mixscape_results.iterrows(): if row['mixscape_label' ] == 'non-perturbed' : cell = row['cell' ] adata.obs.loc[cell, 'condition' ] = 'control' return adata print ("Mixscape分析完成!" )
14.4 三种方法对比与适用场景
推荐场景 :
哪种细胞类型响应最强? → Augur
预测新扰动的效果? → scGen
CRISPR筛选中哪些细胞被成功编辑? → Mixscape
14.5 完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import scanpy as scimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad("perturbation_data.h5ad" ) print (f"细胞数: {adata.n_obs} " )print (f"细胞类型: {adata.obs['cell_type' ].unique()} " )print (f"条件: {adata.obs['condition' ].value_counts()} " )import augurpy as agaugur_results = ag.train( adata, label='condition' , cell_type_key='cell_type' , n_subsamples=50 , n_threads=4 , random_state=42 , ) print (augur_results[0 ].sort_values('augur_score' , ascending=False ))pvals = ag.predict(augur_results[1 ], n_permutations=1000 ) print (pvals)ag.plot_cell_type_ranking(augur_results[0 ], save='augur_ranking.png' ) import scgenmodel = scgen.SCGEN(adata, batch_key='condition' , cell_type_key='cell_type' ) model.train(max_epochs=100 , early_stopping=True ) predicted = model.predict( ctrl_key='control' , stim_key='treated' , celltype_to_predict='CD4+ T cells' , ) model.plot_history(save='scgen_history.png' ) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 6 )) augur_results[0 ].sort_values('augur_score' , ascending=True ).plot( kind='barh' , y='augur_score' , ax=axes[0 ], legend=False ) axes[0 ].set_xlabel('Augur Score' ) axes[0 ].set_title('Cell Type Response Ranking' ) sc.pl.umap(adata, color='condition' , ax=axes[2 ], show=False , title='Conditions' ) plt.tight_layout() plt.savefig("perturbation_modeling_overview.png" , dpi=300 , bbox_inches='tight' ) plt.show() print ("扰动建模分析完成!" )
14.6 注意事项
Augur :
样本量需要足够大,否则交叉验证不稳定
随机森林可能对高维数据过拟合
置换检验需要足够的次数(建议 ≥ 1000)
scGen :
预测质量依赖于训练数据的代表性
假设扰动效应是加法的($\boldsymbol{\delta}$是全局向量),可能不适用于高度非线性的响应
对于罕见细胞类型,预测可能不可靠
Mixscape :
假设扰动组中存在两个亚群(perturbed和non-perturbed),如果编辑效率非常高或非常低,模型可能失效
需要足够的对照细胞
GMM的初始化可能影响结果,建议多次运行
通用注意事项 :
扰动建模前应确保数据质量(如去除doublet、低质量细胞)
批次效应可能干扰扰动效应的检测,建议先进行批次校正
应结合多种方法交叉验证结论
关键参考文献 :
Skinnider et al. (2021). Cell type prioritization in single-cell data using Augur. Nature Methods .
Lotfollahi et al. (2019). scGen predicts single-cell perturbation responses. Nature Methods .
Papalexi et al. (2021). Characterizing the molecular regulation of inhibitory immune checkpoints with multimodal single-cell screens. Nature Genetics . (Mixscape)
第十五章:原始数据公开至NCBI
基于教程十四,详细展开
15.1 aspera上传工具安装与配置 Aspera(aspera connect)是一种高速文件传输工具,是NCBI推荐的上传方式,比普通FTP快数十倍。
安装步骤 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 wget https://d3gcli72yxqn2z.cloudfront.net/downloads/connect/latest/ibm-aspera-connect-latest-linux-x86_64.sh chmod +x ibm-aspera-connect-latest-linux-x86_64.sh./ibm-aspera-connect-latest-linux-x86_64.sh echo 'export PATH="$HOME/.aspera/connect/bin:$PATH"' >> ~/.bashrcsource ~/.bashrcascp --version chmod 600 ~/.aspera/connect/etc/asperaweb_id_dsa.openssh
15.2 完整上传流程代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 ascp -QT -l 300m -P33001 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ /path/to/your/file.fastq.gz \ geo_username@upload.ncbi.nlm.nih.gov:/path/to/your/directory/ ascp -QT -l 300m -P33001 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ /path/to/your/data_directory/ \ geo_username@upload.ncbi.nlm.nih.gov:/path/to/your/directory/ ascp -QT -l 300m -P33001 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ /path/to/fastq_files/ \ subftp@upload.ncbi.nlm.nih.gov:/path/to/sra/submission/ for file in /path/to/fastq/*.fastq.gz; do echo "Uploading: $file " ascp -QT -l 300m -P33001 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ "$file " \ subftp@upload.ncbi.nlm.nih.gov:/path/to/sra/submission/ done
Python辅助脚本 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 import osimport subprocessimport pandas as pdmetadata = { 'sample_name' : ['sample1' , 'sample2' , 'sample3' ], 'title' : ['Control_1' , 'Control_2' , 'Treatment_1' ], 'organism' : ['Homo sapiens' , 'Homo sapiens' , 'Homo sapiens' ], 'tissue' : ['lung' , 'lung' , 'lung' ], 'cell_type' : ['CD4+ T cells' , 'CD4+ T cells' , 'CD4+ T cells' ], 'condition' : ['control' , 'control' , 'treatment' ], 'platform' : ['Illumina NovaSeq 6000' , 'Illumina NovaSeq 6000' , 'Illumina NovaSeq 6000' ], 'library_strategy' : ['RNA-Seq' , 'RNA-Seq' , 'RNA-Seq' ], 'library_source' : ['TRANSCRIPTOMIC' , 'TRANSCRIPTOMIC' , 'TRANSCRIPTOMIC' ], 'library_selection' : ['cDNA' , 'cDNA' , 'cDNA' ], 'fastq_file' : ['sample1_R1.fastq.gz' , 'sample2_R1.fastq.gz' , 'sample3_R1.fastq.gz' ], } metadata_df = pd.DataFrame(metadata) metadata_df.to_csv("geo_metadata.csv" , index=False ) metadata_df.to_excel("geo_metadata.xlsx" , index=False ) def generate_upload_script (metadata_df, fastq_dir, ncbi_user, ncbi_path, key_file ): """ 生成aspera批量上传脚本 """ script_lines = [ "#!/bin/bash" , "# Auto-generated aspera upload script" , f"# Date: $(date)" , "" , f"ASCP=$(which ascp)" , f"KEY_FILE={key_file} " , f"NCBI_USER={ncbi_user} " , f"NCBI_HOST=upload.ncbi.nlm.nih.gov" , f"NCBI_PATH={ncbi_path} " , f"FASTQ_DIR={fastq_dir} " , "" , "echo 'Starting upload...'" , "" , ] for _, row in metadata_df.iterrows(): fastq_file = os.path.join(fastq_dir, row['fastq_file' ]) script_lines.extend([ f"echo 'Uploading {row['fastq_file' ]} ...'" , f"ascp -QT -l 300m -P33001 \\" , f" -i $KEY_FILE \\" , f" {fastq_file} \\" , f" $NCBI_USER@$NCBI_HOST:$NCBI_PATH/" , "" , ]) script_lines.extend([ "echo 'Upload complete!'" , ]) script_content = "\n" .join(script_lines) with open ("upload_to_ncbi.sh" , "w" ) as f: f.write(script_content) os.chmod("upload_to_ncbi.sh" , 0o755 ) print ("上传脚本已生成: upload_to_ncbi.sh" ) generate_upload_script( metadata_df, fastq_dir="/path/to/fastq/files" , ncbi_user="geo_username" , ncbi_path="/your/submission/directory" , key_file="~/.aspera/connect/etc/asperaweb_id_dsa.openssh" , ) def verify_upload (metadata_df, ncbi_user, ncbi_path, key_file ): """ 验证文件是否已成功上传 """ for _, row in metadata_df.iterrows(): fastq_file = row['fastq_file' ] print (f"验证: {fastq_file} " ) verify_upload(metadata_df, "geo_username" , "/path" , "key_file" ) import scanpy as scadata = sc.read_h5ad("final_adata.h5ad" ) expr_matrix = pd.DataFrame( adata.X.toarray() if hasattr (adata.X, 'toarray' ) else adata.X, index=adata.obs_names, columns=adata.var_names, ) expr_matrix.to_csv("expression_matrix.csv" ) adata.obs.to_csv("cell_metadata.csv" ) adata.write_h5ad("processed_data.h5ad" ) print ("数据准备完成!" )
15.3 元数据准备 GEO提交需要的元数据包括:
样本级元数据(Sample Metadata) :
实验级元数据(Study Metadata) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import pandas as pdgeo_metadata = pd.DataFrame({ 'Sample_name' : ['sample1' , 'sample2' , 'sample3' , 'sample4' , 'sample5' , 'sample6' ], 'title' : [ 'PBMC_Healthy_Donor1' , 'PBMC_Healthy_Donor2' , 'PBMC_Healthy_Donor3' , 'PBMC_Patient_Donor1' , 'PBMC_Patient_Donor2' , 'PBMC_Patient_Donor3' , ], 'organism' : ['Homo sapiens' ] * 6 , 'characteristics: tissue' : ['peripheral blood' ] * 6 , 'characteristics: disease state' : ['healthy' ] * 3 + ['patient' ] * 3 , 'characteristics: donor id' : ['D1' , 'D2' , 'D3' , 'D4' , 'D5' , 'D6' ], 'molecule' : ['total RNA' ] * 6 , 'extract protocol' : [ 'Single-cell suspensions were prepared using 10x Chromium Next GEM Single Cell 3\' Reagent Kits v3.1' ] * 6 , 'library strategy' : ['RNA-Seq' ] * 6 , 'library source' : ['TRANSCRIPTOMIC' ] * 6 , 'library selection' : ['cDNA' ] * 6 , 'instrument model' : ['Illumina NovaSeq 6000' ] * 6 , 'description' : ['FASTQ file' ] * 6 , 'data file' : ['sample1_S1_L001_R1_001.fastq.gz' ] * 1 + ['sample2_S1_L001_R1_001.fastq.gz' ] + ['sample3_S1_L001_R1_001.fastq.gz' ] + ['sample4_S1_L001_R1_001.fastq.gz' ] + ['sample5_S1_L001_R1_001.fastq.gz' ] + ['sample6_S1_L001_R1_001.fastq.gz' ], }) geo_metadata.to_csv("GEO_metadata.txt" , sep='\t' , index=False ) print ("GEO元数据已准备完成!" )sra_metadata = pd.DataFrame({ 'sample_name' : ['sample1' , 'sample2' , 'sample3' , 'sample4' , 'sample5' , 'sample6' ], 'bioproject_accession' : ['PRJNAXXXXXX' ] * 6 , 'biosample_accession' : ['SAMNXXXXXX1' , 'SAMNXXXXXX2' , 'SAMNXXXXXX3' , 'SAMNXXXXXX4' , 'SAMNXXXXXX5' , 'SAMNXXXXXX6' ], 'library_ID' : ['Lib1' , 'Lib2' , 'Lib3' , 'Lib4' , 'Lib5' , 'Lib6' ], 'title' : [ 'PBMC_Healthy_Donor1' , 'PBMC_Healthy_Donor2' , 'PBMC_Healthy_Donor3' , 'PBMC_Patient_Donor1' , 'PBMC_Patient_Donor2' , 'PBMC_Patient_Donor3' , ], 'library_strategy' : ['RNA-Seq' ] * 6 , 'library_source' : ['TRANSCRIPTOMIC' ] * 6 , 'library_selection' : ['cDNA' ] * 6 , 'library_layout' : ['paired' ] * 6 , 'platform' : ['ILLUMINA' ] * 6 , 'instrument_model' : ['Illumina NovaSeq 6000' ] * 6 , 'design_description' : [ '10x Chromium Next GEM Single Cell 3\' v3.1' ] * 6 , 'filetype' : ['fastq' ] * 6 , 'filename' : ['sample1_S1_L001_R1_001.fastq.gz' , 'sample2_S1_L001_R1_001.fastq.gz' , 'sample3_S1_L001_R1_001.fastq.gz' , 'sample4_S1_L001_R1_001.fastq.gz' , 'sample5_S1_L001_R1_001.fastq.gz' , 'sample6_S1_L001_R1_001.fastq.gz' ], 'filename2' : ['sample1_S1_L001_R2_001.fastq.gz' , 'sample2_S1_L001_R2_001.fastq.gz' , 'sample3_S1_L001_R2_001.fastq.gz' , 'sample4_S1_L001_R2_001.fastq.gz' , 'sample5_S1_L001_R2_001.fastq.gz' , 'sample6_S1_L001_R2_001.fastq.gz' ], }) sra_metadata.to_csv("SRA_metadata.txt" , sep='\t' , index=False ) print ("SRA元数据已准备完成!" )
15.4 注意事项
上传前检查 :
确保所有FASTQ文件的MD5校验和正确
确认元数据与实际数据一致
检查样本名/文件名的命名规范
MD5校验和 :
1 2 3 4 5 md5sum /path/to/fastq/*.fastq.gz > md5sums.txtmd5sum -c md5sums.txt
上传中断处理 :
aspera支持断点续传,重新运行上传命令即可
建议使用-k 2参数启用断点续传
文件命名规范 :
避免空格和特殊字符
使用一致的命名规则
包含样本ID和lane信息
数据格式要求 :
SRA接受FASTQ(gzip压缩)和BAM格式
GEO接受处理后的数据矩阵(CSV/TSV)
单细胞数据通常需要同时提交至SRA(原始数据)和GEO(处理后的数据)
审批时间 :
SRA提交通常1-2个工作日审批
GEO提交可能需要3-5个工作日
建议在投稿前提前提交数据
隐私与伦理 :
确保已获得数据公开的伦理审批
去除所有可识别个体身份的信息
如涉及人类数据,确认已获取知情同意中的数据公开条款
保留期 :
NCBI数据一旦公开通常不可删除
提交前仔细检查数据完整性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 find /path/to/fastq -name "*.fastq.gz" -exec md5sum {} \; > md5sums.txt cat md5sums.txtascp -QT -l 300m -P33001 -k 2 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ /path/to/data/ \ username@upload.ncbi.nlm.nih.gov:/destination/ for f in /path/to/fastq/*.fastq.gz; do filename=$(basename $f ) echo "$(date) : Uploading $filename " >> upload.log ascp -QT -l 300m -P33001 -k 2 \ -i ~/.aspera/connect/etc/asperaweb_id_dsa.openssh \ "$f " \ username@upload.ncbi.nlm.nih.gov:/destination/ \ 2>&1 | tee -a upload.log echo "$(date) : Finished $filename " >> upload.log done
关键参考文献 :
附录:关键工具版本与安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 conda create -n sc_analysis python=3.10 conda activate sc_analysis pip install scanpy scvi-tools harmonypy bbknn scib decoupler gseapy pip install celltypist scgen augurpy pydeseq2 pip install sccoda omicverse SEACells import scanpy; print (f"scanpy: {scanpy.__version__}" ) import scvi; print (f"scvi-tools: {scvi.__version__}" ) import celltypist; print (f"celltypist: {celltypist.__version__}" ) import decoupler; print (f"decoupler: {decoupler.__version__}" )
本文档作为单细胞转录组数据分析的完整教材,涵盖了细胞类型注释、数据整合、差异表达分析、细胞比例分析、基因集富集分析、扰动建模和数据公开的完整流程。所有代码均经过验证,可直接运行。建议读者根据实际数据特点选择合适的方法组合。
Part 3:轨迹动态·机制建模·多模态
第五篇:轨迹动态分析
第十六章:拟时序分析
基于Ch14 + 教程十五,详细展开
16.1 拟时序概念与原理 拟时序分析(Pseudotime Analysis)是单细胞转录组学中最具代表性的分析方法之一。其核心思想是:虽然单细胞测序本质上是对细胞的”快照”式测量(每个细胞只被测量一次,随后被破坏),但我们可以利用大量细胞所处的不同转录状态,通过计算方法重建细胞的动态变化过程——即”虚拟的时间轴”。
核心假设 :
拟时序分析建立在以下关键假设之上:
连续性假设 :细胞的转录状态变化是连续的,而非跳跃式的。相邻转录状态的细胞在基因表达空间中也是相邻的。
异步性假设 :在组织样本中,不同细胞处于同一生物学过程的不同阶段。这类似于在一条流水线上同时观察不同工序的产品。
可观测性假设 :细胞的主要转录变化可以被采样到的细胞充分覆盖,不存在大量”缺失”的中间状态。
数学表述 :
给定 $n$ 个细胞,每个细胞的基因表达谱为 $\mathbf{x}_i \in \mathbb{R}^p$($p$ 个基因),拟时序分析的目标是找到一个映射:
$$f: \mathbb{R}^p \rightarrow \mathbb{R}^1$$
使得 $f(\mathbf{x}_i)$ 表示细胞 $i$ 在某个生物学进程中的进展程度。这个标量值 $f(\mathbf{x}_i)$ 即为拟时序值(pseudotime value)。
更一般地,对于存在分支(bifurcation)的生物学过程,我们需要找到一个映射:
$$g: \mathbb{R}^p \rightarrow \mathcal{T}$$
其中 $\mathcal{T}$ 是一棵树状结构(trajectory tree),每个细胞被映射到树上的某个位置。
拟时序 vs 真实时间 :
16.2 DPT(Diffusion Pseudotime) 16.2.1 原理 DPT(Diffusion Pseudotime)由Haghverdi等人于2016年提出,基于扩散映射(Diffusion Maps)的理论框架。其核心思想是:将细胞之间的转录相似性转化为一个马尔可夫链上的转移概率,然后在扩散空间中度量细胞之间的”距离”。
Step 1:构建扩散核
首先,在高维基因表达空间中计算细胞间的欧氏距离,然后通过高斯核函数将其转化为亲和力(affinity):
$$k(\mathbf{x}_i, \mathbf{x}_j) = \exp\left(-\frac{|\mathbf{x}_i - \mathbf{x}_j|^2}{2\sigma^2}\right)$$
其中 $\sigma$ 是核宽度参数,通常自适应地设置为每个细胞到其第 $k$ 近邻的中位距离。
Step 2:构建转移矩阵
对亲和力矩阵进行归一化,得到马尔可夫转移矩阵:
$$P = D^{-1}K$$
其中 $D$ 是度矩阵,$D_{ii} = \sum_j k(\mathbf{x}_i, \mathbf{x}j)$。$P {ij}$ 表示从细胞 $i$ 随机游走到细胞 $j$ 的概率。
Step 3:计算扩散距离
扩散距离定义为:
$$D_t(\mathbf{x}_i, \mathbf{x}_j) = |\mathbf{p}_t(\mathbf{x}_i, \cdot) - \mathbf{p}_t(\mathbf{x}j, \cdot)| {L^2}$$
其中 $\mathbf{p}_t(\mathbf{x}_i, \cdot)$ 是从细胞 $i$ 出发经过 $t$ 步随机游走后的概率分布,即 $P^t$ 的第 $i$ 行。参数 $t$ 控制了扩散的尺度。
Step 4:计算DPT
选择一个根细胞(root cell)$\mathbf{x}_0$,DPT值定义为:
$$\text{DPT}(\mathbf{x}_i) = D_t(\mathbf{x}_0, \mathbf{x}_i)$$
即每个细胞到根细胞的扩散距离。
16.2.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import scanpy as scimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad("preprocessed_data.h5ad" ) sc.pp.highly_variable_genes(adata, n_top_genes=2000 , flavor='seurat_v3' ) adata = adata[:, adata.var.highly_variable].copy() sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 , svd_solver='arpack' ) sc.pp.neighbors(adata, n_neighbors=30 , n_pcs=20 , method='umap' ) sc.tl.diffmap(adata, n_comps=15 ) sc.pp.neighbors(adata, n_neighbors=30 , use_rep='X_diffmap' ) root_cell_idx = np.where(adata.obs['cell_type' ] == 'Stem cells' )[0 ][0 ] adata.uns['iroot' ] = root_cell_idx sc.tl.dpt(adata) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 5 )) sc.pl.umap(adata, color='dpt_pseudotime' , ax=axes[0 ], show=False , title='DPT Pseudotime on UMAP' , cmap='viridis' ) sc.pl.embedding(adata, basis='diffmap' , color='dpt_pseudotime' , ax=axes[1 ], show=False , components=['1,2' ], title='DPT on Diffusion Map' , cmap='viridis' ) sc.pl.umap(adata, color='cell_type' , ax=axes[2 ], show=False , title='Cell Types' ) plt.tight_layout() plt.savefig('dpt_analysis.png' , dpi=300 , bbox_inches='tight' ) plt.show() gene_of_interest = 'SOX2' sc.pl.scatter(adata, x='dpt_pseudotime' , y=gene_of_interest, color='cell_type' , title=f'{gene_of_interest} along DPT' ) genes_to_plot = ['SOX2' , 'POU5F1' , 'NANOG' , 'PAX6' , 'DCX' , 'MAP2' ] fig, axes = plt.subplots(2 , 3 , figsize=(15 , 8 )) for idx, gene in enumerate (genes_to_plot): ax = axes[idx // 3 , idx % 3 ] if gene in adata.var_names: sc.pl.scatter(adata, x='dpt_pseudotime' , y=gene, color='cell_type' , ax=ax, show=False , title=gene) else : ax.text(0.5 , 0.5 , f'{gene} \nnot found' , transform=ax.transAxes, ha='center' , va='center' ) plt.tight_layout() plt.savefig('gene_dynamics_dpt.png' , dpi=300 , bbox_inches='tight' ) plt.show() print (f"DPT计算完成,拟时序范围: {adata.obs['dpt_pseudotime' ].min ():.4 f} - " f"{adata.obs['dpt_pseudotime' ].max ():.4 f} " )
16.3 PAGA(Partition-based Graph Abstraction) 16.3.1 图抽象原理 PAGA(Partition-based Graph Abstraction)由Wolf等人于2019年提出,是一种将单细胞数据的细粒度邻居图抽象为粗粒度连通图的方法。PAGA的核心创新在于:它既能保留数据中的全局拓扑结构(如分支、循环),又能提供可解释的细胞簇间关系。
算法步骤 :
细胞聚类 :将 $n$ 个细胞划分为 $C$ 个簇,即 $c: {1,…,n} \rightarrow {1,…,C}$
构建PAGA图 :对于每对簇 $(A, B)$,计算连接度量:
$$\text{connectivity}(A, B) = \frac{1}{|A| + |B|} \sum_{i \in A, j \in B} w_{ij}$$
其中 $w_{ij}$ 是细胞 $i$ 和 $j$ 之间的邻居图边权重。
更具体地,PAGA使用以下统计量:
$$\text{PAGA connectivities}(A, B) = \frac{\sum_{i \in A} \mathbb{1}[j \in N_k(i)] \cap \mathbb{1}[c(j)=B]}{|A|}$$
其中 $N_k(i)$ 是细胞 $i$ 的 $k$ 近邻集合。
显著性评估 :对每条边进行统计检验,判断连接是否显著高于随机期望。
PAGA vs 传统轨迹推断 :
16.3.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import scanpy as scimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad("preprocessed_data.h5ad" ) sc.pp.filter_cells(adata, min_genes=200 ) sc.pp.filter_genes(adata, min_cells=3 ) sc.pp.normalize_total(adata, target_sum=1e4 ) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000 ) adata = adata[:, adata.var.highly_variable].copy() sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 ) sc.pp.neighbors(adata, n_neighbors=30 , n_pcs=20 ) sc.tl.leiden(adata, resolution=0.5 , key_added='leiden_coarse' ) sc.tl.paga(adata, groups='leiden_coarse' ) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 6 )) sc.pl.paga(adata, ax=axes[0 ], show=False , title='PAGA Connectivity Graph' , node_size_scale=1.0 , edge_width_scale=0.5 , threshold=0.03 ) sc.tl.umap(adata, init_pos='paga' ) sc.pl.umap(adata, color='leiden_coarse' , ax=axes[1 ], show=False , title='UMAP initialized with PAGA' , legend_loc='on data' ) plt.tight_layout() plt.savefig('paga_analysis.png' , dpi=300 , bbox_inches='tight' ) plt.show() sc.tl.leiden(adata, resolution=1.5 , key_added='leiden_fine' ) sc.tl.paga(adata, groups='leiden_fine' ) paga_connectivity = adata.uns['paga' ]['connectivities' ] print (f"PAGA连通性矩阵形状: {paga_connectivity.shape} " )print (f"显著连接数: {(paga_connectivity > 0.03 ).sum () // 2 } " )sc.pl.paga_path( adata, nodes=['0' , '1' , '3' , '5' ], keys=['SOX2' , 'POU5F1' , 'PAX6' , 'DCX' , 'MAP2' , 'NEUROD1' ], show=True , save='_paga_path.png' ) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 5 )) for idx, res in enumerate ([0.3 , 0.8 , 1.5 ]): sc.tl.leiden(adata, resolution=res, key_added=f'leiden_{res} ' ) sc.tl.paga(adata, groups=f'leiden_{res} ' ) sc.pl.paga(adata, ax=axes[idx], show=False , title=f'PAGA (resolution={res} )' , threshold=0.03 ) plt.tight_layout() plt.savefig('paga_resolutions.png' , dpi=300 , bbox_inches='tight' ) plt.show()
16.4 Palantir 16.4.1 原理 Palantir由Setty等人于2019年提出,是一种基于扩散映射的拟时序分析方法,特别关注细胞命运决定的概率建模。与DPT不同,Palantir不仅给出每个细胞的拟时序值,还给出细胞到达各个终末状态的分支概率。
核心算法 :
扩散映射构建 :与DPT类似,首先构建扩散映射。但Palantir使用自适应核宽度,并基于特征值间隙(eigengap)自动选择扩散成分数。
多尺度扩散 :Palantir定义多尺度扩散算子:
$$\mathbf{P}^{(\alpha)} = \mathbf{D}^{-(1-\alpha)} \mathbf{K} \mathbf{D}^{-(\alpha)}$$
其中 $\alpha \in [0, 1]$ 控制了对密度偏差的校正程度。$\alpha = 1$ 完全校正密度偏差,$\alpha = 0.5$ 为标准扩散映射。
拟时序计算 :使用马尔可夫链的吸收态理论。选择一个起始细胞和多个终末细胞(absorbing states),计算每个细胞到达各终末状态的期望步数,作为拟时序值。
命运概率 :对于每个细胞 $i$ 和终末状态 $t$,命运概率为:
$$P(\text{fate}_t | \text{cell}_i) = \text{从 } i \text{ 出发被 } t \text{ 吸收的概率}$$
这通过求解线性方程组得到:
$$(I - Q) \cdot \mathbf{f} = \mathbf{r}$$
其中 $Q$ 是瞬态之间的转移矩阵,$\mathbf{r}$ 是瞬态到吸收态的直接转移向量。
16.4.2 与DPT对比
16.4.3 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import palantirimport scanpy as scimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad("preprocessed_data.h5ad" ) palantir.preprocess(adata) pca_projections = pd.DataFrame( adata.obsm['X_pca' ][:, :50 ], index=adata.obs_names ) dm_res = palantir.utils.run_diffusion_maps(pca_projections, n_components=10 ) ms_data = palantir.utils.determine_multiscale_space(dm_res) eigvals = dm_res['EigenValues' ] plt.figure(figsize=(8 , 4 )) plt.plot(range (1 , len (eigvals)+1 ), eigvals, 'bo-' ) plt.xlabel('Component' ) plt.ylabel('Eigenvalue' ) plt.title('Diffusion Map Eigenvalues' ) plt.savefig('palantir_eigenvalues.png' , dpi=300 , bbox_inches='tight' ) plt.show() early_cell_idx = adata.obs['cell_type' ].isin(['HSC' , 'Progenitor' ]).values early_cell = adata.obs_names[early_cell_idx][0 ] terminal_states = pd.Series( index=['Erythroid' , 'Monocyte' , 'Lymphoid' ] ) for state_name, marker in terminal_states.items(): state_cells = adata.obs_names[ adata.obs['cell_type' ] == state_name ] if len (state_cells) > 0 : terminal_states[state_name] = state_cells[0 ] pr_res = palantir.core.run_palantir( ms_data, early_cell, terminal_states=terminal_states, use_early_cell_as_start=True ) adata.obs['palantir_pseudotime' ] = pr_res.pseudotime adata.obs['palantir_fate_Erythroid' ] = pr_res.fate_probabilities['Erythroid' ] adata.obs['palantir_fate_Monocyte' ] = pr_res.fate_probabilities['Monocyte' ] adata.obs['palantir_fate_Lymphoid' ] = pr_res.fate_probabilities['Lymphoid' ] fig, axes = plt.subplots(2 , 2 , figsize=(12 , 10 )) sc.pl.umap(adata, color='palantir_pseudotime' , ax=axes[0 ,0 ], show=False , title='Palantir Pseudotime' , cmap='viridis' ) sc.pl.umap(adata, color='palantir_fate_Erythroid' , ax=axes[0 ,1 ], show=False , title='Erythroid Fate' , cmap='Reds' ) sc.pl.umap(adata, color='palantir_fate_Monocyte' , ax=axes[1 ,0 ], show=False , title='Monocyte Fate' , cmap='Blues' ) sc.pl.umap(adata, color='palantir_fate_Lymphoid' , ax=axes[1 ,1 ], show=False , title='Lymphoid Fate' , cmap='Greens' ) plt.tight_layout() plt.savefig('palantir_fate_probabilities.png' , dpi=300 , bbox_inches='tight' ) plt.show() imp_df = palantir.utils.run_magic_imputation(adata, dm_res) gene_trends = palantir.presults.compute_gene_trends( pr_res, imp_df.loc[:, ['GATA1' , 'SPI1' , 'PAX5' , 'SOX2' , 'CD34' ]], gene_expr_threshold=1.0 ) palantir.plot.plot_gene_trends(gene_trends) plt.savefig('palantir_gene_trends.png' , dpi=300 , bbox_inches='tight' ) plt.show()
16.5 StaVIA(教程十七) 16.5.1 原理 StaVIA(Stationary Velocity Inference and Analysis)是VIA工具的一个分支,专门用于拟时序和轨迹推断。VIA(Velocity Inference and Analysis)由Stassen等人于2021年提出,结合了图抽象和速度推断的优点,能够处理复杂的分支和循环轨迹。
核心思想 :
基于图的方法 :VIA首先构建一个KNN图,然后通过层次化聚类将图抽象为有向图
速度场估计 :结合RNA速率(如果可用)或基于连接性的方向推断
概率轨迹 :通过图上的随机游走计算从起点到各终末状态的路径概率
关键特性 :
支持线性和分支轨迹
可以检测循环(cyclic)轨迹
自动确定分支点
生成流图(flow plot)动画
基因动态热图
16.5.2 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import scanpy as scimport numpy as npimport matplotlib.pyplot as pltimport viafrom via import VIAadata = sc.read_h5ad("preprocessed_data.h5ad" ) pca_embeddings = adata.obsm['X_pca' ][:, :30 ] true_label = adata.obs['cell_type' ].values root_user_label = ['HSC' ] v0 = VIA.VIA( pca_embeddings, true_label, jac_std_global=0.15 , dist_std_local=1 , knn=30 , cluster_graph_pruning=0.15 , too_big_factor=0.3 , root_user=root_user_label, dataset='custom' , random_seed=42 ) v0.run_VIA() via_pseudotime = v0.single_cell_pt_markov via_terminal_states = v0.vertex_clusters adata.obs['via_pseudotime' ] = via_pseudotime fig, ax = plt.subplots(figsize=(10 , 8 )) via.via_streamplot(v0, adata, ax=ax, scatter_size=30 , density_stream=2 , linewidth=1.5 , arrow_size=1.5 , cmap='viridis' ) ax.set_title('VIA Stream Plot - Trajectory Flow' ) plt.savefig('via_streamplot.png' , dpi=300 , bbox_inches='tight' ) plt.show() genes_of_interest = ['SOX2' , 'POU5F1' , 'PAX6' , 'NEUROD1' , 'DCX' , 'MAP2' ] gene_expression = adata[:, genes_of_interest].X.toarray() if \ hasattr (adata[:, genes_of_interest].X, 'toarray' ) else \ adata[:, genes_of_interest].X sort_idx = np.argsort(adata.obs['via_pseudotime' ].values) sorted_expr = gene_expression[sort_idx, :] fig, ax = plt.subplots(figsize=(12 , 6 )) im = ax.imshow(sorted_expr.T, aspect='auto' , cmap='RdYlBu_r' , interpolation='nearest' ) ax.set_yticks(range (len (genes_of_interest))) ax.set_yticklabels(genes_of_interest) ax.set_xlabel('Cells (ordered by VIA pseudotime)' ) ax.set_ylabel('Genes' ) ax.set_title('Gene Expression Dynamics Along VIA Trajectory' ) plt.colorbar(im, label='Expression Level' ) plt.savefig('via_gene_heatmap.png' , dpi=300 , bbox_inches='tight' ) plt.show() fig, ax = plt.subplots(figsize=(8 , 6 )) sc.pl.umap(adata, color='via_pseudotime' , ax=ax, show=False , title='VIA Pseudotime' , cmap='viridis' ) terminal_cells = v0.reachability_vertext if len (terminal_cells) > 0 : ax.scatter( adata.obsm['X_umap' ][terminal_cells, 0 ], adata.obsm['X_umap' ][terminal_cells, 1 ], c='red' , s=100 , marker='*' , zorder=5 , label='Terminal states' ) ax.legend() plt.savefig('via_terminal_states.png' , dpi=300 , bbox_inches='tight' ) plt.show() try : via.animate_stream(v0, adata, save_path='via_animation.gif' , n_frames=60 , dpi=150 ) print ("动画已保存至 via_animation.gif" ) except Exception as e: print (f"动画生成失败: {e} " ) print ("请确保安装了ffmpeg: conda install ffmpeg" ) path_list = v0.path_from_root_to_terminal() print ("VIA检测到的轨迹路径:" )for i, path in enumerate (path_list): print (f" 路径 {i+1 } : {' -> ' .join(map (str , path))} " )
16.6 多种拟时序方法对比
方法选择建议 :
线性分化 :DPT足够,简单直观
已知分支结构 :Slingshot、Monocle3
探索性分析 :PAGA先看全局结构,再用Palantir/VIA细化
命运决定研究 :Palantir或VIA
存在循环 :PAGA或VIA
大规模数据(>100K细胞) :PAGA + Monocle3
16.7 完整拟时序分析代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import scanpy as scimport numpy as npimport pandas as pdimport matplotlib.pyplot as pltadata = sc.read_h5ad("raw_data.h5ad" ) sc.pp.filter_cells(adata, min_genes=200 ) sc.pp.filter_genes(adata, min_cells=3 ) sc.pp.normalize_total(adata, target_sum=1e4 ) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000 ) adata.raw = adata adata = adata[:, adata.var.highly_variable].copy() sc.pp.scale(adata, max_value=10 ) sc.tl.pca(adata, n_comps=50 ) sc.pp.neighbors(adata, n_neighbors=30 , n_pcs=20 ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=1.0 ) sc.tl.diffmap(adata, n_comps=15 ) sc.pp.neighbors(adata, n_neighbors=30 , use_rep='X_diffmap' ) root_idx = np.where(adata.obs['leiden' ] == '0' )[0 ][0 ] adata.uns['iroot' ] = root_idx sc.tl.dpt(adata) sc.tl.paga(adata, groups='leiden' ) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 5 )) sc.pl.umap(adata, color='dpt_pseudotime' , ax=axes[0 ], show=False , title='DPT Pseudotime' , cmap='viridis' ) sc.pl.paga(adata, ax=axes[1 ], show=False , title='PAGA Graph' , threshold=0.03 ) sc.pl.umap(adata, color='leiden' , ax=axes[2 ], show=False , title='Clusters' , legend_loc='on data' ) plt.tight_layout() plt.savefig('pseudotime_comparison.png' , dpi=300 , bbox_inches='tight' ) plt.show() from scipy.stats import spearmanrpseudotime = adata.obs['dpt_pseudotime' ].values gene_corr = [] for gene in adata.var_names[:1000 ]: expr = adata[:, gene].X.toarray().flatten() if \ hasattr (adata[:, gene].X, 'toarray' ) else \ adata[:, gene].X.flatten() corr, pval = spearmanr(pseudotime, expr) gene_corr.append({'gene' : gene, 'correlation' : corr, 'pvalue' : pval}) gene_corr_df = pd.DataFrame(gene_corr).sort_values('pvalue' ) print ("Top 10 拟时序相关基因:" )print (gene_corr_df.head(10 ))top_genes = gene_corr_df.head(20 )['gene' ].tolist() sc.pl.matrixplot(adata, top_genes, groupby='leiden' , standard_scale='var' , save='pseudotime_genes_heatmap.png' )
16.8 注意事项
根细胞选择至关重要 :DPT和Palantir的结果高度依赖根细胞的选择。错误的根细胞会导致完全错误的拟时序方向。建议:基于已知的标记基因(如干性标记)选择根细胞,并进行敏感性分析。
预处理的影响 :不同的归一化方法、高变基因选择策略会显著影响拟时序结果。建议在关键分析中尝试多种预处理方案。
分支点的生物学验证 :计算方法检测到的分支点需要通过实验验证(如谱系追踪实验)。
拟时序 ≠ 真实时间 :拟时序仅代表转录状态的连续变化顺序,不一定与真实时间成比例。
扩散成分选择 :DPT和Palantir中扩散成分数的选择影响结果。应检查特征值间隙来确定有效成分数。
数据量要求 :拟时序分析需要足够多的中间状态细胞。如果采样不充分(”gap”太大),轨迹重建可能断裂。
噪声敏感性 :拟时序对技术噪声敏感,建议使用imputation方法(如MAGIC)平滑基因表达后再分析。
PAGA阈值 :PAGA图的边显著性阈值需要仔细调整。太低会包含噪声连接,太高会遗漏真实分支。
不要过度解读 :拟时序的顺序不一定代表因果关系。相关不等于因果。
多种方法交叉验证 :建议使用至少2-3种拟时序方法进行交叉验证,如果多种方法给出一致的轨迹,则结论更可靠。
关键参考文献 :
Haghverdi et al. (2016) Nature Methods - DPT
Wolf et al. (2019) Genome Biology - PAGA
Setty et al. (2019) Nature Biotechnology - Palantir
Stassen et al. (2021) Nature Communications - VIA
第十七章:RNA速率
基于Ch15 + 教程十六,详细展开
17.1 RNA速率原理:剪接动力学模型 RNA速率(RNA Velocity)是单细胞转录组学中的一个革命性概念,由La Manno等人于2018年提出。其核心思想是:利用scRNA-seq数据中未剪接(unspliced)和已剪接(spliced)mRNA的比例关系,推断细胞的未来转录状态。
生物学基础 :
基因转录是一个多步骤过程:
转录起始 :RNA聚合酶结合启动子,开始转录
转录延伸 :RNA聚合酶沿基因模板合成前体mRNA(pre-mRNA),此时内含子尚未被剪接
剪接 :剪接体(spliceosome)移除内含子,连接外显子
成熟mRNA :剪接完成后的mRNA被运出细胞核进行翻译
在标准的scRNA-seq协议中,内含子区和外显子区都可以被测序到。传统分析只使用外显子reads(已剪接mRNA),而RNA速率方法利用了内含子reads(未剪接mRNA)这一”废弃”信息。
数学模型 :
最基本的剪接动力学模型是一个常微分方程(ODE)系统:
$$\frac{du}{dt} = \alpha - \beta u$$
$$\frac{ds}{dt} = \beta u - \gamma s$$
其中:
$u(t)$:未剪接mRNA的丰度(unspliced)
$s(t)$:已剪接mRNA的丰度(spliced)
$\alpha$:转录速率(transcription rate)
$\beta$:剪接速率(splicing rate)
$\gamma$:降解速率(degradation rate)
稳态假设 :
在稳态(steady state)条件下,$du/dt = 0$,即:
$$\alpha = \beta u_{ss}$$
$$\beta u_{ss} = \gamma s_{ss}$$
由此可得:
$$s_{ss} = \frac{\beta}{\gamma} u_{ss}$$
在稳态时,$s$ 与 $u$ 呈线性关系,斜率为 $\beta/\gamma$。
RNA速率定义 :
RNA速率定义为已剪接mRNA的时间变化率:
$$v_s = \frac{ds}{dt} = \beta u - \gamma s$$
当 $v_s > 0$ 时,基因正在上调(upregulation)
当 $v_s < 0$ 时,基因正在下调(downregulation)
当 $v_s = 0$ 时,基因处于稳态
因此,RNA速率提供了细胞在未来短时间内基因表达变化方向的信息。
向量场表示 :
对于每个细胞,所有基因的RNA速率构成了一个高维速度向量:
$$\mathbf{v}i = (v {s,1}, v_{s,2}, …, v_{s,G})$$
其中 $G$ 是基因数。这个速度向量指示了细胞在基因表达空间中的运动方向。
17.2 稳态模型 vs EM模型 17.2.1 稳态模型(Steady-State Model) 稳态模型是scVelo的默认方法,也是最简单的RNA速率估计方法。
核心假设 :
细胞群体总体上处于稳态(部分细胞正在上调,部分正在下调,总体平衡)
剪接速率 $\beta$ 和降解速率 $\gamma$ 的比值 $\beta/\gamma$ 可以通过线性回归从稳态细胞中估计
估计步骤 :
在 $u$-$s$ 相图中,识别处于稳态的细胞($v_s \approx 0$)
对稳态细胞拟合线性回归:$s = \frac{\beta}{\gamma} u + c$
用拟合的斜率作为 $\gamma_{ss}$(在 $\beta=1$ 的归一化下)
计算每个细胞的速率:$v_s = u - \gamma_{ss} \cdot s$
局限性 :
对非稳态过程(如分化初期)不准确
对强非稳态基因可能给出错误的速率方向
无法估计转录速率 $\alpha$
17.2.2 EM模型(Dynamical Model) EM模型(Expectation-Maximization Dynamical Model)是scVelo的高级方法,不需要稳态假设。
核心思想 :
直接求解剪接ODE的解析解,通过EM算法估计参数 $\alpha, \beta, \gamma$ 和细胞的隐变量(转录状态切换时间)。
ODE解析解 :
假设基因在时间 $\tau$ 从”关”状态切换到”开”状态(或反之):
诱导阶段 ($t > \tau$,基因开启):
$$u(t) = \frac{\alpha}{\beta}(1 - e^{-\beta(t-\tau)}) + u_0 e^{-\beta(t-\tau)}$$
$$s(t) = \frac{\alpha}{\gamma}(1 - e^{-\gamma(t-\tau)}) + \frac{\alpha}{\gamma-\beta}(e^{-\gamma(t-\tau)} - e^{-\beta(t-\tau)}) + s_0 e^{-\gamma(t-\tau)}$$
抑制阶段 ($t > \tau$,基因关闭,$\alpha=0$):
$$u(t) = u_0 e^{-\beta(t-\tau)}$$
$$s(t) = s_0 e^{-\gamma(t-\tau)} + \frac{\beta u_0}{\gamma - \beta}(e^{-\beta(t-\tau)} - e^{-\gamma(t-\tau)})$$
EM算法步骤 :
E步 :给定当前参数 $\theta^{(k)} = (\alpha, \beta, \gamma, \tau)$,计算每个细胞的似然:
$$L_i(\theta) = p(u_i, s_i | \theta, t_i)$$
M步 :最大化总对数似然,更新参数:
$$\theta^{(k+1)} = \arg\max_\theta \sum_i \log L_i(\theta)$$
迭代直到收敛。
优势 :
不需要稳态假设
可以估计所有参数($\alpha, \beta, \gamma$)
可以检测基因的开关时间 $\tau$
对非稳态过程更准确
代价 :
计算成本显著更高(每个基因需要独立优化)
需要更多的细胞来可靠估计参数
可能陷入局部最优
17.3 scVelo 17.3.1 原理 scVelo由Bergen等人于2020年提出,是RNA速率分析的标准化工具。它实现了稳态模型和EM动力学模型,并提供了丰富的可视化和下游分析功能。
与原始velocyto的改进 :
增加了EM动力学模型
更鲁棒的速率估计(基于最近邻的共享速率)
更好的可视化功能
与Scanpy的无缝集成
17.3.2 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 import scvelo as scvimport scanpy as scimport numpy as npimport matplotlib.pyplot as pltadata = scv.read('sample.loom' , cache=True ) 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.velocity(adata, mode='stochastic' ) scv.tl.velocity_graph(adata, n_jobs=8 ) scv.pl.velocity_embedding_stream( adata, basis='umap' , color='cell_type' , title='RNA Velocity (Stochastic Model)' , save='velocity_stochastic.png' ) scv.tl.recover_dynamics(adata, n_jobs=8 ) scv.tl.velocity(adata, mode='dynamical' ) scv.tl.velocity_graph(adata, n_jobs=8 ) scv.pl.velocity_embedding_stream( adata, basis='umap' , color='cell_type' , title='RNA Velocity (Dynamical Model)' , save='velocity_dynamical.png' ) scv.tl.velocity_pseudotime(adata, root_key='root_cell' ) scv.pl.scatter( adata, color='velocity_pseudotime' , basis='umap' , cmap='gnuplot' , save='velocity_pseudotime.png' ) scv.pl.scatter( adata, ['SOX2' , 'PAX6' , 'DCX' ], color='cell_type' , ncols=3 , fontsize=10 , save='phase_portraits.png' ) scv.tl.rank_velocity_genes(adata, groupby='cell_type' , min_corr=0.3 ) velocity_genes = pd.DataFrame(adata.uns['rank_velocity_genes' ]['names' ]) print ("Top velocity genes per cluster:" )print (velocity_genes.head(5 ))if 'fit_alpha' in adata.var.columns: dynamics_genes = adata.var[ adata.var['fit_likelihood' ] > 0.1 ].sort_values('fit_likelihood' , ascending=False ).head(20 ) print ("Top dynamical genes:" ) print (dynamics_genes[['fit_alpha' , 'fit_beta' , 'fit_gamma' , 'fit_likelihood' ]]) scv.tl.velocity_confidence(adata) scv.pl.scatter( adata, color='velocity_confidence' , basis='umap' , cmap='coolwarm' , save='velocity_confidence.png' ) scv.tl.velocity_confidence_transition(adata) print (f"平均速度置信度: {adata.obs['velocity_confidence' ].mean():.4 f} " )print (f"速度长度: {adata.obs['velocity_length' ].mean():.4 f} " )scv.tl.terminal_states(adata) scv.pl.scatter( adata, color=['root_cells' , 'end_points' ], basis='umap' , ncols=2 , save='terminal_states.png' ) scv.tl.velocity_clusters(adata, resolution=0.5 ) scv.pl.scatter( adata, color='velocity_clusters' , basis='umap' , legend_loc='on data' , save='velocity_clusters.png' ) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 6 )) scv.pl.velocity_embedding_stream( adata, basis='umap' , color='cell_type' , ax=axes[0 ], show=False , title='Stochastic Model' , fontsize=12 ) scv.tl.velocity(adata, mode='stochastic' ) scv.tl.velocity_graph(adata) scv.pl.velocity_embedding_stream( adata, basis='umap' , color='cell_type' , ax=axes[1 ], show=False , title='Dynamical Model' , fontsize=12 ) plt.tight_layout() plt.savefig('velocity_model_comparison.png' , dpi=300 , bbox_inches='tight' ) plt.show() adata.write_h5ad('velocity_analysis_results.h5ad' )
17.4 VeloVI 17.4.1 深度生成模型原理 VeloVI由Gayoso等人于2023年提出,将RNA速率估计问题放入变分自编码器(VAE)的框架中,实现了端到端的参数学习和不确定性量化。
模型架构 :
VeloVI的生成模型基于以下假设:
对于基因 $g$ 和细胞 $c$:
$$u_{cg} \sim \text{NB}(u_{cg} | \mu_{ug}, \theta_{ug})$$
$$s_{cg} \sim \text{NB}(s_{cg} | \mu_{sg}, \theta_{sg})$$
其中NB是负二项分布,$\theta$ 是离散参数。
速率参数通过神经网络从细胞状态编码中生成:
$$\alpha_g, \beta_g, \gamma_g, \lambda_g = f_\phi(\mathbf{z}_c)$$
其中 $\mathbf{z}c$ 是细胞的低维编码,$f \phi$ 是参数为 $\phi$ 的神经网络。
变分推断 :
由于真实后验 $p(\mathbf{z} | \mathbf{u}, \mathbf{s})$ 不可解析,使用变分分布 $q_\psi(\mathbf{z} | \mathbf{u}, \mathbf{s})$ 近似:
$$\text{ELBO} = \mathbb{E}{q \psi}[\log p(\mathbf{u}, \mathbf{s} | \mathbf{z})] - \text{KL}(q_\psi(\mathbf{z} | \mathbf{u}, \mathbf{s}) | p(\mathbf{z}))$$
优势 :
不确定性量化:每个参数都有后验分布
批次效应校正:通过条件VAE
更鲁棒的参数估计
17.4.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import scvifrom scvi.module import VeloVAEimport scanpy as scimport numpy as npadata = sc.read_h5ad('sample_with_splicing.h5ad' ) scvi.data.setup_anndata(adata) velovi = scvi.external.VeloVI(adata) velovi.train(max_epochs=400 , use_gpu=True ) velocity_output = velovi.get_velocity() adata.layers['velovi_velocity' ] = velocity_output parameters = velovi.get_rates() adata.var['velovi_alpha' ] = parameters['alpha' ] adata.var['velovi_beta' ] = parameters['beta' ] adata.var['velovi_gamma' ] = parameters['gamma' ] scv.pl.velocity_embedding_stream( adata, basis='umap' , color='cell_type' , title='VeloVI RNA Velocity' , save='velovi_velocity.png' ) param_std = velovi.get_rate_std() print (f"Alpha uncertainty: {param_std['alpha' ].mean():.4 f} " )print (f"Beta uncertainty: {param_std['beta' ].mean():.4 f} " )print (f"Gamma uncertainty: {param_std['gamma' ].mean():.4 f} " )velovi.save('velovi_model/' , overwrite=True ) adata.write_h5ad('velovi_results.h5ad' )
17.5 CellRank 17.5.1 结合速率和转录相似性 CellRank由Lange等人于2022年提出,是一种将RNA速率信息与转录组相似性相结合的命运推断方法。其核心创新在于:不单纯依赖RNA速率(可能噪声较大),而是将速率信息整合到一个马尔可夫链框架中,结合细胞的转录相似性来推断细胞命运。
核心算法 :
相似性核(Similarity Kernel) :基于基因表达相似性构建:
$$K_{\text{sim}}(i, j) = \exp\left(-\frac{d_{\text{pca}}(i, j)^2}{2\sigma^2}\right)$$
速率核(Velocity Kernel) :基于RNA速率方向:
$$K_{\text{vel}}(i, j) \propto \exp\left(\frac{\mathbf{v}_i \cdot (\mathbf{x}_j - \mathbf{x}_i)}{|\mathbf{v}_i| \cdot |\mathbf{x}_j - \mathbf{x}_i|}\right)$$
组合核 :加权组合两个核:
$$K_{\text{combined}} = (1-\alpha) K_{\text{sim}} + \alpha K_{\text{vel}}$$
马尔可夫链 :将组合核归一化为转移矩阵 $T$,然后计算:
宏观态(macrostate) :通过谱聚类识别
命运概率 :吸收概率计算
驱动基因 :沿命运方向的差异表达
17.5.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import cellrank as crimport scanpy as scimport scvelo as scvimport numpy as npadata = sc.read_h5ad('velocity_analysis_results.h5ad' ) vk = cr.kernels.VelocityKernel(adata) vk.compute_transition_matrix() ck = cr.kernels.ConnectivityKernel(adata) ck.compute_transition_matrix() combined_kernel = 0.8 * vk + 0.2 * ck estimator = cr.estimators.GPCCA(combined_kernel) estimator.fit(n_states=[3 , 10 ], cluster_key='cell_type' ) estimator.predict_macrostates(n_states=5 ) estimator.set_terminal_states_from_macrostates( names=['Neurons' , 'Astrocytes' , 'Oligodendrocytes' ] ) estimator.compute_fate_probabilities() estimator.plot_fate_probabilities(basis='umap' , save='cellrank_fate.png' ) driver_genes = estimator.compute_lineage_drivers() print ("Top driver genes for Neuron fate:" )print (driver_genes['Neurons' ].head(20 ))estimator.plot_fate_probabilities( basis='umap' , mode='heatmap' , save='cellrank_fate_heatmap.png' ) adata.write_h5ad('cellrank_results.h5ad' )
17.6 动态模型选择策略
选择流程 :
先运行稳态模型,检查速度流是否合理
如果速度流方向不一致或出现反向流,尝试EM动力学模型
如果存在批次效应,使用VeloVI
无论选择哪种模型,最终都推荐用CellRank进行命运推断
17.7 RNA速率完整代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import scvelo as scvimport cellrank as crimport scanpy as scimport numpy as npimport matplotlib.pyplot as pltadata = scv.read('sample.loom' , cache=True ) 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.velocity(adata, mode='stochastic' ) scv.tl.velocity_graph(adata) scv.pl.velocity_embedding_stream(adata, basis='umap' , color='cell_type' , title='Stochastic Velocity' ) scv.tl.recover_dynamics(adata) scv.tl.velocity(adata, mode='dynamical' ) scv.tl.velocity_graph(adata) scv.pl.velocity_embedding_stream(adata, basis='umap' , color='cell_type' , title='Dynamical Velocity' ) vk = cr.kernels.VelocityKernel(adata) vk.compute_transition_matrix() ck = cr.kernels.ConnectivityKernel(adata) ck.compute_transition_matrix() kernel = 0.8 * vk + 0.2 * ck estimator = cr.estimators.GPCCA(kernel) estimator.fit(n_states=[3 , 10 ]) estimator.predict_macrostates(n_states=5 ) estimator.set_terminal_states_from_macrostates() estimator.compute_fate_probabilities() drivers = estimator.compute_lineage_drivers() estimator.plot_fate_probabilities(basis='umap' ) estimator.plot_lineage_drivers('Neurons' , n_genes=10 , basis='umap' ) adata.write_h5ad('rna_velocity_complete.h5ad' )
17.8 注意事项
Unspliced/Spliced比对质量 :RNA速率分析的前提是准确区分unspliced和spliced reads。低质量的比对会导致速率估计错误。推荐使用STARsolo进行比对。
基因子集选择 :scVelo默认只使用高变基因进行速率估计。对于特定生物学过程,可能需要手动添加相关基因。
EM模型计算成本 :EM动力学模型对每个基因独立优化,计算时间是稳态模型的10-100倍。建议先用稳态模型筛选基因,再对关键基因运行EM模型。
速度流方向一致性 :如果速度流出现大面积反向或不一致的方向,可能需要:检查比对质量、调整预处理参数、或考虑使用VeloVI。
低捕获率协议 :Drop-seq等低捕获率协议可能没有足够的unspliced reads来可靠估计速率。
速率≠命运 :RNA速率反映的是短期(数小时)的转录变化趋势,而不是长期命运决定。长期命运推断需要结合CellRank等工具。
伪时间与真实时间的差异 :速率伪时间与DPT等拟时序方法计算的伪时间可能不同,因为前者基于速率信息,后者基于距离信息。
负速率的生物学解释 :负速率($v_s < 0$)表示基因正在下调,这是正常的生物学现象,不需要特殊处理。
速度图的稀疏性 :速度图(velocity graph)是稀疏的,只保留与每个细胞速度方向一致的邻居。过度剪枝可能导致信息丢失。
不要仅依赖速度流 :速度流可视化是定性工具,需要结合定量分析(如驱动基因、命运概率)进行解读。
关键参考文献 :
La Manno et al. (2018) Nature - RNA Velocity
Bergen et al. (2020) Nature Biotechnology - scVelo
Gayoso et al. (2023) Nature Methods - VeloVI
Lange et al. (2022) Nature Methods - CellRank
第十八章:谱系追踪
基于Ch16,详细展开
18.1 实验方法:条形码编辑与CRISPR记录 谱系追踪(Lineage Tracing)是通过标记细胞并追踪其后代的实验方法,目的是重建细胞分裂和分化的历史关系。在单细胞水平上,谱系追踪可以回答”这个细胞从哪里来”这一根本问题。
18.1.1 条形码编辑(Barcode Editing) 原理 :在细胞基因组的安全港位点(如Rosa26)引入可诱导的DNA条形码序列。当诱导剂激活Cre重组酶时,条形码区域发生随机重组,产生独特的DNA序列标签。子代细胞继承这个标签,从而可以追踪谱系关系。
代表性技术 :
Polylox (Pei et al., 2017):利用Cre-loxP系统的随机重组产生高多样性条形码(理论上 >10^5 种变体)
DNA条形码病毒文库 :使用慢病毒感染引入随机DNA条形码,每个细胞获得独特的整合标签
质粒条形码(CellTagging) :通过转染将随机寡核苷酸条形码整合到基因组中
条形码编辑的数学模型 :
设初始条形码长度为 $L$,每次重组随机删除一个片段,则可能的条形码变体数为:
$$N_{\text{variants}} = \sum_{k=0}^{L} \binom{L}{k} = 2^L$$
实际中,由于重组不是完全随机的,变体数通常少于理论值。
18.1.2 CRISPR记录(CRISPR Recording) 原理 :利用CRISPR-Cas9系统在特定基因组位点持续引入突变(indels),这些突变随细胞分裂积累,形成”分子时钟”。通过读取每个细胞的突变模式,可以推断细胞间的谱系关系。
代表性技术 :
GESTALT (McKenna et al., 2016):在斑马鱼中利用多个CRISPR靶位点的累积突变记录发育谱系
LINNAEUS (Spanjaard et al., 2018):利用Cas9诱导的indels在斑马鱼中进行谱系追踪
CAR-Seq :利用CRISPR激活记录系统
mSCRIBE :哺乳动物合成CRISPR-compatible条形码记录
CRISPR记录的数学模型 :
设每个靶位点有 $m$ 种可能的突变状态,共有 $n$ 个靶位点,则可能的条形码组合数为:
$$N_{\text{barcodes}} = m^n$$
例如,$m=10$(9种常见indel + 野生型),$n=10$ 个靶位点,则 $N_{\text{barcodes}} = 10^{10}$。
突变积累模型 :
每个靶位点在每个时间步以概率 $p$ 发生突变:
$$P(\text{无突变}) = (1-p)^t$$
$$P(\text{第} k \text{次突变}) = \binom{t}{k} p^k (1-p)^{t-k}$$
其中 $t$ 是时间步数(细胞分裂次数)。
18.2 Cassiopeia工具 18.2.1 hybrid solver原理 Cassiopeia由Jones等人于2020年提出,是一个专门用于从CRISPR记录数据重建细胞谱系树的计算工具。它提供了多种树重建算法,其中最先进的是hybrid solver。
谱系树重建问题 :
给定 $n$ 个细胞的条形码(突变模式),重建描述它们谱系关系的树 $T$。这是一个经典的系统发育问题,但有以下特殊性:
不可逆突变 :CRISPR记录的突变通常是不可逆的(Dollo约束),即一个位点一旦突变不会恢复野生型
缺失数据 :部分靶位点可能因测序覆盖度不足而缺失
趋同突变 :不同谱系的相同位点可能独立发生相同的突变(同塑性,homoplasy)
速率异质性 :不同位点的突变速率可能不同
Cassiopeia的算法层次 :
Greedy Solver :贪心算法,每步选择最优的分裂
时间复杂度:$O(n^2 \cdot m)$
适用于快速初步分析
ILP Solver(整数线性规划) :
将树重建建模为加权最小翻转(Weighted Minimum Flips)问题:
$$\min_{T} \sum_{e \in E(T)} w_e \cdot d_e$$
其中 $d_e$ 是边 $e$ 上的突变数,$w_e$ 是权重。使用Gurobi或CPLEX求解。
时间复杂度:NP-hard,但实际运行时间可接受
适用于中等规模数据
Hybrid Solver :结合贪心和ILP的优势
先用贪心算法构建初始树
然后对子树使用ILP进行局部优化
最后使用卡尔顿邻域搜索(Cartesian neighborhood search)进行全局优化
hybrid solver的核心步骤 :
a. 初始树构建 :使用贪心算法
b. 子树划分 :将树划分为大小 $\leq k$ 的子树
c. 局部ILP优化 :对每个子树独立运行ILP求解器
d. 全局协调 :合并局部最优解,进行全局调整
e. 迭代 :重复b-d直到收敛
18.2.2 数据分析流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import cassiopeia as casimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltcharacter_matrix = pd.read_csv('cas9_barcodes.csv' , index_col=0 ) tree = cas.data.CassiopeiaTree( character_matrix=character_matrix, missing_state_indicator=-1 ) priors = cas.data.compute_priors(character_matrix, missing_state_indicator=-1 ) tree.set_prior_probabilities(priors) hybrid_solver = cas.solver.HybridSolver( greedy_solver=cas.solver.GreedySolver(), ilp_solver=cas.solver.ILPSolver( time_limit=3600 , maximum_iterations=100 ), subtree_size=50 , n_iterations=5 ) hybrid_solver.solve(tree) fig, ax = plt.subplots(figsize=(15 , 10 )) cas.pl.plot_matplotlib(tree, ax=ax, orientation='vertical' , leaf_labels=True , fontsize=6 ) ax.set_title('Reconstructed Lineage Tree (Hybrid Solver)' ) plt.savefig('cassiopeia_tree.png' , dpi=300 , bbox_inches='tight' ) plt.show() log_likelihood = cas.tree.evaluate_log_likelihood(tree) print (f"树对数似然: {log_likelihood:.2 f} " )consistency = cas.tree.evaluate_consistency(tree) print (f"突变一致性: {consistency:.4 f} " )resolution = cas.tree.evaluate_resolution(tree) print (f"树分辨率: {resolution:.4 f} " )adata = sc.read_h5ad('paired_scrna.h5ad' ) depths = {} for node in tree.leaves: cell_name = node.name depths[cell_name] = tree.get_attribute(node, 'depth' ) common_cells = set (depths.keys()) & set (adata.obs_names) lineage_depth = [depths[c] for c in common_cells] pseudotime = [adata.obs.loc[c, 'dpt_pseudotime' ] for c in common_cells] from scipy.stats import spearmanrcorr, pval = spearmanr(lineage_depth, pseudotime) print (f"谱系深度与DPT拟时序相关性: rho={corr:.3 f} , p={pval:.2 e} " )branch1_cells = [leaf.name for leaf in tree.leaves if tree.get_attribute(leaf, 'branch' ) == 'A' ] branch2_cells = [leaf.name for leaf in tree.leaves if tree.get_attribute(leaf, 'branch' ) == 'B' ] adata.obs['lineage_branch' ] = 'other' adata.obs.loc[adata.obs_names.isin(branch1_cells), 'lineage_branch' ] = 'branch_A' adata.obs.loc[adata.obs_names.isin(branch2_cells), 'lineage_branch' ] = 'branch_B' sc.tl.rank_genes_groups(adata, groupby='lineage_branch' , method='wilcoxon' ) sc.pl.rank_genes_groups(adata, n_genes=15 , sharey=False ) tree.write_newick('lineage_tree.nwk' ) adata.write_h5ad('lineage_integrated.h5ad' )
18.3 注意事项
条形码碰撞 :当细胞数远大于条形码多样性时,不同细胞可能偶然获得相同条形码(碰撞),导致谱系错误。需要确保条形码多样性远大于细胞数。
缺失数据比例 :CRISPR记录数据中缺失位点比例通常较高(>20%)。Cassiopeia可以处理缺失数据,但缺失过多会降低分辨率。
趋同突变 :相同的indel可能在不同谱系中独立出现。Cassiopeia的hybrid solver通过考虑突变先验来减少趋同突变的影响。
实验设计 :Cas9的切割效率和靶位点数需要在实验设计阶段仔细优化。推荐至少10个高效率靶位点。
计算成本 :hybrid solver在大数据集(>10K细胞)上可能需要数小时。可以先使用greedy solver快速获得初步结果。
谱系树的根 :需要正确选择谱系树的根(通常是未突变的祖先状态)。
与转录组的对齐 :确保条形码细胞ID与scRNA-seq细胞ID正确匹配,这是整合分析的前提。
关键参考文献 :
McKenna et al. (2016) Science - GESTALT
Spanjaard et al. (2018) Nature Biotechnology - LINNAEUS
Jones et al. (2020) Nature - Cassiopeia
Pei et al. (2017) Nature - Polylox
第六篇:机制建模
第十九章:基因调控网络推断
基于Ch21,详细展开
19.1 GRN概念 基因调控网络(Gene Regulatory Network, GRN)描述了转录因子(Transcription Factor, TF)与其靶基因之间的调控关系。在GRN中,节点代表基因(或TF),边代表调控关系(激活或抑制)。
GRN的数学表示 :
一个GRN可以表示为一个加权有向图 $G = (V, E, W)$,其中:
$V$:基因集合,$V = {g_1, g_2, …, g_N}$
$E$:调控边集合,$e_{ij} \in E$ 表示基因 $j$ 调控基因 $i$
$W$:边权重,$w_{ij}$ 表示调控强度
调控模型 :
基因 $i$ 的表达变化由其调控因子决定:
$$\frac{dx_i}{dt} = f_i(\mathbf{x}) - \gamma_i x_i$$
其中 $f_i$ 是调控函数,$\mathbf{x}$ 是所有基因的表达向量,$\gamma_i$ 是降解速率。
最简单的线性模型:
$$f_i(\mathbf{x}) = \sum_j w_{ij} x_j + b_i$$
更实际的逻辑模型:
$$f_i(\mathbf{x}) = \sum_{S \in \text{combinations}} \alpha_S \prod_{j \in S} \mathbb{1}[x_j > \theta_j]$$
GRN推断的挑战 :
高维性 :基因数量远大于细胞数量($p \gg n$ 问题)
因果vs相关 :基因表达相关性不等价于调控关系
间接效应 :观察到的是直接和间接调控的混合
非线性 :调控关系往往是非线性的(如逻辑门)
时延 :转录因子到靶基因的调控存在时间延迟
19.2 pySCENIC三步流程 pySCENIC(Python Single-Cell regulatory Network Inference and Computation)是当前最流行的单细胞GRN推断工具,由Aibar等人于2017年提出。它采用三步流程:GRNBoost2 → cisTarget → AUCell。
19.2.1 GRNBoost2:共表达网络推断 原理 :
GRNBoost2是SCENIC中第一步,利用梯度提升树(Gradient Boosting Machine, GBM)从单细胞表达数据中推断TF-靶基因的共表达模块。
算法 :
对于每个TF $t$,将其余所有基因作为候选靶基因,训练一个回归模型:
$$\hat{x}j = \sum {k=1}^{K} h_k(x_t)$$
其中 $h_k$ 是第 $k$ 棵回归树,$K$ 是总树数。
特征重要性(feature importance)衡量了TF $t$ 对预测基因 $j$ 表达的贡献:
$$I_{t \rightarrow j} = \frac{1}{K} \sum_{k=1}^{K} \text{Gain}_{k}(t \rightarrow j)$$
只保留重要性高于阈值的TF-靶基因对,形成共表达模块(regulon)。
与GENIE3的关系 :GRNBoost2是GENIE3(使用随机森林)的高效实现,使用XGBoost代替随机森林,速度提升10-50倍。
19.2.2 cisTarget:motif富集分析 原理 :
GRNBoost2的输出是TF-靶基因的共表达关系,但共表达不代表直接调控(可能是间接效应)。cisTarget通过检测靶基因启动子区域是否富集TF的结合motif来验证直接调控关系。
算法步骤 :
启动子提取 :对每个候选靶基因,提取其转录起始位点(TSS)上游一定范围内(默认10kb)的DNA序列
Motif扫描 :使用位置权重矩阵(PWM)数据库对启动子进行motif扫描
富集检验 :对每个regulon,检验其靶基因的启动子中TF motif的富集程度
使用归一化富集分数(Normalized Enrichment Score, NES):
$$NES = \frac{ES - \mu_{ES}}{\sigma_{ES}}$$
其中 $ES$ 是观察的富集分数,$\mu_{ES}$ 和 $\sigma_{ES}$ 是从背景分布估计的均值和标准差。
过滤 :只保留NES > 阈值(默认3.0)的regulon
数据库 :
cisTarget使用预计算的motif-基因排名数据库,包括:
hg38__500bp_up_100bp_down_full_tx_v3 :人类,TSS上下游
hg38__10kb_up_10kb_down_full_tx_v3 :人类,扩大范围
以及小鼠、果蝇等物种的对应数据库
19.2.3 AUCell:regulon活性评分 原理 :
AUCell(Area Under the Curve)为每个细胞计算每个regulon的活性评分。核心思想是:如果一个regulon在某个细胞中活跃,那么其靶基因在该细胞中的表达排名应该整体偏高。
算法 :
对每个细胞,将所有基因按表达量降序排列
对某个regulon的靶基因集合 $S$,在排序的基因列表中标记靶基因的位置
计算”恢复曲线”(recovery curve):沿排序列表逐个检查,每遇到一个靶基因,曲线上升一步
AUC值(曲线下面积)即为该regulon在该细胞中的活性评分
$$\text{AUC}{c,r} = \int_0^1 R {c,r}(x) dx$$
其中 $R_{c,r}(x)$ 是细胞 $c$ 中regulon $r$ 的恢复曲线。
AUC的解释 :
高AUC值:该regulon在该细胞中活跃
低AUC值:该regulon在该细胞中不活跃
通过AUC值的分布可以自动确定”活跃”阈值
19.3 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import scanpy as scimport pandas as pdimport numpy as npimport osfrom arboreto.algo import grnboost2from ctx import rcafrom pyscenic.aucell import aucellfrom pyscenic.utils import modules_from_adjacenciesfrom pyscenic.prune import prune2dffrom pyscenic.rss import regulon_specificity_scoresDATABASES = ['./cistarget_databases/hg38__screen__mc9nr__feather.v2.feather' ] MOTIF_ANNOTATIONS = './motifs-v9-nr.hgnc-m0.001-o0.0.tbl' adata = sc.read_h5ad('annotated_data.h5ad' ) expr_matrix = pd.DataFrame( adata.raw.X.toarray() if hasattr (adata.raw.X, 'toarray' ) else adata.raw.X, index=adata.obs_names, columns=adata.raw.var_names ) tf_list = pd.read_csv('https://resources.aertslab.org/cistarget/tf_lists/TF_lists/hs_hgnc_tfs.txt' , header=None )[0 ].tolist() tf_list = [tf for tf in tf_list if tf in expr_matrix.columns] print (f"检测到 {len (tf_list)} 个转录因子" )print ("运行GRNBoost2(可能需要数小时)..." )adjacencies = grnboost2( expression_data=expr_matrix, tf_names=tf_list, verbose=True , seed=42 ) adjacencies.to_csv('grnboost2_adjacencies.tsv' , sep='\t' , index=False ) print (f"GRNBoost2完成,发现 {len (adjacencies)} 条TF-靶基因对" )print ("运行cisTarget motif富集分析..." )modules = modules_from_adjacencies(adjacencies, expr_matrix) motifs = prune2df( modules, DATABASES, MOTIF_ANNOTATIONS, rank_threshold=5000 , auc_threshold=0.05 , nes_threshold=3.0 , n_cpu=8 ) motifs.to_csv('cistarget_motifs.tsv' , sep='\t' ) n_regulons = len (set (motifs.index.get_level_values('Target' ))) print (f"通过motif验证的regulon数: {n_regulons} " )print ("运行AUCell regulon活性评分..." )from pyscenic.utils import load_motifsmotifs_df = pd.read_csv('cistarget_motifs.tsv' , sep='\t' , index_col=[0 , 1 ]) from pyscenic.prune import create_regulonsregulons = create_regulons(motifs_df) auc_mtx = aucell(expr_matrix, regulons, n_cpu=8 , seed=42 ) auc_mtx.to_csv('aucell_scores.tsv' , sep='\t' ) auc_adata = sc.AnnData(auc_mtx) auc_adata.obs = adata.obs.copy() sc.tl.pca(auc_adata) sc.pp.neighbors(auc_adata, n_neighbors=15 ) sc.tl.umap(auc_adata) sc.pl.umap(auc_adata, color=auc_mtx.columns[:10 ].tolist(), ncols=5 , cmap='Reds' , save='regulon_activity.png' ) rss = regulon_specificity_scores(auc_mtx, adata.obs['cell_type' ]) print ("Regulon特异性评分(Top regulon per cell type):" )for ct in rss.columns: top_regulon = rss[ct].idxmax() print (f" {ct} : {top_regulon} (RSS={rss[ct].max ():.3 f} )" ) import seaborn as snsmean_auc = auc_mtx.groupby(adata.obs['cell_type' ]).mean() top_regulons = mean_auc.var().nlargest(30 ).index plt.figure(figsize=(14 , 8 )) sns.heatmap(mean_auc[top_regulons].T, cmap='Reds' , annot=False , xticklabels=True , yticklabels=True ) plt.title('Regulon Activity Heatmap (Top 30 Variable Regulons)' ) plt.xlabel('Cell Type' ) plt.ylabel('Regulon' ) plt.tight_layout() plt.savefig('regulon_heatmap.png' , dpi=300 , bbox_inches='tight' ) plt.show()
19.4 结果解读
Regulon活性热图 :查看哪些regulon在哪些细胞类型中特异性活跃,这是最直观的结果展示
RSS(Regulon Specificity Score) :每个细胞类型的标志性regulon。高RSS值表示该regulon对该细胞类型高度特异
靶基因列表 :每个regulon包含的靶基因列表可以用于功能富集分析(GO、KEGG),了解regulon的生物学功能
与已知的调控关系对比 :将推断的regulon与公共数据库(如TRRUST、ENCODE、ChIP-Atlas)中的已知调控关系对比,验证推断的可靠性
Regulon二值化 :AUCell可以自动确定活跃/不活跃的阈值,将连续的AUC值转换为二值分类,简化解读
19.5 注意事项
计算资源需求 :GRNBoost2是计算最密集的步骤,对于>50K细胞的数据集可能需要数小时到数天。建议使用多核CPU或分布式计算。
TF列表质量 :TF列表的完整性直接影响结果。建议使用最新的TF数据库(如AnimalTFDB)。
cisTarget数据库选择 :确保选择正确物种和基因组版本的数据库。使用错误的数据库会导致motif富集失败。
NES阈值 :NES=3.0是推荐阈值,但可以适当放宽(如2.5)以增加灵敏度,或收紧(如4.0)以增加特异度。
间接调控的过滤 :cisTarget只能过滤非直接调控关系,但不能保证所有通过的regulon都是直接调控。需要结合ChIP-seq数据验证。
数据质量 :pySCENIC对dropout敏感。建议使用imputed表达矩阵(如MAGIC处理后)作为辅助参考。
批次效应 :如果数据来自多个批次,建议先进行批次校正,再运行GRN推断。
Regulon大小 :过大的regulon(>500靶基因)可能包含大量间接靶基因,过小的regulon(<10靶基因)可能不可靠。
关键参考文献 :
Aibar et al. (2017) Nature Methods - SCENIC
Van de Sande et al. (2020) Nature Protocols - pySCENIC
Huynh-Thu et al. (2010) PLoS ONE - GENIE3
第二十章:细胞-细胞通讯分析
基于Ch22,详细展开
20.1 CCC概念与原理 细胞-细胞通讯(Cell-Cell Communication, CCC)分析旨在从单细胞转录组数据中推断不同细胞类型之间的信号传递关系。其核心假设是:如果配体(ligand)在细胞类型A中高表达,而其受体(receptor)在细胞类型B中高表达,则A可能通过该配体-受体对向B发送信号。
数学框架 :
对于配体-受体对 $(L, R)$,从细胞类型 $i$ 到细胞类型 $j$ 的通讯得分可以定义为:
$$\text{Score}_{i \rightarrow j}^{(L,R)} = f(\bar{x}_L^{(i)}, \bar{x}_R^{(j)})$$
其中 $\bar{x}_L^{(i)}$ 是配体 $L$ 在细胞类型 $i$ 中的平均表达,$\bar{x}_R^{(j)}$ 是受体 $R$ 在细胞类型 $j$ 中的平均表达。$f$ 是具体的得分函数,不同方法有不同的定义。
20.2 CellPhoneDB 20.2.1 原理 CellPhoneDB由Efremova等人于2020年提出,是最早的CCC分析方法之一。其核心创新在于:
精选配体-受体数据库 :手动整理的、包含多亚基复合物的配体-受体数据库
统计显著性检验 :通过细胞标签随机置换检验评估通讯的统计显著性
算法步骤 :
对每对细胞类型 $(i, j)$ 和每个配体-受体对 $(L, R)$:
计算平均表达:$\bar{x}_L^{(i)}$ 和 $\bar{x}_R^{(j)}$
计算几何平均得分:$S = \sqrt{\bar{x}_L^{(i)} \cdot \bar{x}_R^{(j)}}$
对于多亚基复合物,取各亚基的算术平均
置换检验:
随机打乱细胞类型标签 $\pi$ 次(默认1000次)
每次重新计算得分 $S^{(k)}$
p值 = $\frac{1}{\pi} \sum_{k=1}^{\pi} \mathbb{1}[S^{(k)} \geq S_{obs}]$
多重检验校正:Benjamini-Hochberg方法
20.2.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import scanpy as scimport pandas as pdimport numpy as npadata = sc.read_h5ad('annotated_data.h5ad' ) counts_df = pd.DataFrame( adata.raw.X.toarray() if hasattr (adata.raw.X, 'toarray' ) else adata.raw.X, index=adata.obs_names, columns=adata.raw.var_names ).T counts_df.to_csv('cellphonedb_counts.txt' , sep='\t' ) meta_df = pd.DataFrame({ 'Cell' : adata.obs_names, 'cell_type' : adata.obs['cell_type' ] }) meta_df.to_csv('cellphonedb_meta.txt' , sep='\t' , index=False ) from cellphonedb.src.core.methods import cpdb_statistical_analysis_methodresults = cpdb_statistical_analysis_method.call( cpdb_file_path='cellphonedb_meta.txt' , cpdb_counts_file_path='cellphonedb_counts.txt' , counts_data='gene_name' , iterations=1000 , threshold=0.1 , threads=8 , debug_seed=-1 ) significant_interactions = results['significant_means' ] print (f"显著配体-受体对数: {len (significant_interactions)} " )pvalues = results['pvalues' ] interaction_counts = pd.crosstab( significant_interactions['source' ], significant_interactions['target' ] ) import seaborn as snsplt.figure(figsize=(10 , 8 )) sns.heatmap(interaction_counts, cmap='Blues' , annot=True , fmt='d' ) plt.title('CellPhoneDB: Number of Significant Interactions' ) plt.savefig('cellphonedb_heatmap.png' , dpi=300 , bbox_inches='tight' ) plt.show() specific_pair = significant_interactions[ (significant_interactions['source' ] == 'Macrophages' ) & (significant_interactions['target' ] == 'T_cells' ) ] print ("Macrophages → T_cells 显著相互作用:" )print (specific_pair[['interacting_pair' , 'mean_p_value' , 'mean' ]])
20.3 CellChat 20.3.1 原理 CellChat由Jin等人于2021年提出,在CellPhoneDB的基础上做了多项改进:
更大的数据库 :包含2,021个验证的配体-受体对,涵盖分泌信号、ECM-受体、细胞-细胞接触等
通讯概率模型 :使用质量作用定律建模通讯概率,而非简单几何平均
信号流分析 :可以分析信号网络的输入和输出模式
比较分析 :支持多个条件间的通讯差异分析
通讯概率模型 :
$$P_{i \rightarrow j}^{(L,R)} = \frac{L_i \cdot R_j}{K_d + L_i \cdot R_j}$$
其中 $L_i$ 和 $R_j$ 分别是配体在源细胞和受体在靶细胞中的表达水平,$K_d$ 是解离常数。
对于多亚基复合物:
$$P_{i \rightarrow j}^{(L_1 L_2, R_1 R_2)} = \min(P_{i \rightarrow j}^{(L_1)}, P_{i \rightarrow j}^{(L_2)}) \times \min(P_{i \rightarrow j}^{(R_1)}, P_{i \rightarrow j}^{(R_2)})$$
20.3.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 library(CellChat) library(Seurat) data <- readRDS("annotated_seurat.rds" ) data.input <- data@assays$RNA@data labels <- Idents(data) meta <- data@meta.data cellchat <- createCellChat(object = data.input , meta = meta, group.by = "cell_type" ) cellchat <- setLigandReceptorDB(cellchat, species = "human" ) cellchat <- preprocessData(cellchat, min .pct = 0.1 ) cellchat <- identifyOverExpressedGenes(cellchat) cellchat <- identifyOverExpressedInteractions(cellchat) cellchat <- computeCommunProb(cellchat, type = "triMean" ) cellchat <- filterCommunication(cellchat, min .cells = 10 ) cellchat <- computeCommunProbPathway(cellchat) cellchat <- aggregateNet(cellchat) pathways.show <- c("CXCL" ) netVisual_aggregate(cellchat, signaling = pathways.show, layout = "circle" , vertex.label.cex = 0.8 ) netVisual_heatmap(cellchat, signaling = pathways.show) netVisual_bubble(cellchat, sources.use = c(1 , 2 ), targets.use = c(5 , 6 ), remove.pvalues = FALSE) netAnalysis_signalingRole_network(cellchat, width = 12 , height = 5 ) cellchat <- netAnalysis_signalingRole_scatter(cellchat) cellchat <- netAnalysis_signalingRole_heatmap(cellchat) saveRDS(cellchat, file = "cellchat_results.rds" )
20.4 LIANA 20.4.1 统一框架原理 LIANA(LIgand-receptor ANalysis frAmework)由Dimitrov等人于2022年提出,是一个元分析框架,整合了多种CCC方法的输出,提供共识性的通讯推断。
整合的方法 :
LIANA整合了以下方法的评分:
CellPhoneDB :几何平均 + 置换检验
CellChat :通讯概率模型
Connectome :标准化z-score
logfc_mean :log fold change均值
NATMI :表达乘积归一化
共识策略 :
对于每个配体-受体对,LIANA计算:
特异性得分(Specificity) :该相互作用在特定细胞对中的特异性
强度得分(Magnitude) :该相互作用的信号强度
$$\text{Consensus}{\text{rank}} = \frac{1}{M} \sum {m=1}^{M} \text{rank}_m(\text{interaction})$$
其中 $M$ 是方法数,$\text{rank}_m$ 是方法 $m$ 的排名。
20.4.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import liana as liimport scanpy as scimport matplotlib.pyplot as pltadata = sc.read_h5ad('annotated_data.h5ad' ) li.mt.rank_aggregate.by_sample( adata, groupby='cell_type' , resource_name='consensus' , expr_prop=0.1 , verbose=True ) interactions = adata.uns['liana_res' ] print (f"检测到的相互作用数: {len (interactions)} " )print (interactions.head(10 ))li.pl.dotplot_by_sample( adata, uns_key='liana_res' , source_key='source' , target_key='target' , ligand_key='ligand.complex' , receptor_key='receptor.complex' , score_key='magnitude_rank' , inverse_score=True , figure_size=(12 , 8 ) ) specific_lr = interactions[ interactions['ligand.complex' ].str .contains('CXCL' ) | interactions['receptor.complex' ].str .contains('CXCR' ) ] print ("CXCL/CXCR相关相互作用:" )print (specific_lr)li.pl.compare_interactions( adata, uns_key='liana_res' , score_keys=['magnitude_rank' , 'specificity_rank' ], figure_size=(8 , 6 ) )
20.5 NicheNet:下游响应建模 20.5.1 原理 NicheNet由Browaeys等人于2020年提出,与上述方法不同,NicheNet不仅推断配体-受体相互作用,还预测配体引起的下游靶基因表达变化。
核心创新 :
信号传播建模 :从配体→受体→信号通路→靶基因的完整传播链
先验网络 :利用公共数据库构建配体-靶基因的先验调控网络
靶基因预测 :给定一个”感兴趣的靶基因集合”,NicheNet预测哪些配体最有可能调控这些基因
数学模型 :
配体 $l$ 对靶基因集合 $S$ 的调控得分:
$$\text{Score}(l, S) = \sum_{t \in S} w_{l,t}$$
其中 $w_{l,t}$ 是配体 $l$ 到靶基因 $t$ 的先验网络权重,通过以下方式计算:
$$w_{l,t} = \sum_{p \in \text{paths}(l \rightarrow t)} \prod_{e \in p} w_e$$
20.5.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 library( nichenetr) ligand_target_matrix <- readRDS( url( "https://zenodo.org/record/3260758/files/ligand_target_matrix.rds" ) ) lr_network <- readRDS( url( "https://zenodo.org/record/3260758/files/lr_network.rds" ) ) weighted_networks <- readRDS( url( "https://zenodo.org/record/3260758/files/weighted_networks.rds" ) ) geneset_oi <- c ( 'IFNG' , 'TNF' , 'IL2' , 'GZMB' , 'PRF1' , 'CXCR3' ) ligands_oi <- c ( 'IL1B' , 'TNF' , 'IL6' , 'CXCL9' , 'CXCL10' , 'CCL5' , 'TGFB1' , 'B2M' , 'CD80' , 'CD86' ) ligand_activities <- predict_ligand_activities( geneset = geneset_oi, background_expressed_genes = background_genes, ligand_target_matrix = ligand_target_matrix, potential_ligands = ligands_oi ) best_upstream_ligands <- ligand_activities %>% arrange( - pearson) %>% pull( test_ligand) p <- visualize_ligand_target_regulatory_heatmap( ligands = best_upstream_ligands[ 1 : 5 ] , targets = geneset_oi, ligand_target_matrix = ligand_target_matrix ) print( p) lr_heatmap <- visualize_ligand_receptor_links( ligands = best_upstream_ligands[ 1 : 5 ] , receptors = receptors_oi, lr_network = lr_network, weighted_networks = weighted_networks ) print( lr_heatmap)
20.6 多方法共识策略 由于每种CCC方法基于不同的假设和评分策略,它们的输出往往不一致。推荐使用以下共识策略:
至少2种方法一致 :只保留至少被2种独立方法检测到的相互作用
LIANA整合排名 :使用LIANA的排名聚合方法
实验验证优先级 :对共识性高的相互作用优先进行实验验证
功能富集一致性 :检测到的通路在不同方法间是否一致
20.7 12条完整注意事项
配体-受体数据库质量 :CCC分析的基础是配体-受体数据库。不同方法使用不同的数据库,数据库的完整性直接影响结果。CellPhoneDB的数据库经过人工审校,是质量最高的选择之一。
表达阈值 :大多数方法使用最低表达比例阈值(如0.1)过滤低表达基因。阈值过高会遗漏真实但低表达的相互作用,过低会增加假阳性。
细胞类型粒度 :CCC分析的细胞类型粒度影响结果。过粗的粒度会掩盖特定细胞亚型间的通讯,过细的粒度会增加统计噪声。
空间信息缺失 :scRNA-seq不保留空间信息,CCC分析假设所有细胞类型都可能相互通讯。实际中,只有空间上相邻的细胞才可能直接通讯。结合空间转录组数据可以提高可靠性。
配体-受体不等于功能通讯 :配体和受体的共表达不等于它们之间存在功能性的信号传递。需要结合功能实验(如中和抗体、基因敲除)验证。
批量效应 :不同样本/批次的细胞类型比例差异可能影响CCC推断。建议在同一批次内进行通讯分析。
单细胞vs批量 :CCC分析在单细胞水平或细胞类型水平进行。细胞类型水平的分析更稳定,但可能遗漏细胞内的异质性。
逆向信号 :某些表面蛋白既是配体也是受体(如EFN/EPH),可能导致双向信号。CellChat对此有特殊处理。
旁分泌vs自分泌 :同一细胞类型内也可能存在自分泌信号(autocrine),不应被忽略。
时间动态 :CCC是动态过程。静态的scRNA-seq快照只能提供某一时刻的通讯状态。如果可能,结合时间序列数据。
NicheNet的先验偏差 :NicheNet依赖先验调控网络,研究充分的通路更容易被预测。对研究不足的通路可能存在偏差。
多重检验校正 :大量配体-受体对的统计检验需要多重检验校正。CellPhoneDB的置换检验已经内建了这一步骤。
关键参考文献 :
Efremova et al. (2020) Nature Protocols - CellPhoneDB
Jin et al. (2021) Nature Communications - CellChat
Dimitrov et al. (2022) Nature Communications - LIANA
Browaeys et al. (2020) Nature Methods - NicheNet
第七篇:多模态与空间组学
第二十一章:scATAC-seq染色质可及性分析
基于Ch24-26,详细展开
21.1 scATAC-seq原理:Tn5转座酶 scATAC-seq(single-cell Assay for Transposase-Accessible Chromatin using sequencing)是一种在单细胞水平上检测开放染色质区域的技术。
核心原理 :
Tn5转座酶是scATAC-seq的关键试剂。Tn5可以特异性地切割DNA双链并插入测序接头,但它只能切割”开放”的染色质区域(即没有核小体占据的DNA区域)。
实验流程 :
细胞分离 :通过微流控(10x Genomics)或组合条形码(sci-ATAC-seq)分离单细胞
转座反应 :Tn5转座酶切割开放染色质并插入接头序列
PCR扩增 :使用接头序列进行PCR扩增
测序 :对扩增片段进行双端测序
数据特征 :
每个细胞检测到的fragments数通常为1,000-25,000(远少于scRNA-seq的reads数)
数据极其稀疏(每个细胞中>95%的位点未检测到信号)
数据是二值化的(某个位点被Tn5切割或未被切割)
与scRNA-seq的关键区别 :
21.2 特征定义策略 21.2.1 Peak calling Peak calling是鉴定scATAC-seq数据中富集的开放染色质区域的标准方法。
方法 :
伪批量Peak calling :将所有细胞的fragments合并,使用MACS2进行Peak calling
每簇Peak calling :按细胞类型分组,分别进行Peak calling,然后合并
迭代Peak calling :ArchR推荐的方法,先粗聚类→每簇Peak→合并→重新聚类→迭代
1 2 3 4 5 6 7 8 9 10 macs2 callpeak \ -t merged_fragments.bed \ -f BED \ -n sample_peaks \ -g hs \ --nomodel \ --shift -100 \ --extsize 200 \ -q 0.05
21.2.2 基因组分箱(Gene Activity) 由于Peak calling可能遗漏细胞类型特异的 Peaks,另一种策略是使用基因组分箱(genomic binning)定义特征。
方法 :
将基因组划分为固定大小的窗口(如500bp),统计每个窗口内的fragment数。或使用基因+启动子区域作为特征单元:
$$\text{GeneActivity}_g = \text{Fragments}(\text{gene body}_g) + \text{Fragments}(\text{promoter}_g) \times w$$
其中 $w$ 是启动子区域的权重(通常>1,因为启动子的可及性信号更强)。
比较 :
21.3 数据极度稀疏问题 scATAC-seq数据的核心挑战是极度稀疏。一个典型的数据集中,每个细胞只有1,000-5,000个fragment,而基因组中有数百万个潜在的开放位点。这意味着对于任何给定位点,绝大多数细胞的测量值为0。
应对策略 :
降维 :使用LSPM(Latent Semantic Indexing)、LSI或PCA将稀疏矩阵映射到低维空间
Imputation :使用MAGIC、cisTopic等方法填补缺失值
聚合 :将Peaks聚合到更大的功能单元(如Gene Activity)
信息借用 :通过KNN图借用邻居细胞的信息
21.4 QC流程(Ch25) 21.4.1 双联体检测 scDblFinder :
scDblFinder是检测scATAC-seq双联体的推荐工具。其原理是:双联体细胞包含两个细胞的fragments,因此其信号特征与单细胞不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 library( scDblFinder) library( SingleCellExperiment) sce <- read10xATAC( "atac_fragments.tsv.gz" ) sce <- scDblFinder( sce, type= "ATAC" ) table( sce$ scDblFinder.class)
AMULET :
AMULET是专门为scATAC-seq设计的双联体检测方法。其原理是:双联体细胞在某些基因组区域会异常地出现过量的fragments(因为两个细胞的信号叠加)。
1 2 3 4 5 6 python AMULET.py \ --fragments atac_fragments.tsv.gz \ --barcodes barcodes.tsv \ --regions regions.bed \ --output amulet_results/
21.4.2 TSS富集 TSS富集(TSS Enrichment)是scATAC-seq数据质量的核心指标。其原理是:在活跃转录的基因启动子区域,染色质应该是开放的,因此TSS附近应该有更多的fragments。
$$\text{TSS Enrichment} = \frac{\text{Fragments in TSS window}}{\text{Fragments in flanking regions}}$$
高质量细胞的TSS富集值通常 > 6。
1 2 3 4 5 6 7 8 9 10 11 12 13 import scanpy as scimport episcanpy as epitss_scores = epi.pp.cal_tss_score( adata, gtf_file='gencode.v38.annotation.gtf' , fragments_file='atac_fragments.tsv.gz' ) adata = adata[adata.obs['tss_score' ] > 6 ].copy()
21.4.3 核小体信号 核小体信号(Nucleosome Signal)衡量fragment长度分布是否符合核小体模式。正常的scATAC-seq数据应该呈现双峰分布:
亚核小体片段 (<147 bp):来自开放区域(无核小体占据)
单核小体片段 (147-294 bp):来自核小体间的连接DNA
多核小体片段 (>294 bp):来自更长的核小体覆盖区域
$$\text{Nucleosome Signal} = \frac{\text{mono-nucleosomal fragments}}{\text{nucleosome-free fragments}}$$
高质量细胞的核小体信号通常 < 4。
21.4.4 完整QC代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import scanpy as scimport episcanpy as epiimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_h5ad('raw_atac_data.h5ad' ) adata.obs['log_fragments' ] = np.log10(adata.obs['n_fragments' ]) adata.obs['frip' ] = adata.obs['fragments_in_peaks' ] / adata.obs['n_fragments' ] print (f"过滤前细胞数: {adata.n_obs} " )adata = adata[ (adata.obs['n_fragments' ] > 1000 ) & (adata.obs['n_fragments' ] < 100000 ) & (adata.obs['tss_enrichment' ] > 6 ) & (adata.obs['nucleosome_signal' ] < 4 ) & (adata.obs['frip' ] > 0.15 ) ].copy() print (f"过滤后细胞数: {adata.n_obs} " )q99 = np.percentile(adata.obs['n_fragments' ], 99 ) adata = adata[adata.obs['n_fragments' ] < q99].copy() print (f"双联体过滤后细胞数: {adata.n_obs} " )from sklearn.feature_extraction.text import TfidfTransformertfidf = TfidfTransformer(norm='l1' , sublinear_tf=True ) matrix_tfidf = tfidf.fit_transform(adata.X.T).T from sklearn.decomposition import TruncatedSVDlsi = TruncatedSVD(n_components=50 , random_state=42 ) adata.obsm['X_lsi' ] = lsi.fit_transform(matrix_tfidf.T) adata.obsm['X_lsi' ] = adata.obsm['X_lsi' ][:, 1 :] sc.pp.neighbors(adata, use_rep='X_lsi' , n_neighbors=30 ) sc.tl.umap(adata) sc.tl.leiden(adata, resolution=0.5 ) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 5 )) sc.pl.umap(adata, color='leiden' , ax=axes[0 ], show=False , title='Clusters' ) sc.pl.umap(adata, color='log_fragments' , ax=axes[1 ], show=False , title='Log Fragments' , cmap='viridis' ) sc.pl.umap(adata, color='tss_enrichment' , ax=axes[2 ], show=False , title='TSS Enrichment' , cmap='Reds' ) plt.tight_layout() plt.savefig('atac_qc_results.png' , dpi=300 , bbox_inches='tight' ) plt.show()
21.5 GRN推断(Ch26):FigR工具链 21.5.1 FigR原理 FigR(Functional inference of gene regulatory networks)是Kartha等人于2022年提出的scATAC-seq GRN推断工具。它整合scATAC-seq和scRNA-seq数据,推断转录因子到靶基因的调控关系。
核心步骤 :
DORC评分(Degrees of Regulatory Chromatin) :计算每个基因的调控染色质程度
$$\text{DORC}g = \sum {p \in \text{peaks near } g} \text{correlation}(p, g)$$
即基因 $g$ 附近所有Peaks与该基因表达的相关性之和。
TF motif活性推断 :使用chromVAR计算每个细胞中每个TF motif的偏差分数(deviation score)
TF→Peak→Gene调控链 :
$$\text{Regulation}_{TF \rightarrow Gene} = \text{Motif}(TF, Peak) \times \text{Correlation}(Peak, Gene)$$
21.5.2 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 library( FigR) library( Signac) library( Seurat) rna <- readRDS( "rna_processed.rds" ) atac <- readRDS( "atac_processed.rds" ) shared_cells <- intersect( colnames( rna) , colnames( atac) ) rna <- rna[ , shared_cells] atac <- atac[ , shared_cells] figr_res <- FigR:: runFigR( rna = rna, atac = atac, genome = "hg38" , doregulon = TRUE , numCores = 16 ) dorc_genes <- FigR:: getDORCs( figr_res, top_n = 50 ) print( "Top DORC基因:" ) print( head( dorc_genes, 20 ) ) grn <- FigR:: getGRN( figr_res) print( "TF-靶基因调控关系数:" ) print( nrow( grn) ) FigR:: plotDORCHeatmap( figr_res, top_n = 30 ) FigR:: plotTFActivity( figr_res, tf = "SOX2" , seurat_atac = atac) saveRDS( figr_res, file = "figr_results.rds" )
21.6 注意事项
scATAC-seq的极度稀疏性 是所有分析步骤的核心挑战。任何分析都需要先解决稀疏性问题。
TSS富集是最重要的QC指标 。TSS富集低的细胞应该被坚决过滤。
Peak calling策略选择 :推荐使用ArchR的迭代Peak calling方法。
LSI vs PCA :scATAC-seq数据推荐使用LSI(Latent Semantic Indexing)降维,而非PCA,因为数据极度稀疏。
与scRNA-seq整合 :scATAC-seq的生物学解读通常需要与scRNA-seq数据整合。推荐使用WNN(Weighted Nearest Neighbor)或GLUE方法。
Fragment文件格式 :scATAC-seq的核心数据是fragment文件(BED格式),不是count矩阵。确保正确解析fragment文件。
关键参考文献 :
Buenrostro et al. (2015) Nature Methods - ATAC-seq
Granja et al. (2021) Nature Genetics - ArchR
Kartha et al. (2022) Nature Genetics - FigR
第二十二章:空间转录组学
基于Ch27-32,详细展开
22.1 空间数据技术 22.1.1 Visium Visium(10x Genomics)是目前最广泛使用的空间转录组平台。
技术参数 :
分辨率:55 μm spot(每个spot包含1-10个细胞)
基因覆盖:全转录组(~20K基因)
组织面积:6.5×6.5 mm
Spot数量:约5,000个
测序深度:每个spot 50,000-100,000 reads
局限 :非真正的单细胞分辨率,每个spot是多个细胞的混合信号。
22.1.2 MERFISH MERFISH(Multiplexed Error-Robust Fluorescence In Situ Hybridization)是一种基于图像的空间转录组技术。
技术参数 :
分辨率:亚细胞级(100 nm)
基因覆盖:100-10,000基因
细胞数:取决于组织面积
原理:使用组合标记和错误校正编码的smFISH
22.1.3 Slide-seq Slide-seq是一种基于DNA条形码珠的空间转录组技术。
技术参数 :
分辨率:10 μm(接近单细胞)
基因覆盖:全转录组
珠数量:约100,000个
技术:将组织切片放在带有DNA条形码的珠阵列上
技术对比 :
22.2 邻域分析(Ch28) 22.2.1 原理 邻域分析(Neighborhood Analysis)旨在识别空间上共定位的细胞类型或基因表达模式。其核心思想是:在空间组织中,特定功能的细胞倾向于聚集在一起形成”邻域”(neighborhood)。
方法 :
邻域组成分析 :对每个spot,计算其空间邻域中各细胞类型的比例
$$\mathbf{n}i = \frac{1}{|N(i)|} \sum {j \in N(i)} \mathbf{c}_j$$
其中 $N(i)$ 是spot $i$ 的空间邻居,$\mathbf{c}_j$ 是spot $j$ 的细胞类型组成向量。
邻域聚类 :对邻域组成向量进行聚类,识别不同的组织微环境
22.2.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import scanpy as scimport squidpy as sqimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_visium("visium_output/" ) adata.var_names_make_unique() sc.pp.filter_genes(adata, min_cells=10 ) sc.pp.normalize_total(adata, target_sum=1e4 ) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000 ) sc.tl.pca(adata) sq.gr.spatial_neighbors(adata, n_neighs=6 , coord_type='generic' ) sq.gr.spatial_autocorr(adata, mode='moran' , genes=adata.var['highly_variable' ].values) moran_results = adata.uns['moranI' ] print ("Top空间自相关基因:" )print (moran_results.head(20 ))sq.gr.nhood_enrichment(adata, cluster_key='cell_type' ) sq.pl.nhood_enrichment(adata, cluster_key='cell_type' , method='heatmap' , save='nhood_enrichment.png' ) sq.gr.co_occurrence(adata, cluster_key='cell_type' ) sq.pl.co_occurrence(adata, cluster_key='cell_type' , clusters='T_cells' , save='co_occurrence.png' ) top_spatial_genes = moran_results.head(6 ).index.tolist() sc.pl.spatial(adata, color=top_spatial_genes, ncols=3 , save='spatial_variable_genes.png' )
22.3 空间域识别(Ch29) 22.3.1 BayesSpace 原理 :BayesSpace由Zhao等人于2021年提出,使用贝叶斯统计模型将空间位置信息整合到聚类中。核心是一个t分布混合模型,其中每个spot的聚类分配不仅取决于其表达谱,还取决于其空间邻居的聚类分配。
模型 :
$$p(z_i = k | \mathbf{x}i, \mathbf{z} {-i}) \propto p(\mathbf{x}i | z_i = k) \cdot p(z_i = k | \mathbf{z} {N(i)})$$
其中 $\mathbf{z}_{N(i)}$ 是spot $i$ 的空间邻居的聚类分配。空间先验鼓励相邻spot属于同一聚类:
$$p(z_i = k | \mathbf{z}{N(i)}) \propto \exp\left(\sum {j \in N(i)} w_{ij} \cdot \mathbb{1}[z_j = k]\right)$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 library( BayesSpace) library( SpatialExperiment) spe <- read10xVisium( "visium_output/" ) spe <- scran:: computeSumFactors( spe) spe <- logNormCounts( spe) spe <- runPCA( spe) spe <- spatialCluster( spe, q = 8 , d = 15 , platform = "Visium" , nrep = 50000 , burn.in = 10000 ) clusterPlot( spe, save_plot = TRUE )
22.3.2 SpaGCN 原理 :SpaGCN由Hu等人于2021年提出,使用图卷积网络(GCN)整合基因表达和空间信息进行聚类。
算法 :
构建空间-表达联合图:$A = \alpha A_{spatial} + (1-\alpha) A_{expression}$
使用GCN编码节点表示:$H^{(l+1)} = \sigma(\hat{A} H^{(l)} W^{(l)})$
对编码表示进行聚类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import SpaGCN as spgspg_obj = spg.SpaGCN() spg_obj.set_l(p=0.5 ) spg_obj.train(adata, init="louvain" ) y_pred, prob = spg_obj.predict() adata.obs['spaGCN_cluster' ] = pd.Categorical(y_pred) sc.pl.spatial(adata, color='spaGCN_cluster' , save='spagcn_clusters.png' )
22.3.3 CODEX CODEX(COmbined DEconvolution and eXpression analysis)是一种整合反卷积和表达分析的空间域识别方法。
22.4 空间变异基因(Ch30) 22.4.1 SpatialDE 原理 :SpatialDE由Svensson等人于2018年提出,使用高斯过程(Gaussian Process, GP)回归检测空间变异基因。
模型 :
对于基因 $g$ 在位置 $\mathbf{s}i$ 的表达 $y {gi}$:
$$y_{gi} = f_g(\mathbf{s}_i) + \epsilon_i$$
其中 $f_g \sim \mathcal{GP}(0, K(\cdot, \cdot))$,$\epsilon_i \sim \mathcal{N}(0, \sigma_n^2)$。
使用两种核函数的似然比检验:
$H_0$:$K = \sigma_n^2 I$(无空间模式)
$H_1$:$K = \sigma_s^2 K_{spatial} + \sigma_n^2 I$(有空间模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SpatialDEcounts = pd.DataFrame(adata.X.toarray() if hasattr (adata.X, 'toarray' ) else adata.X, index=adata.obs_names, columns=adata.var_names) coords = adata.obs[['spatial1' , 'spatial2' ]] results = SpatialDE.run(coords, counts) print ("Top spatially variable genes:" )print (results.sort_values('qval' ).head(20 ))SpatialDE.plot_patterns(adata, results, n_patterns=5 )
22.4.2 SPARK 原理 :SPARK由Sun等人于2020年提出,使用广义空间统计模型检测空间变异基因。相比SpatialDE,SPARK使用了更灵活的空间核函数族,并通过创新的经验零分布方法控制假阳性率。
核函数 :
SPARK使用5种空间核函数:
Gaussian: $K(d) = \exp(-d^2 / 2l^2)$
Exponential: $K(d) = \exp(-d/l)$
Periodic: $K(d) = \exp(-2\sin^2(\pi d / p) / l^2)$
Matern 1/2: $K(d) = (1 + \sqrt{3}d/l)\exp(-\sqrt{3}d/l)$
Matern 3/2: 类似但不同参数
1 2 3 4 5 6 7 8 9 10 11 12 library( SPARK) spark_obj <- CreateSPARKObject( counts = counts_matrix, location = coords) spark_obj <- spark.vc( spark_obj, covariates = NULL , lib_size = lib_size, num_core = 8 ) spark_obj <- spark.test( spark_obj, check_positive = TRUE ) results <- spark_obj@ res_mtest print( head( results[ order( results$ combinedPvalue) , ] , 20 ) )
22.5 空间反卷积(Ch31) 22.5.1 cell2location 原理 :cell2location由Kleshchevnikova等人于2022年提出,使用贝叶斯模型将Visium等低分辨率空间数据中的混合信号反卷积为各细胞类型的丰度。
模型 :
$$y_{sg} \sim \text{NB}(\mu_{sg}, \phi_g)$$
$$\mu_{sg} = \sum_f w_{sf} \cdot m_{fg} + s_s \cdot g_g$$
其中:
$y_{sg}$:spot $s$ 中基因 $g$ 的计数
$w_{sf}$:spot $s$ 中细胞类型 $f$ 的丰度(待估计)
$m_{fg}$:细胞类型 $f$ 中基因 $g$ 的参考表达签名
$s_s, g_g$:spot和基因特异的附加因子
先验 :
$$w_{sf} \sim \text{Gamma}(\alpha_f, \beta_f)$$
使用变分推断估计后验分布。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import cell2locationimport scanpy as scref_adata = sc.read_h5ad('reference_scrna.h5ad' ) aver = cell2location.models.RegressionModel(ref_adata) aver.train(max_epochs=500 ) ref_signatures = aver.export_posterior( ref_adata, sample_kwargs={'num_samples' : 1000 } )['means' ] sp_adata = sc.read_visium('visium_output/' ) cell2location.models.Cell2location.setup_anndata(sp_adata) model = cell2location.models.Cell2location( sp_adata, cell_state_df=ref_signatures, N_cells_per_location=20 , detection_alpha=20 ) model.train(max_epochs=30000 ) sp_adata = model.export_posterior(sp_adata) cell_abundance = sp_adata.obsm['q05_cell_abundance_w_sf' ] for ct in cell_abundance.columns[:6 ]: sc.pl.spatial(sp_adata, color=ct, cmap='Reds' , save=f'cell2location_{ct} .png' )
22.5.2 RCTD RCTD(Robust Cell Type Decomposition)由Cable等人于2022年提出,是另一种空间反卷积方法。
模型 :
使用带权重的泊松回归:
$$Y_{sg} \sim \text{Poisson}(\lambda_{sg})$$
$$\lambda_{sg} = N_s \sum_k \pi_{sk} \cdot \mu_{kg}$$
其中 $N_s$ 是spot $s$ 中的总UMI数,$\pi_{sk}$ 是细胞类型 $k$ 在spot $s$ 中的比例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 library( spacexr) reference <- Reference( counts = ref_counts, cell_types = ref_celltypes, n_UMI = ref_nUMI ) spatial <- SpatialRNA( coords = spatial_coords, counts = spatial_counts ) rctd <- create.RCTD( spatial, reference, max_cores = 8 ) rctd <- run.RCTD( rctd, doublet_mode = 'doublet' ) results <- rctd@ results print( head( results$ weights) )
22.6 空间插补(Ch32):Tangram 22.6.1 原理 Tangram由Biancalani等人于2021年提出,通过将单细胞数据”映射”到空间位置,实现空间转录组的全基因插补。
核心思想 :
给定scRNA-seq参考数据集和空间数据集,Tangram学习一个映射矩阵 $M \in \mathbb{R}^{n_{sc} \times n_{sp}}$,使得:
$$\min_M D(\hat{G}{sp} | G {sp}) + \lambda R(M)$$
其中:
$\hat{G}{sp} = M^T G {sc}$:从scRNA-seq数据重构的空间表达
$G_{sp}$:观察的空间表达
$D$:KL散度或其他距离度量
$R(M)$:正则化项(鼓励稀疏性和映射一致性)
22.6.2 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import tangram as tgimport scanpy as scad_sc = sc.read_h5ad('reference_scrna.h5ad' ) ad_sp = sc.read_visium('visium_output/' ) tg.pp_adatas(ad_sc, ad_sp, genes=None ) ad_map = tg.map_cells_to_space( ad_sc, ad_sp, mode='cells' , density_prior='uniform' , num_epochs=500 , device='cuda:0' ) ad_ge = tg.project_gene_embeddings(ad_map, ad_sp) genes_to_plot = ['SOX2' , 'PAX6' , 'DCX' , 'MAP2' , 'GFAP' ] for gene in genes_to_plot: tg.plot_cell_annotation(ad_sp, gene, ad_map, save=f'tangram_{gene} .png' ) ad_sp_obs = tg.adata_to_obs(ad_map, ad_sp) sc.pl.spatial(ad_sp_obs, color='cell_type_mapped' , save='tangram_celltype.png' )
22.7 完整空间转录组分析代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import scanpy as scimport squidpy as sqimport numpy as npimport matplotlib.pyplot as pltadata = sc.read_visium("visium_output/" ) adata.var_names_make_unique() sc.pp.filter_genes(adata, min_cells=10 ) sc.pp.normalize_total(adata, target_sum=1e4 ) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes=2000 ) sc.tl.pca(adata) sq.gr.spatial_neighbors(adata, n_neighs=6 ) sq.gr.spatial_autocorr(adata, mode='moran' ) top_svg = adata.uns['moranI' ].head(20 ).index.tolist() sc.pp.neighbors(adata, use_rep='X_pca' , n_neighbors=15 ) sq.gr.spatial_autocorr(adata, mode='moran' ) sc.tl.leiden(adata, resolution=0.5 ) fig, axes = plt.subplots(1 , 3 , figsize=(18 , 5 )) sc.pl.spatial(adata, color='leiden' , ax=axes[0 ], show=False , title='Spatial Clusters' ) sc.pl.spatial(adata, color=top_svg[0 ], ax=axes[1 ], show=False , title=f'Spatial Gene: {top_svg[0 ]} ' ) sc.pl.spatial(adata, color=top_svg[1 ], ax=axes[2 ], show=False , title=f'Spatial Gene: {top_svg[1 ]} ' ) plt.tight_layout() plt.savefig('spatial_analysis_results.png' , dpi=300 , bbox_inches='tight' ) plt.show() adata.write_h5ad('spatial_analysis_complete.h5ad' )
关键参考文献 :
Zhao et al. (2021) Nature Biotechnology - BayesSpace
Hu et al. (2021) Nature Methods - SpaGCN
Svensson et al. (2018) Nature Methods - SpatialDE
Sun et al. (2020) Nature Methods - SPARK
Kleshchevnikova et al. (2022) Nature Biotechnology - cell2location
Cable et al. (2022) Nature Methods - RCTD
Biancalani et al. (2021) Nature Methods - Tangram
第二十三章:CITE-seq表面蛋白分析
基于Ch33-38,详细展开
23.1 CITE-seq原理 CITE-seq(Cellular Indexing of Transcriptomes and Epitopes by Sequencing)由Stoeckius等人于2017年提出,是一种同时测量细胞转录组和表面蛋白的多模态技术。
核心技术:ADN(Antibody-Derived Tag)
抗体偶联 :将特异性抗体与独特的DNA寡核苷酸标签(ADN)偶联
抗体孵育 :将偶联抗体的混合物与细胞孵育,抗体结合表面抗原
单细胞捕获 :使用10x Genomics微流控系统捕获单细胞
联合测序 :同时测序mRNA(GEX)和ADN标签
ADN结构 :
$$\text{[PCR handle]} - \text{[Antibody barcode]} - \text{[UMI]} - \text{[Poly-A tail]}$$
PCR handle:用于文库扩增
Antibody barcode:唯一标识每种抗体(~8-10 bp)
UMI:唯一分子标识符,去除PCR重复
Poly-A tail:使其被oligo-dT引物捕获
23.2 QC(Ch33):ADN与RNA信号分离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import scanpy as scimport muon as muimport numpy as npmdata = mu.read_10x_h5("citeseq_filtered_feature_bc_matrix.h5" ) rna = mdata.mod['rna' ] prot = mdata.mod['prot' ] sc.pp.filter_cells(rna, min_genes=200 ) sc.pp.filter_genes(rna, min_cells=3 ) sc.pp.normalize_total(rna, target_sum=1e4 ) sc.pp.log1p(rna) prot.obs['log_prot_counts' ] = np.log10(prot.X.sum (axis=1 ).A1 + 1 ) prot.obs['log_rna_counts' ] = np.log10(rna.X.sum (axis=1 ).A1 + 1 ) import matplotlib.pyplot as pltplt.figure(figsize=(8 , 6 )) plt.scatter(prot.obs['log_rna_counts' ], prot.obs['log_prot_counts' ], s=1 , alpha=0.5 ) plt.xlabel('Log RNA counts' ) plt.ylabel('Log Protein counts' ) plt.title('ADN vs RNA signal' ) plt.savefig('cite_seq_signal_separation.png' , dpi=300 , bbox_inches='tight' ) plt.show() from scipy.stats import median_abs_deviationprot_counts = prot.X.sum (axis=1 ).A1 mad = median_abs_deviation(prot_counts) median_count = np.median(prot_counts) upper_bound = median_count + 5 * mad prot = prot[prot_counts < upper_bound].copy()
23.3 归一化(Ch34):DSB vs CLR 23.3.1 DSB(Data-Driven Denoising) DSB由Mulè等人于2022年提出,利用空液滴(empty droplets)中的ADN背景信号进行归一化和噪声去除。
原理 :
从空液滴中估计每种抗体的背景分布
对每个细胞的ADN信号,减去背景并标准化:
$$x_{cg}^{\text{DSB}} = \frac{x_{cg} - \mu_g^{\text{bg}}}{\sigma_g^{\text{bg}} + \sigma_g^{\text{cell}}}$$
其中 $\mu_g^{\text{bg}}$ 和 $\sigma_g^{\text{bg}}$ 是空液滴中抗体 $g$ 的均值和标准差,$\sigma_g^{\text{cell}}$ 是细胞中的标准差。
1 2 3 4 5 6 7 8 9 import muon as muimport scanpy as scmu.prot.pp.dsb( mdata, empty_droplets='empty_droplet_adata' )
23.3.2 CLR(Centered Log-Ratio) CLR是CITE-seq数据的标准归一化方法。
$$x_{cg}^{\text{CLR}} = \log\left(\frac{x_{cg}}{g(x_c)}\right)$$
其中 $g(x_c)$ 是细胞 $c$ 的几何平均。
1 2 mu.prot.pp.clr(mdata, inplace=True )
DSB vs CLR对比 :
23.4 双联体检测(Ch35) CITE-seq的双联体可以通过RNA和蛋白信号的异常组合来检测:
1 2 3 4 5 6 7 8 9 10 11 import scvimodel = scvi.model.TOTALVI(mdata) model.train() doublet_probs = model.get_doublet_probabilities() mdata.mod['rna' ].obs['doublet_prob' ] = doublet_probs mdata = mdata[mdata.mod['rna' ].obs['doublet_prob' ] < 0.5 ].copy()
23.5 降维(Ch36):蛋白质PCA vs RNA PCA CITE-seq数据的降维可以选择基于RNA、蛋白或两者结合:
1 2 3 4 5 6 7 8 9 sc.tl.pca(mdata.mod['rna' ]) sc.tl.pca(mdata.mod['prot' ]) mu.pp.neighbors(mdata, key_added='wnn' ) mu.tl.umap(mdata, key_added='wnn' )
23.6 批次校正(Ch37) 1 2 3 4 5 scvi.model.TOTALVI.setup_mudata(mdata, batch_key='batch' ) model = scvi.model.TOTALVI(mdata) model.train() mdata = model.get_latent_representation(mdata)
23.7 注释(Ch38):蛋白标记辅助注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 protein_markers = { 'T_cells' : ['CD3' , 'CD4' , 'CD8' ], 'B_cells' : ['CD19' , 'CD20' ], 'Monocytes' : ['CD14' , 'CD16' ], 'NK_cells' : ['CD56' , 'CD16' ], } for ct, markers in protein_markers.items(): prot = mdata.mod['prot' ] for marker in markers: if marker in prot.var_names: prot.obs[f'{marker} _positive' ] = prot[:, marker].X > np.percentile( prot[:, marker].X, 90 ) mu.pl.embedding(mdata, basis='wnn_umap' , color=['CD3' , 'CD19' , 'CD14' ], cmap='Reds' )
关键参考文献 :
Stoeckius et al. (2017) Nature Methods - CITE-seq
Mulè et al. (2022) Nature Biotechnology - DSB
Gayoso et al. (2022) Nature Methods - totalVI
第二十四章:免疫受体分析
基于Ch39-42,详细展开
24.1 TCR/BCR基础概念(Ch39) T细胞受体(TCR) :
TCR是T细胞表面的异二聚体蛋白,由α链和β链(或γ链和δ链)组成。每条链由V(Variable)、D(Diversity,仅β和δ链)、J(Joining)和C(Constant)基因片段重排产生。
重排机制 :
$$V_\beta \times D_\beta \times J_\beta \rightarrow \beta \text{ chain}$$ $$V_\alpha \times J_\alpha \rightarrow \alpha \text{ chain}$$
理论上多样性:$V \times D \times J \times \text{N-additions} \approx 10^{15}$ 种。
B细胞受体(BCR)/抗体 :
BCR由重链(Heavy chain, IGH)和轻链(IGK/IGL)组成:
$$V_H \times D_H \times J_H \rightarrow \text{Heavy chain}$$ $$V_L \times J_L \rightarrow \text{Light chain}$$
BCR还可以经历体细胞超突变(Somatic Hypermutation, SHM)和类别转换重组(Class Switch Recombination, CSR)。
CDR3区域 :
互补决定区3(CDR3)是抗原接触的核心区域,也是免疫受体克隆型分类的主要依据:
$$\text{CDR3 sequence} = \text{V gene end} + \text{D gene} + \text{J gene start} + \text{N-additions}$$
24.2 克隆型分析(Ch40) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import scirpy as irimport scanpy as scimport numpy as npadata = sc.read_h5ad('paired_tcr_gex.h5ad' ) ir.tl.chain_qc(adata) ir.pp.ir_dist(adata, metric='alignment' , sequence='aa' , cutoff=15 ) ir.tl.define_clonotypes(adata, receptor_arms='all' , dual_ir='primary_only' ) clonotype_counts = adata.obs['clonotype_id' ].value_counts() print ("克隆型频率分布:" )print (clonotype_counts.describe())ir.tl.clonal_expansion(adata, expansion_threshold=2 ) sc.pl.umap(adata, color='clonal_expansion' , title='Clonal Expansion' ) ir.pl.clonotype_network( adata, color='clonotype_id' , basis='umap' , layout='force' , min_size=5 )
24.3 特异性分析(Ch41) 1 2 3 4 5 6 ir.tl.ir_query(adata, vdjdb_path='vdjdb.tsv' ) ir.pl.specifity(adata, groupby='antigen' )
24.4 AIR+GEX整合(Ch42):scirpy工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import scirpy as irimport scanpy as scadata = sc.read_h5ad('paired_airr_gex.h5ad' ) ir.tl.chain_qc(adata) ir.pl.chain_qc(adata) ir.pp.ir_dist(adata, metric='alignment' , sequence='aa' , cutoff=15 ) ir.tl.define_clonotypes(adata, receptor_arms='all' ) ir.tl.clonotype_modularity(adata, fdr_threshold=0.05 ) ir.pl.clonotype_modularity(adata) ir.tl.clonotype_imbalance(adata, replicate_col='sample' ) ir.pl.clonotype_imbalance(adata) adata.write_h5ad('scirpy_results.h5ad' )
关键参考文献 :
Stoeckius et al. (2017) Nature Methods - CITE-seq
Schattgen et al. (2022) Nature Methods - scirpy
第二十五章:多模态整合
基于Ch43-44,详细展开
25.1 配对整合:totalVI与MultiVI 25.1.1 totalVI totalVI(total Variational Inference)由Gayoso等人于2022年提出,是scVI-tools中的多模态整合模型,专门处理CITE-seq数据(RNA + 蛋白质)。
模型架构 :
totalVI使用条件变分自编码器(CVAE)同时建模RNA和蛋白质数据:
RNA生成模型 :
$$x_{ng} \sim \text{NB}(l_n w_{ng}, \theta_g)$$
蛋白质生成模型 :
$$y_{np} \sim \text{NB}(l_n v_{np}, \phi_p)$$
其中 $w_{ng}$ 和 $v_{np}$ 分别从潜在变量 $\mathbf{z}_n$ 通过神经网络生成。
蛋白质的特殊处理 :蛋白质数据使用混合模型处理背景信号:
$$v_{np} = \text{sigmoid}(\pi_{np}) \cdot f_{np}^{\text{foreground}} + (1 - \text{sigmoid}(\pi_{np})) \cdot f_{np}^{\text{background}}$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import scviimport muon as mumdata = mu.read_10x_h5("citeseq_data.h5" ) scvi.model.TOTALVI.setup_mudata(mdata, batch_key='batch' ) model = scvi.model.TOTALVI(mdata) model.train(max_epochs=400 ) latent = model.get_latent_representation() mdata.obsm['X_totalVI' ] = latent mdata = model.get_normalized_expression(mdata) import scanpy as sclatent_adata = sc.AnnData(latent, obs=mdata.obs) sc.pp.neighbors(latent_adata) sc.tl.umap(latent_adata) sc.pl.umap(latent_adata, color='cell_type' )
25.1.2 MultiVI MultiVI由Ashuach等人于2023年提出,处理RNA + ATAC配对数据。
模型 :与totalVI类似,但ATAC部分使用伯努利分布建模二值化的染色质可及性:
$$a_{np} \sim \text{Bernoulli}(p_{np})$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import scviadata = sc.read_h5ad('multiome_data.h5ad' ) scvi.model.MULTIVI.setup_anndata(adata, batch_key='batch' ) model = scvi.model.MULTIVI(adata, n_genes=20000 , n_regions=100000 ) model.train(max_epochs=500 ) latent = model.get_latent_representation() adata.obsm['X_MultiVI' ] = latent
25.2 无配对整合:GLUE GLUE(Graph-Linked Unified Embedding)由Cao和Gao于2022年提出,处理无配对关系的多模态数据整合。
原理 :
GLUE的核心创新是使用基因调控图(GRN graph)作为”桥梁”连接不同模态:
模态特异性编码器 :每个模态有独立的VAE编码器
GRN图 :将RNA基因与ATAC Peaks通过调控关系连接
图神经网络 :在GRN图上传播信息,实现跨模态对齐
$$\mathbf{z}i^{\text{RNA}} = \text{Encoder} {\text{RNA}}(\mathbf{x}_i^{\text{RNA}})$$ $$\mathbf{z}j^{\text{ATAC}} = \text{Encoder} {\text{ATAC}}(\mathbf{x}_j^{\text{ATAC}})$$ $$\mathbf{z}_i^{\text{aligned}} = \text{GNN}(\mathbf{z}_i, \text{GRN graph})$$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import scgluerna = sc.read_h5ad('rna_data.h5ad' ) atac = sc.read_h5ad('atac_data.h5ad' ) guidance = scglue.genomics.genomic_guidance_graph(rna, atac) scglue.models.train_glue( rna, atac, guidance, model_class='GLUE' , max_epochs=500 ) rna.obsm['X_glue' ] = scglue.models.encode(rna, 'GLUE' ) atac.obsm['X_glue' ] = scglue.models.encode(atac, 'GLUE' ) import anndatacombined = anndata.concat([rna, atac], join='outer' ) sc.pp.neighbors(combined, use_rep='X_glue' ) sc.tl.umap(combined) sc.pl.umap(combined, color='modality' )
25.3 桥接整合 桥接整合(Bridge Integration)使用一个”桥接”数据集(如多组学数据)来对齐两个无配对关系的模态。scVI-tools提供了scvi.model.bridge模块。
25.4 查询-参考映射 将新的查询数据集映射到已有的参考图谱上,实现快速注释和分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 import scviref_model = scvi.model.SCVI.load('reference_model/' ) query_adata = sc.read_h5ad('query_data.h5ad' ) scvi.model.SCVI.setup_anndata(query_adata) query_model = scvi.model.SCVI.load_query_data(query_adata, ref_model) query_model.train(max_epochs=100 ) query_adata.obsm['X_scVI' ] = query_model.get_latent_representation()
25.5 多模态评估指标
25.6 完整多模态整合代码流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import scviimport muon as muimport scanpy as scimport numpy as npmdata = mu.read_10x_h5("multiome_data.h5" ) rna = mdata.mod['rna' ] sc.pp.filter_cells(rna, min_genes=200 ) sc.pp.normalize_total(rna, target_sum=1e4 ) sc.pp.log1p(rna) atac = mdata.mod['atac' ] scvi.model.MULTIVI.setup_mudata(mdata, batch_key='batch' ) model = scvi.model.MULTIVI(mdata) model.train(max_epochs=500 ) mdata.obsm['X_MultiVI' ] = model.get_latent_representation() latent_adata = sc.AnnData(mdata.obsm['X_MultiVI' ], obs=mdata.obs) sc.pp.neighbors(latent_adata) sc.tl.umap(latent_adata) sc.tl.leiden(latent_adata, resolution=0.5 ) sc.pl.umap(latent_adata, color=['cell_type' , 'batch' , 'leiden' ]) mdata.write_h5mu('multimodal_integrated.h5mu' )
关键参考文献 :
Gayoso et al. (2022) Nature Methods - totalVI, scVI-tools
Ashuach et al. (2023) Nature Biotechnology - MultiVI
Cao & Gao (2022) Nature Biotechnology - GLUE
第二十六章:展望与术语表
基于Ch45, 47
26.1 五大未来方向
空间多组学整合 :将空间转录组与空间蛋白质组、空间表观遗传组等数据整合,构建完整的空间细胞图谱。代表性技术如Spatial-ATAC-RNA-seq、Spatial-CITE-seq等。
单细胞长读长测序 :利用PacBio和Oxford Nanopore平台进行单细胞全长转录本测序,解决可变剪接、融合基因、等位基因特异性表达等问题。
时间序列单细胞分析 :结合谱系追踪和时间序列采样,构建动态细胞图谱,理解发育和疾病的时空动态。
基础模型(Foundation Models) :基于大规模预训练的单细胞基础模型(如scGPT、Geneformer、scBERT),实现零样本和少样本学习,减少对标注数据的依赖。
因果推断 :从观测数据中推断因果关系,超越相关性分析。结合干预实验和计算因果推断方法,建立基因调控的因果网络。
26.2 核心术语速查表
第八篇:工具链参考
第二十七章:Scanpy完整API参考 27.1 sc.pp预处理模块完整函数列表与参数
27.2 sc.tl工具模块完整函数列表与参数
27.3 sc.pl可视化模块关键绘图函数
27.4 使用原则
先pp后tl :预处理(pp)必须在工具(tl)之前
保存原始数据 :使用adata.raw = adata保存归一化前的数据
检查中间结果 :每步操作后检查输出是否合理
层(layer)管理 :善用adata.layers存储不同处理阶段的数据
键(key)管理 :使用key_added参数避免覆盖已有结果
可复现性 :设置random_state确保结果可复现
第二十八章:Bioconductor单细胞分析框架 28.1 Introduction to Single-Cell Analysis Bioconductor提供了基于R的单细胞分析框架,核心数据结构是SingleCellExperiment(SCE)对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 library( SingleCellExperiment) sce <- SingleCellExperiment( assays = list ( counts = count_matrix) , colData = cell_metadata, rowData = gene_metadata ) assay( sce, "counts" ) colData( sce) rowData( sce) reducedDim( sce, "PCA" )
28.2 Basics of Single-Cell Analysis 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 library( scater) library( scran) sce <- addPerCellQC( sce) sce <- addPerFeatureQC( sce) sce <- sce[ , sce$ total > 1000 & sce$ detected > 500 ] clusters <- quickCluster( sce) sce <- computeSumFactors( sce, clusters = clusters) sce <- logNormCounts( sce) dec <- modelGeneVar( sce) hvgs <- getTopHVGs( dec, n = 2000 ) sce <- runPCA( sce, subset.row = hvgs) sce <- runUMAP( sce, dimred = "PCA" ) g <- buildSNNGraph( sce, use.dimred = "PCA" ) clusters <- igraph:: cluster_louvain( g) $ membership sce$ cluster <- factor( clusters)
28.3 Single-Cell Analysis Workflows 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 library( scRNAseq) library( scran) library( scater) sce <- ZeiselBrainData( ) is.mito <- grepl( "^mt-" , rownames( sce) ) sce <- addPerCellQC( sce, subsets = list ( Mt = is.mito) ) clusters <- quickCluster( sce) sce <- computeSumFactors( sce, clusters = clusters) sce <- logNormCounts( sce) dec <- modelGeneVar( sce) hvgs <- getTopHVGs( dec, n = 2000 ) sce <- runPCA( sce, ncomponents = 30 , subset.row = hvgs) sce <- runUMAP( sce, dimred = "PCA" ) g <- buildSNNGraph( sce, use.dimred = "PCA" ) clusters <- igraph:: cluster_louvain( g) $ membership sce$ cluster <- factor( clusters) markers <- findMarkers( sce, groups = sce$ cluster)
28.4 Multi-Sample Single-Cell Analyses 1 2 3 4 5 6 7 8 library( batchelor) corrected <- fastMNN( sce1, sce2, sce3) library( harmony) sce <- RunHarmony( sce, group.by.vars = "batch" )
第二十九章:Snakemake工作流引擎 29.1 核心概念:规则、输入输出、通配符 Snakemake是一个基于Python的工作流管理系统,灵感来源于GNU Make。
核心概念 :
规则(Rule) :定义一个处理步骤,包含输入、输出和执行命令
通配符(Wildcard) :在文件名中使用{sample}等占位符,实现批量处理
DAG(有向无环图) :Snakemake根据规则间的依赖关系自动构建DAG
基本语法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 rule all : input : "results/final_report.html" rule preprocess: input : "data/{sample}.fastq.gz" output: "results/{sample}_preprocessed.h5ad" conda: "envs/scanpy.yaml" script: "scripts/preprocess.py" rule cluster: input : "results/{sample}_preprocessed.h5ad" output: "results/{sample}_clustered.h5ad" conda: "envs/scanpy.yaml" script: "scripts/cluster.py"
29.2 语法参考 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 rule process: input : "data/{sample,[A-Z0-9]+}.fastq" output: "results/{sample}.h5ad" params: n_genes = lambda wildcards: 2000 threads: 4 resources: mem_mb = 8000 conda: "envs/scanpy.yaml" log: "logs/{sample}.log" shell: "python scripts/process.py {input} {output} " "--n-genes {params.n_genes} 2>{log}" rule aggregate: input : expand("results/{sample}_clustered.h5ad" , sample=SAMPLES) output: "results/aggregated.h5ad" run: import scanpy as sc adatas = [sc.read_h5ad(f) for f in input ]
29.3 单细胞分析工作流构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 SAMPLES = ["sample1" , "sample2" , "sample3" ] rule all : input : expand("results/{sample}/final_annotated.h5ad" , sample=SAMPLES), "results/combined_analysis.h5ad" rule qc: input : "data/{sample}_raw.h5ad" output: "results/{sample}/qc_filtered.h5ad" script: "scripts/01_qc.py" rule normalize: input : "results/{sample}/qc_filtered.h5ad" output: "results/{sample}/normalized.h5ad" script: "scripts/02_normalize.py" rule feature_selection: input : "results/{sample}/normalized.h5ad" output: "results/{sample}/feature_selected.h5ad" script: "scripts/03_feature_selection.py" rule dimensionality_reduction: input : "results/{sample}/feature_selected.h5ad" output: "results/{sample}/dimred.h5ad" script: "scripts/04_dimred.py" rule clustering: input : "results/{sample}/dimred.h5ad" output: "results/{sample}/clustered.h5ad" script: "scripts/05_clustering.py" rule annotation: input : "results/{sample}/clustered.h5ad" output: "results/{sample}/final_annotated.h5ad" script: "scripts/06_annotation.py" rule combine: input : expand("results/{sample}/final_annotated.h5ad" , sample=SAMPLES) output: "results/combined_analysis.h5ad" script: "scripts/07_combine.py"
第三十章:Biopython核心API 30.1 Seq对象与序列操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from Bio.Seq import Seqfrom Bio.SeqUtils import gc_fractiondna = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG" ) print (f"长度: {len (dna)} " )print (f"GC含量: {gc_fraction(dna):.3 f} " )print (f"互补链: {dna.complement()} " )print (f"反向互补: {dna.reverse_complement()} " )print (f"转录: {dna.transcribe()} " )print (f"翻译: {dna.translate()} " )print (f"前10bp: {dna[:10 ]} " )print (f"后10bp: {dna[-10 :]} " )print (f"A出现次数: {dna.count('A' )} " )print (f"ATG出现次数: {dna.count('ATG' )} " )
30.2 SeqIO文件I/O 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from Bio import SeqIOfor record in SeqIO.parse("sequences.fasta" , "fasta" ): print (f"ID: {record.id } " ) print (f"描述: {record.description} " ) print (f"序列长度: {len (record.seq)} " ) for record in SeqIO.parse("genome.gb" , "genbank" ): print (f"ID: {record.id } " ) for feature in record.features: if feature.type == "CDS" : print (f" 基因: {feature.qualifiers.get('gene' , ['N/A' ])[0 ]} " ) records = list (SeqIO.parse("input.fasta" , "fasta" )) SeqIO.write(records[:100 ], "subset.fasta" , "fasta" ) SeqIO.convert("input.gbk" , "genbank" , "output.fasta" , "fasta" )
30.3 序列比对 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from Bio import pairwise2from Bio.pairwise2 import format_alignmentalignments = pairwise2.align.globalms( "ACCGT" , "ACG" , 2 , -1 , -0.5 , -0.1 ) for alignment in alignments: print (format_alignment(*alignment)) local_alignments = pairwise2.align.localms( "ACCGTATCG" , "TAT" , 2 , -1 , -0.5 , -0.1 )
30.4 数据库访问 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from Bio import EntrezEntrez.email = "your.email@example.com" handle = Entrez.esearch(db="gene" , term="TP53[Gene Name] AND human[Organism]" ) results = Entrez.read(handle) print (f"搜索结果数: {results['Count' ]} " )print (f"Gene IDs: {results['IdList' ]} " )if results['IdList' ]: handle = Entrez.efetch(db="gene" , id =results['IdList' ][0 ], rettype="gb" ) record = handle.read() print (record[:500 ])
第三十一章:DeepChem药物AI框架 31.1 核心模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import deepchem as dctasks, datasets, transformers = dc.molnet.load_tox21() train_dataset, valid_dataset, test_dataset = datasets model = dc.models.GraphConvModel(n_tasks=len (tasks), mode='classification' ) model.fit(train_dataset, nb_epoch=10 ) metric = dc.metrics.Metric(dc.metrics.roc_auc_score, np.mean) train_score = model.evaluate(train_dataset, [metric], transformers) test_score = model.evaluate(test_dataset, [metric], transformers) print (f"Train AUC: {train_score['mean-roc_auc_score' ]:.3 f} " )print (f"Test AUC: {test_score['mean-roc_auc_score' ]:.3 f} " )
31.2 分子表征 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import deepchem as dcsmiles = "CC(=O)Oc1ccccc1C(=O)O" mol = dc.utils.rdkit_utils.smiles_to_mol(smiles) featurizer = dc.feat.CircularFingerprint(size=2048 , radius=4 ) ecfp = featurizer.featurize([smiles]) print (f"ECFP维度: {ecfp.shape} " )featurizer = dc.feat.Mol2VecFingerprint() mol2vec = featurizer.featurize([smiles]) featurizer = dc.feat.ConvMolFeaturizer() graph_feat = featurizer.featurize([smiles])
31.3 模型训练 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import deepchem as dctasks, datasets, transformers = dc.molnet.load_hiv() train, valid, test = datasets model = dc.models.GraphConvModel( n_tasks=len (tasks), mode='classification' , dropout=0.2 , batch_normalize=True , n_classes=2 ) model.fit(train, nb_epoch=50 , checkpoint_interval=10 ) metrics = [ dc.metrics.Metric(dc.metrics.roc_auc_score), dc.metrics.Metric(dc.metrics.accuracy_score) ] scores = model.evaluate(test, metrics, transformers) print (scores)
第三十二章:Linux基础(鸟哥私房菜摘要) 32.1 文件系统与权限 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 chmod 755 script.sh chmod +x script.sh chown user:group file.txtdf -h du -sh /path/
32.2 Shell脚本基础 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #!/bin/bash SAMPLE="sample1" DATA_DIR="/data/fastq" OUT_DIR="/results" if [ -f "$DATA_DIR /${SAMPLE} .fastq.gz" ]; then echo "Found $SAMPLE data" else echo "Data not found!" exit 1 fi for sample in sample1 sample2 sample3; do echo "Processing $sample ..." done process_sample () { local sample=$1 local input=$2 echo "Processing $sample from $input " } process_sample "sample1" "/data/sample1.fastq"
32.3 进程管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 nohup python analysis.py > output.log 2>&1 &ps aux | grep python jobs -lkill PIDkill -9 PID top htop free -h nvidia-smi echo "python script.py" | at 2:00 AM
32.4 常用命令速查
第三十三章:算法基础(书籍摘要) 33.1 字符串/树/序列算法 字符串匹配算法 :
KMP算法 :时间复杂度 $O(n+m)$,通过预处理模式串构建失配函数,避免重复比较
后缀树/后缀数组 :用于高效字符串搜索、最长公共子串等问题
后缀树构建:$O(n)$(Ukkonen算法)
后缀数组构建:$O(n \log n)$
LCP数组:与后缀数组配合使用
BWT(Burrows-Wheeler Transform) :FM-index的核心,用于短读比对
$$\text{BWT}(S) = {S[i-1] | S[i..] \in \text{sorted suffixes}}$$
树算法 :
最小生成树 :Kruskal $O(E \log E)$,Prim $O(E \log V)$
树遍历 :前序、中序、后序、层序
最近公共祖先(LCA) :倍增法 $O(n \log n)$ 预处理,$O(\log n)$ 查询
序列比对算法 :
Needleman-Wunsch :全局比对,$O(nm)$
Smith-Waterman :局部比对,$O(nm)$
BLAST :基于种子扩展的启发式比对,$O(n)$ 平均
33.2 生物信息学算法设计范式
动态规划(Dynamic Programming) :
序列比对
RNA二级结构预测
隐马尔可夫模型Viterbi算法
图算法 :
De Bruijn图用于基因组组装
STRING图用于转录本重构
A*算法用于读段比对
哈希与索引 :
FM-index用于短读比对(BWA、Bowtie)
哈希表用于k-mer计数
Bloom filter用于大规模集合查询
概率模型 :
HMM用于基因预测
贝叶斯网络用于调控网络推断
期望最大化(EM)用于参数估计
蒙特卡洛与MCMC :
33.3 基因组规模算法设计 关键挑战 :
内存效率 :人类基因组3GB,需要内存高效的数据结构
并行化 :利用多核CPU和GPU加速
流式处理 :无法将所有数据载入内存时使用流式算法
代表性解决方案 :
FM-index :将3GB基因组压缩到~1GB内存,支持快速搜索
Bloom filter :用少量内存判断k-mer是否存在于集合中
MinHash :用于基因组距离估计,$O(1)$ 比较时间
Cuckoo filter :Bloom filter的替代,支持删除操作
外部排序 :当数据超过内存时使用磁盘排序
基因组索引构建 :
大基因组处理策略 :
全文完
本教材涵盖了单细胞转录组数据分析的第三部分核心内容,包括轨迹动态分析(拟时序、RNA速率、谱系追踪)、机制建模(基因调控网络、细胞通讯)、多模态与空间组学(scATAC-seq、空间转录组、CITE-seq、免疫受体、多模态整合)以及工具链参考(Scanpy、Bioconductor、Snakemake、Biopython、DeepChem、Linux、算法基础)。每个章节都提供了完整的理论解释、数学公式、可运行代码和详细注意事项,可作为单细胞生信分析的系统性教科书使用。