单细胞转录组分析 · 从入门到精通

本教材基于「生信学习库」知识库内容深度整理而成,涵盖:

  • 📖 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):

  1. 文库构建:将RNA逆转录为cDNA,片段化后加上接头(adaptor)
  2. 桥式PCR扩增:在flow cell表面进行簇生成(cluster generation)
  3. 测序:每次加入一种带荧光标记的dNTP,合成一个碱基后拍摄荧光信号,确定碱基类型
  4. 去荧光:切除荧光基团,进入下一个循环

第二代测序的特点:

  • 通量极高(一次运行可产生数十亿条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修饰信息)

三代测序技术对比:

特征 第一代(Sanger) 第二代(Illumina) 第三代(PacBio/Nanopore)
通量 极低 极高 高
读长 700-1000 bp 75-300 bp 10 kb - 数 Mb
准确率 >99.99% >99.5% 95-99%(HiFi>99.9%)
成本/碱基 极高 极低 中等
是否需要PCR 是 是 否
运行时间 数小时 1-3天 数分钟-数天
scRNA-seq适用性 不适用 主要平台 新兴应用

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协议。

原理:

  1. 单细胞通过FACS(荧光激活细胞分选)分选到96孔或384孔板中
  2. 裂解细胞,释放RNA
  3. 使用oligo-dT引物进行逆转录,合成第一链cDNA
  4. 利用模板切换(Template Switching)机制:当逆转录酶到达mRNA的5’端时,会添加数个非模板的胞嘧啶(C),然后加入含有核糖鸟嘌呤(rG)的模板切换寡核苷酸(TSO),逆转录酶切换到TSO模板继续延伸
  5. 通过PCR扩增全长的cDNA
  6. 构建测序文库,进行Illumina测序

优势:

  • 覆盖转录本全长,可检测可变剪接(Alternative Splicing)
  • 可检测SNP和等位基因特异性表达
  • 灵敏度高,对低表达基因检测能力更强
  • 适用于转录本异构体分析

劣势:

  • 不使用UMI,无法消除PCR扩增偏差
  • 通量低(通常96-384个细胞/批次)
  • 成本高(每个细胞成本约为10x的10-50倍)
  • 手工操作步骤多,实验周期长

Smart-seq3

Smart-seq3由Hagemann-Jensen等人于2020年开发,是Smart-seq2的升级版本。

关键改进:

  • 引入了5’端的UMI标记
  • 结合了全长式和标签式的优势
  • 通过5’端的UMI实现分子计数,同时保留全长转录本信息
  • 灵敏度进一步提升

原理:

  1. 与Smart-seq2类似的模板切换过程
  2. 但TSO中包含了UMI序列
  3. 因此每个cDNA分子的5’端带有唯一标识
  4. 测序时先读取5’端的UMI,再读取转录本序列

CEL-seq / CEL-seq2

CEL-seq(Cell Expression by Linear amplification and Sequencing)由Hashimshony等人于2012年开发,是最早的标签式协议之一。

原理:

  1. 使用FACS分选单细胞到孔板中
  2. 使用带有细胞barcode和poly-dT的引物进行逆转录
  3. 使用体外转录(IVT)进行线性扩增(而非PCR指数扩增)
  4. 测序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年推出。

原理:

  1. 细胞悬液与含有barcode的凝胶珠(Gel Bead)在微流控芯片中混合
  2. 每个凝胶珠表面带有约750,000条寡核苷酸引物,每条引物包含:
    • Illumina P5接头序列
    • 16 nt的细胞barcode(同一凝胶珠上的所有引物共享相同的barcode)
    • 10 nt的UMI(同一凝胶珠上的不同引物有不同的UMI)
    • 30 nt的poly-dT序列(用于捕获mRNA的poly-A尾)
  3. 在微流控芯片中,单个细胞与单个凝胶珠被包裹在同一个液滴(GEM, Gel Bead-in-Emulsion)中
  4. 液滴内的细胞被裂解,mRNA被带有barcode和UMI的引物捕获
  5. 逆转录产生带有barcode和UMI的cDNA
  6. 打破液滴,收集所有cDNA,混合进行PCR扩增和文库构建
  7. 测序时读取3’端序列

核心优势:

  • 超高通量:每次可捕获500-10,000个细胞(Chromium X可达数万)
  • 使用UMI消除PCR扩增偏差
  • 自动化程度高,实验流程标准化
  • 可重复性好
  • 成本相对较低

核心劣势:

  • 仅测3’端,无法检测可变剪接和转录本异构体
  • 对低表达基因的灵敏度低于Smart-seq2
  • Dropout率较高(部分基因在某些细胞中未被检测到)
  • 双细胞(Doublet)问题:两个细胞被包裹在同一个液滴中

Drop-seq

Drop-seq由Macosko等人于2015年开发,是10x Chromium的前驱技术。

原理:
与10x Chromium类似,也是基于液滴微流控技术:

  1. 细胞与带有barcode的微珠在微流控装置中共包裹在液滴中
  2. 每个微珠上带有约10^8条引物,包含细胞barcode和UMI
  3. 液滴内裂解细胞、捕获mRNA、逆转录

与10x的差异:

  • 使用普通微珠(非凝胶珠),细胞裂解和逆转录在液滴内完成
  • 细胞捕获率较低
  • 成本更低但数据质量略逊
  • 是学术界的开源方案

inDrop

inDrop由Klein等人于2015年开发,同样基于液滴微流控技术。

原理与特点:

  • 使用可降解的水凝胶微珠(degradable hydrogel beads)
  • 微珠上的barcode引物可以在液滴内释放
  • 使用IVT(体外转录)进行线性扩增,而非PCR
  • 线性扩增可避免PCR偏差,但扩增效率较低

1.2.3 全长式 vs 标签式详细对比表

特征 全长式(Smart-seq2) 标签式(10x/Drop-seq/inDrop)
测序覆盖 转录本全长 3’端(或5’端)
UMI 不使用(Smart-seq3使用) 使用
扩增方式 PCR(指数扩增) PCR或IVT
PCR偏差 无法通过UMI校正 通过UMI校正
可变剪接检测 ✅ 可以 ❌ 不可以
SNP检测 ✅ 可以 ❌ 不可以
灵敏度 高(低表达基因检测更强) 较低
Dropout率 较低 较高
通量 低(96-384细胞) 高(500-10,000+细胞)
单细胞成本 高(~$50-200/细胞) 低(~$0.5-5/细胞)
稀有细胞类型 不适合(通量低) 适合(通量高)
细胞分离 FACS(板式) 液滴微流控
实验标准化 低(手工操作多) 高(自动化)
适用场景 转录本异构体、SNP分析 大规模细胞图谱、稀有细胞发现

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分子需要经过以下步骤:

  1. 逆转录(RT):mRNA → cDNA
  2. 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去重的基本步骤:

  1. 对每个细胞中每个基因的所有reads,按照UMI序列分组
  2. 相同UMI的reads视为来自同一原始分子,只计为1
  3. 不同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的优势总结

优势 说明
消除PCR偏差 通过去重,reads数准确反映原始分子数
提高定量准确性 分子级别的计数,而非reads级别
简化归一化 UMI数据可用更简单的归一化方法
降低技术噪音 减少扩增引入的假变异

1.4 液滴式 vs 板式完整对比

1.4.1 液滴式(Droplet-based)

代表平台: 10x Genomics Chromium, Drop-seq, inDrop

原理: 利用微流控技术将单个细胞与单个barcode微珠包裹在水-油乳液的液滴中。每个液滴作为一个独立的反应体系,其中的细胞被裂解,mRNA被带有特异性barcode的引物捕获。

流程(以10x Chromium为例):

  1. 制备单细胞悬液(要求细胞活性>80%,无团块)
  2. 在Chromium Controller上将细胞悬液与凝胶珠和试剂混合
  3. 微流控芯片生成GEM液滴(约100,000个液滴/运行)
  4. GEM内裂解→逆转录→cDNA带barcode+UMI
  5. 打破GEM,收集cDNA,PCR扩增
  6. 构建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为例):

  1. 制备单细胞悬液
  2. 使用FACS将单个细胞分选到含裂解液的96/384孔板中
  3. 在孔中进行裂解→逆转录(含模板切换)→PCR扩增
  4. 每孔的cDNA单独进行文库构建(或使用indexing PCR合并)
  5. Illumina测序

优势:

  • 全长转录本覆盖(Smart-seq2/3)
  • 可检测可变剪接和转录本异构体
  • 灵敏度高,对低表达基因检测更好
  • Dropout率相对较低
  • 无双细胞问题(除非FACS分选错误)
  • 可结合FACS表面标记信息进行预筛选

劣势:

  • 通量极低(通常<1000细胞/实验)
  • 实验操作繁琐,手工步骤多
  • 单细胞成本高
  • 不使用UMI(Smart-seq2),PCR偏差无法校正
  • 难以发现稀有细胞类型(通量限制)

1.4.3 液滴式 vs 板式详细对比表

特征 液滴式 板式
代表平台 10x Chromium, Drop-seq, inDrop Smart-seq2/3, CEL-seq2, MARS-seq
细胞分离方式 微流控液滴包裹 FACS/手动分选到孔板
通量 高(500-10,000+) 低(96-384)
单细胞成本 低($0.5-5) 高($50-200)
测序覆盖 3’/5’端 全长
UMI 使用 Smart-seq2不使用;Smart-seq3使用
灵敏度 中等 高
Dropout率 较高 较低
双细胞率 0.8-8% ~0%(FACS精度高)
环境RNA污染 有 极少
可变剪接检测 ❌ ✅(全长协议)
实验自动化 高 低
可重复性 高 中等
表面标记预筛选 不方便 方便(FACS可用)
稀有细胞发现 适合 不适合

1.5 每种技术的适用场景

1.5.1 场景推荐表

研究场景 推荐技术 理由
大规模细胞图谱构建 10x Chromium 高通量、低成本、标准化
稀有细胞类型发现 10x Chromium / Chromium X 高通量才能捕获到稀有细胞
转录本异构体分析 Smart-seq2/3 全长覆盖,可检测可变剪接
等位基因特异性表达 Smart-seq2 全长覆盖,可检测SNP
少量细胞的深度分析 Smart-seq2 高灵敏度,低表达基因检测能力强
临床样本(细胞量少) Smart-seq2 适合少量细胞,通量不是瓶颈
胚胎发育研究 10x Chromium 需要捕捉发育过程中的稀有细胞状态
免疫细胞分群 10x Chromium + CITE-seq 高通量 + 表面蛋白信息
空间转录组验证 10x Chromium 高通量匹配空间转录组数据
低预算研究 Drop-seq 成本最低的液滴式方案

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 关键参考文献

  1. 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.
  2. Illumina测序: Bentley, D.R. et al. (2008). Accurate whole human genome sequencing using reversible terminator chemistry. Nature, 456(7218), 53-59.
  3. Smart-seq2: Picelli, S. et al. (2013). Smart-seq2 for sensitive full-length transcriptome profiling in single cells. Nature Methods, 10(11), 1096-1098.
  4. 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.
  5. 10x Genomics Chromium: Zheng, G.X.Y. et al. (2017). Massively parallel digital transcriptional profiling of single cells. Nature Communications, 8, 14049.
  6. 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.
  7. inDrop: Klein, A.M. et al. (2015). Droplet barcoding for single-cell transcriptomics applied to embryonic stem cells. Cell, 161(5), 1187-1201.
  8. CEL-seq: Hashimshony, T. et al. (2012). CEL-seq: single-cell RNA-seq by multiplexed linear amplification. Cell Reports, 2(3), 666-673.
  9. 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.
  10. UMI原理: Kivioja, T. et al. (2011). Counting absolute numbers of molecules using unique molecular identifiers. Nature Methods, 9(1), 72-74.
  11. scRNA-seq综述: Aldridge, S. & Teichmann, S.A. (2020). Single cell transcriptomics comes of age. Nature Communications, 11, 4307.
  12. 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的测序结构。

原理:

  1. Illumina测序仪输出的原始数据是BCL格式(每个循环每个cluster的碱基调用)
  2. mkfastq将BCL文件按照样本index(I7/I5)拆分为不同样本的FASTQ文件
  3. 对于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

# sample_sheet.csv 格式示例:
# Lane,Sample,Index
# 1,Sample1,SI-GA-A1
# 2,Sample2,SI-GA-B1

参数说明:

参数 说明
--run BCL文件所在目录路径
--output-dir FASTQ输出目录
--csv 样本信息表(含Lane, Sample, Index)
--localcores 使用的CPU核心数
--localmem 使用的内存(GB)

2.1.2 cellranger count:FASTQ→计数矩阵

功能: 对单个样本的FASTQ文件进行比对、过滤、UMI计数和基因表达定量,生成gene-barcode计数矩阵。

完整流程:

  1. 比对(Alignment)

    • 使用STAR(Spliced Transcripts Alignment to a Reference)将R2 reads比对到参考基因组
    • Cell Ranger内置了修改版的STAR,针对10x数据进行了优化
    • 比对时考虑了剪接位点,可以将跨越内含子的reads正确比对
  2. 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
  3. UMI计数(UMI Counting)

    • 对于比对到同一基因、具有相同cell barcode的reads
    • 按UMI分组,相同UMI视为来自同一原始分子
    • 对UMI也进行容错校正(编辑距离≤1的UMI合并)
    • 最终得到每个基因在每个细胞中的UMI计数
  4. 细胞呼叫(Cell Calling)

    • 区分真实细胞和空液滴(不含细胞但可能含有环境RNA)
    • Cell Ranger使用空液滴模型:先假设所有barcode都是空液滴,拟合背景分布
    • 然后识别总UMI数显著高于背景的barcode为真实细胞
    • 默认方法:对每个barcode的总UMI数取log10排序,寻找”拐点”(knee point)
  5. 生成输出文件

    • 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

# 参数说明:
# --id: 输出目录名称
# --transcriptome: 参考基因组路径(需预先用cellranger mkref构建)
# --fastqs: FASTQ文件所在目录
# --sample: 样本名称(与mkfastq输出一致)
# --expect-cells: 预期细胞数(帮助细胞呼叫算法)
# --localcores: CPU核心数
# --localmem: 内存(GB)

构建参考基因组:

1
2
3
4
5
6
7
8
9
# 下载10x参考基因组
# 人类:refdata-gex-GRCh38-2020-A
# 小鼠:refdata-gex-mm10-2020-A

# 或自定义参考基因组
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
# 创建aggregation CSV文件 (aggregation.csv)
# library_id,chemical,lanes,molecule_h5
# sample1,three_prime_v3,1,/path/to/sample1/outs/molecule_info.h5
# sample2,three_prime_v3,1,/path/to/sample2/outs/molecule_info.h5

# 运行aggr
cellranger aggr \
--id=pooled_analysis \
--csv=aggregation.csv \
--normalize=map \
--localcores=32 \
--localmem=128

# 参数说明:
# --id: 输出目录名称
# --csv: 聚合信息表
# --normalize: 归一化方法(map=基于映射reads数;none=不归一化)

注意事项:

  • 所有文库必须使用相同版本的参考基因组
  • 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索引
salmon index \
-t transcriptome.fa \
-i salmon_index \
-k 31

# 运行salmon alevin(伪比对+初始定量)
salmon alevin \
-l ISR \
-i salmon_index \
-1 R1.fastq.gz \
-2 R2.fastq.gz \
-o alevin_output \
-p 16 \
--chromiumV3

# 参数说明:
# -l ISR: 文库类型(I=反向,S=有义链,R=配对)
# -i: salmon索引路径
# -1: R1 FASTQ(含barcode+UMI)
# -2: R2 FASTQ(含cDNA序列)
# -o: 输出路径
# -p: 线程数
# --chromiumV3: 指定10x Chromium v3化学方案

步骤2:生成允许列表(permit list)

1
2
3
4
5
6
7
8
9
10
11
12
# 识别真实细胞的barcode
alevin-fry generate-permit-list \
-i alevin_output \
-o alevin_fry_output \
--expected-ori 5000 \
--knee-dist

# 参数说明:
# -i: salmon alevin的输出路径
# -o: alevin-fry的输出路径
# --expected-ori: 预期细胞数
# --knee-dist: 使用knee-point方法识别细胞

步骤3:定量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# UMI去重和定量
alevin-fry quantify \
-i alevin_fry_output \
-o alevin_fry_output/quant \
-m salmon_index \
--use-mtx \
--rad

# 参数说明:
# -i: alevin-fry输出路径
# -o: 定量结果输出路径
# -m: 映射文件(来自salmon索引)
# --use-mtx: 以MTX格式输出计数矩阵
# --rad: 使用resolution-aware deduplication

输出文件:

  • 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 sc
import pandas as pd
from scipy.io import mmread
from scipy.sparse import csr_matrix

# 读取计数矩阵
matrix = 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

# 创建AnnData对象
adata = sc.AnnData(
X=matrix.T, # 转置:行为细胞,列为基因
obs=pd.DataFrame(index=barcodes),
var=pd.DataFrame(index=genes)
)

print(adata)
# AnnData object with n_obs × n_vars = 5000 × 33538

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

特征 Cell Ranger (修改版STAR) alevin-fry (salmon伪比对)
比对方式 精确比对到基因组 伪比对到转录组
速度 较慢(~2-4小时/样本) 快(~30分钟/样本)
内存需求 高(~32-64 GB) 低(~8-16 GB)
剪接位点检测 ✅ 可检测新型剪接 ❌ 依赖已知转录本
基因组比对 ✅ 提供BAM文件 ❌ 无BAM输出
定量准确性 高 高(与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 sc

# 方法2:在Python中合并多个样本
adata1 = 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)
# AnnData object with n_obs × n_vars = 10000 × 33538

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 sc

# 方法1:读取过滤后的计数矩阵
adata = sc.read_10x_mtx(
'path/to/filtered_feature_bc_matrix/',
var_names='gene_symbols',
cache=True
)
print(adata)
# AnnData object with n_obs × n_vars = 5000 × 33538

# 方法2:读取h5格式
adata = sc.read_10x_h5(
'path/to/filtered_feature_bc_matrix.h5'
)

# 方法3:读取未过滤的矩阵(用于SoupX环境RNA校正)
adata_raw = sc.read_10x_mtx(
'path/to/raw_feature_bc_matrix/',
var_names='gene_symbols'
)

# 保存为h5ad格式(后续分析使用)
adata.write_h5ad('raw_counts.h5ad')

2.7 注意事项

  1. Cell Ranger版本一致性:同一项目的所有样本应使用相同版本的Cell Ranger,不同版本的结果可能存在细微差异
  2. 参考基因组版本:确保使用与实验设计一致的参考基因组(如GRCh38 vs GRCh37)
  3. 内存需求:Cell Ranger count通常需要32-64 GB内存,建议在服务器上运行
  4. FASTQ文件完整性:运行前检查FASTQ文件大小和read数量是否正常
  5. 细胞悬液质量:Cell Ranger的细胞呼叫算法依赖于细胞barcode的UMI分布,低质量悬液可能导致错误呼叫
  6. 双细胞率:10x Chromium的双细胞率约为每1000个细胞0.8%,即8000个细胞中约有64个双细胞
  7. 环境RNA污染:死细胞释放的mRNA会污染液滴,导致计数偏差,可通过SoupX校正
  8. alevin-fry的定量模式:cr-like模式输出与Cell Ranger最接近的结果;full模式提供更精细的UMI去重
  9. 多映射reads处理:Cell Ranger将多映射reads分配给最可能的基因,alevin-fry默认丢弃多映射reads
  10. 数据备份:始终保留原始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
# 访问X
adata.X # 完整矩阵
adata.X[0:5, 0:5] # 前5个细胞和5个基因的子矩阵
adata.X.sum(axis=1) # 每个细胞的总counts

obs — 细胞元数据

  • 类型:pandas.DataFrame
  • 行索引:细胞barcode
  • 每列是一个细胞级别的注释信息
1
2
3
4
5
6
7
8
9
10
11
12
# 常见obs列
# - 'n_genes_by_counts': 每个细胞检测到的基因数
# - 'total_counts': 每个细胞的总UMI数
# - 'pct_counts_mt': 线粒体基因比例
# - 'cell_type': 细胞类型注释
# - 'batch': 批次信息
# - 'leiden': Leiden聚类结果
# - 'sample': 样本来源

# 添加细胞元数据
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
# 常见var列
# - 'gene_ids': Ensembl基因ID
# - 'mt': 是否为线粒体基因
# - 'ribo': 是否为核糖体基因
# - 'n_cells_by_counts': 表达该基因的细胞数
# - 'mean_counts': 基因的平均counts
# - 'highly_variable': 是否为高可变基因
# - 'dispersions': 基因离散度

# 添加基因元数据
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
# 常见obsm键
# - 'X_pca': PCA降维结果 (n_obs × n_pcs)
# - 'X_umap': UMAP嵌入 (n_obs × 2)
# - 'X_tsne': t-SNE嵌入 (n_obs × 2)
# - 'X_mde': MDE嵌入 (n_obs × 2)
# - 'X_diffmap': 扩散映射 (n_obs × n_components)

# 访问obsm
adata.obsm['X_pca'] # PCA结果矩阵
adata.obsm['X_umap'] # UMAP坐标

# omicverse特殊格式
adata.obsm['scaled|original|X_pca'] # 缩放后的PCA
adata.obsm['counts|original|X_pca'] # 基于原始计数的PCA

varm — 基因的多维注释

  • 类型:Dict[str, numpy.ndarray]
  • 每个值的维度:n_vars × m
  • 较少使用,可存储基因的PCA载荷(loadings)等
1
2
# 示例:存储PCA载荷
adata.varm['PCs'] = pca_model.components_.T # n_vars × n_pcs

uns — 非结构化元数据

  • 类型:Dict
  • 可存储任意Python对象
  • 通常存储分析参数、颜色映射、邻域图信息等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 常见uns键
# - 'hvg': 高可变基因选择参数和结果
# - 'neighbors': KNN邻域图参数
# - 'leiden': Leiden聚类参数
# - 'pca': PCA参数
# - 'rank_genes_groups': 差异表达基因结果

# 添加自定义元数据
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
# 常见layers用法
adata.layers['counts'] = adata.X.copy() # 保存原始UMI计数
adata.layers['log1p_norm'] = log_normalized # 移位对数归一化
adata.layers['scran_norm'] = scran_normalized # scran归一化
adata.layers['pearson_residuals'] = pearson # Pearson残差

# 在不同layers之间切换
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
# 常见obsp键
# - 'distances': KNN距离矩阵
# - 'connectivities': KNN连接性矩阵(用于Leiden聚类)

# 访问
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 np
import pandas as pd
import anndata as ad
from scipy.sparse import csr_matrix

# ========== 方法1:从计数矩阵创建 ==========
# 创建稀疏计数矩阵(100个细胞 × 2000个基因)
counts = csr_matrix(np.random.poisson(1, size=(100, 2000)), dtype=np.float32)
adata = ad.AnnData(counts)
print(adata)
# AnnData object with n_obs × n_vars = 100 × 2000

# ========== 方法2:带完整注释创建 ==========
# 计数矩阵
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),
}

# 创建AnnData对象
adata = ad.AnnData(
X=counts,
obs=obs,
var=var,
obsm=obsm,
uns=uns,
layers=layers,
)

print(adata)
# AnnData object with n_obs × n_vars = 1000 × 5000
# obs: 'cell_type', 'batch', 'n_genes'
# var: 'gene_id', 'mt'
# obsm: 'X_pca', 'X_umap'
# uns: 'experiment', 'organism'
# layers: 'counts', 'normalized'

切片(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] # 前500个细胞
print(adata_subset.shape) # (500, 5000)

# 通过布尔掩码切片
mask = adata.obs['cell_type'] == 'T cell'
adata_t_cells = adata[mask]
print(adata_t_cells.shape) # (~333, 5000)

# 通过细胞名切片
adata_selected = adata[['cell_0', 'cell_1', 'cell_2']]

# ========== 基因切片 ==========
# 通过基因名切片
adata_genes = adata[:, ['gene_0', 'gene_1', 'gene_2']]
print(adata_genes.shape) # (1000, 3)

# 通过布尔掩码切片(如选择高可变基因)
mask_hvg = adata.var['highly_variable'] == True # 假设已计算
adata_hvg = adata[:, mask_hvg]

# ========== 同时切片 ==========
adata_sub = adata[:500, :1000] # 前500个细胞,前1000个基因

# ========== 注意:切片返回视图(View) ==========
print(adata_subset.is_view) # True
# 修改视图会影响原始对象
# 若需独立副本:
adata_copy = adata_subset.copy()
print(adata_copy.is_view) # False

合并(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 ad

# ========== 沿观测(细胞)维度合并 ==========
adata1 = 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) # (250, 2000)

# 外连接(保留所有基因,缺失值填充为0)
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) # (100, 1500)

# ========== 合并时的merge参数 ==========
# merge='same': 相同值保留,不同值设为缺失(默认)
# merge='unique': 仅保留唯一值
# merge='first': 保留第一个对象的值
# merge=None: 不合并元数据

读写AnnData对象

1
2
3
4
5
6
7
8
9
10
11
12
13
# 保存为h5ad文件
adata.write_h5ad('my_data.h5ad', compression='gzip')

# 读取h5ad文件
adata_loaded = ad.read_h5ad('my_data.h5ad')

# 大文件可使用backed模式(按需读取,不全部载入内存)
adata_backed = ad.read_h5ad('my_data.h5ad', backed='r')
# 注意:backed模式下只能读取,不能修改

# 从10x Genomics输出读取
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 MuData
import anndata as ad
import numpy as np
import pandas as pd
from scipy.sparse import csr_matrix

# 创建RNA模态数据
n_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)

# 创建蛋白质模态数据(CITE-seq)
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)]),
)

# 创建MuData对象
mdata = MuData({'rna': rna_data, 'prot': prot_data})
print(mdata)
# MuData object with n_obs × n_vars = 1000 × 20100
# 2 modalities
# rna: 1000 x 20000
# prot: 1000 x 100

# 访问单个模态
print(mdata['rna']) # 返回RNA的AnnData对象

# 更新MuData(在修改模态后)
mdata.update()

# 保存和读取
mdata.write('my_mudata.h5mu')
mdata_loaded = MuData.read('my_mudata.h5mu')

# backed模式读取
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
# 创建SCE对象
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对象
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") # PCA结果

3.4 Python/R互操作

在单细胞分析中,Python(scanpy)和R(Seurat/SingleCellExperiment)是最常用的两大分析生态。互操作性允许研究者在两种语言之间灵活切换,利用各自的优势工具。

3.4.1 数据格式映射表

数据 AnnData (Python) SingleCellExperiment (R) Seurat (R)
活跃表达矩阵 adata.X assay(sce) GetAssayData(seu)
替代表达矩阵 adata.layers['counts'] counts(sce) GetAssay(seu)@counts
细胞元数据 adata.obs colData(sce) seu@meta.data
基因元数据 adata.var rowData(sce) GetAssay(seu)@meta.features
降维结果 adata.obsm reducedDim(sce) seu@reductions
细胞ID adata.obs_names colnames(sce) colnames(seu)
基因ID adata.var_names rownames(sce) rownames(seu)
细胞相似性图 adata.obsp — seu@graphs

3.4.2 转换工具

方法1:zellkonverter(R → Python)

1
2
3
4
5
6
7
8
9
# 在R中安装
BiocManager::install("zellkonverter")

# 将SCE保存为h5ad格式
library(zellkonverter)
writeH5AD(sce, "my_data.h5ad")

# 从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
# 在Python中安装
# pip install anndata2ri

import anndata2ri
from rpy2.robjects import globalenv
import anndata as ad

# 激活anndata2ri转换
anndata2ri.activate()

# 读取AnnData对象
adata = ad.read_h5ad('my_data.h5ad')

# 将AnnData传递到R环境
globalenv['adata'] = adata

# 在R中(通过rpy2的%%R magic):
# library(SingleCellExperiment)
# sce <- adata # 自动转换为SCE对象

方法3:MuDataSeurat(MuData ↔ Seurat)

1
2
3
4
5
6
7
8
9
# 安装
remotes::install_github("zilongstudio/MuDataSeurat")

# 从h5ad/h5mu读取到Seurat
library(MuDataSeurat)
seu <- ReadH5AD("my_data.h5ad")

# 从h5mu读取多模态数据到Seurat
seu <- ReadH5MU("my_data.h5mu")

方法4:sceasy

1
2
3
4
5
6
7
8
9
10
11
# pip install sceasy

import sceasy
import anndata as ad

# AnnData → Seurat (保存为RDS)
adata = ad.read_h5ad('my_data.h5ad')
sceasy.convert(adata, 'my_data.rds', output_file_type='anndata2seurat')

# Seurat → AnnData
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 ro
from rpy2.robjects import pandas2ri
from rpy2.robjects.conversion import localconverter

# 在Python中调用R的scran归一化
import anndata2ri
anndata2ri.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)

# 传递数据到R
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

# 在R中运行scran
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 注意事项

  1. 数据类型一致性:R和Python对稀疏矩阵的支持不同,转换时注意矩阵格式
  2. 行/列方向:R中表达矩阵通常是基因×细胞,Python中是细胞×基因,需要转置
  3. 基因名规范:确保基因名在Python和R中一致(如使用Ensembl ID而非symbol)
  4. 版本兼容:zellkonverter和anndata2ri的版本需要与anndata包版本匹配
  5. 内存管理:大对象在Python和R之间传递时需要双倍内存
  6. 大文件处理:对于超大数据集,建议通过磁盘文件(h5ad/h5mu)进行转换,而非内存传递

3.5 关键参考文献

  1. 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.
  2. Scanpy: Wolf, F.A., Angerer, P. & Theis, F.J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology, 19, 15.
  3. MuData: Danilenko, A. et al. (2022). MuData: multimodal data object for Python. Journal of Open Source Software, 7(73), 4176.
  4. SingleCellExperiment: Amezquita, R.A. et al. (2020). Orchestrating single-cell analysis with Bioconductor. Nature Methods, 17(2), 137-145.
  5. Cell Ranger: 10x Genomics. Cell Ranger - Official Documentation. https://support.10xgenomics.com/single-cell-gene-expression/software/pipelines/latest/what-is-cell-ranger
  6. 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.
  7. STAR: Dobin, A. et al. (2013). STAR: ultrafast universal RNA-seq aligner. Bioinformatics, 29(1), 15-21.
  8. 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 sc

# 计算QC指标
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
)

# nUMIs存储在 adata.obs['total_counts']
print(adata.obs['total_counts'].describe())
# count 14814.000000
# mean 12000.523456
# std 8000.123456
# min 50.000000
# 25% 7000.000000
# 50% 11000.000000
# 75% 16000.000000
# max 150000.000000

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过滤的优势

特征 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 np
import pandas as pd

def 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:.2f}, MAD={mad:.2f}, "
f"range=[{lower:.2f}, {upper:.2f}], "
f"filtered={(~(values >= lower) | ~(values <= upper)).sum()}")

return adata[mask].copy()

# 使用MAD过滤
metrics = {
'total_counts': 'lower', # 低nUMIs过滤(去除低质量细胞)
'n_genes_by_counts': 'lower', # 低基因数过滤
'pct_counts_mt': 'upper', # 高线粒体比例过滤
}

adata_filtered = mad_filter(adata, metrics, nmads=5)

4.4 Seurat式手动阈值 vs MAD自动阈值对比

特征 Seurat式手动阈值 MAD自动阈值
方法 人为设定固定阈值 基于数据分布自动计算
示例 nUMIs > 500, mito < 15% nUMIs > median - 5*MAD
优点 简单直观、易于理解 客观、自适应、可重复
缺点 主观性强、每个数据集需调整 可能过滤掉生物学有意义的极端值
适用 有经验的用户、特定阈值已知 大规模分析、自动化流程
推荐 作为初步筛选 作为主要方法(推荐)

Seurat式手动阈值代码:

1
2
3
4
5
6
7
# Seurat风格手动阈值过滤
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的工作原理:

  1. 使用raw_feature_bc_matrix(未过滤的矩阵,包含所有barcode)估计环境RNA的组成
  2. 假设空液滴(不含细胞的barcode)中的RNA完全来自环境RNA
  3. 通过低表达空液滴的基因表达谱构建环境RNA的轮廓(soup profile)
  4. 估计每个细胞中环境RNA的污染比例(soup fraction,$\rho$)
  5. 对每个基因在每个细胞中的计数进行校正:

$$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
# SoupX通常在R中运行,可通过rpy2在Python中调用
# 以下是R代码示例

# 安装SoupX
# install.packages("SoupX")

library(SoupX)

# 方法1:从10x Cell Ranger输出加载
sc <- load10X(data_dir = "path/to/cellranger_output/")

# 方法2:手动加载
sc <- SoupX::load10X(
data_dir = "path/to/cellranger_output/",
keep_droplets = FALSE # TRUE则保留空液滴
)

# 自动估计污染比例
sc <- autoEstCont(sc)

# 手动指定污染比例
# sc <- setContFraction(sc, 0.05) # 设置5%的污染比例

# 校正计数矩阵
sc <- adjustCounts(sc)

# 导出校正后的矩阵
counts_corrected <- getCounts(sc)

# 保存为10x格式
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 ro
from rpy2.robjects import numpy2ri, pandas2ri
from rpy2.robjects.conversion import localconverter

# 激活转换器
numpy2ri.activate()
pandas2ri.activate()

# 在R中运行SoupX
ro.r('''
library(SoupX)

# 加载数据
sc <- load10X("path/to/cellranger_output/")

# 自动估计污染比例
sc <- autoEstCont(sc)

# 校正
sc <- adjustCounts(sc)

# 获取校正后的计数矩阵
counts_corrected <- getCounts(sc)
''')

# 将结果转回Python
counts_corrected = ro.r('counts_corrected')

4.5.3 注意事项

  1. 需要raw_feature_bc_matrix:SoupX需要未过滤的计数矩阵来估计环境RNA,但该文件通常不公开提供
  2. 可能产生非整数计数:校正后的矩阵可能包含小数值,某些下游工具可能不支持
  3. 仅在污染严重时使用:如果环境RNA污染不严重,不建议使用SoupX,因为校正可能引入新的偏差
  4. 污染比例估计:自动估计不一定准确,建议手动检查并调整

4.6 双细胞检测

4.6.1 Scrublet原理与代码

原理: Scrublet(Single-cell Remover of Doublets)由Wolock等人于2019年开发,是一种基于模拟双细胞的Python双细胞检测工具。

核心思路:

  1. 模拟双细胞: 从实际数据中随机选择两个细胞的表达谱,将它们相加,创建模拟双细胞(synthetic doublet)
  2. 构建KNN图: 在PCA空间中,对真实细胞和模拟双细胞构建KNN图
  3. 计算双细胞评分: 对每个真实细胞,计算其K个最近邻中模拟双细胞的比例,作为双细胞评分(doublet score)
  4. 阈值判定: 基于双细胞评分的分布,设定阈值将细胞分类为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 scr
import scipy.io

# 加载计数矩阵
counts_matrix = scipy.io.mmread('counts.mtx').T.tocsr()

# 初始化Scrublet
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()

# 将结果添加到AnnData
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}")

参数说明:

参数 说明 默认值
expected_doublet_rate 预期双细胞率 0.06 (6%)
min_counts 基因最小counts阈值 2
min_cells 最小表达细胞数 3
min_gene_variability_pctl 基因变异度百分位 85
n_prin_comps PCA组件数 30

4.6.2 scDblFinder原理与代码

原理: scDblFinder由Germain等人于2022年开发,是基于R/Bioconductor的双细胞检测工具。与Scrublet不同,scDblFinder使用机器学习分类器(梯度提升树)而非KNN方法。

核心思路:

  1. 模拟双细胞: 类似Scrublet,随机选择两个细胞创建模拟双细胞
  2. 特征工程: 为每个细胞(真实+模拟)计算一系列特征:
    • KNN中模拟双细胞的比例
    • 表达矩阵的PCs
    • 聚类信息
    • nUMIs、n_genes等QC指标
  3. 训练分类器: 使用模拟数据训练梯度提升分类器
  4. 预测: 对真实细胞进行双细胞概率预测

优势(相比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
# 安装scDblFinder
BiocManager::install("scDblFinder")

library(scDblFinder)
library(SingleCellExperiment)

# 创建SCE对象(或从文件读取)
sce <- SingleCellExperiment(
assays = list(counts = counts_matrix)
)

# 运行scDblFinder
sce <- scDblFinder(sce)

# 查看结果
table(sce$scDblFinder.class)
# singlet doublet
# 7500 500

# 双细胞评分
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 anndata2ri
anndata2ri.activate()

# 将AnnData传递到R
from rpy2.robjects import globalenv
globalenv['adata'] = adata

# 在R中运行scDblFinder
%%R
library(scDblFinder)
library(SingleCellExperiment)

# 从AnnData创建SCE
sce <- SingleCellExperiment(assays = list(counts = t(adata$X)))

# 运行scDblFinder
sce <- scDblFinder(sce, dbr = 0.06) # dbr: expected doublet rate

# 将结果传回Python
doublet_class = %R sce$scDblFinder.class
doublet_score = %R sce$scDblFinder.score

adata.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 ov
import scanpy as sc

# 读取数据
adata = 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)]')

# 计算QC指标
sc.pp.calculate_qc_metrics(
adata,
qc_vars=['mt', 'ribo', 'hb'],
percent_top=None,
log1p=False,
inplace=True
)

# 一站式QC(Seurat模式:手动阈值)
adata = ov.pp.qc(
adata,
mode='seurat',
tresh={
'mito_perc': 0.05, # 线粒体基因比例上限5%
'nUMIs': 500, # nUMIs下限500
'detected_genes': 250, # 检测基因数下限250
},
min_cells=3, # 基因至少在3个细胞中表达
min_genes=200, # 细胞至少表达200个基因
)

# 一站式QC(MAD模式:自动阈值)
adata = ov.pp.qc(
adata,
mode='mads',
nmads=5, # 5倍MAD
min_cells=3,
min_genes=200,
)

# 保存QC后的数据
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 ov
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

ov.ov_plot_set()

# ============================================
# Step 1: 读取数据
# ============================================
adata = sc.read_10x_mtx('filtered_feature_bc_matrix/', var_names='gene_symbols')
print(f"原始数据: {adata.shape}")

# ============================================
# Step 2: 标记特殊基因
# ============================================
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)]')

# ============================================
# Step 3: 计算QC指标
# ============================================
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():.0f} (median)")
print(f" n_genes_by_counts: {adata.obs['n_genes_by_counts'].median():.0f} (median)")
print(f" pct_counts_mt: {adata.obs['pct_counts_mt'].median():.1f}% (median)")

# ============================================
# Step 4: 可视化QC指标分布
# ============================================
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()

# ============================================
# Step 5: 过滤低质量基因
# ============================================
# 基因至少在3个细胞中表达
sc.pp.filter_genes(adata, min_cells=3)
print(f"过滤低质量基因后: {adata.shape}")

# ============================================
# Step 6: 双细胞检测(Scrublet)
# ============================================
import scrublet as scr

scrub = 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:.1f}%)")

# 过滤双细胞
adata = adata[~adata.obs['predicted_doublet']].copy()
print(f"去除双细胞后: {adata.shape}")

# ============================================
# Step 7: 基于MAD的质量过滤
# ============================================
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

# 计算各指标的MAD阈值
nmads = 5

lower_nUMIs, med, mad = compute_mad_threshold(
adata.obs['total_counts'], nmads, 'lower'
)
print(f"nUMIs: median={med:.0f}, MAD={mad:.0f}, threshold={lower_nUMIs:.0f}")

lower_genes, med, mad = compute_mad_threshold(
adata.obs['n_genes_by_counts'], nmads, 'lower'
)
print(f"n_genes: median={med:.0f}, MAD={mad:.0f}, threshold={lower_genes:.0f}")

upper_mt, med, mad = compute_mad_threshold(
adata.obs['pct_counts_mt'], nmads, 'upper'
)
print(f"pct_mt: median={med:.1f}%, MAD={mad:.1f}%, threshold={upper_mt:.1f}%")

# 应用过滤
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}")

# ============================================
# Step 8: 保存QC后的数据
# ============================================
adata.write_h5ad('quality_control.h5ad')
print(f"QC完成! 最终数据: {adata.shape}")

4.9 注意事项和常见陷阱

  1. 不要过度过滤:过滤太严格可能丢失生物学有意义的细胞。例如,某些小型细胞(如血小板)天然具有低nUMIs和低基因数。
  2. 线粒体基因比例的陷阱:高线粒体比例不一定意味着低质量细胞。某些细胞类型(心肌细胞、运动神经元、肾小管细胞)天然具有高线粒体含量。建议结合其他指标联合判断。
  3. MAD倍数选择:nmads=5是较宽松的阈值(约3.4σ),nmads=3更严格(约2σ)。建议先用5,再根据数据特点调整。
  4. 双细胞检测的时机:应在归一化之前进行双细胞检测,因为归一化可能改变双细胞的特征。
  5. SoupX的使用条件:需要raw_feature_bc_matrix文件,且仅在环境RNA污染严重时使用。轻度污染不建议使用。
  6. QC指标的相关性:nUMIs和n_genes高度相关,不要对两者都进行严格的上下限过滤,否则可能过度去除细胞。
  7. 批次差异:不同批次的QC指标分布可能不同,建议每个批次独立设定阈值,或使用MAD方法自动适应。
  8. 细胞类型特异性:过滤前考虑是否了解数据中可能存在的细胞类型及其预期QC特征。
  9. 空液滴检测:对于10x数据,Cell Ranger已经进行了细胞呼叫,但有时会遗漏一些真实细胞。可以检查raw_feature_bc_matrix中的低UMI barcode是否包含被遗漏的细胞。
  10. 过滤后重新计算QC指标:如果对数据进行了大幅过滤,建议重新计算QC指标并检查分布是否合理。

5. 归一化

5.1 为什么需要归一化

5.1.1 技术变异的来源

单细胞RNA测序数据中,细胞间的计数差异并不完全反映真实的生物学差异。由于技术原因,即使同一类型的细胞,其测序计数深度也可能存在巨大差异。这些技术变异的来源包括:

  1. 捕获效率差异:不同细胞的mRNA被捕获的效率不同
  2. 逆转录效率差异:不同细胞的mRNA被逆转录为cDNA的效率不同
  3. PCR扩增偏差:不同的cDNA分子被扩增的倍数不同
  4. 测序深度差异:不同细胞的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 sc
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

# 方法1:使用scanpy的标准流程
# 先归一化到中位计数深度
scales_counts = sc.pp.normalize_total(adata, target_sum=None, inplace=False)

# 再进行log1p变换
adata.layers["log1p_norm"] = sc.pp.log1p(scales_counts["X"], copy=True)

# 方法2:直接修改adata.X(推荐用于下游分析)
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的方法:

  1. 对细胞进行初步聚类(粗粒度),将相似细胞分组
  2. 在每组内,将多个细胞的counts求和(pooling),形成”伪细胞”
  3. 对伪细胞计算尺寸因子,减少零膨胀的影响
  4. 通过线性回归从伪细胞的尺寸因子中解卷积出单个细胞的尺寸因子

数学公式:

假设将$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 sc
import numpy as np
from scipy.sparse import csr_matrix

# scran需要R环境,以下是通过rpy2调用的完整代码

# Step 1: 准备数据 - 初步聚类
adata_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')

# Step 2: 将数据传递到R环境
import rpy2.robjects as ro
from rpy2.robjects import numpy2ri, pandas2ri
from rpy2.robjects.conversion import localconverter

data_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"]

# Step 3: 在R中计算尺寸因子
ro.r('''
library(scran)
library(BiocParallel)

size_factors <- sizeFactors(
computeSumFactors(
SingleCellExperiment(list(counts=data_mat)),
clusters = input_groups,
min.mean = 0.1,
BPPARAM = MulticoreParam()
)
)
''')

# Step 4: 获取尺寸因子并应用归一化
size_factors = ro.r('size_factors')
adata.obs["size_factors"] = size_factors

# 手动应用scran归一化
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 sc
from scipy.sparse import csr_matrix
import seaborn as sns
import matplotlib.pyplot as plt

# 方法1:使用scanpy的experimental模块
analytic_pearson = sc.experimental.pp.normalize_pearson_residuals(adata, inplace=False)
adata.layers["analytic_pearson_residuals"] = csr_matrix(analytic_pearson["X"])

# 方法2:使用omicverse
import omicverse as ov
adata_pearson = ov.pp.preprocess(adata, mode='pearson|pearson', n_HVGs=2000)

# 可视化Pearson残差的分布
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 三种方法完整对比

特征 移位对数 scran反卷积 Pearson残差
核心思想 方差稳定化(delta方法) 更准确的尺寸因子 统计模型残差
公式 $\log(y/s + 1)$ $\log(y/s_{\text{scran}} + 1)$ $(y - \hat\mu)/\sqrt{\hat\mu + \hat\mu^2/\theta}$
输出值范围 $\geq 0$ $\geq 0$ $(-\infty, +\infty)$
需要伪计数 是 是 否
需要log变换 是 是 否
保留生物变异 中等 中等 高
方差稳定化 好 好 最好
稀有细胞检测 一般 一般 好
计算速度 快 慢(需R环境) 中等
实现工具 scanpy scran (R) scanpy (experimental)
Nature Methods基准 ⭐ 推荐用于大多数任务 适合批次校正 适合HVG选择

5.7 基于Nature Method基准测试的推荐

2023年4月Nature Methods发表的基准测试(Ahlmann-Eltze & Huber, 2023)比较了22种归一化算法,主要结论:

  1. 移位对数在大多数任务中表现良好,是最稳健的通用选择
  2. Pearson残差在选择高可变基因方面表现最佳
  3. scran在批次校正任务中有优势
  4. 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 ov
import scanpy as sc

# 读取QC后的数据
adata = sc.read_h5ad('quality_control.h5ad')

# 保存原始计数到layers
adata.layers['counts'] = adata.X.copy()

# 方法1:移位对数 + Pearson HVG选择(推荐)
adata_log = ov.pp.preprocess(
adata.copy(),
mode='shiftlog|pearson',
n_HVGs=2000,
)

# 方法2:Pearson残差归一化 + Pearson HVG选择
adata_pearson = ov.pp.preprocess(
adata.copy(),
mode='pearson|pearson',
n_HVGs=2000,
)

# 对比两种归一化结果
import matplotlib.pyplot as plt
import seaborn as sns

fig, 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 注意事项

  1. 始终保留原始计数:归一化后,将原始UMI计数保存在adata.layers['counts']中,因为某些下游分析(如差异表达)需要原始计数
  2. 选择合适的target_sum:Scanpy默认使用中位数,这在大多数情况下是合理的。避免使用过小或过大的值
  3. 归一化方法与下游分析的兼容性:差异表达分析通常需要原始计数(或归一化计数),而不是log变换或Pearson残差
  4. Pearson残差的输出范围:Pearson残差可为负值,某些可视化工具可能不兼容
  5. scran的R依赖:scran需要R环境和BiocParallel,配置可能较复杂
  6. 不要对归一化后的数据再次归一化:每种归一化方法只需应用一次
  7. 测序深度极低的细胞:如果某些细胞的测序深度极低(<500 UMIs),归一化可能不可靠,建议先通过QC过滤
  8. 多批次数据:不同批次的测序深度可能差异很大,建议在归一化前先进行批次校正,或使用批次感知的归一化方法

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
# 安装scry
BiocManager::install("scry")

library(scry)

# 计算偏离度
deviances <- devianceFeatureSelection(counts_matrix)

# 选择top基因
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年提出,通过拟合基因均值-方差关系来选择高可变基因。

数学步骤:

  1. 对每个基因$g$,计算其均值$\bar{x}_g$和方差$\sigma_g^2$

  2. 对均值和方差进行log变换,拟合二阶多项式(局部多项式回归):

$$\log(\sigma_g^2) = a \cdot [\log(\bar{x}_g)]^2 + b \cdot \log(\bar{x}_g) + c$$

  1. 对每个基因计算标准化方差(VST后的方差):

$$\text{standardized variance}_g = \frac{\sigma_g^2}{\hat{\sigma}_g^2}$$

其中$\hat{\sigma}_g^2$是从拟合曲线中得到的期望方差。

  1. 按标准化方差排序,选择top基因

与偏离度法的关键区别: VST方法试图校正均值-方差关系,而偏离度法直接基于多项式模型计算统计量。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import scanpy as sc

# Seurat v3 VST方法(需要原始计数数据)
adata_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())
# False 18171
# True 2000

6.2.3 基因离散度法(Seurat Classic)

原理:

传统的Seurat离散度方法,步骤如下:

  1. 计算每个基因的均值(mean)和离散度(dispersion = variance / mean)
  2. 将基因按均值分为20个bin
  3. 在每个bin内,对离散度进行z-score标准化
  4. 选择标准化离散度最高的基因

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 sc

# 先归一化和log变换(Seurat classic需要归一化后的数据)
sc.pp.normalize_total(adata)
sc.pp.log1p(adata)

# 方法1:指定HVG数量
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())
# False 18171
# True 2000

# 方法2:指定离散度和均值阈值
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 三种方法完整对比

特征 偏离度法(Deviance) 基因方差法(VST) 离散度法(Seurat Classic)
输入数据 原始计数 原始计数 归一化+log后的数据
零模型 多项式分布 均值-方差回归 分bin z-score
是否需要归一化 否 否 是
基准测试表现 ⭐ 最佳 好 一般
计算速度 快 中等 快
scanpy实现 无(需R) flavor='seurat_v3' flavor='seurat'
受均值-方差关系影响 否 否(已校正) 部分
推荐基因数 4000 2000-3000 2000-3000

三种方法的重叠度: 在典型数据集中,三种方法选择的2000个HVGs可能只有约600-700个基因是共同的,说明方法选择对下游分析有显著影响。


6.4 推荐方法:偏离度法 + 4000基因

基于基准测试的结果,偏离度法(Deviance)配合4000个基因是目前推荐的HVG选择策略:

  1. 偏离度法在特征选择基准测试中表现最佳
  2. 4000个基因比2000个基因保留了更多稀有细胞类型的marker
  3. 偏离度法不需要先归一化,避免了归一化方法对HVG选择的影响

实用建议: 由于偏离度法在scanpy中没有原生实现,可以使用Pearson残差法作为替代(两者在概念上相似,都是基于统计模型提取高变异基因)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import scanpy as sc

# 使用Pearson残差法选择HVG(近似偏离度法的效果)
sc.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 ov
import scanpy as sc

# 读取数据
adata = sc.read_h5ad('quality_control.h5ad')
ov.utils.store_layers(adata, layers='counts')
adata.layers['counts'] = adata.X.copy()

# 一站式预处理:移位对数归一化 + Pearson HVG选择
adata = ov.pp.preprocess(
adata,
mode='shiftlog|pearson',
n_HVGs=2000,
)

# 或者:Pearson残差归一化 + Pearson HVG选择
adata_pearson = ov.pp.preprocess(
adata.copy(),
mode='pearson|pearson',
n_HVGs=2000,
)

# 查看HVG信息
print(adata.var['highly_variable_features'].value_counts())

6.6 注意事项

  1. HVG数量选择:2000-4000是常用范围。2000适合大多数场景,4000适合复杂组织或稀有细胞检测
  2. 非HVG基因的保留:过滤到仅HVG后,非HVG的原始计数会丢失。建议在过滤前将完整数据保存到adata.raw
  3. 方法选择的影响:不同方法选择的HVG可能只有30-40%重叠,方法选择会影响下游聚类结果
  4. 避免在归一化后的数据上选择HVG:某些方法(如VST和Pearson残差)需要在原始计数上计算
  5. 线粒体/核糖体基因:通常这些基因不应被选为HVG,除非它们确实包含有意义的生物学变异
  6. 批次效应:如果数据包含多个批次,建议使用批次感知的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 sc
import numpy as np

# 归一化和log变换(如果尚未完成)
sc.pp.normalize_total(adata)
sc.pp.log1p(adata)

# 选择高可变基因
sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor='seurat_v3', layer='counts')

# 缩放数据(Z-score标准化)
sc.pp.scale(adata, max_value=10)

# 运行PCA
sc.tl.pca(adata, n_comps=50, use_highly_variable=True)

# 查看PCA结果
print(adata.obsm['X_pca'].shape) # (n_obs, 50)

# 可视化方差解释比例
sc.pl.pca_variance_ratio(adata, n_pcs=50, log=True)

# 在PCA空间中可视化
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
# 使用omicverse选择PC数量
import omicverse as ov

# omicverse会自动选择合适的PC数
adata = ov.pp.preprocess(adata, mode='shiftlog|pearson', n_HVGs=2000)

# 在ov.pp.scale和sc.tl.pca后,可以通过以下方式选择PC
# 方法1:肘部法(手动观察)
sc.pl.pca_variance_ratio(adata, n_pcs=50)

# 方法2:基于molecular cross-validation
# 通过交叉验证确定最佳PC数量

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年提出,是一种非线性降维方法,特别适合可视化高维数据的局部结构。

核心思想:

  1. 在高维空间中,计算每对数据点之间的相似度(条件概率)
  2. 在低维空间中,同样计算每对数据点之间的相似度
  3. 通过优化,使低维空间中的相似度分布尽可能接近高维空间中的分布

数学公式:

高维空间相似度(高斯分布):

$$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 sc

# 运行t-SNE(基于PCA结果)
sc.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年提出,基于流形学习和拓扑数据分析。

核心思想:

  1. 构建高维数据的模糊拓扑表示(fuzzy simplicial set)
  2. 在低维空间中优化一个类似的拓扑结构
  3. 通过最小化两个拓扑结构之间的交叉熵来优化低维嵌入

数学步骤:

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 sc

# 先构建邻域图(UMAP需要KNN图)
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)

# 运行UMAP
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 sc

# 运行扩散映射
sc.tl.diffmap(adata, n_comps=15)

# 可视化
sc.pl.diffmap(adata, color='cell_type', components=['1,2', '1,3'])

# 使用扩散分量计算伪时间
# 选择root细胞
root_cell = adata.obs_names[0] # 选择一个root细胞
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 ov

# 基于缩放后的PCA结果计算MDE
adata.obsm["X_mde_scaled"] = ov.utils.mde(adata.obsm["scaled|original|X_pca"])

# 基于原始计数的PCA结果计算MDE
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 各方法对比与适用场景

方法 类型 保留结构 速度 适用场景
PCA 线性 全局结构 极快 降维预处理、选择PC数量
t-SNE 非线性 局部结构 慢 精细的局部结构可视化
UMAP 非线性 局部+全局 中等 通用可视化、细胞类型分离
Diffusion Map 非线性 连续过渡 中等 轨迹推断、伪时间分析
MDE 非线性 可调节 快(GPU) 大规模数据快速可视化

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 ov
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt

ov.ov_plot_set()

# 读取预处理后的数据
adata = sc.read_h5ad('preprocessed.h5ad')

# ============================================
# Step 1: 缩放数据(Z-score标准化)
# ============================================
# omicverse将缩放结果存储在layers而非替换X
sc.pp.scale(adata, max_value=10)

# ============================================
# Step 2: PCA降维
# ============================================
sc.tl.pca(adata, n_comps=50, use_highly_variable=True)

# 查看方差解释比例
sc.pl.pca_variance_ratio(adata, n_pcs=50, log=True)

# ============================================
# Step 3: 构建KNN邻域图
# ============================================
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)

# ============================================
# Step 4: UMAP降维
# ============================================
sc.tl.umap(adata, min_dist=0.3)

# ============================================
# Step 5: t-SNE降维
# ============================================
sc.tl.tsne(adata, n_pcs=30, perplexity=30)

# ============================================
# Step 6: 扩散映射
# ============================================
sc.tl.diffmap(adata, n_comps=15)

# ============================================
# Step 7: MDE降维(omicverse)
# ============================================
adata.obsm["X_mde"] = ov.utils.mde(adata.obsm["scaled|original|X_pca"])

# ============================================
# Step 8: 可视化对比
# ============================================
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 注意事项

  1. PCA是必需步骤:t-SNE、UMAP等非线性方法都需要先进行PCA降维,以减少噪声和计算量
  2. PC数量选择:过多PC引入噪声,过少PC丢失信息。20-50是常用范围
  3. UMAP/t-SNE距离不可解释:低维嵌入中的距离不反映真实的高维距离,不要基于UMAP距离做定量比较
  4. t-SNE的随机性:t-SNE每次运行结果可能不同,需要设置随机种子保证可重复性
  5. perplexity/min_dist的调整:不同参数可能展示不同的数据结构,建议尝试多个参数值
  6. 扩散映射的适用性:仅当数据包含连续过渡结构时使用,否则PCA/UMAP更合适
  7. MDE的速度优势:对于超过100,000个细胞的大数据集,MDE比UMAP快得多
  8. 缩放对PCA的影响:缩放(Z-score标准化)会去除基因间的表达量差异,使得低表达基因和高表达基因的贡献更均等

8. 聚类

8.1 KNN图构建

8.1.1 原理

KNN(K-Nearest Neighbors,K最近邻)图是聚类的基础。在单细胞分析中,KNN图的构建步骤如下:

  1. 在PCA降维后的空间中,计算所有细胞之间的欧氏距离
  2. 对每个细胞,找到其$k$个最相似的细胞(最近邻)
  3. 将每个细胞与其$k$个最近邻用边连接,形成图

KNN图的性质:

  • 节点 = 细胞
  • 边 = 近邻关系
  • 表达空间中的密集区域 → 图中的密集连接区域
  • 反映了数据的潜在拓扑结构

8.1.2 K值选择

K值 效果 适用场景
5-10 关注精细的局部结构 细粒度聚类、稀有细胞检测
15-30(推荐) 平衡局部和全局结构 通用场景
50-100 关注全局结构 粗粒度聚类、大规模数据

推荐值: 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 sc

# 构建KNN邻域图
sc.pp.neighbors(
adata,
n_neighbors=15, # K值
n_pcs=30, # 使用前30个PC
use_rep='X_pca', # 使用PCA结果
method='umap', # 计算方法:'umap'(推荐)或'gauss'
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
# 使用scVI的潜在空间构建KNN图
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算法的步骤:

  1. 局部移动(Local Moving):

    • 从每个节点自成一群开始
    • 遍历所有节点,将每个节点移到使模块度增益最大的相邻社区
    • 模块度增益:$\Delta Q = Q_{\text{after}} - Q_{\text{before}}$
  2. 细化(Refinement):

    • 对第一步得到的分区进行细化
    • 确保每个社区内部是”良连通的”(well-connected)
    • 这是Leiden相比Louvain的关键改进
  3. 聚合(Aggregation):

    • 基于细化后的分区,将同一社区的节点聚合为一个超级节点
    • 构建聚合网络
  4. 迭代:

    • 在聚合网络上重复步骤1-3
    • 直到分区不再变化

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 sc

# 运行Leiden聚类(需先构建KNN图)
sc.tl.leiden(
adata,
resolution=1.0, # 分辨率参数
key_added='leiden', # 结果存储的obs列名
flavor='leidenalg', # 实现库
)

# 查看聚类结果
print(adata.obs['leiden'].value_counts())
# 0 2500
# 1 2100
# 2 1800
# 3 1500
# ...

# 可视化
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 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 ov
import scanpy as sc

# 多分辨率聚类
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")

# 输出示例:
# Resolution 0.25: 7 clusters
# Resolution 0.5: 14 clusters
# Resolution 1.0: 21 clusters
# Resolution 2.0: 32 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',
)

分辨率选择指南:

分辨率 聚类数 适用场景
0.1-0.5 少(5-10) 粗粒度细胞类型
0.5-1.0 中等(10-20) 标准细胞类型鉴定
1.0-2.0 多(20-40) 细粒度细胞状态
>2.0 很多(40+) 亚群分析

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_score

# 计算轮廓系数
score = silhouette_score(
adata.obsm['X_pca'][:, :30], # 使用前30个PC
adata.obs['leiden'].astype(int)
)
print(f"Silhouette Score: {score:.3f}")

8.5.2 模块度

1
2
3
4
5
6
7
8
9
10
11
import igraph as ig
import leidenalg

# 从scanpy的邻域图创建igraph对象
g = 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:.3f}")

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 ov
import scanpy as sc
import numpy as np

ov.ov_plot_set()

# ============================================
# Step 1: 读取降维后的数据
# ============================================
adata = sc.read_h5ad('dimensionality_reduction.h5ad')
print(f"数据维度: {adata.shape}")

# ============================================
# Step 2: 构建KNN邻域图
# ============================================
sc.pp.neighbors(
adata,
n_neighbors=15,
n_pcs=30,
use_rep='scaled|original|X_pca', # omicverse格式
)

# ============================================
# Step 3: 多分辨率Leiden聚类
# ============================================
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")

# ============================================
# Step 4: 可视化聚类结果
# ============================================
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',
)

# ============================================
# Step 5: 选择最佳分辨率
# ============================================
# 通常根据生物学知识选择
# 例如:如果已知有10种主要细胞类型,选择产生约10个聚类的分辨率
best_resolution = 0.5 # 假设0.5产生14个聚类
adata.obs['leiden_final'] = adata.obs[f'leiden_res{best_resolution}']

# ============================================
# Step 6: 聚类评估
# ============================================
from sklearn.metrics import silhouette_score

score = silhouette_score(
adata.obsm['scaled|original|X_pca'][:, :30],
adata.obs['leiden_final'].astype('category').cat.codes
)
print(f"Silhouette Score: {score:.3f}")

# ============================================
# Step 7: 保存聚类结果
# ============================================
adata.write_h5ad('clustering_results.h5ad')
print("聚类完成!")

8.7 注意事项

  1. UMAP距离不可解释:UMAP/t-SNE嵌入中的距离不反映真实的高维距离。不要基于UMAP图上的距离来判断细胞类型的相似性,应使用PCA空间中的距离。

  2. 分辨率选择没有标准答案:需要结合生物学知识和数据特点来选择合适的分辨率。建议从0.5-1.0开始,根据聚类结果调整。

  3. 子聚类的风险:对某个聚类进行子聚类(subclustering)可以发现更细的细胞状态,但也可能过度拟合噪声。建议子聚类时降低分辨率。

  4. KNN图的影响:K值选择会影响聚类结果。K太小会导致聚类过于细碎,K太大会导致聚类过于粗糙。

  5. 聚类结果的命名:Leiden聚类的标签(0, 1, 2, …)是随机的,不代表任何生物学顺序。需要通过标记基因(marker genes)进行注释。

  6. 避免基于单一分辨率下结论:建议检查多个分辨率的结果,确保结论不依赖于特定分辨率。

  7. 批次效应的影响:如果数据包含批次效应,聚类可能反映的是批次差异而非生物学差异。建议在聚类前进行批次校正。

  8. Leiden vs Louvain:始终使用Leiden算法,Louvain算法已不推荐使用。

  9. 大数据集的加速:对于超过50,000个细胞的数据集,可以使用sc.pp.neighbors的method='rapids'(GPU加速)或使用rapids-singlecell包。

  10. 聚类不是目的,而是手段:聚类的目的是为了后续的细胞类型注释和差异表达分析。聚类结果本身不具备生物学意义,需要通过marker基因赋予生物学解释。


8.8 关键参考文献

  1. Leiden算法: Traag, V.A., Waltman, L. & van Eck, N.J. (2019). From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports, 9, 5233.
  2. Louvain算法: Blondel, V.D. et al. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics, P10008.
  3. 模块度: Newman, M.E. (2006). Modularity and community structure in networks. Proceedings of the National Academy of Sciences, 103(23), 8577-8582.
  4. PCA: Jolliffe, I.T. & Cadima, J. (2016). Principal component analysis: a review and recent developments. Philosophical Transactions of the Royal Society A, 374, 20150202.
  5. t-SNE: van der Maaten, L. & Hinton, G. (2008). Visualizing data using t-SNE. Journal of Machine Learning Research, 9, 2579-2605.
  6. UMAP: McInnes, L., Healy, J. & Melville, J. (2020). UMAP: Uniform Manifold Approximation and Projection for dimension reduction. arXiv preprint arXiv:1802.03426.
  7. 扩散映射: Coifman, R.R. & Lafon, S. (2006). Diffusion maps. Applied and Computational Harmonic Analysis, 21(1), 5-30.
  8. 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.
  9. 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.
  10. SoupX: Young, M.D. & Behjati, S. (2020). SoupX removes ambient RNA contamination from droplet-based single-cell RNA sequencing data. GigaScience, 9(12), giaa151.
  11. 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.
  12. Pearson残差: Lause, J. et al. (2021). Analytic Pearson residuals for normalization of single-cell RNA-seq UMI data. Genome Biology, 22, 258.
  13. 偏离度法: 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.
  14. 归一化基准测试: Ahlmann-Eltze, C. & Huber, W. (2023). Comparison of transformations for single-cell RNA-seq data. Nature Methods, 20, 665-672.
  15. Single-cell best practices: Heumos, L. et al. (2023). Best practices for single-cell analysis across modalities. Nature Reviews Genetics, 24, 550-572.
  16. omicverse: Xu, Z. et al. (2024). OmicVerse: a single cell multi-omics framework. bioRxiv.
  17. scanpy: Wolf, F.A., Angerer, P. & Theis, F.J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology, 19, 15.
  18. MDE: Agrawal, A. et al. PyMDE: Minimum-Distortion Embedding. arXiv preprint.
  19. Seurat v3 VST: Stuart, T. et al. (2019). Comprehensive integration of single-cell data. Cell, 177(7), 1888-1902.
  20. 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 ov
import scanpy as sc
import numpy as np

ov.ov_plot_set()

# ============================================
# Step 1: 读取数据
# ============================================
adata = sc.read_10x_mtx('filtered_feature_bc_matrix/', var_names='gene_symbols')
print(f"原始数据: {adata.shape}")

# ============================================
# Step 2: 质量控制
# ============================================
# 标记特殊基因
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)]')

# 计算QC指标
sc.pp.calculate_qc_metrics(
adata,
qc_vars=['mt', 'ribo', 'hb'],
percent_top=None,
log1p=False,
inplace=True
)

# 一站式QC
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}")

# ============================================
# Step 3: 归一化 + HVG选择
# ============================================
# 保存原始计数
adata.layers['counts'] = adata.X.copy()

# 一站式预处理
adata = ov.pp.preprocess(
adata,
mode='shiftlog|pearson',
n_HVGs=2000,
)
print(f"预处理后: {adata.shape}")

# ============================================
# Step 4: 降维
# ============================================
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)

# MDE可视化
adata.obsm["X_mde"] = ov.utils.mde(adata.obsm["scaled|original|X_pca"])

# ============================================
# Step 5: 聚类
# ============================================
# 多分辨率聚类
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']

# ============================================
# Step 6: 可视化
# ============================================
ov.utils.embedding(
adata,
basis='X_mde',
color=['leiden_res0.5', 'leiden_res1.0'],
frameon='small',
)

# ============================================
# Step 7: 保存结果
# ============================================
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: 聚类结果不稳定(每次运行结果不同)

可能原因:

  • Leiden算法的随机性
  • KNN图构建的随机性

解决方案:

  • 设置随机种子:sc.tl.leiden(adata, random_state=0)
  • 增加n_neighbors值使图更稳定

Q6: scrone归一化报错

可能原因:

  • R环境未正确配置
  • scran包未安装

解决方案:

  • 确保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
# Python环境
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

# R环境(用于scran/scDblFinder/SoupX)
# 在R中:
# install.packages("BiocManager")
# BiocManager::install(c("scran", "scDblFinder", "SoupX", "scry", "SingleCellExperiment"))

# Python-R桥接
pip install rpy2
pip install anndata2ri

# Cell Ranger(需在Linux服务器上)
# 从10x Genomics官网下载:https://support.10xgenomics.com/

推荐版本(截至2024年):

软件包 推荐版本
scanpy ≥1.9.0
anndata ≥0.10.0
omicverse ≥1.4.13
scrublet ≥0.2.3
leidenalg ≥0.10.0
Cell Ranger ≥7.0.0

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
# 规范的AnnData对象结构(预处理完成后)
adata
# AnnData object with n_obs × n_vars = 14814 × 20171
# obs: 'n_genes_by_counts', 'log1p_n_genes_by_counts',
# 'total_counts', 'log1p_total_counts',
# 'pct_counts_in_top_20_genes', 'total_counts_mt',
# 'log1p_total_counts_mt', 'pct_counts_mt',
# 'total_counts_ribo', 'log1p_total_counts_ribo', 'pct_counts_ribo',
# 'doublet_score', 'predicted_doublet',
# 'leiden_res0.25', 'leiden_res0.5', 'leiden_res1.0', 'leiden_res2.0',
# 'leiden_final'
# var: 'gene_ids', 'mt', 'ribo', 'hb',
# 'n_cells_by_counts', 'mean_counts', 'log1p_mean_counts',
# 'pct_dropout_by_counts', 'total_counts', 'log1p_total_counts',
# 'highly_variable', 'highly_variable_rank',
# 'means', 'variances', 'variances_norm',
# 'highly_variable_nbatches', 'highly_variable_intersection'
# uns: 'hvg', 'leiden', 'leiden_res0.25', 'leiden_res0.5',
# 'leiden_res1.0', 'leiden_res2.0',
# 'neighbors', 'pca', 'umap'
# obsm: 'X_pca', 'X_umap', 'X_mde',
# 'scaled|original|X_pca', 'counts|original|X_pca'
# varm: 'PCs'
# layers: 'counts', 'log1p_norm', 'scran_normalization'
# obsp: 'distances', 'connectivities'

以上便是单细胞RNA测序分析从入门基础到预处理全流程的完整详尽教材。每一章节都包含了理论基础、数学公式、完整代码和注意事项,读者可以按顺序学习,也可以根据需要直接查阅特定章节。建议初学者先通读全文建立整体概念,再结合实际数据逐步实践每个步骤。

A.6 预处理参数速查表

以下是预处理过程中最常用的参数及其推荐值的快速参考表,供读者在实际分析中随时查阅。

步骤 参数 推荐值 说明
QC min_genes 200-250 每个细胞最少检测到的基因数
QC min_cells 3 每个基因最少在多少细胞中表达
QC mito_perc 5-20% 线粒体基因比例上限(组织特异性)
QC nmads 5 MAD倍数(5≈3.4σ)
QC expected_doublet_rate 0.06 预期双细胞率(10x Chromium)
归一化 target_sum None(中位数) 移位对数的目标计数深度
归一化 method 移位对数 默认推荐方法
HVG n_top_genes 2000-4000 选择的高可变基因数量
HVG flavor pearson_residuals / seurat_v3 HVG选择方法
PCA n_comps 50 计算的主成分数量
PCA use_highly_variable True 仅使用HVG计算PCA
KNN n_neighbors 15 K最近邻的K值
KNN n_pcs 20-50 用于KNN的PC数量
UMAP min_dist 0.3 最小距离参数
t-SNE perplexity 30 困惑度参数
Leiden resolution 0.5-1.0 分辨率参数
Scale max_value 10 缩放截断值

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 从原始数据到发表级结果的检查清单

在完成全部预处理流程后,建议按照以下检查清单逐项验证分析质量,确保结果可靠、可重复。

数据质量检查:

  • 原始FASTQ文件的reads数量是否符合预期(通过Cell Ranger summary检查)
  • 细胞捕获数是否在预期范围内(参考10x官方指标)
  • 测序饱和度是否足够(建议>60%)
  • 有效比对率是否正常(建议>80%)

质量控制检查:

  • QC指标分布是否合理(nUMIs、n_genes、pct_mt的分布图)
  • 过滤后剩余细胞数是否在合理范围(通常为原始barcode数的30-70%)
  • 双细胞比例是否符合预期(约0.8% per 1000 cells)
  • 是否有特定细胞类型被过度去除

归一化检查:

  • 归一化后总counts分布是否合理(移位对数后应为近似正态分布)
  • 是否保留了原始计数矩阵(adata.layers[‘counts’])
  • 归一化方法是否与研究目标匹配

特征选择检查:

  • 选出的高可变基因是否包含已知的细胞类型标记基因
  • 线粒体和核糖体基因是否被排除在HVG之外
  • HVG数量是否合理(2000-4000范围)

降维检查:

  • PCA的方差解释比例是否合理(前30个PC应解释30-60%的方差)
  • UMAP/t-SNE图是否展示出清晰的聚类结构
  • QC指标在降维空间中的分布是否有明显的偏差模式

聚类检查:

  • 聚类数量是否与预期的细胞类型数量匹配
  • 不同分辨率下的聚类结果是否一致
  • 每个聚类是否有足够数量的细胞(建议≥20个)
  • 聚类结果是否与已知的生物学知识一致

可重复性检查:

  • 是否设置了随机种子(sc.tl.leiden, sc.tl.umap等)
  • 分析流程是否记录了完整的参数
  • 是否保存了中间结果(每个步骤的h5ad文件)
  • 代码是否版本控制(git)

完成以上所有检查后,预处理阶段的输出即可用于下游的细胞类型注释、差异表达分析等高级分析任务。


Part 2:细胞识别与下游分析


第三篇:细胞识别


第九章:细胞类型注释

基于Ch12 + 教程六,详细展开

9.1 细胞类型概念与分类层次

在单细胞转录组数据分析中,细胞类型注释(Cell Type Annotation)是最关键的分析步骤之一。它决定了后续所有生物学解读的方向和可靠性。细胞类型注释的核心目标是:将基于无监督聚类得到的每个细胞簇(cluster),赋予有生物学意义的标签。

细胞类型的定义层次

细胞类型的定义并非一个简单的二元问题,而是存在多层次的分类体系:

  1. 细胞大类(Cell Class):如免疫细胞(Immune cells)、上皮细胞(Epithelial cells)、基质细胞(Stromal cells)、内皮细胞(Endothelial cells)等。这是最粗粒度的分类。

  2. 细胞类型(Cell Type):在细胞大类之下进一步细分,如免疫细胞可细分为T细胞(T cells)、B细胞(B cells)、巨噬细胞(Macrophages)、树突状细胞(Dendritic cells)等。

  3. 细胞亚型(Cell Subtype):在同一细胞类型内进一步区分功能状态,如T细胞可细分为CD4+ T细胞、CD8+ T细胞、调节性T细胞(Treg)、γδ T细胞等。

  4. 细胞状态(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细胞中均表达)

常见细胞类型标记基因参考表:

细胞类型 经典标记基因
T细胞 CD3D, CD3E, CD3G, CD2
CD4+ T细胞 CD4, IL7R
CD8+ T细胞 CD8A, CD8B
Treg FOXP3, IL2RA(CD25), CTLA4
B细胞 CD79A, CD79B, MS4A1(CD20), CD19
浆细胞 IGHG1, IGHG2, JCHAIN, MZB1
NK细胞 NCAM1(CD56), KLRD1, NKG7, GNLY
单核细胞 CD14, LST1, S100A8, S100A9
巨噬细胞 CD68, CD163, MARCO, MRC1
树突状细胞 CD1C, FCER1A, CLEC10A
pDC LILRA4, IRF7, IRF8, GZMB
上皮细胞 EPCAM, KRT8, KRT18, KRT19
内皮细胞 PECAM1(CD31), VWF, FLT1, CDH5
成纤维细胞 COL1A1, COL1A2, DCN, LUM
肥大细胞 TPSAB1, TPSB2, KIT, MS4A2
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 sc
import pandas as pd
import numpy as np

# 加载经过聚类分析的AnnData对象
adata = sc.read_h5ad("clustered_data.h5ad")

# 使用Wilcoxon秩和检验鉴定标记基因
sc.tl.rank_genes_groups(
adata,
groupby='leiden', # 按leiden聚类分组
method='wilcoxon', # 统计方法
corr_method='benjamini-hochberg', # 多重检验校正方法
key_added='wilcoxon', # 结果存储的键名
use_raw=True, # 使用raw层(未归一化的计数)
pts=True, # 计算表达比例,用于过滤低特异基因
)

# 查看每个簇的Top标记基因
sc.pl.rank_genes_groups(
adata,
n_genes=15, # 每个簇显示的基因数
sharey=False, # 不共享Y轴
key='wilcoxon', # 使用上面计算的结果
fontsize=12,
)

# 将结果导出为DataFrame
marker_df = sc.get.rank_genes_groups_df(
adata,
group=None, # None表示所有组
key='wilcoxon',
)

# 过滤标记基因:高特异性、高表达比例
marker_df_filtered = marker_df[
(marker_df['pvals_adj'] < 0.05) & # 校正后p值显著
(marker_df['logfoldchanges'] > 1.0) & # log2倍数变化 > 1
(marker_df['pct_nz_group'] > 0.5) # 在目标簇中表达比例 > 50%
]

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 sc
import matplotlib.pyplot as plt

# 基础UMAP——查看聚类结果
fig, 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()

# 在UMAP上可视化标记基因表达
# 方法1:使用scanpy内置函数
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', # 若使用ensembl ID需指定gene_name列
cmap='Reds', # 红色渐变色图
vmin=0,
vmax='p99', # 99百分位截断,避免极端值影响可视化
frameon=False,
ncols=3, # 每行3个图
save='_marker_genes.png',
)

# 方法2:使用小提琴图查看标记基因在不同簇中的分布
sc.pl.violin(
adata,
['CD3D', 'CD79A', 'CD14', 'NCAM1'],
groupby='leiden',
stripplot=False, # 不显示散点
multi_panel=True, # 多面板
rotation=45,
save='_markers_violin.png',
)

# 方法3:使用点图(dotplot)同时展示多个基因和簇
sc.pl.dotplot(
adata,
marker_genes,
groupby='leiden',
dendrogram=True, # 按层次聚类排序
standard_scale='var', # 标准化到0-1
save='_marker_dotplot.png',
)

# 方法4:使用堆叠小提琴图
sc.pl.stacked_violin(
adata,
marker_genes,
groupby='leiden',
dendrogram=True,
swap_axes=True, # 交换轴
save='_marker_stacked_violin.png',
)

# 方法5:矩阵图(matrixplot)
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 sc
import pandas as pd
import numpy as np

# ============================================================
# Step 1: 加载数据
# ============================================================
adata = sc.read_h5ad("clustered_data.h5ad")
print(f"细胞数: {adata.n_obs}, 基因数: {adata.n_vars}")
print(f"聚类数: {adata.obs['leiden'].nunique()}")

# ============================================================
# Step 2: 鉴定标记基因
# ============================================================
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)

# ============================================================
# Step 3: 可视化与注释
# ============================================================
# 定义已知的标记基因组合
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'],
}

# 使用dotplot可视化标记基因
sc.pl.dotplot(
adata,
marker_genes_dict,
groupby='leiden',
dendrogram=True,
standard_scale='var',
figsize=(14, 6),
save='_annotation_dotplot.png',
)

# ============================================================
# Step 4: 基于标记基因创建注释映射
# ============================================================
# 根据可视化结果,为每个leiden簇分配细胞类型
cluster_annotation = {
'0': 'CD4+ T cells', # 高表达CD3D, CD4, IL7R
'1': 'Monocytes', # 高表达CD14, S100A8, LYZ
'2': 'B cells', # 高表达CD79A, MS4A1
'3': 'CD8+ T cells', # 高表达CD3D, CD8A, CD8B
'4': 'NK cells', # 高表达NCAM1, NKG7, GNLY
'5': 'Fibroblasts', # 高表达COL1A1, DCN, LUM
'6': 'Epithelial', # 高表达EPCAM, KRT8
'7': 'Endothelial', # 高表达PECAM1, VWF
'8': 'Dendritic cells', # 高表达CD1C, FCER1A
'9': 'Plasma cells', # 高表达IGHG1, JCHAIN
'10': 'Treg', # 高表达FOXP3, IL2RA
'11': 'pDC', # 高表达LILRA4, IRF7
'12': 'Macrophages', # 高表达CD68, CD163
}

# 将注释添加到adata.obs
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',
)

# ============================================================
# Step 5: 保存注释结果
# ============================================================
adata.write_h5ad("annotated_data.h5ad")
print("注释完成!数据已保存。")

9.3 自动注释方法详解

自动注释方法利用已标注的参考数据集,通过机器学习或统计模型预测新数据中细胞的类型。这些方法大大提高了注释效率,但需要与手动注释结合使用。

9.3.1 CellTypist

原理:

CellTypist是一个基于逻辑回归(Logistic Regression)的自动细胞类型注释工具,由Teichmann实验室开发。其核心思路是:

  1. 从大规模已标注的参考数据集中训练多分类逻辑回归模型
  2. 使用正则化(L1/L2)防止过拟合
  3. 对新数据集进行概率预测,输出每个细胞属于各细胞类型的概率

逻辑回归模型的形式为:

$$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
# ============================================================
# CellTypist 自动注释
# ============================================================
import celltypist
from celltypist import models
import scanpy as sc

# Step 1: 下载模型
# 查看可用模型
models.download_models(force_update=True)
model_list = models.models_description()
print(model_list)

# 下载Immune_All_Low.pkl模型(适用于免疫细胞的精细注释)
model_name = "Immune_All_Low.pkl"
models.download_models(model=[model_name])

# 也可以下载其他模型:
# "Immune_All_High.pkl" - 免疫细胞粗粒度注释
# "Developing_Human.pkl" - 发育相关细胞
# "Adult_Human.pbmc.pkl" - 成人PBMC

# Step 2: 准备数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# CellTypist要求log1p归一化的数据
# 确保adata.X是log-normalized的
print(f"数据维度: {adata.shape}")
print(f"X矩阵类型: {type(adata.X)}")

# Step 3: 基本预测
predictions = celltypist.annotate(
adata,
model=model_name,
majority_voting=False, # 不使用多数投票
)

# 查看预测结果
print(predictions.predicted_labels.head(20))
# 输出包含:
# - predicted_labels: 预测的细胞类型
# - over_clustering: over-clustering结果(用于majority voting)
# - probability: 最高概率值

# 将预测结果添加到adata
adata = predictions.to_adata()
# 此时adata.obs中会添加:
# - cell_type_fine: CellTypist预测的精细细胞类型
# - over_clustering: over-clustering标签
# - cell_type_fine_prob: 预测概率

# Step 4: 使用Majority Voting
predictions_voting = celltypist.annotate(
adata,
model=model_name,
majority_voting=True, # 启用多数投票
# over_clustering可以自定义:
# over_clustering='leiden', # 使用已有的leiden聚类
# over_clustering=0.5, # 指定over-clustering分辨率
)

adata_voting = predictions_voting.to_adata()
adata_voting.obs['celltypist_label'] = adata_voting.obs['majority_voting']

# Step 5: 可视化对比
sc.pl.umap(
adata_voting,
color=['celltypist_label', 'cell_type_fine', 'leiden'],
ncols=2,
frameon=False,
save='_celltypist_comparison.png',
)

# Step 6: 查看预测概率分布
sc.pl.umap(
adata_voting,
color='cell_type_fine_prob',
cmap='RdYlGn',
frameon=False,
title='CellTypist Prediction Probability',
save='_celltypist_probability.png',
)

# Step 7: 使用自定义模型(如果已有标注数据)
# 准备训练数据
# train_adata需要包含细胞类型标签
train_adata = sc.read_h5ad("annotated_reference.h5ad")

# 训练CellTypist模型
model = celltypist.train(
train_adata,
labels='cell_type', # 标签列名
n_jobs=10, # 并行数
feature_selection=True, # 特征选择
use_SGD=True, # 使用SGD加速
)

# 保存模型
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
# ============================================================
# CellAssign 自动注释
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 方式1:使用PyCellAssign(推荐)
# pip install py-cellassign

from cellassign import CellAssign

# Step 1: 准备标记基因矩阵
# 创建marker_gene_matrix: 行=基因, 列=细胞类型, 值=1表示该基因是该类型的标记
marker_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']
)

# Step 2: 准备数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# CellAssign需要count数据(未归一化)
# 确保adata.X或adata.raw.X包含原始计数
if adata.raw is not None:
adata_cellassign = adata.raw.to_adata()
else:
adata_cellassign = adata.copy()

# Step 3: 运行CellAssign
ca = CellAssign(
adata=adata_cellassign,
marker_gene_matrix=marker_gene_mat,
)

# 拟合模型
ca.fit()

# 获取预测结果
predictions = ca.predict()
print(predictions.head())

# 将预测添加到adata
adata.obs['cellassign_label'] = predictions['cell_type']
adata.obs['cellassign_prob'] = predictions['max_prob']

# Step 4: 可视化
sc.pl.umap(
adata,
color=['cellassign_label', 'cellassign_prob'],
frameon=False,
save='_cellassign.png',
)

# 方式2:使用scvi-tools中的CellAssign
# import scvi
# from scvi.tools import CellAssign as SCVICellAssign
#
# # 准备标记基因矩阵(同上)
# # 运行
# model = SCVICellAssign(adata)
# model.train()
# predictions = model.predict()

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
# ============================================================
# Garnett 自动注释
# ============================================================
# 安装: pip install garnett-census

import scanpy as sc
import pandas as pd

# Garnett通常在R中运行更成熟,以下是R代码示例
# 在Python中可以使用rpy2调用

# ---- R代码 ----
# library(garnett)
# library(monocle3)
#
# # 从标记文件创建CellTypeHierarchy
# marker_file_path <- "marker_file.txt"
#
# # 创建标记文件
# marker_content <- "
# > 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
# > cluster Monocyte
# expressed: CD14, LYZ
# not expressed: CD3D, CD79A
# "
# writeLines(marker_content, marker_file_path)
#
# # 创建CellTypeHierarchy对象
# cth <- new("CellTypeHierarchy")
# cth <- addCellTypeHierarchy(cth, marker_file_path)
#
# # 加载数据(monocle3格式)
# cds <- load_cellranger_data("path/to/data")
#
# # 训练分类器
# marker_genes <- c("CD3D", "CD3E", "CD2", "CD79A", "MS4A1",
# "CD4", "IL7R", "CD8A", "CD8B", "CD14", "LYZ")
# cds <- detect_genes(cds)
# classifier <- train_cell_classifier(
# cds,
# cth,
# marker_genes,
# db = org.Hs.eg.db, # 基因注释数据库
# cds_subtype = "cell_type" # 已知标签列
# )
#
# # 预测
# cds <- classify_cells(cds, classifier, db = org.Hs.eg.db)
#
# # 查看结果
# table(pData(cds)$cell_type)
# ---- R代码结束 ----

# Python替代方案:使用层级逻辑回归
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import numpy as np

# 准备标记基因矩阵
marker_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)

# 训练层级分类器
# 第一层:区分大类
# 第二层:在T细胞内区分CD4+ vs CD8+
y = adata.obs['cell_type'].values

# 第一层分类器
clf_l1 = LogisticRegression(
penalty='elasticnet', # 弹性网络
solver='saga',
l1_ratio=0.5, # L1/L2混合比例
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)是一种基于标记基因富集分析的自动注释方法。其核心思路是:

  1. 对每个簇鉴定差异表达基因(DEG)
  2. 将DEG列表与已知的细胞类型标记基因数据库进行富集分析
  3. 根据富集得分和统计显著性为每个簇分配最可能的细胞类型

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
# ============================================================
# SCSA 自动注释
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np
from scipy.stats import hypergeom

# 安装: pip install SCSA
# 或者从GitHub克隆: git clone https://github.com/bioinfo-ibms-pumc/SCSA.git

# Step 1: 准备标记基因数据
adata = 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)

# Step 2: 使用SCSA命令行工具
# 准备输入文件(SCSA特定格式)
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')

# Step 3: 运行SCSA
# python SCSA.py -i scsa_input.csv -s -m txt -p -E -f 1.5 -o scsa_output.txt
# 参数说明:
# -i: 输入文件
# -s: 使用scRNA-seq模式
# -m txt: 输入格式为文本
# -p: 使用p值过滤
# -E: 使用Enrichr数据库
# -f 1.5: fold change阈值
# -o: 输出文件

# Step 4: 解析SCSA输出
# SCSA输出包含每个簇的富集细胞类型和对应得分
# scsa_result = pd.read_csv("scsa_output.txt", sep='\t')
# print(scsa_result)

# Step 5: 手动实现SCSA的核心逻辑
def scsa_annotation(adata, groupby='leiden', db='cellmarker', top_n=100):
"""
简化版SCSA注释:基于超几何检验的标记基因富集
"""
# 加载CellMarker数据库(示例)
# 实际使用时需要下载完整数据库
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) # DEG中在数据库中的基因数

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:.2e}, score={best_score:.2f})")

return annotations

# 运行注释
annotations = scsa_annotation(adata)

# 添加到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注释完成!")
9.3.5 CellTypist + MetaTiME(教程九)

原理:

MetaTiME是一种整合了单细胞转录组数据和TME(Tumor Microenvironment)知识的细胞类型注释方法。它结合了CellTypist的自动预测能力和TiME(Tumor Immune Microenvironment)的领域知识,特别适用于肿瘤免疫微环境的注释。

MetaTiME的核心步骤:

  1. 从大规模肿瘤微环境数据中学习元组件(Meta-components)
  2. 每个元组件对应一种细胞状态或功能程序
  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
# ============================================================
# CellTypist + MetaTiME 注释
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install MetaTiME
import MetaTiME as mt

# Step 1: 加载数据
adata = sc.read_h5ad("tumor_tme_data.h5ad")

# Step 2: 运行MetaTiME
# 获取MetaTiME模型
me_model = mt.get_me_model()

# 计算MetaTiME富集得分
adata = mt.score_me(adata, me_model)

# Step 3: 基于MetaTiME得分注释细胞类型
# MetaTiME会计算每个细胞对各个元组件的得分
# 可以通过得分模式推断细胞类型

# 查看MetaTiME结果
print(adata.obsm.keys()) # 应包含MetaTiME相关键

# Step 4: 可视化MetaTiME组件
# 识别主要细胞类型
mt.plot_me_score(
adata,
me_model,
n_top=10, # 显示前10个元组件
figsize=(12, 8),
)

# Step 5: 结合CellTypist
import celltypist
from celltypist import models

# 下载TME相关模型
models.download_models(model=["Immune_All_Low.pkl"])

predictions = celltypist.annotate(
adata,
model="Immune_All_Low.pkl",
majority_voting=True,
)
adata = predictions.to_adata()

# Step 6: 整合两种注释结果
# 比较CellTypist和MetaTiME的注释
# 对于不一致的细胞,需要手动验证

# Step 7: 可视化对比
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架构的迁移学习细胞类型注释方法。其核心创新在于:

  1. Transformer注意力机制:使用自注意力(Self-Attention)机制捕获基因间的长程依赖关系
  2. 迁移学习:在大规模参考数据上预训练,然后迁移到新数据集上
  3. 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
# ============================================================
# TOSICA 迁移注释
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install TOSICA
import TOSICA as tos

# Step 1: 准备参考数据集(已标注)
ref_adata = sc.read_h5ad("annotated_reference.h5ad")
print(f"参考数据集: {ref_adata.n_obs} cells, {ref_adata.n_vars} genes")

# Step 2: 准备查询数据集(待注释)
query_adata = sc.read_h5ad("query_data.h5ad")
print(f"查询数据集: {query_adata.n_obs} cells, {query_adata.n_vars} genes")

# Step 3: 训练TOSICA模型
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, # Transformer层数
hidden_dim=256, # 隐藏层维度
dropout=0.1,
save_dir='./tosica_model', # 模型保存目录
)

# Step 4: 预测查询数据
predictions = tos.predict(
query_adata,
model_path='./tosica_model',
batch_size=256,
)

# Step 5: 添加预测结果
query_adata.obs['tosica_label'] = predictions['predicted_label']
query_adata.obs['tosica_prob'] = predictions['probability']

# Step 6: 可视化
sc.pl.umap(
query_adata,
color=['tosica_label', 'tosica_prob'],
frameon=False,
ncols=2,
save='_tosica.png',
)

# Step 7: 与手动注释对比
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:.4f}")
print(f"TOSICA NMI: {nmi:.4f}")

print("TOSICA注释完成!")

TOSICA注意事项:

  • 需要足够大的参考数据集进行训练
  • 基因特征文件(gff)的准备是关键步骤
  • GPU加速可显著提高训练速度
  • 对于与参考数据差异较大的查询数据(如不同物种),迁移效果可能下降

9.4 手动+自动结合策略

最佳实践是结合手动注释和自动注释的优势:

  1. 先自动后手动:先使用自动注释获得初步标签,再手动验证和修正
  2. 交叉验证:使用多种自动注释方法,一致性高的注释更可信
  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
# ============================================================
# 手动+自动结合策略
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np
from sklearn.metrics import adjusted_rand_score

adata = sc.read_h5ad("preprocessed_data.h5ad")

# Step 1: 运行多种自动注释
# CellTypist
import celltypist
from celltypist import models
models.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']

# Step 2: 对比不同方法
# 比较CellTypist与手动注释(如果已有)
if 'manual_label' in adata.obs.columns:
# 计算一致性
agreement = (adata.obs['celltypist_label'] == adata.obs['manual_label']).mean()
print(f"CellTypist与手动注释一致率: {agreement:.2%}")

# Step 3: 识别不一致的细胞
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']

# Step 4: 对不一致的细胞进行手动检查
# 查看这些细胞的标记基因表达
review_cells = adata[adata.obs['needs_review']].copy()
print(f"需要手动验证的细胞数: {review_cells.n_obs}")

# Step 5: 最终注释
# 综合多种信息,确定最终标签
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
# 使用CellMarker数据库
import pandas as pd

# 下载CellMarker数据库
cellmarker_url = "http://bio-bigdata.hrbmu.edu.cn/CellMarker/download/all_cell_markers.txt"
# cellmarker_db = pd.read_csv(cellmarker_url, sep='\t')

# 本地加载(假设已下载)
# cellmarker_db = pd.read_csv("CellMarker_all_markers.txt", sep='\t')
# print(cellmarker_db.head())
# print(f"细胞类型数: {cellmarker_db['cell_type'].nunique()}")
# print(f"标记基因数: {cellmarker_db['gene_symbol'].nunique()}")

# 查询特定细胞类型的标记基因
# tcell_markers = cellmarker_db[cellmarker_db['cell_type'] == 'T cell']['gene_symbol'].unique()
# print(f"T cell标记基因: {tcell_markers[:20]}")
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
# 使用PanglaoDB数据库
import pandas as pd

# 下载PanglaoDB标记基因
panglao_url = "https://panglaodb.se/markers/PanglaoDB_markers_27_Mar_2020.tsv"
# panglao_db = pd.read_csv(panglao_url, sep='\t')

# 本地加载(假设已下载)
# panglao_db = pd.read_csv("PanglaoDB_markers.tsv", sep='\t')
# print(panglao_db.head())

# 过滤人类标记基因
# human_markers = panglao_db[panglao_db['species'] == 'Hs']
# print(f"人类细胞类型数: {human_markers['cell type'].nunique()}")

# 获取高特异性标记基因
# high_spec = human_markers[human_markers['specificity'] > 0.8]

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 sc
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ============================================
# Step 1: 数据加载与预处理
# ============================================
adata = sc.read_h5ad("after_clustering.h5ad")
print(f"数据维度: {adata.shape}")
print(f"聚类数: {adata.obs['leiden'].nunique()}")

# ============================================
# Step 2: 标记基因鉴定
# ============================================
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)

# ============================================
# Step 3: 自动注释(CellTypist)
# ============================================
import celltypist
from celltypist import models

models.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']

# ============================================
# Step 4: 手动验证
# ============================================
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',
)

# ============================================
# Step 5: 综合注释
# ============================================
# 结合自动注释和手动验证结果
# 创建最终注释
final_annotation = {}
for cluster in adata.obs['leiden'].unique():
# 获取该簇的CellTypist预测
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)

# ============================================
# Step 6: 保存结果
# ============================================
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 注意事项

  1. 不要过度依赖自动注释:自动注释工具可能产生错误,特别是对于稀有细胞类型或参考数据集中未包含的细胞类型。

  2. 标记基因的物种特异性:人和小鼠的同源基因可能有不同的表达模式,跨物种注释需要格外谨慎。

  3. 细胞状态vs细胞类型:注意区分细胞类型和细胞状态的注释。同一细胞类型在不同状态下可能表达不同的标记基因。

  4. 聚类分辨率的影响:注释结果依赖于聚类分辨率。分辨率过低会合并不同细胞类型,过高会将同一类型拆分为多个簇。

  5. doublet的影响:未去除的doublet可能同时表达多种细胞类型的标记基因,干扰注释。

  6. 数据库更新:标记基因数据库需要定期更新,以纳入最新发现的标记基因。

  7. 命名规范:使用标准化的细胞类型命名,推荐参考Cell Ontology(https://cellontology.org/)。

  8. 可重复性:记录注释过程中的所有决策和依据,确保分析可重复。

关键参考文献:

  • 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)是不可避免的系统性变异。理解批次效应的来源对于选择合适的整合方法至关重要。

技术批次效应:

技术批次效应来源于实验过程中的系统性差异:

  1. 样本制备差异:不同批次的组织消化时间、酶浓度、温度等条件的微小差异会导致细胞转录组的系统性偏移。

  2. 测序平台差异:10x Chromium v2 vs v3、Smart-seq2 vs 10x、不同测序深度等平台间存在固有差异。

  3. 建库策略差异:3’端测序 vs 5’端测序、全长测序 vs 3’端富集等建库方法的不同。

  4. 操作人员差异:不同实验人员的操作习惯会导致系统性的技术变异。

  5. 实验室环境差异:不同实验室的仪器、试剂批次、环境条件等。

生物批次效应:

生物变异是我们希望保留的信号,但有时会被误认为是批次效应:

  1. 个体差异:不同供体的遗传背景、年龄、性别等导致的真实生物学差异。

  2. 组织微环境差异:同一器官不同区域、原发灶vs转移灶等空间异质性。

  3. 疾病状态差异:健康vs疾病、不同分期/分级间的生物学差异。

  4. 时间动态差异:发育或疾病进展过程中的真实时间变化。

区分原则:整合方法的目标是消除技术批次效应,同时保留真实的生物学变异。过度整合会抹除真实的生物学信号,整合不足则遗留技术噪声。

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
# ============================================================
# scVI 整合
# ============================================================
import scanpy as sc
import scvi
import pandas as pd
import numpy as np

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")
print(f"数据维度: {adata.shape}")
print(f"批次信息: {adata.obs['batch'].value_counts()}")

# Step 2: 设置scVI
# 指定批次标签列和细胞类型列(如果有的话)
scvi.model.SCVI.setup_anndata(
adata,
layer="counts", # 使用原始计数层
batch_key="batch", # 批次标签列
# labels_key="cell_type", # 如果有标签(用于scANVI)
categorical_covariate_keys=["sample"], # 其他分类协变量
continuous_covariate_keys=["percent_mito"], # 连续协变量
)

# Step 3: 创建scVI模型
model = scvi.model.SCVI(
adata,
n_latent=30, # 潜在空间维度
n_layers=2, # 编码器/解码器层数
n_hidden=128, # 隐藏层维度
gene_likelihood="zinb", # 基因表达分布(zinb/nb/poisson)
dispersion="gene-batch", # dispersion参数化方式
use_observed_lib_size=False, # 是否使用观测的library size
)

# Step 4: 训练模型
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()

# Step 5: 获取潜在表示
adata.obsm["X_scVI"] = model.get_latent_representation()

# Step 6: 获取归一化表达矩阵
adata.layers["scvi_normalized"] = model.get_normalized_expression()

# Step 7: 基于scVI潜在表示进行下游分析
sc.pp.neighbors(adata, use_rep="X_scVI")
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.8)

# Step 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的训练过程:

  1. 先用scVI预训练(无标签信息)
  2. 然后用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
# ============================================================
# scANVI 半监督整合+注释
# ============================================================
import scanpy as sc
import scvi
import pandas as pd
import numpy as np

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")

# 假设部分细胞已有标签(如通过手动注释获得)
# 未标注细胞设为"Unknown"
# adata.obs['cell_type'] 中已知标签和"Unknown"混合

print(f"已知标签细胞数: {(adata.obs['cell_type'] != 'Unknown').sum()}")
print(f"未知标签细胞数: {(adata.obs['cell_type'] == 'Unknown').sum()}")

# Step 2: 先训练scVI预训练模型
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)

# Step 3: 创建scANVI模型(基于scVI预训练)
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",
)

# Step 4: 训练scANVI
scanvi_model.train(max_epochs=200, early_stopping=True)

# Step 5: 获取预测结果
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)

# Step 6: 下游分析
sc.pp.neighbors(adata, use_rep="X_scANVI")
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.8)

# Step 7: 可视化
sc.pl.umap(
adata,
color=['batch', 'scanvi_label', 'scanvi_prob'],
frameon=False,
ncols=2,
save='_scanvi_integration.png',
)

# Step 8: 评估注释质量
# 查看预测概率分布
import matplotlib.pyplot as plt
plt.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再微调)
推荐场景 纯整合需求 整合+注释联合需求

推荐策略:

  • 如果只需要整合:使用scVI
  • 如果需要同时整合和注释:使用scANVI
  • 如果标签质量不确定:先用scVI整合,手动验证后再用scANVI
  • 如果有高质量标签:直接使用scANVI
10.2.4 Harmony

原理:

Harmony是一种基于迭代聚类的数据整合方法,由Regev实验室开发。其核心思想是在降维后的PCA空间中,通过迭代软聚类和线性校正来消除批次效应。

Harmony的算法步骤:

  1. 初始化:对合并的数据进行PCA降维,得到 $\mathbf{Z}^{(0)} \in \mathbb{R}^{d \times N}$

  2. 迭代更新:

    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$$

  3. 收敛判断:当 $|\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
# ============================================================
# Harmony 整合
# ============================================================
import scanpy as sc
import harmonypy as hm
import pandas as pd
import numpy as np

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")

# Step 2: PCA降维
sc.pp.pca(adata, n_comps=50)

# Step 3: 运行Harmony
harmony_out = hm.run_harmony(
adata.obsm['X_pca'], # PCA嵌入
adata.obs, # 元数据
'batch', # 批次标签列
max_iter_harmony=20, # 最大迭代次数
epsilon_harmony=1e-5, # 收敛阈值
sigma=0.1, # 聚类带宽
theta=2.0, # 批次校正强度(越大越强)
)

# Step 4: 使用Harmony校正后的PCA
adata.obsm['X_harmony'] = harmony_out.Z_corr.T

# Step 5: 基于Harmony结果进行下游分析
sc.pp.neighbors(adata, use_rep='X_harmony')
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.8)

# Step 6: 可视化
sc.pl.umap(
adata,
color=['batch', 'leiden'],
frameon=False,
ncols=2,
save='_harmony_integration.png',
)

# 方式2:使用scanpy内置的Harmony接口
# sc.external.pp.harmony_integrate(
# adata,
# key='batch',
# basis='X_pca',
# adjusted_basis='X_harmony',
# max_iter_harmony=20,
# )

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图时,确保每个细胞的邻居来自各个批次而非仅来自同一批次。

算法步骤:

  1. 对每个细胞,在每个批次中分别寻找 $k/b$ 个最近邻($b$ 为批次数)
  2. 合并所有批次的邻居,构建批次平衡的KNN图
  3. 基于该图进行后续的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
# ============================================================
# BBKNN 整合
# ============================================================
import scanpy as sc
import bbknn

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")

# Step 2: PCA降维
sc.pp.pca(adata, n_comps=50)

# Step 3: 运行BBKNN
# 方式1:使用bbknn包
bbknn.bbknn(
adata,
batch_key='batch', # 批次标签列
neighbors_within_batch=15, # 每个批次内的邻居数
n_pcs=50, # 使用的PCA维度
trim=None, # 修剪参数
)

# 方式2:使用scanpy内置接口
# sc.external.pp.bbknn(
# adata,
# batch_key='batch',
# neighbors_within_batch=15,
# n_pcs=50,
# )

# Step 4: 下游分析
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.8)

# Step 5: 可视化
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
# ============================================================
# ComBat 批次校正
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")

# Step 2: 使用scanpy内置ComBat
sc.pp.combat(
adata,
key='batch', # 批次标签列
# covariates=['condition'], # 保留的生物协变量
)

# Step 3: 下游分析
sc.pp.pca(adata, n_comps=50)
sc.pp.neighbors(adata)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.8)

# Step 4: 可视化
sc.pl.umap(
adata,
color=['batch', 'leiden'],
frameon=False,
ncols=2,
save='_combat_integration.png',
)

# 方式2:使用inpyconbat包(更灵活的ComBat实现)
# import inpyconbat
# corrected = inpyconbat.combat(
# data=adata.X.T, # 基因x细胞矩阵
# batch=adata.obs['batch'].values,
# mod=None, # 协变量设计矩阵
# par_prior=True, # 使用参数先验
# prior_plots=False,
# )
# adata.X = corrected.T

adata.write_h5ad("combat_integrated_data.h5ad")
print("ComBat校正完成!")

ComBat注意事项:

  • ComBat假设数据近似正态分布,对log-normalized数据效果更好
  • 不适用于count数据
  • 对于少量样本的批次,经验贝叶斯估计可能不稳定
  • ComBat是线性校正方法,无法处理非线性的批次效应

10.3 整合效果评估:scIB指标

scIB(single-cell Integration Benchmarking)是一个系统评估单细胞数据整合效果的工具,包含多个评估指标。

评估维度:

  1. 批次效应去除(Batch Correction):

    • graph connectivity (gc):评估批次内KNN图的连通性,值越高表示同一细胞类型的细胞越紧密连接
    • silhouette score (asw_label):基于细胞类型标签的轮廓系数,评估聚类质量
    • iLISI:局部逆Simpson指数,评估批次混合程度
  2. 生物学变异保留(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
# ============================================================
# scIB 整合效果评估
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install scib
import scib

# Step 1: 准备评估数据
adata = sc.read_h5ad("integrated_data.h5ad")

# 确保数据包含:
# - adata.obs['batch']: 批次标签
# - adata.obs['cell_type']: 细胞类型标签(ground truth)
# - adata.obsm['X_pca']: PCA嵌入
# - adata.obsm['X_umap']: UMAP嵌入
# - adata.uns['neighbors']: 邻居图

# Step 2: 计算所有scIB指标
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, # 计算NMI和ARI
silhouette_=True, # 计算轮廓系数
graph_conn_=True, # 计算图连通性
pcr_=True, # 计算PCR批次效应
isolated_labels_=True, # 计算孤立标签F1
lisi_graph_=True, # 计算LISI指标
)

print(metrics)

# Step 3: 单独计算关键指标
# 图连通性
gc = scib.me.graph_connectivity(adata, label_key='cell_type')
print(f"Graph Connectivity: {gc:.4f}")

# 轮廓系数
sil = scib.me.silhouette(adata, label_key='cell_type', embed='X_pca')
print(f"Silhouette Score: {sil:.4f}")

# NMI
nmi = scib.me.nmi(adata, label_key='cell_type', cluster_key='leiden')
print(f"NMI: {nmi:.4f}")

# ARI
ari = scib.me.ari(adata, label_key='cell_type', cluster_key='leiden')
print(f"ARI: {ari:.4f}")

# iLISI(批次混合程度)
ilisi = scib.me.ilisi_graph(adata, batch_key='batch')
print(f"iLISI: {ilisi:.4f}")

# cLISI(细胞类型纯度)
clisi = scib.me.clisi_graph(adata, batch_key='batch', label_key='cell_type')
print(f"cLISI: {clisi:.4f}")

# Step 4: 比较不同整合方法
# 分别对scVI、Harmony、BBKNN等整合结果计算指标
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 plt
fig, 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
# ============================================================
# omicverse 一站式整合
# ============================================================
import omicverse as ov
import scanpy as sc

# Step 1: 准备数据
adata = sc.read_h5ad("multi_batch_data.h5ad")

# Step 2: 使用omicverse的整合接口
# ov.single.batch_correction支持多种方法
adata_integrated = ov.single.batch_correction(
adata,
batch_key='batch',
method='scvi', # 可选: 'scvi', 'scanvi', 'harmony', 'bbknn', 'combat'
n_pcs=50,
n_neighbors=15,
# scvi参数
n_latent=30,
max_epochs=400,
)

# 也可以分别调用
# scVI
adata_scvi = ov.single.scvi_integrate(
adata,
batch_key='batch',
n_latent=30,
max_epochs=400,
)

# Harmony
adata_harmony = ov.single.harmony_integrate(
adata,
batch_key='batch',
theta=2.0,
)

# Step 3: 下游分析
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 sc
import scvi
import harmonypy as hm
import bbknn
import scib
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ============================================
# Step 1: 加载多批次数据
# ============================================
# 合并多个批次的数据
batch1 = sc.read_h5ad("batch1.h5ad")
batch2 = sc.read_h5ad("batch2.h5ad")
batch3 = sc.read_h5ad("batch3.h5ad")

# 使用anndata合并
adata = batch1.concatenate(batch2, batch3, batch_key='batch', batch_categories=['B1', 'B2', 'B3'])

# 或者从单个文件加载
# adata = sc.read_h5ad("multi_batch_data.h5ad")

print(f"总细胞数: {adata.n_obs}")
print(f"各批次细胞数: {adata.obs['batch'].value_counts()}")

# ============================================
# Step 2: 预处理
# ============================================
# 基本过滤
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]

# 归一化(保留原始计数用于scVI)
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,
)

# ============================================
# Step 3: 整合前可视化(查看批次效应)
# ============================================
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')

# ============================================
# Step 4: scVI整合
# ============================================
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()

# ============================================
# Step 5: Harmony整合
# ============================================
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

# ============================================
# Step 6: BBKNN整合
# ============================================
# BBKNN直接修改邻居图
adata_bbknn = adata.copy()
bbknn.bbknn(adata_bbknn, batch_key='batch', neighbors_within_batch=15)

# ============================================
# Step 7: 比较整合效果
# ============================================
# 使用scVI的潜在表示
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')

# 使用Harmony的表示
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')

# ============================================
# Step 8: 保存最终结果
# ============================================
adata.write_h5ad("fully_integrated_data.h5ad")
model_scvi.save("final_scvi_model/", overwrite=True)
print("数据整合完成!")

10.6 注意事项:过度整合风险

过度整合(Over-integration)是数据整合中最常见的陷阱之一,其表现为:

  1. 稀有细胞类型丢失:过度校正可能将稀有细胞类型”整合”到相近但不同的常见细胞类型中。

  2. 真实的生物学差异被抹除:不同条件下的同一细胞类型可能存在真实的转录组差异(如激活vs静息T细胞),过度整合会将这些差异消除。

  3. 信号平滑化:过度整合使所有细胞看起来过于相似,失去了数据的异质性信息。

避免过度整合的策略:

  1. 调整整合强度:

    • Harmony的theta参数:减小theta值降低整合强度
    • scVI的模型容量:减少n_latent或n_layers可能减少过度整合
    • BBKNN的neighbors_within_batch:减小此值降低整合强度
  2. 保留已知的生物协变量:

    • 在ComBat中将已知条件作为协变量保留
    • 在scVI中使用categorical_covariate_keys显式建模生物协变量
  3. 评估稀有细胞类型:整合后检查稀有细胞类型是否仍然存在。

  4. 分层整合:先在大类层面整合,再在亚类层面整合,避免跨大类过度整合。

  5. 多方法比较:使用多种整合方法,比较结果的一致性。

关键参考文献:

  • 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
# ============================================================
# Wilcoxon秩和检验 DEG分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 加载数据
adata = sc.read_h5ad("annotated_data.h5ad")

# 方法1:使用rank_genes_groups(最常用)
sc.tl.rank_genes_groups(
adata,
groupby='cell_type', # 按细胞类型分组
method='wilcoxon', # Wilcoxon秩和检验
corr_method='benjamini-hochberg',
pts=True, # 计算表达比例
use_raw=True, # 使用raw层
)

# 查看结果
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)

# 方法2:在特定条件下比较(如疾病vs健康)
# 先选择特定细胞类型
t_cells = adata[adata.obs['cell_type'] == 'CD4+ T cells'].copy()

sc.tl.rank_genes_groups(
t_cells,
groupby='condition', # 按条件分组(如疾病vs健康)
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)

# 过滤显著DEG
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')

局限性:

  1. 伪重复问题(Pseudoreplication):Wilcoxon检验将每个细胞视为独立样本,但来自同一个体的细胞之间存在相关性。这会严重膨胀统计量,导致假阳性率升高。

  2. 对大量细胞敏感:当细胞数很多时(如 >10,000),即使非常小的差异也可能被检测为”显著”。

  3. 无法处理混杂变量:简单的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
# ============================================================
# MAST DEG分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装MAST(R包)
# 在Python中使用rpy2调用

# 方式1:通过rpy2调用MAST
import rpy2.robjects as ro
from rpy2.robjects import pandas2ri
from rpy2.robjects.packages import importr

pandas2ri.activate()
mast = importr('MAST')

# 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")
t_cells = adata[adata.obs['cell_type'] == 'CD4+ T cells'].copy()

# ---- R代码等价 ----
# library(MAST)
#
# # 创建SingleCellAssay对象
# sca <- FromMatrix(
# exprsArray = t_cells@assays$RNA@data,
# cData = t_cells@meta.data,
# fData = data.frame(gene = rownames(t_cells))
# )
#
# # 定义条件
# cond <- factor(sca$condition)
# sca$condition <- cond
#
# # 定义协变量(如细胞检测基因数cngeneson)
# sca$cngeneson <- scale(sca$n_genes)
#
# # 构建模型公式
# zlmCond <- zlm(~ condition + cngeneson, sca)
#
# # 提取差异表达结果
# summaryCond <- summary(zlmCond, doLRT='conditiondisease')
# summaryDt <- summaryCond$datatable
#
# # 获取结果
# contrast <- summaryDt[summaryDt$component == 'H',
# c('primerid', 'contrast', 'stat', 'p.value')]
# fc <- summaryDt[summaryDt$component == 'logFC',
# c('primerid', 'contrast', 'coef', 'ci.hi', 'ci.lo')]
#
# # 合并结果
# result <- merge(contrast, fc, by=c('primerid', 'contrast'))
# result$FDR <- p.adjust(result$p.value, method='BH')
# ---- R代码结束 ----

# 方式2:使用Python实现简化版MAST
from statsmodels.api import Logit, OLS
from scipy.stats import chi2

def 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]: # 示例:只分析前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)

# 离散部分:logistic回归
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

# 运行
# deg_results = mast_like_deg(adata, 'cell_type', 'condition')
# print(deg_results.sort_values('pval_adj').head(20))
11.1.3 Pseudobulk方法

原理:

Pseudobulk(伪批量)方法是当前最推荐的差异表达分析方法。其核心思想是:将来自同一个体的同一细胞类型的所有细胞的表达值求和或取平均,生成”伪bulk”样本,然后使用成熟的bulk RNA-seq差异表达工具进行分析。

为什么推荐Pseudobulk:

关键问题在于伪重复(Pseudoreplication)。在单细胞数据中,来自同一个体的细胞不是独立的——它们共享同一个体的遗传背景、微环境等。将每个细胞视为独立样本的统计方法(如Wilcoxon、MAST)违反了独立性假设,导致:

  1. 假阳性率膨胀:因为同一个体的多个细胞提供的是”伪重复”而非真正的生物学重复
  2. p值不可靠:统计检验假设样本独立,违反此假设导致p值不可信
  3. 效应量估计偏差:伪重复放大了表观效应量

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
# ============================================================
# Pseudobulk DEG分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np
import decoupler as dc # 用于生成pseudobulk

# Step 1: 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")

# 确保数据包含:
# - adata.obs['cell_type']: 细胞类型标签
# - adata.obs['sample']: 样本/个体标签
# - adata.obs['condition']: 实验条件

# Step 2: 生成Pseudobulk数据
# 使用decoupler的get_pseudobulk函数
pseudobulk = dc.get_pseudobulk(
adata,
sample_col='sample', # 样本标识列
groups_col='cell_type', # 细胞类型列
layer='counts', # 使用原始计数
mode='sum', # 求和(也可用'mean')
min_cells=10, # 每个伪bulk样本最少包含的细胞数
min_counts=1000, # 每个伪bulk样本最少总计数
)

print(f"Pseudobulk数据维度: {pseudobulk.shape}")
print(f"样本数: {pseudobulk.n_obs}")

# Step 3: 使用DESeq2进行差异分析
# 安装: pip install pydeseq2
from pydeseq2.DeseqDataSet import DeseqDataSet
from pydeseq2.DeseqStats import DeseqStats

# 选择特定细胞类型
cell_type = 'CD4+ T cells'
pb_ct = pseudobulk[pseudobulk.obs['cell_type'] == cell_type].copy()

# 过滤低表达基因
sc.pp.filter_genes(pb_ct, min_cells=3)

# 创建DESeq2数据集
dds = DeseqDataSet(
counts=pb_ct.X.astype(int),
metadata=pb_ct.obs[['condition', 'sample']],
design_factors='condition', # 设计公式
)

# 运行DESeq2
dds.deseq2()

# 提取结果
stat_res = DeseqStats(dds, contrast=['condition', 'disease', 'healthy'])
stat_res.summary()

# 获取结果DataFrame
deseq_results = stat_res.results_df
deseq_results = deseq_results.sort_values('pvalue')
print(deseq_results.head(20))

# 过滤显著DEG
sig_degs = deseq_results[
(deseq_results['padj'] < 0.05) &
(deseq_results['log2FoldChange'].abs() > 1.0)
]
print(f"显著DEG数: {len(sig_degs)}")

# Step 4: 可视化
# 火山图
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 8))
plt.scatter(
deseq_results['log2FoldChange'],
-np.log10(deseq_results['pvalue']),
c='gray', alpha=0.5, s=10,
)
# 高亮显著DEG
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
# ============================================================
# DESeq2 完整代码(基于Pseudobulk)
# ============================================================
from pydeseq2.DeseqDataSet import DeseqDataSet
from pydeseq2.DeseqStats import DeseqStats
import scanpy as sc
import pandas as pd
import numpy as np

# Step 1: 生成Pseudobulk
import 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,
)

# Step 2: 对每种细胞类型运行DESeq2
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 # 至少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: # DESeq2需要足够的样本数
print(f"{cell_type}: 样本数不足({n_samples}),跳过")
continue

try:
# 创建DESeq2对象
dds = DeseqDataSet(
counts=pb_ct.X.astype(int),
metadata=pb_ct.obs,
design_factors='condition',
)

# 运行DESeq2
dds.deseq2()

# 提取结果
stat_res = DeseqStats(dds, contrast=['condition', 'disease', 'healthy'])
stat_res.summary()

results = stat_res.results_df
all_results[cell_type] = results

# 报告显著DEG
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}")

# Step 3: 保存所有结果
with pd.ExcelWriter("all_celltype_degs.xlsx") as writer:
for ct, res in all_results.items():
res.to_excel(writer, sheet_name=ct[:31]) # Excel sheet名最长31字符

print("DESeq2分析完成!")
11.1.5 SEACells元细胞方法

原理:

SEACells(Single-cell Aggregation via Elliptical Atoms)是一种元细胞(Metacell)方法,通过将相似的细胞聚合为”元细胞”,减少数据噪声同时保留生物学信号。

SEACells的算法核心是使用非负矩阵分解(NMF)和贪心算法来寻找元细胞:

  1. 计算细胞间的核相似度矩阵:$K_{ij} = \exp(-|\mathbf{x}_i - \mathbf{x}_j|^2 / 2\sigma^2)$
  2. 使用NMF学习元细胞权重矩阵 $G \in \mathbb{R}^{N \times M}$($M$ 为元细胞数,$N$ 为细胞数)
  3. 约束 $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
# ============================================================
# SEACells 元细胞方法
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install SEACells
import SEACells

# Step 1: 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")

# Step 2: 确定元细胞数量
# 经验法则:每100-1000个细胞对应1个元细胞
n_metacells = max(adata.n_obs // 100, 10)
print(f"建议元细胞数: {n_metacells}")

# Step 3: 运行SEACells
# 基于PCA空间构建
model = SEACells.core.SEACells(
adata,
n_cells=n_metacells, # 元细胞数量
build_kernel_on='X_pca', # 基于PCA空间
n_waypoint_eigs=10, # 用于初始化的特征向量数
convergence_epsilon=1e-5, # 收敛阈值
max_iter=100, # 最大迭代次数
)

# 拟合模型
model.fit()

# Step 4: 获取元细胞标签
adata.obs['metacell'] = model.get_assignments()

# Step 5: 生成元细胞表达矩阵
metacell_adata = SEACells.core.summarize_by_soft_assignment(
adata,
model,
summarize_layer='counts', # 使用计数层
)

print(f"元细胞数: {metacell_adata.n_obs}")

# Step 6: 基于元细胞进行DEG分析
# 使用元细胞作为"伪bulk"样本
# 然后用DESeq2或edgeR分析
# ...(参见11.1.3的Pseudobulk代码)

# Step 7: 可视化
sc.pl.umap(
adata,
color='metacell',
frameon=False,
save='_metacells.png',
)
11.1.6 三种方法对比与推荐
特征 Wilcoxon MAST Pseudobulk
统计单位 单个细胞 单个细胞 个体/样本
伪重复问题 严重 严重 无
协变量控制 无 有 有
假阳性率 高 中等 低
统计功效 高(假阳性高) 中等 适中
计算速度 快 慢 中等
适用场景 初步筛选 控制协变量 正式分析
推荐程度 ★★ ★★★ ★★★★★

推荐策略:

  1. 正式发表的DEG分析:必须使用Pseudobulk方法
  2. 初步探索:可以使用Wilcoxon快速筛选
  3. 需要控制协变量:MAST或Pseudobulk
  4. 样本数少:Pseudobulk可能统计功效不足,可结合Wilcoxon结果
11.1.7 多重检验校正

在DEG分析中,需要同时检验数千个基因,因此必须进行多重检验校正以控制假阳性率。

常用方法:

  1. Benjamini-Hochberg (BH) FDR:控制错误发现率(False Discovery Rate),最常用

$$\text{FDR} = \frac{V}{\max(R, 1)}$$

其中 $V$ 是假阳性数,$R$ 是总阳性数。BH方法保证 $\text{FDR} \leq \alpha$。

  1. Bonferroni校正:控制家族错误率(Family-Wise Error Rate),最保守

$$p_{\text{adj}} = \min(p \times m, 1)$$

其中 $m$ 是检验总数。过于保守,可能导致假阴性增加。

  1. Benjamini-Yekutieli:适用于检验间存在相关性时
1
2
3
4
5
6
7
from statsmodels.stats.multitest import multipletests

# BH校正(最常用)
pvals_adj_bh = multipletests(pvals, alpha=0.05, method='fdr_bh')[1]

# Bonferroni校正(最保守)
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 sc
import pandas as pd
import numpy as np
import decoupler as dc
import matplotlib.pyplot as plt

# ============================================
# Step 1: 数据准备
# ============================================
adata = sc.read_h5ad("annotated_data.h5ad")
print(f"细胞数: {adata.n_obs}")
print(f"细胞类型: {adata.obs['cell_type'].unique()}")
print(f"条件: {adata.obs['condition'].unique()}")

# ============================================
# Step 2: 初步筛选(Wilcoxon)
# ============================================
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}',
)

# ============================================
# Step 3: 正式分析(Pseudobulk + DESeq2)
# ============================================
# 生成Pseudobulk
pseudobulk = dc.get_pseudobulk(
adata,
sample_col='sample',
groups_col='cell_type',
layer='counts',
mode='sum',
min_cells=10,
)

# 对每种细胞类型运行DESeq2
from pydeseq2.DeseqDataSet import DeseqDataSet
from pydeseq2.DeseqStats import DeseqStats

all_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}")

# ============================================
# Step 4: 可视化
# ============================================
# 火山图
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()

# ============================================
# Step 5: 保存结果
# ============================================
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 注意事项

  1. 必须使用Pseudobulk:对于需要正式发表的DEG分析,Pseudobulk是唯一能正确处理伪重复问题的方法。

  2. 最小样本数:Pseudobulk方法需要足够的生物学重复(建议 ≥ 3 per condition per cell type)。

  3. 低表达基因过滤:DESeq2等工具对低表达基因敏感,应在分析前过滤。

  4. 效应量与统计显著性:不仅要看p值,还要看log2FC。大量细胞可能使微小差异变得”显著”。

  5. 多重检验校正:始终使用FDR校正,不要仅依赖原始p值。

  6. 协变量控制:在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$),这使得:

  1. 比例间存在人为的负相关(一个增加必然导致其他减少)
  2. 不能直接使用标准统计检验(如t检验、卡方检验)
  3. 需要专门的统计模型

零膨胀问题:某些细胞类型在部分样本中可能完全不存在(比例为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
# ============================================================
# scCODA 细胞比例分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install scCODA
from sccoda.util import comp_ana as mod
from sccoda.util import cell_composition_data as dat
from sccoda.model import other_models as om

# Step 1: 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")

# 确保数据包含:
# - adata.obs['cell_type']: 细胞类型标签
# - adata.obs['sample']: 样本标识
# - adata.obs['condition']: 实验条件

# Step 2: 创建scCODA数据对象
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}")

# Step 3: 选择参考细胞类型
# 参考类型应选择在条件间比例最稳定的细胞类型
# 可以通过初步可视化判断

# 查看各细胞类型的比例
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 plt
fig, 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()

# Step 4: 运行scCODA
# 选择参考细胞类型(如CD4+ T cells,通常比例较大且稳定)
model_sc = mod.CompositionalAnalysis(
data_sc,
formula="condition", # 模型公式
reference_cell_type="CD4+ T cells", # 参考细胞类型
)

# MCMC采样
samples = model_sc.sample_hmc(
num_samples=10000, # MCMC采样数
num_warmup=5000, # warm-up采样数
num_chains=4, # MCMC链数
rng_key=42, # 随机种子
)

# Step 5: 查看结果
# 计算后验概率
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()

# Step 6: 提取显著变化的细胞类型
# 后验概率 > 0.95 的细胞类型被认为有显著变化
significant = results[results['Infinite probability'] > 0.95]
print(f"显著变化的细胞类型: {significant.index.tolist()}")

# Step 7: 自动选择参考类型
# scCODA提供了自动选择参考类型的方法
# 搜索最稳定的参考细胞类型
ref_types = dat.auto_reference(data_sc)
print(f"推荐的参考细胞类型: {ref_types}")

12.3 tascCODA

原理:

tascCODA(tree-aggregated scCODA)是scCODA的扩展,利用细胞类型之间的层级关系(树结构)来提高检测能力和可解释性。

tascCODA的核心思想是:

  1. 构建细胞类型的层级树(如从细胞大类到细胞亚型)
  2. 在树的节点上建模比例变化,而非仅在叶节点
  3. 通过树结构的稀疏先验,同时检测不同粒度上的比例变化

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
# ============================================================
# tascCODA 树结构比例分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

from sccoda.util import comp_ana as mod
from sccoda.util import cell_composition_data as dat
from sccoda.model import other_models as om
import sccoda

# Step 1: 准备数据(同scCODA)
adata = sc.read_h5ad("annotated_data.h5ad")

data_sc = dat.from_scanpy(
adata,
cell_type_identifier='cell_type',
sample_identifier='sample',
covariate_obs=['condition'],
)

# Step 2: 定义细胞类型层级树
# 创建树结构
# 格式:Newick格式或手动定义

# 方式1:从已有注释自动构建
# 树结构示例:
# Immune -> T cells -> CD4+ T cells
# CD8+ T cells
# Treg
# -> B cells
# -> Myeloid -> Monocytes
# -> Macrophages
# -> Dendritic cells
# Epithelial
# Stroma -> Fibroblasts
# -> Endothelial

tree_str = "((CD4_T_cells, CD8_T_cells, Treg)T_cells, B_cells, (Monocytes, Macrophages, DCs)Myeloid)Immune, Epithelial, (Fibroblasts, Endothelial)Stroma"

# 创建tascCODA数据对象
data_tasccoda = dat.from_scanpy(
adata,
cell_type_identifier='cell_type',
sample_identifier='sample',
covariate_obs=['condition'],
tree=tree_str, # 层级树
)

# Step 3: 运行tascCODA
model_tasc = mod.CompositionalAnalysisTree(
data_tasccoda,
formula="condition",
reference_cell_type="CD4_T_cells",
reg="scaled_by_tree", # 正则化方式
pen_args=dict(alpha=0.5), # 稀疏性参数
)

# MCMC采样
samples_tasc = model_tasc.sample_hmc(
num_samples=10000,
num_warmup=5000,
)

# Step 4: 查看结果
results_tasc = mod.summary(samples_tasc)
print(results_tasc)

# Step 5: 可视化树结构上的变化
mod.plot_tree_effects(
samples_tasc,
data_tasccoda,
figsize=(14, 8),
)
plt.savefig("tasccoda_tree_effects.png", dpi=300, bbox_inches='tight')
plt.show()

# Step 6: 提取显著变化的节点
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分析的关键步骤:

选择原则:

  1. 选择在条件间比例最稳定的细胞类型
  2. 优先选择比例较大的细胞类型(减少统计不确定性)
  3. 不应选择预期会发生变化的细胞类型

选择方法:

  1. 可视化评估:查看各细胞类型在不同条件下的比例分布
  2. scCODA自动选择:使用auto_reference()函数
  3. 敏感性分析:尝试不同的参考类型,检查结论是否一致

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 sc
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from sccoda.util import comp_ana as mod
from sccoda.util import cell_composition_data as dat

# ============================================
# Step 1: 数据准备
# ============================================
adata = sc.read_h5ad("annotated_data.h5ad")

# ============================================
# Step 2: 查看细胞组成
# ============================================
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()

# ============================================
# Step 3: 创建scCODA数据对象
# ============================================
data_sc = dat.from_scanpy(
adata,
cell_type_identifier='cell_type',
sample_identifier='sample',
covariate_obs=['condition'],
)

# ============================================
# Step 4: 选择参考细胞类型
# ============================================
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]

# ============================================
# Step 5: 运行scCODA
# ============================================
model_sc = mod.CompositionalAnalysis(
data_sc,
formula="condition",
reference_cell_type=ref_type,
)

samples = model_sc.sample_hmc(
num_samples=10000,
num_warmup=5000,
)

# ============================================
# Step 6: 结果解读
# ============================================
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:.3f}, 后验概率={prob:.3f})")

# ============================================
# Step 7: 保存结果
# ============================================
results.to_csv("sccoda_results.csv")
print("细胞比例分析完成!")

12.7 注意事项

  1. 参考类型的选择至关重要:不同的参考类型可能导致不同的结论。建议进行敏感性分析。

  2. 样本量要求:scCODA需要足够的生物学重复。建议每组至少3个样本。

  3. 零比例的处理:如果某细胞类型在某些样本中完全缺失,可能需要特殊处理。

  4. 协变量控制:可以在模型公式中纳入协变量(如年龄、性别)。

  5. MCMC收敛性:检查MCMC链的收敛性,可通过trace plot和$\hat{R}$统计量判断。

  6. 比例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
# ============================================================
# ORA 富集分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 使用gseapy进行ORA
# 安装: pip install gseapy
import gseapy as gp

# Step 1: 准备DEG列表
adata = sc.read_h5ad("annotated_data.h5ad")

# 获取DEG
sc.tl.rank_genes_groups(adata, groupby='condition', method='wilcoxon')
degs = sc.get.rank_genes_groups_df(adata, group='disease')

# 过滤显著DEG
sig_degs = degs[
(degs['pvals_adj'] < 0.05) &
(degs['logfoldchanges'] > 0.5)
]
gene_list = sig_degs['names'].tolist()

print(f"显著DEG数: {len(gene_list)}")

# Step 2: 运行ORA
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,
)

# Step 3: 查看结果
print(enr.results.head(20))

# Step 4: 可视化
# 条形图
from gseapy.plot import barplot, dotplot
barplot(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()

# Step 5: 使用本地基因集文件
# 自定义GMT文件
enr_custom = gp.enrichr(
gene_list=gene_list,
gene_sets='custom_genesets.gmt', # 本地GMT文件
organism='Human',
outdir='enrichr_custom_results',
)

13.2 GSEA(Gene Set Enrichment Analysis)

原理:

GSEA由Subramanian等人于2005年提出,与ORA不同,GSEA不需要预先设定差异表达的阈值,而是使用所有基因的排序信息。

GSEA算法步骤:

  1. 排序:按某种相关性度量(如log2FC或t统计量)对基因排序

  2. 计算富集得分(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)。

  1. 归一化ES:$NES = ES / \mu_{ES_{null}}$

  2. 显著性评估:通过排列检验估计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
# ============================================================
# GSEA 富集分析(使用fgsea)
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 方法1:使用fgsea(快速GSEA)
# 安装: pip install fgsea
# fgsea是R包,Python中使用gseapy替代

# 方法2:使用gseapy
import gseapy as gp

# Step 1: 准备排序基因列表
adata = 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']))

# Step 2: 运行GSEA
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, # 基因集最大大小
)

# Step 3: 查看结果
print(pre_res.res2d.head(20))

# Step 4: 可视化
# GSEA富集图
from gseapy.plot import gseaplot

# 绘制特定通路的富集图
for term in pre_res.res2d.index[:5]:
gseaplot(
rank_metric=pre_res.ranking,
term=term,
**pre_res.results[term],
ofname=f'gsea_{term}.png',
)

# Step 5: 使用fgsea(R)
# ---- R代码 ----
# library(fgsea)
#
# # 准备排序向量
# rnk <- setNames(rnk_scores, rnk_genes)
#
# # 加载基因集
# pathways <- gmtPathways("hallmark.gmt")
#
# # 运行fgsea
# fgsea_res <- fgsea(
# pathways = pathways,
# stats = rnk,
# nperm = 10000,
# minSize = 15,
# maxSize = 500,
# )
#
# # 查看结果
# head(fgsea_res[order(padj), ], 10)
#
# # 可视化
# plotEnrichment(pathways[["HALLMARK_INFLAMMATORY_RESPONSE"]], rnk)
# plotGseaTable(pathways, rnk, fgsea_res, gseaParam=0.5)
# ---- R代码结束 ----

13.3 AUCell

原理:

AUCell是一种基于基因集活性评分的方法,通过计算每个细胞中基因集内基因在表达排序中的曲线下面积(Area Under the Curve, AUC)来评估基因集活性。

算法步骤:

  1. 基因表达排序:对每个细胞,将所有基因按表达量从高到低排序
  2. 计算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$ 中的排名。

  1. 归一化: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
# ============================================================
# AUCell 基因集活性评分
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install pySCENIC (包含AUCell)
# 或使用decoupler的替代实现

# 方法1:使用decoupler实现AUCell-like评分
import decoupler as dc

# Step 1: 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")

# Step 2: 定义基因集
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'],
}

# Step 3: 使用AUCell计算活性得分
# 使用decoupler的ulm方法(类似于AUCell)
gene_set_df = pd.DataFrame([
{'source': name, 'target': gene}
for name, genes in gene_sets.items()
for gene in genes
])

# 运行AUCell-like评分
dc.run_aucell(
adata,
net=gene_set_df,
source='source',
target='target',
use_raw=False,
)

# Step 4: 查看结果
print(adata.obsm.keys()) # 应包含aucell_estimate
aucell_scores = dc.get_acts(adata, obsm_key='aucell_estimate')

# Step 5: 可视化
# 在UMAP上展示基因集活性
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',
)

# Step 6: 小提琴图——不同细胞类型的基因集活性
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是一个统一的基因集富集和调控网络推断框架,整合了多种方法:

  1. ORA:超几何检验
  2. GSEA:基于排序的富集分析
  3. GSVA:基因集变异分析
  4. AUCell:曲线下面积
  5. Viper:调控子活性推断
  6. ulm:单样本线性模型
  7. mlm:多元线性模型
  8. wmean:加权均值
  9. 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
# ============================================================
# decoupler 统一富集分析框架
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np
import decoupler as dc

# Step 1: 准备数据
adata = sc.read_h5ad("annotated_data.h5ad")

# Step 2: 获取基因集
# 从MSigDB获取
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)

# Step 3: 运行多种方法

# 方法1: ORA
dc.run_ora(
adata,
net=hallmark,
source='geneset',
target='genesymbol',
use_raw=False,
)
ora_scores = dc.get_acts(adata, obsm_key='ora_estimate')

# 方法2: GSEA(GSVA)
dc.run_gsva(
adata,
net=hallmark,
source='geneset',
target='genesymbol',
use_raw=False,
)
gsva_scores = dc.get_acts(adata, obsm_key='gsva_estimate')

# 方法3: AUCell
dc.run_aucell(
adata,
net=hallmark,
source='geneset',
target='genesymbol',
use_raw=False,
)
aucell_scores = dc.get_acts(adata, obsm_key='aucell_estimate')

# 方法4: ULM (Univariate Linear Model)
dc.run_ulm(
adata,
net=hallmark,
source='geneset',
target='genesymbol',
use_raw=False,
)
ulm_scores = dc.get_acts(adata, obsm_key='ulm_estimate')

# Step 4: 比较方法结果
# 提取某基因集的各方法得分
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())

# Step 5: 可视化
# UMAP上的基因集活性
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通路活性
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 基因集数据库

  1. GO(Gene Ontology):生物过程(BP)、分子功能(MF)、细胞组分(CC)

  2. KEGG:代谢和信号通路

  3. MSigDB:包含多个子集

    • Hallmark(50个核心通路)
    • C2( curated gene sets,包含KEGG、Reactome等)
    • C5(GO gene sets)
    • C7(免疫相关基因集)
    • H(Hallmark)
  4. Reactome:生物学通路数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 获取不同数据库
# GO
go = dc.get_resource('GO')
print(f"GO基因集数: {go['geneset'].nunique()}")

# KEGG
kegg = dc.get_resource('KEGG')
print(f"KEGG通路数: {kegg['geneset'].nunique()}")

# Reactome
reactome = dc.get_resource('Reactome')
print(f"Reactome通路数: {reactome['geneset'].nunique()}")

# MSigDB
msigdb = dc.get_resource('MSigDB')
collections = msigdb['collection'].unique()
print(f"MSigDB集合: {collections}")

13.6 各方法对比

方法 输入 单细胞级 优势 局限
ORA DEG列表 否 简单快速 依赖阈值,忽略基因排名
GSEA 排序基因列表 否 无需阈值,考虑排名 计算量较大
GSVA 表达矩阵 是 单样本得分 计算量最大
AUCell 表达矩阵 是 适合scRNA-seq 对基因集大小敏感
ULM 表达矩阵 是 统计框架 假设线性关系

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 sc
import pandas as pd
import numpy as np
import decoupler as dc
import gseapy as gp
import matplotlib.pyplot as plt

# ============================================
# Step 1: 数据准备
# ============================================
adata = sc.read_h5ad("annotated_data.h5ad")

# 获取DEG
sc.tl.rank_genes_groups(adata, groupby='condition', method='wilcoxon')
degs = sc.get.rank_genes_groups_df(adata, group='disease')

# ============================================
# Step 2: ORA分析
# ============================================
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,
)

# ============================================
# Step 3: GSEA分析
# ============================================
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,
)

# ============================================
# Step 4: 细胞级活性评分(AUCell/GSVA)
# ============================================
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')

# 添加得分到adata
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的算法流程:

  1. 特征选择:对每种细胞类型,使用随机森林或逻辑回归区分条件标签
  2. 交叉验证:通过多次交叉验证评估分类准确率
  3. 重要性排序:根据分类准确率对细胞类型排序,准确率越高表示该细胞类型对扰动响应越强
  4. 置换检验:通过置换标签计算统计显著性

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
# ============================================================
# Augur 扰动响应分析
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install augurpy
import augurpy as ag

# Step 1: 准备数据
adata = sc.read_h5ad("perturbation_data.h5ad")

# 数据需要包含:
# - adata.obs['cell_type']: 细胞类型标签
# - adata.obs['condition']: 扰动条件(如'treated', 'control')

print(f"细胞数: {adata.n_obs}")
print(f"细胞类型: {adata.obs['cell_type'].unique()}")
print(f"条件: {adata.obs['condition'].unique()}")

# Step 2: 运行Augur(随机森林)
augur_results = ag.create_augur_app()

# 训练Augur模型
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', # 分类器:'random_forest' 或 'lr'
select_features=True, # 特征选择
)

# Step 3: 查看结果
print(auc_df.sort_values('augur_score', ascending=False))

# Augur得分越高,表示该细胞类型对扰动响应越强

# Step 4: 置换检验
pvals_df = ag.predict(
adata_augur,
n_permutations=1000, # 置换次数
n_subsamples=50,
n_threads=4,
random_state=42,
)

# Step 5: 可视化
# 细胞类型排序
ag.plot_cell_type_ranking(auc_df, save='augur_ranking.png')

# 特征重要性
ag.plot_feature_importance(auc_df, save='augur_feature_importance.png')

# Step 6: 使用逻辑回归分类器(速度更快)
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
# ============================================================
# scGen 扰动预测
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# 安装: pip install scgen
import scgen

# Step 1: 准备数据
adata = sc.read_h5ad("perturbation_data.h5ad")

# 确保条件标签列包含control和perturbation
print(adata.obs['condition'].value_counts())

# Step 2: 训练scGen模型
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,
)

# Step 3: 预测扰动响应
# 预测特定细胞类型在扰动条件下的表达
predicted_adata = model.predict(
ctrl_key='control', # 对照条件名
stim_key='treated', # 扰动条件名
celltype_to_predict='CD4+ T cells', # 预测的细胞类型
)

# Step 4: 验证预测
# 比较预测值与真实观测值
# 选择有真实扰动数据的细胞类型进行验证
cd4_treated = adata[
(adata.obs['cell_type'] == 'CD4+ T cells') &
(adata.obs['condition'] == 'treated')
].copy()

# 计算相关性
from scipy.stats import pearsonr
pred_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:.4f}, p={pval:.4e}")

# Step 5: 识别差异响应基因
# 找出预测与对照差异最大的基因
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]:.3f}")

# Step 6: 保存模型
model.save("scgen_model/")
print("scGen预测完成!")

14.3 Mixscape:识别扰动细胞

原理:

Mixscape是一种识别成功扰动的细胞的方法,特别适用于CRISPR筛选等实验。其核心思想是:

  1. 在扰动组中,并非所有细胞都成功被扰动(如CRISPR编辑效率 < 100%)
  2. Mixscape使用混合模型将扰动组细胞分为”扰动响应”(perturbed)和”非扰动响应”(non-perturbed)两类
  3. 基于PCA空间中的分布差异进行分类

Mixscape的算法:

  1. 对每种细胞类型和扰动,计算扰动组和对照组在PCA空间中的质心差
  2. 将每个扰动细胞的投影到质心差方向上
  3. 使用高斯混合模型(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)$$

  1. 根据后验概率将每个细胞分配为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
# ============================================================
# Mixscape 扰动识别
# ============================================================
import scanpy as sc
import pandas as pd
import numpy as np

# Mixscape通常在Seurat(R)中运行
# 以下为R代码的Python替代实现

# Step 1: 准备数据
adata = sc.read_h5ad("crispr_data.h5ad")

# 数据需要包含:
# - adata.obs['cell_type']: 细胞类型
# - adata.obs['perturbation']: 扰动类型(如gene_knockout或NT_control)
# - adata.obs['condition']: control或perturbation

# Step 2: Python实现Mixscape逻辑
from sklearn.mixture import GaussianMixture
from sklearn.decomposition import PCA

def 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

# PCA
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分类
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))

# 确定哪个组分是perturbed
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)

# 运行Mixscape
# mixscape_results = mixscape(adata)
# adata.obs['mixscape_label'] = adata.obs_names.map(
# dict(zip(mixscape_results['cell'], mixscape_results['mixscape_label']))
# )

# ---- Seurat R代码 ----
# library(Seurat)
# library(Mixscape)
#
# # 准备Seurat对象
# obj <- RunMixscape(
# object = seurat_obj,
# assay = "RNA",
# slot = "data",
# labels = "cell_type",
# perturbation = "perturbation",
# control = "NT_control",
# npcs = 50,
# )
#
# # 可视化
# DensityPlot(obj, group.by = "mixscape_label",
# reduction = "pca", dim = c(1, 2))
# ---- R代码结束 ----

# Step 3: 批量移除扰动效应
# Mixscape还提供了"扰动效应校正"功能
# 将perturbed细胞的表达校正为类似control
# 这在下游分析中非常有用(避免扰动效应干扰聚类)

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

# adata = mixscape_correct(adata, mixscape_results)
print("Mixscape分析完成!")

14.4 三种方法对比与适用场景

特征 Augur scGen Mixscape
目标 识别响应最强的细胞类型 预测扰动响应 识别成功扰动的细胞
输入 scRNA-seq + 条件标签 scRNA-seq + 条件标签 scRNA-seq + 扰动标签
输出 细胞类型排序 预测的基因表达谱 perturbed/non-perturbed标签
适用场景 比较不同细胞类型的响应 预测未见过的扰动 CRISPR筛选质控
机器学习 随机森林/LR VAE GMM
计算开销 中等 高 低

推荐场景:

  • 哪种细胞类型响应最强? → 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 sc
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ============================================
# Step 1: 数据准备
# ============================================
adata = 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()}")

# ============================================
# Step 2: Augur分析——哪些细胞类型响应最强
# ============================================
import augurpy as ag

augur_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')

# ============================================
# Step 3: scGen预测——预测扰动响应
# ============================================
import scgen

model = 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')

# ============================================
# Step 4: Mixscape——识别扰动细胞(如CRISPR数据)
# ============================================
# (参见14.3的完整代码)

# ============================================
# Step 5: 综合可视化
# ============================================
# Augur排序 + scGen预测 + 原始数据对比
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Augur排序
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')

# scGen预测
# (绘制预测vs真实的散点图)

# 原始数据UMAP
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 注意事项

  1. Augur:

    • 样本量需要足够大,否则交叉验证不稳定
    • 随机森林可能对高维数据过拟合
    • 置换检验需要足够的次数(建议 ≥ 1000)
  2. scGen:

    • 预测质量依赖于训练数据的代表性
    • 假设扰动效应是加法的($\boldsymbol{\delta}$是全局向量),可能不适用于高度非线性的响应
    • 对于罕见细胞类型,预测可能不可靠
  3. Mixscape:

    • 假设扰动组中存在两个亚群(perturbed和non-perturbed),如果编辑效率非常高或非常低,模型可能失效
    • 需要足够的对照细胞
    • GMM的初始化可能影响结果,建议多次运行
  4. 通用注意事项:

    • 扰动建模前应确保数据质量(如去除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
# ============================================================
# aspera 安装与配置
# ============================================================

# Step 1: 下载aspera connect
# 访问 https://www.ibm.com/aspera/connect/ 下载最新版本
# 或使用命令行下载
wget https://d3gcli72yxqn2z.cloudfront.net/downloads/connect/latest/ibm-aspera-connect-latest-linux-x86_64.sh

# Step 2: 安装
chmod +x ibm-aspera-connect-latest-linux-x86_64.sh
./ibm-aspera-connect-latest-linux-x86_64.sh

# Step 3: 配置环境变量
# aspera默认安装到 ~/.aspera/connect/bin/
echo 'export PATH="$HOME/.aspera/connect/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Step 4: 验证安装
ascp --version

# Step 5: 获取NCBI的aspera密钥
# 从NCBI上传页面获取密钥文件
# 通常保存为 ~/.aspera/connect/etc/asperaweb_id_dsa.openssh
# 或从NCBI下载特定的密钥文件

# Step 6: 设置权限
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
# ============================================================
# NCBI GEO/SRA 上传完整流程
# ============================================================

# ============================================
# 方式1: 上传至GEO (Gene Expression Omnibus)
# ============================================

# Step 1: 创建GEO提交
# 访问 https://www.ncbi.nlm.nih.gov/geo/
# 点击 "Submit to GEO"
# 填写元数据表

# Step 2: 准备上传文件
# 需要准备的文件:
# - 处理后的数据矩阵(表达矩阵)
# - 原始数据文件(FASTQ/BAM)
# - 元数据表(样本信息)

# Step 3: 使用aspera上传
# 获取GEO的aspera上传信息
# Host: upload.ncbi.nlm.nih.gov
# User: geo_username (从GEO获取)
# 密钥文件:从GEO获取

# 上传单个文件
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/

# 参数说明:
# -Q: 自适应速率
# -T: 不加密(提高速度)
# -l 300m: 目标速率300Mbps
# -P33001: 端口号
# -i: 密钥文件

# ============================================
# 方式2: 上传至SRA (Sequence Read Archive)
# ============================================

# Step 1: 创建BioProject
# 访问 https://submit.ncbi.nlm.nih.gov/subs/bioproject/
# 创建新的BioProject

# Step 2: 注册BioSample
# 访问 https://submit.ncbi.nlm.nih.gov/subs/biosample/
# 为每个样本创建BioSample
# 填写样本属性(组织、物种、处理等)

# Step 3: 创建SRA提交
# 访问 https://submit.ncbi.nlm.nih.gov/subs/sra/
# 选择BioProject和BioSample
# 上传原始数据(FASTQ文件)

# Step 4: 使用aspera上传FASTQ文件
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
# ============================================================
# NCBI上传辅助脚本
# ============================================================
import os
import subprocess
import pandas as pd

# Step 1: 准备元数据
# 创建GEO元数据表
metadata = {
'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)

# Step 2: 生成上传脚本
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",
)

# Step 3: 验证上传
def verify_upload(metadata_df, ncbi_user, ncbi_path, key_file):
"""
验证文件是否已成功上传
"""
for _, row in metadata_df.iterrows():
fastq_file = row['fastq_file']
# 检查文件是否存在(通过aspera或FTP)
print(f"验证: {fastq_file}")

verify_upload(metadata_df, "geo_username", "/path", "key_file")

# Step 4: 准备GEO处理后的数据
import scanpy as sc

adata = 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")

# 导出处理后的数据(h5ad格式)
adata.write_h5ad("processed_data.h5ad")

print("数据准备完成!")

15.3 元数据准备

GEO提交需要的元数据包括:

样本级元数据(Sample Metadata):

字段 说明 示例
sample_name 样本唯一标识 sample_001
title 样本标题 PBMC_Control_001
organism 物种 Homo sapiens
tissue 组织来源 peripheral blood
cell_type 细胞类型 CD4+ T cells
disease 疾病状态 healthy
treatment 处理条件 no treatment
platform 测序平台 Illumina NovaSeq 6000
library_strategy 文库策略 RNA-Seq
library_source 文库来源 TRANSCRIPTOMIC
library_selection 文库选择方法 cDNA
instrument_model 仪器型号 Illumina NovaSeq 6000

实验级元数据(Study Metadata):

字段 说明 示例
study_title 研究标题 Single-cell RNA-seq of…
study_type 研究类型 Expression profiling by high throughput sequencing
summary 研究摘要 This study investigates…
overall_design 实验设计 We performed scRNA-seq on…
contributor 贡献者 Zhang, San
pubmed_id PubMed ID 12345678
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 pd

# 创建GEO格式元数据
geo_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元数据
sra_metadata = pd.DataFrame({
'sample_name': ['sample1', 'sample2', 'sample3', 'sample4', 'sample5', 'sample6'],
'bioproject_accession': ['PRJNAXXXXXX'] * 6, # 替换为实际BioProject号
'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 注意事项

  1. 上传前检查:

    • 确保所有FASTQ文件的MD5校验和正确
    • 确认元数据与实际数据一致
    • 检查样本名/文件名的命名规范
  2. MD5校验和:

1
2
3
4
5
# 生成MD5校验和
md5sum /path/to/fastq/*.fastq.gz > md5sums.txt

# 验证MD5
md5sum -c md5sums.txt
  1. 上传中断处理:

    • aspera支持断点续传,重新运行上传命令即可
    • 建议使用-k 2参数启用断点续传
  2. 文件命名规范:

    • 避免空格和特殊字符
    • 使用一致的命名规则
    • 包含样本ID和lane信息
  3. 数据格式要求:

    • SRA接受FASTQ(gzip压缩)和BAM格式
    • GEO接受处理后的数据矩阵(CSV/TSV)
    • 单细胞数据通常需要同时提交至SRA(原始数据)和GEO(处理后的数据)
  4. 审批时间:

    • SRA提交通常1-2个工作日审批
    • GEO提交可能需要3-5个工作日
    • 建议在投稿前提前提交数据
  5. 隐私与伦理:

    • 确保已获得数据公开的伦理审批
    • 去除所有可识别个体身份的信息
    • 如涉及人类数据,确认已获取知情同意中的数据公开条款
  6. 保留期:

    • 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
# ============================================================
# MD5校验与上传验证脚本
# ============================================================

# 生成MD5
find /path/to/fastq -name "*.fastq.gz" -exec md5sum {} \; > md5sums.txt
cat md5sums.txt

# 带断点续传的上传
ascp -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
# Python环境配置
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

# R包安装(通过rpy2调用)
# install.packages(c("MAST", "fgsea", "Mixscape"))
# BiocManager::install(c("DESeq2", "edgeR", "limma", "Garnett"))

# 版本检查
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)是单细胞转录组学中最具代表性的分析方法之一。其核心思想是:虽然单细胞测序本质上是对细胞的”快照”式测量(每个细胞只被测量一次,随后被破坏),但我们可以利用大量细胞所处的不同转录状态,通过计算方法重建细胞的动态变化过程——即”虚拟的时间轴”。

核心假设:

拟时序分析建立在以下关键假设之上:

  1. 连续性假设:细胞的转录状态变化是连续的,而非跳跃式的。相邻转录状态的细胞在基因表达空间中也是相邻的。
  2. 异步性假设:在组织样本中,不同细胞处于同一生物学过程的不同阶段。这类似于在一条流水线上同时观察不同工序的产品。
  3. 可观测性假设:细胞的主要转录变化可以被采样到的细胞充分覆盖,不存在大量”缺失”的中间状态。

数学表述:

给定 $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 真实时间:

维度 拟时序(Pseudotime) 真实时间(Real Time)
测量方式 从基因表达谱计算推断 通过实验时间点直接记录
分辨率 受限于采样细胞数 受限于实验时间点设计
分支信息 可以推断分支点 需要独立标记追踪
适用范围 单次采样即可分析 需要时间序列实验
噪声来源 技术噪声+生物学噪声 主要为技术噪声

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 sc
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# DPT(Diffusion Pseudotime)完整代码流程
# ============================================================

# Step 1: 加载预处理后的数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# Step 2: 高变基因选择与归一化
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)

# Step 3: PCA降维
sc.tl.pca(adata, n_comps=50, svd_solver='arpack')

# Step 4: 计算邻居图
# DPT需要基于扩散映射的邻居图
sc.pp.neighbors(adata, n_neighbors=30, n_pcs=20, method='umap')

# Step 5: 扩散映射
# n_comps指定扩散成分数,这是DPT的核心步骤
sc.tl.diffmap(adata, n_comps=15)

# Step 6: 基于扩散映射重新计算邻居
# 使用扩散空间中的距离代替PCA空间中的距离
sc.pp.neighbors(adata, n_neighbors=30, use_rep='X_diffmap')

# Step 7: 选择根细胞
# 方法一:手动指定(基于生物学知识)
# 例如选择干细胞的索引作为根
root_cell_idx = np.where(adata.obs['cell_type'] == 'Stem cells')[0][0]
adata.uns['iroot'] = root_cell_idx

# 方法二:基于扩散成分选择
# 选择DC1上最极端的点作为根
# adata.uns['iroot'] = np.argmax(adata.obsm['X_diffmap'][:, 1])

# Step 8: 计算DPT
sc.tl.dpt(adata)

# Step 9: 可视化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# DPT在UMAP上的投影
sc.pl.umap(adata, color='dpt_pseudotime', ax=axes[0], show=False,
title='DPT Pseudotime on UMAP', cmap='viridis')

# DPT在扩散映射空间
sc.pl.embedding(adata, basis='diffmap', color='dpt_pseudotime',
ax=axes[1], show=False, components=['1,2'],
title='DPT on Diffusion Map', cmap='viridis')

# 细胞类型与DPT的关系
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()

# Step 10: 沿DPT排序的基因动态分析
# 选择沿拟时序变化的基因
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():.4f} - "
f"{adata.obs['dpt_pseudotime'].max():.4f}")

16.3 PAGA(Partition-based Graph Abstraction)

16.3.1 图抽象原理

PAGA(Partition-based Graph Abstraction)由Wolf等人于2019年提出,是一种将单细胞数据的细粒度邻居图抽象为粗粒度连通图的方法。PAGA的核心创新在于:它既能保留数据中的全局拓扑结构(如分支、循环),又能提供可解释的细胞簇间关系。

算法步骤:

  1. 细胞聚类:将 $n$ 个细胞划分为 $C$ 个簇,即 $c: {1,…,n} \rightarrow {1,…,C}$

  2. 构建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$ 近邻集合。

  1. 显著性评估:对每条边进行统计检验,判断连接是否显著高于随机期望。

PAGA vs 传统轨迹推断:

特征 PAGA 传统轨迹推断
输出 粗粒度连通图 细粒度轨迹
分支处理 天然支持多分支 部分方法支持
循环检测 可以检测 通常不支持
可扩展性 适合大规模数据 受限于数据规模
解释性 高(簇间关系明确) 中等
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 sc
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# PAGA完整代码流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# Step 2: 标准预处理
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)

# Step 3: 邻居图与聚类
sc.pp.neighbors(adata, n_neighbors=30, n_pcs=20)
sc.tl.leiden(adata, resolution=0.5, key_added='leiden_coarse')

# Step 4: 运行PAGA
sc.tl.paga(adata, groups='leiden_coarse')

# Step 5: 可视化PAGA图
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# PAGA连通图
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) # 只显示显著的边

# PAGA与UMAP叠加
sc.tl.umap(adata, init_pos='paga') # 使用PAGA初始化UMAP
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()

# Step 6: PAGA与细粒度聚类结合
# 先做细粒度聚类
sc.tl.leiden(adata, resolution=1.5, key_added='leiden_fine')
sc.tl.paga(adata, groups='leiden_fine')

# 查看PAGA连通性矩阵
paga_connectivity = adata.uns['paga']['connectivities']
print(f"PAGA连通性矩阵形状: {paga_connectivity.shape}")
print(f"显著连接数: {(paga_connectivity > 0.03).sum() // 2}")

# Step 7: 基于PAGA的轨迹分析
# 沿PAGA路径提取基因动态
sc.pl.paga_path(
adata,
nodes=['0', '1', '3', '5'], # 指定PAGA路径上的节点
keys=['SOX2', 'POU5F1', 'PAX6', 'DCX', 'MAP2', 'NEUROD1'],
show=True,
save='_paga_path.png'
)

# Step 8: PAGA比较不同分辨率
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不仅给出每个细胞的拟时序值,还给出细胞到达各个终末状态的分支概率。

核心算法:

  1. 扩散映射构建:与DPT类似,首先构建扩散映射。但Palantir使用自适应核宽度,并基于特征值间隙(eigengap)自动选择扩散成分数。

  2. 多尺度扩散:Palantir定义多尺度扩散算子:

$$\mathbf{P}^{(\alpha)} = \mathbf{D}^{-(1-\alpha)} \mathbf{K} \mathbf{D}^{-(\alpha)}$$

其中 $\alpha \in [0, 1]$ 控制了对密度偏差的校正程度。$\alpha = 1$ 完全校正密度偏差,$\alpha = 0.5$ 为标准扩散映射。

  1. 拟时序计算:使用马尔可夫链的吸收态理论。选择一个起始细胞和多个终末细胞(absorbing states),计算每个细胞到达各终末状态的期望步数,作为拟时序值。

  2. 命运概率:对于每个细胞 $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对比
特征 DPT Palantir
分支处理 隐式(通过扩散距离) 显式(命运概率)
终末状态 不明确指定 用户指定或自动检测
密度校正 基本无 $\alpha$ 参数自适应校正
输出 单一拟时序值 拟时序值 + 命运概率
适用场景 线性轨迹为主 分支命运决定
可视化 UMAP着色 命运概率热图
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 palantir
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# Palantir完整代码流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# Step 2: Palantir预处理
# Palantir需要原始计数值进行归一化
palantir.preprocess(adata)

# Step 3: 运行扩散映射
# n_components根据特征值间隙自动确定
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)

# Step 4: 确定多尺度扩散空间
ms_data = palantir.utils.determine_multiscale_space(dm_res)

# Step 5: 可视化扩散成分
# 查看特征值以确定有效成分数
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()

# Step 6: 运行Palantir
# 选择起始细胞(early cell)和终末状态
# 方法一:基于已知标记基因选择
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]

# 运行Palantir核心算法
pr_res = palantir.core.run_palantir(
ms_data,
early_cell,
terminal_states=terminal_states,
use_early_cell_as_start=True
)

# Step 7: 将Palantir结果添加到AnnData
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']

# Step 8: 可视化命运概率
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()

# Step 9: 基因表达沿拟时序动态
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年提出,结合了图抽象和速度推断的优点,能够处理复杂的分支和循环轨迹。

核心思想:

  1. 基于图的方法:VIA首先构建一个KNN图,然后通过层次化聚类将图抽象为有向图
  2. 速度场估计:结合RNA速率(如果可用)或基于连接性的方向推断
  3. 概率轨迹:通过图上的随机游走计算从起点到各终末状态的路径概率

关键特性:

  • 支持线性和分支轨迹
  • 可以检测循环(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 sc
import numpy as np
import matplotlib.pyplot as plt
import via
from via import VIA

# ============================================================
# StaVIA/VIA完整代码流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad("preprocessed_data.h5ad")

# Step 2: 准备VIA输入
# VIA需要PCA嵌入和元数据
pca_embeddings = adata.obsm['X_pca'][:, :30]
true_label = adata.obs['cell_type'].values # 或leiden聚类标签

# Step 3: 运行VIA
# 定义根细胞群(起始状态)
root_user_label = ['HSC'] # 或多个起始标签

v0 = VIA.VIA(
pca_embeddings,
true_label,
jac_std_global=0.15, # 全局Jaccard标准化参数
dist_std_local=1, # 局部距离标准化
knn=30, # K近邻数
cluster_graph_pruning=0.15,# 簇图剪枝阈值
too_big_factor=0.3, # 大簇分裂因子
root_user=root_user_label, # 起始细胞类型
dataset='custom',
random_seed=42
)

# Step 4: 运行核心算法
v0.run_VIA()

# Step 5: 获取拟时序结果
via_pseudotime = v0.single_cell_pt_markov # 单细胞拟时序
via_terminal_states = v0.vertex_clusters # 终末状态

# 添加到AnnData
adata.obs['via_pseudotime'] = via_pseudotime

# Step 6: 可视化 - 流图(Flow Plot)
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()

# Step 7: 基因动态热图
# 选择沿轨迹变化的基因
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()

# Step 8: 终末状态可视化
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()

# Step 9: 生成流图动画(GIF)
# 注意:此步骤需要ffmpeg或Pillow
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")

# Step 10: VIA路径分析
# 提取从根到各终末状态的路径
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 扩散距离 隐式 否 否 $O(n^2)$ 线性/简单分支
PAGA 图抽象 显式 是 否 $O(n \log n)$ 多分支复杂拓扑
Palantir 扩散+吸收态 显式 否 是 $O(n^2)$ 命运决定
StaVIA 图+速度 显式 是 是 $O(n \log n)$ 复杂动态系统
Monocle3 反向图嵌入 显式 是 否 $O(n)$ 大规模数据
Slingshot MST+平滑 显式 否 否 $O(n \log n)$ 已知全局结构

方法选择建议:

  1. 线性分化:DPT足够,简单直观
  2. 已知分支结构:Slingshot、Monocle3
  3. 探索性分析:PAGA先看全局结构,再用Palantir/VIA细化
  4. 命运决定研究:Palantir或VIA
  5. 存在循环:PAGA或VIA
  6. 大规模数据(>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 sc
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ============================================================
# 完整拟时序分析流程(整合多种方法)
# ============================================================

# ---------- 数据加载与预处理 ----------
adata = 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)

# ---------- 方法1: DPT ----------
# 扩散映射
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)

# ---------- 方法2: PAGA ----------
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 spearmanr

pseudotime = adata.obs['dpt_pseudotime'].values
gene_corr = []
for gene in adata.var_names[:1000]: # 示例:检查前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 注意事项

  1. 根细胞选择至关重要:DPT和Palantir的结果高度依赖根细胞的选择。错误的根细胞会导致完全错误的拟时序方向。建议:基于已知的标记基因(如干性标记)选择根细胞,并进行敏感性分析。

  2. 预处理的影响:不同的归一化方法、高变基因选择策略会显著影响拟时序结果。建议在关键分析中尝试多种预处理方案。

  3. 分支点的生物学验证:计算方法检测到的分支点需要通过实验验证(如谱系追踪实验)。

  4. 拟时序 ≠ 真实时间:拟时序仅代表转录状态的连续变化顺序,不一定与真实时间成比例。

  5. 扩散成分选择:DPT和Palantir中扩散成分数的选择影响结果。应检查特征值间隙来确定有效成分数。

  6. 数据量要求:拟时序分析需要足够多的中间状态细胞。如果采样不充分(”gap”太大),轨迹重建可能断裂。

  7. 噪声敏感性:拟时序对技术噪声敏感,建议使用imputation方法(如MAGIC)平滑基因表达后再分析。

  8. PAGA阈值:PAGA图的边显著性阈值需要仔细调整。太低会包含噪声连接,太高会遗漏真实分支。

  9. 不要过度解读:拟时序的顺序不一定代表因果关系。相关不等于因果。

  10. 多种方法交叉验证:建议使用至少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的比例关系,推断细胞的未来转录状态。

生物学基础:

基因转录是一个多步骤过程:

  1. 转录起始:RNA聚合酶结合启动子,开始转录
  2. 转录延伸:RNA聚合酶沿基因模板合成前体mRNA(pre-mRNA),此时内含子尚未被剪接
  3. 剪接:剪接体(spliceosome)移除内含子,连接外显子
  4. 成熟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速率估计方法。

核心假设:

  1. 细胞群体总体上处于稳态(部分细胞正在上调,部分正在下调,总体平衡)
  2. 剪接速率 $\beta$ 和降解速率 $\gamma$ 的比值 $\beta/\gamma$ 可以通过线性回归从稳态细胞中估计

估计步骤:

  1. 在 $u$-$s$ 相图中,识别处于稳态的细胞($v_s \approx 0$)
  2. 对稳态细胞拟合线性回归:$s = \frac{\beta}{\gamma} u + c$
  3. 用拟合的斜率作为 $\gamma_{ss}$(在 $\beta=1$ 的归一化下)
  4. 计算每个细胞的速率:$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算法步骤:

  1. E步:给定当前参数 $\theta^{(k)} = (\alpha, \beta, \gamma, \tau)$,计算每个细胞的似然:

$$L_i(\theta) = p(u_i, s_i | \theta, t_i)$$

  1. M步:最大化总对数似然,更新参数:

$$\theta^{(k+1)} = \arg\max_\theta \sum_i \log L_i(\theta)$$

  1. 迭代直到收敛。

优势:

  • 不需要稳态假设
  • 可以估计所有参数($\alpha, \beta, \gamma$)
  • 可以检测基因的开关时间 $\tau$
  • 对非稳态过程更准确

代价:

  • 计算成本显著更高(每个基因需要独立优化)
  • 需要更多的细胞来可靠估计参数
  • 可能陷入局部最优

17.3 scVelo

17.3.1 原理

scVelo由Bergen等人于2020年提出,是RNA速率分析的标准化工具。它实现了稳态模型和EM动力学模型,并提供了丰富的可视化和下游分析功能。

与原始velocyto的改进:

  1. 增加了EM动力学模型
  2. 更鲁棒的速率估计(基于最近邻的共享速率)
  3. 更好的可视化功能
  4. 与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 scv
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# scVelo完整代码流程(预处理→速度估计→速度图→伪时间)
# ============================================================

# Step 1: 加载数据
# scVelo需要包含spliced和unspliced计数的loom文件
# 通常由velocyto或STARsolo生成
adata = scv.read('sample.loom', cache=True)

# 或者从已处理的AnnData加载
# adata = sc.read_h5ad('sample_with_splicing.h5ad')

# Step 2: 预处理
# 2.1 基本过滤
scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=2000)

# 2.2 计算一阶和二阶矩(用于速率估计)
scv.pp.moments(adata, n_pcs=30, n_neighbors=30)

# Step 3: 速率估计 - 稳态模型
scv.tl.velocity(adata, mode='stochastic')

# Step 4: 速率图
# 计算速度在低维空间中的投影
scv.tl.velocity_graph(adata, n_jobs=8)

# Step 5: 可视化 - 稳态模型
scv.pl.velocity_embedding_stream(
adata, basis='umap', color='cell_type',
title='RNA Velocity (Stochastic Model)',
save='velocity_stochastic.png'
)

# Step 6: 速率估计 - EM动力学模型
scv.tl.recover_dynamics(adata, n_jobs=8)

# Step 7: EM模型速率估计
scv.tl.velocity(adata, mode='dynamical')

# Step 8: EM模型速率图
scv.tl.velocity_graph(adata, n_jobs=8)

# Step 9: 可视化 - EM动力学模型
scv.pl.velocity_embedding_stream(
adata, basis='umap', color='cell_type',
title='RNA Velocity (Dynamical Model)',
save='velocity_dynamical.png'
)

# Step 10: 速率伪时间
scv.tl.velocity_pseudotime(adata, root_key='root_cell')

# Step 11: 伪时间可视化
scv.pl.scatter(
adata, color='velocity_pseudotime', basis='umap',
cmap='gnuplot', save='velocity_pseudotime.png'
)

# Step 12: 基因水平分析
# 12.1 相图(phase portrait)
scv.pl.scatter(
adata, ['SOX2', 'PAX6', 'DCX'], color='cell_type',
ncols=3, fontsize=10, save='phase_portraits.png'
)

# 12.2 速率基因
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))

# 12.3 动力学参数
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']])

# Step 13: 速率置信度
scv.tl.velocity_confidence(adata)
scv.pl.scatter(
adata, color='velocity_confidence',
basis='umap', cmap='coolwarm',
save='velocity_confidence.png'
)

# Step 14: 速率一致性检查
scv.tl.velocity_confidence_transition(adata)
print(f"平均速度置信度: {adata.obs['velocity_confidence'].mean():.4f}")
print(f"速度长度: {adata.obs['velocity_length'].mean():.4f}")

# Step 15: 终末状态检测
scv.tl.terminal_states(adata)
scv.pl.scatter(
adata, color=['root_cells', 'end_points'],
basis='umap', ncols=2,
save='terminal_states.png'
)

# Step 16: 速率聚类
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'
)

# Step 17: 比较稳态和动力学模型
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()

# Step 18: 保存结果
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 scvi
from scvi.module import VeloVAE
import scanpy as sc
import numpy as np

# ============================================================
# VeloVI完整代码流程
# ============================================================

# Step 1: 加载数据(需要spliced/unspliced计数)
adata = sc.read_h5ad('sample_with_splicing.h5ad')

# Step 2: 预处理
scvi.data.setup_anndata(adata)
# VeloVI需要spliced和unspliced层
# adata.layers['spliced'] = ...
# adata.layers['unspliced'] = ...

# Step 3: 初始化VeloVI模型
velovi = scvi.external.VeloVI(adata)

# Step 4: 训练模型
velovi.train(max_epochs=400, use_gpu=True)

# Step 5: 获取速率估计
velocity_output = velovi.get_velocity()
adata.layers['velovi_velocity'] = velocity_output

# Step 6: 获取参数估计(含不确定性)
parameters = velovi.get_rates()
adata.var['velovi_alpha'] = parameters['alpha']
adata.var['velovi_beta'] = parameters['beta']
adata.var['velovi_gamma'] = parameters['gamma']

# Step 7: 可视化
scv.pl.velocity_embedding_stream(
adata, basis='umap', color='cell_type',
title='VeloVI RNA Velocity',
save='velovi_velocity.png'
)

# Step 8: 参数不确定性
param_std = velovi.get_rate_std()
print(f"Alpha uncertainty: {param_std['alpha'].mean():.4f}")
print(f"Beta uncertainty: {param_std['beta'].mean():.4f}")
print(f"Gamma uncertainty: {param_std['gamma'].mean():.4f}")

# Step 9: 保存模型
velovi.save('velovi_model/', overwrite=True)
adata.write_h5ad('velovi_results.h5ad')

17.5 CellRank

17.5.1 结合速率和转录相似性

CellRank由Lange等人于2022年提出,是一种将RNA速率信息与转录组相似性相结合的命运推断方法。其核心创新在于:不单纯依赖RNA速率(可能噪声较大),而是将速率信息整合到一个马尔可夫链框架中,结合细胞的转录相似性来推断细胞命运。

核心算法:

  1. 相似性核(Similarity Kernel):基于基因表达相似性构建:

$$K_{\text{sim}}(i, j) = \exp\left(-\frac{d_{\text{pca}}(i, j)^2}{2\sigma^2}\right)$$

  1. 速率核(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)$$

  1. 组合核:加权组合两个核:

$$K_{\text{combined}} = (1-\alpha) K_{\text{sim}} + \alpha K_{\text{vel}}$$

  1. 马尔可夫链:将组合核归一化为转移矩阵 $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 cr
import scanpy as sc
import scvelo as scv
import numpy as np

# ============================================================
# CellRank完整代码流程
# ============================================================

# Step 1: 加载已计算RNA速率的数据
adata = sc.read_h5ad('velocity_analysis_results.h5ad')

# Step 2: 计算速率核
vk = cr.kernels.VelocityKernel(adata)
vk.compute_transition_matrix()

# Step 3: 计算连接性核
ck = cr.kernels.ConnectivityKernel(adata)
ck.compute_transition_matrix()

# Step 4: 组合核
combined_kernel = 0.8 * vk + 0.2 * ck

# Step 5: 初始化CellRank估计器
estimator = cr.estimators.GPCCA(combined_kernel)

# Step 6: 拟合马尔可夫链
estimator.fit(n_states=[3, 10], cluster_key='cell_type')

# Step 7: 识别宏观态(终末/初始状态)
estimator.predict_macrostates(n_states=5)

# Step 8: 确认终末状态
estimator.set_terminal_states_from_macrostates(
names=['Neurons', 'Astrocytes', 'Oligodendrocytes']
)

# Step 9: 计算命运概率
estimator.compute_fate_probabilities()

# Step 10: 可视化命运概率
estimator.plot_fate_probabilities(basis='umap', save='cellrank_fate.png')

# Step 11: 驱动基因鉴定
driver_genes = estimator.compute_lineage_drivers()
print("Top driver genes for Neuron fate:")
print(driver_genes['Neurons'].head(20))

# Step 12: 命运概率热图
estimator.plot_fate_probabilities(
basis='umap', mode='heatmap',
save='cellrank_fate_heatmap.png'
)

# Step 13: 保存结果
adata.write_h5ad('cellrank_results.h5ad')

17.6 动态模型选择策略

场景 推荐模型 理由
稳态过程为主 稳态模型(stochastic) 简单、快速、足够准确
强非稳态过程 EM动力学模型 直接拟合ODE参数
批次效应显著 VeloVI 通过VAE框架校正批次
命运决定研究 CellRank + 速率 整合速率与相似性
不确定性量化 VeloVI 贝叶斯框架天然支持
大规模数据(>50K细胞) 稳态模型 计算效率最高

选择流程:

  1. 先运行稳态模型,检查速度流是否合理
  2. 如果速度流方向不一致或出现反向流,尝试EM动力学模型
  3. 如果存在批次效应,使用VeloVI
  4. 无论选择哪种模型,最终都推荐用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 scv
import cellrank as cr
import scanpy as sc
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# RNA速率分析完整流程(scVelo + CellRank)
# ============================================================

# Step 1: 数据加载
adata = scv.read('sample.loom', cache=True)

# Step 2: scVelo预处理
scv.pp.filter_and_normalize(adata, min_shared_counts=20, n_top_genes=2000)
scv.pp.moments(adata, n_pcs=30, n_neighbors=30)

# Step 3: 先用稳态模型快速检查
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')

# Step 4: 再用EM动力学模型
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')

# Step 5: CellRank命运推断
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()

# Step 6: 驱动基因
drivers = estimator.compute_lineage_drivers()

# Step 7: 可视化
estimator.plot_fate_probabilities(basis='umap')
estimator.plot_lineage_drivers('Neurons', n_genes=10, basis='umap')

# Step 8: 保存
adata.write_h5ad('rna_velocity_complete.h5ad')

17.8 注意事项

  1. Unspliced/Spliced比对质量:RNA速率分析的前提是准确区分unspliced和spliced reads。低质量的比对会导致速率估计错误。推荐使用STARsolo进行比对。

  2. 基因子集选择:scVelo默认只使用高变基因进行速率估计。对于特定生物学过程,可能需要手动添加相关基因。

  3. EM模型计算成本:EM动力学模型对每个基因独立优化,计算时间是稳态模型的10-100倍。建议先用稳态模型筛选基因,再对关键基因运行EM模型。

  4. 速度流方向一致性:如果速度流出现大面积反向或不一致的方向,可能需要:检查比对质量、调整预处理参数、或考虑使用VeloVI。

  5. 低捕获率协议:Drop-seq等低捕获率协议可能没有足够的unspliced reads来可靠估计速率。

  6. 速率≠命运:RNA速率反映的是短期(数小时)的转录变化趋势,而不是长期命运决定。长期命运推断需要结合CellRank等工具。

  7. 伪时间与真实时间的差异:速率伪时间与DPT等拟时序方法计算的伪时间可能不同,因为前者基于速率信息,后者基于距离信息。

  8. 负速率的生物学解释:负速率($v_s < 0$)表示基因正在下调,这是正常的生物学现象,不需要特殊处理。

  9. 速度图的稀疏性:速度图(velocity graph)是稀疏的,只保留与每个细胞速度方向一致的邻居。过度剪枝可能导致信息丢失。

  10. 不要仅依赖速度流:速度流可视化是定性工具,需要结合定量分析(如驱动基因、命运概率)进行解读。

关键参考文献:

  • 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序列标签。子代细胞继承这个标签,从而可以追踪谱系关系。

代表性技术:

  1. Polylox(Pei et al., 2017):利用Cre-loxP系统的随机重组产生高多样性条形码(理论上 >10^5 种变体)

  2. DNA条形码病毒文库:使用慢病毒感染引入随机DNA条形码,每个细胞获得独特的整合标签

  3. 质粒条形码(CellTagging):通过转染将随机寡核苷酸条形码整合到基因组中

条形码编辑的数学模型:

设初始条形码长度为 $L$,每次重组随机删除一个片段,则可能的条形码变体数为:

$$N_{\text{variants}} = \sum_{k=0}^{L} \binom{L}{k} = 2^L$$

实际中,由于重组不是完全随机的,变体数通常少于理论值。

18.1.2 CRISPR记录(CRISPR Recording)

原理:利用CRISPR-Cas9系统在特定基因组位点持续引入突变(indels),这些突变随细胞分裂积累,形成”分子时钟”。通过读取每个细胞的突变模式,可以推断细胞间的谱系关系。

代表性技术:

  1. GESTALT(McKenna et al., 2016):在斑马鱼中利用多个CRISPR靶位点的累积突变记录发育谱系

  2. LINNAEUS(Spanjaard et al., 2018):利用Cas9诱导的indels在斑马鱼中进行谱系追踪

  3. CAR-Seq:利用CRISPR激活记录系统

  4. 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$。这是一个经典的系统发育问题,但有以下特殊性:

  1. 不可逆突变:CRISPR记录的突变通常是不可逆的(Dollo约束),即一个位点一旦突变不会恢复野生型
  2. 缺失数据:部分靶位点可能因测序覆盖度不足而缺失
  3. 趋同突变:不同谱系的相同位点可能独立发生相同的突变(同塑性,homoplasy)
  4. 速率异质性:不同位点的突变速率可能不同

Cassiopeia的算法层次:

  1. Greedy Solver:贪心算法,每步选择最优的分裂

    • 时间复杂度:$O(n^2 \cdot m)$
    • 适用于快速初步分析
  2. 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,但实际运行时间可接受
    • 适用于中等规模数据
  3. 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 cas
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# Cassiopeia谱系追踪完整代码流程
# ============================================================

# Step 1: 加载条形码数据
# 条形码矩阵:行=细胞,列=靶位点,值=突变状态
# 0=野生型,正整数=特定indel类型,-1=缺失
character_matrix = pd.read_csv('cas9_barcodes.csv', index_col=0)

# Step 2: 创建Cassiopeia树对象
tree = cas.data.CassiopeiaTree(
character_matrix=character_matrix,
missing_state_indicator=-1
)

# Step 3: 估计先验概率
# 基于观察到的突变频率估计每种突变类型的先验
priors = cas.data.compute_priors(character_matrix, missing_state_indicator=-1)
tree.set_prior_probabilities(priors)

# Step 4: 运行hybrid solver
hybrid_solver = cas.solver.HybridSolver(
greedy_solver=cas.solver.GreedySolver(),
ilp_solver=cas.solver.ILPSolver(
time_limit=3600, # ILP求解时间限制(秒)
maximum_iterations=100
),
subtree_size=50, # 子树最大大小
n_iterations=5 # 迭代次数
)
hybrid_solver.solve(tree)

# Step 5: 可视化谱系树
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()

# Step 6: 谱系树质量评估
# 6.1 树似然
log_likelihood = cas.tree.evaluate_log_likelihood(tree)
print(f"树对数似然: {log_likelihood:.2f}")

# 6.2 突变一致性
consistency = cas.tree.evaluate_consistency(tree)
print(f"突变一致性: {consistency:.4f}")

# 6.3 树分辨率
resolution = cas.tree.evaluate_resolution(tree)
print(f"树分辨率: {resolution:.4f}")

# Step 7: 谱系与转录组整合
# 7.1 加载scRNA-seq数据
adata = sc.read_h5ad('paired_scrna.h5ad')

# 7.2 将谱系信息添加到AnnData
# 提取每个细胞在谱系树中的深度
depths = {}
for node in tree.leaves:
cell_name = node.name
depths[cell_name] = tree.get_attribute(node, 'depth')

# 7.3 谱系深度与拟时序比较
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 spearmanr
corr, pval = spearmanr(lineage_depth, pseudotime)
print(f"谱系深度与DPT拟时序相关性: rho={corr:.3f}, p={pval:.2e}")

# Step 8: 谱系依赖的差异表达
# 比较同一谱系不同分支的细胞
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']

# 使用Scanpy进行差异表达
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)

# Step 9: 保存结果
tree.write_newick('lineage_tree.nwk')
adata.write_h5ad('lineage_integrated.h5ad')

18.3 注意事项

  1. 条形码碰撞:当细胞数远大于条形码多样性时,不同细胞可能偶然获得相同条形码(碰撞),导致谱系错误。需要确保条形码多样性远大于细胞数。

  2. 缺失数据比例:CRISPR记录数据中缺失位点比例通常较高(>20%)。Cassiopeia可以处理缺失数据,但缺失过多会降低分辨率。

  3. 趋同突变:相同的indel可能在不同谱系中独立出现。Cassiopeia的hybrid solver通过考虑突变先验来减少趋同突变的影响。

  4. 实验设计:Cas9的切割效率和靶位点数需要在实验设计阶段仔细优化。推荐至少10个高效率靶位点。

  5. 计算成本:hybrid solver在大数据集(>10K细胞)上可能需要数小时。可以先使用greedy solver快速获得初步结果。

  6. 谱系树的根:需要正确选择谱系树的根(通常是未突变的祖先状态)。

  7. 与转录组的对齐:确保条形码细胞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推断的挑战:

  1. 高维性:基因数量远大于细胞数量($p \gg n$ 问题)
  2. 因果vs相关:基因表达相关性不等价于调控关系
  3. 间接效应:观察到的是直接和间接调控的混合
  4. 非线性:调控关系往往是非线性的(如逻辑门)
  5. 时延:转录因子到靶基因的调控存在时间延迟

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来验证直接调控关系。

算法步骤:

  1. 启动子提取:对每个候选靶基因,提取其转录起始位点(TSS)上游一定范围内(默认10kb)的DNA序列

  2. Motif扫描:使用位置权重矩阵(PWM)数据库对启动子进行motif扫描

  3. 富集检验:对每个regulon,检验其靶基因的启动子中TF motif的富集程度

    使用归一化富集分数(Normalized Enrichment Score, NES):

    $$NES = \frac{ES - \mu_{ES}}{\sigma_{ES}}$$

    其中 $ES$ 是观察的富集分数,$\mu_{ES}$ 和 $\sigma_{ES}$ 是从背景分布估计的均值和标准差。

  4. 过滤:只保留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在某个细胞中活跃,那么其靶基因在该细胞中的表达排名应该整体偏高。

算法:

  1. 对每个细胞,将所有基因按表达量降序排列
  2. 对某个regulon的靶基因集合 $S$,在排序的基因列表中标记靶基因的位置
  3. 计算”恢复曲线”(recovery curve):沿排序列表逐个检查,每遇到一个靶基因,曲线上升一步
  4. 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 sc
import pandas as pd
import numpy as np
import os
from arboreto.algo import grnboost2
from ctx import rca
from pyscenic.aucell import aucell
from pyscenic.utils import modules_from_adjacencies
from pyscenic.prune import prune2df
from pyscenic.rss import regulon_specificity_scores

# ============================================================
# pySCENIC完整代码流程
# ============================================================

# ---------- 准备工作 ----------
# 安装:pip install pyscenic
# 下载cisTarget数据库(约5GB):
# wget https://resources.aertslab.org/cistarget/databases/homo_sapiens/hg38/screen/mc9nr/gene_based/hg38__screen__mc9nr__feather.v2.feather

DATABASES = ['./cistarget_databases/hg38__screen__mc9nr__feather.v2.feather']
MOTIF_ANNOTATIONS = './motifs-v9-nr.hgnc-m0.001-o0.0.tbl'

# ---------- Step 1: 数据准备 ----------
adata = sc.read_h5ad('annotated_data.h5ad')

# pySCENIC需要原始计数矩阵(非归一化)
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列表(人类)
# 从pySCENIC提供的列表获取
tf_list = pd.read_csv('https://resources.aertslab.org/cistarget/tf_lists/TF_lists/hs_hgnc_tfs.txt',
header=None)[0].tolist()
# 或使用本地文件
# tf_list = pd.read_csv('hs_hgnc_tfs.txt', header=None)[0].tolist()

# 过滤表达矩阵中的TF
tf_list = [tf for tf in tf_list if tf in expr_matrix.columns]
print(f"检测到 {len(tf_list)} 个转录因子")

# ---------- Step 2: GRNBoost2 ----------
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-靶基因对")

# ---------- Step 3: cisTarget ----------
print("运行cisTarget motif富集分析...")

# 从邻接矩阵构建模块
modules = modules_from_adjacencies(adjacencies, expr_matrix)

# 运行motif富集
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')

# 查看通过motif验证的regulon数量
n_regulons = len(set(motifs.index.get_level_values('Target')))
print(f"通过motif验证的regulon数: {n_regulons}")

# ---------- Step 4: AUCell ----------
print("运行AUCell regulon活性评分...")

# 构建pruned regulon
from pyscenic.utils import load_motifs
motifs_df = pd.read_csv('cistarget_motifs.tsv', sep='\t', index_col=[0, 1])

# 从motifs提取regulon
from pyscenic.prune import create_regulons
regulons = create_regulons(motifs_df)

# 计算AUC评分
auc_mtx = aucell(expr_matrix, regulons, n_cpu=8, seed=42)

# 保存
auc_mtx.to_csv('aucell_scores.tsv', sep='\t')

# ---------- Step 5: 结果整合与可视化 ----------
# 将AUC评分添加到AnnData
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)

# Regulon活性UMAP
sc.pl.umap(auc_adata, color=auc_mtx.columns[:10].tolist(),
ncols=5, cmap='Reds', save='regulon_activity.png')

# Regulon特异性评分(RSS)
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():.3f})")

# 热图:regulon活性 x 细胞类型
import seaborn as sns
mean_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 结果解读

  1. Regulon活性热图:查看哪些regulon在哪些细胞类型中特异性活跃,这是最直观的结果展示

  2. RSS(Regulon Specificity Score):每个细胞类型的标志性regulon。高RSS值表示该regulon对该细胞类型高度特异

  3. 靶基因列表:每个regulon包含的靶基因列表可以用于功能富集分析(GO、KEGG),了解regulon的生物学功能

  4. 与已知的调控关系对比:将推断的regulon与公共数据库(如TRRUST、ENCODE、ChIP-Atlas)中的已知调控关系对比,验证推断的可靠性

  5. Regulon二值化:AUCell可以自动确定活跃/不活跃的阈值,将连续的AUC值转换为二值分类,简化解读

19.5 注意事项

  1. 计算资源需求:GRNBoost2是计算最密集的步骤,对于>50K细胞的数据集可能需要数小时到数天。建议使用多核CPU或分布式计算。

  2. TF列表质量:TF列表的完整性直接影响结果。建议使用最新的TF数据库(如AnimalTFDB)。

  3. cisTarget数据库选择:确保选择正确物种和基因组版本的数据库。使用错误的数据库会导致motif富集失败。

  4. NES阈值:NES=3.0是推荐阈值,但可以适当放宽(如2.5)以增加灵敏度,或收紧(如4.0)以增加特异度。

  5. 间接调控的过滤:cisTarget只能过滤非直接调控关系,但不能保证所有通过的regulon都是直接调控。需要结合ChIP-seq数据验证。

  6. 数据质量:pySCENIC对dropout敏感。建议使用imputed表达矩阵(如MAGIC处理后)作为辅助参考。

  7. 批次效应:如果数据来自多个批次,建议先进行批次校正,再运行GRN推断。

  8. 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分析方法之一。其核心创新在于:

  1. 精选配体-受体数据库:手动整理的、包含多亚基复合物的配体-受体数据库
  2. 统计显著性检验:通过细胞标签随机置换检验评估通讯的统计显著性

算法步骤:

  1. 对每对细胞类型 $(i, j)$ 和每个配体-受体对 $(L, R)$:

    • 计算平均表达:$\bar{x}_L^{(i)}$ 和 $\bar{x}_R^{(j)}$
    • 计算几何平均得分:$S = \sqrt{\bar{x}_L^{(i)} \cdot \bar{x}_R^{(j)}}$
    • 对于多亚基复合物,取各亚基的算术平均
  2. 置换检验:

    • 随机打乱细胞类型标签 $\pi$ 次(默认1000次)
    • 每次重新计算得分 $S^{(k)}$
    • p值 = $\frac{1}{\pi} \sum_{k=1}^{\pi} \mathbb{1}[S^{(k)} \geq S_{obs}]$
  3. 多重检验校正: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 sc
import pandas as pd
import numpy as np

# ============================================================
# CellPhoneDB完整代码流程
# ============================================================

# CellPhoneDB使用Python包,需要单独安装
# pip install cellphonedb

# Step 1: 准备输入文件
adata = sc.read_h5ad('annotated_data.h5ad')

# 1.1 计数矩阵
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 # CellPhoneDB需要基因为行,细胞为列
counts_df.to_csv('cellphonedb_counts.txt', sep='\t')

# 1.2 元数据(细胞类型标签)
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)

# Step 2: 运行CellPhoneDB
# 在命令行中运行:
# cellphonedb method statistical_analysis cellphonedb_meta.txt cellphonedb_counts.txt \
# --iterations=1000 --threads=8 --subsampling

# 或通过Python API
from cellphonedb.src.core.methods import cpdb_statistical_analysis_method

results = 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
)

# Step 3: 解析结果
# 3.1 显著的相互作用
significant_interactions = results['significant_means']
print(f"显著配体-受体对数: {len(significant_interactions)}")

# 3.2 p值矩阵
pvalues = results['pvalues']

# Step 4: 可视化
# 4.1 热图:细胞类型对之间的相互作用数
interaction_counts = pd.crosstab(
significant_interactions['source'],
significant_interactions['target']
)

import seaborn as sns
plt.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()

# 4.2 特定细胞类型对的详细相互作用
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的基础上做了多项改进:

  1. 更大的数据库:包含2,021个验证的配体-受体对,涵盖分泌信号、ECM-受体、细胞-细胞接触等
  2. 通讯概率模型:使用质量作用定律建模通讯概率,而非简单几何平均
  3. 信号流分析:可以分析信号网络的输入和输出模式
  4. 比较分析:支持多个条件间的通讯差异分析

通讯概率模型:

$$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
# CellChat是R包,以下为R代码
# install.packages("CellChat")

library(CellChat)
library(Seurat)

# ============================================================
# CellChat完整代码流程
# ============================================================

# Step 1: 准备数据
data <- readRDS("annotated_seurat.rds")
data.input <- data@assays$RNA@data # 归一化表达矩阵
labels <- Idents(data) # 细胞类型标签
meta <- data@meta.data

# Step 2: 创建CellChat对象
cellchat <- createCellChat(object = data.input, meta = meta, group.by = "cell_type")

# Step 3: 设置配体-受体数据库
cellchat <- setLigandReceptorDB(cellchat, species = "human")

# Step 4: 预处理
cellchat <- preprocessData(cellchat, min.pct = 0.1)

# Step 5: 识别过表达的配体和受体
cellchat <- identifyOverExpressedGenes(cellchat)
cellchat <- identifyOverExpressedInteractions(cellchat)

# Step 6: 推断通讯网络
cellchat <- computeCommunProb(cellchat, type = "triMean")
cellchat <- filterCommunication(cellchat, min.cells = 10)

# Step 7: 推断信号路径层面的通讯
cellchat <- computeCommunProbPathway(cellchat)

# Step 8: 计算聚合的细胞类型间通讯
cellchat <- aggregateNet(cellchat)

# Step 9: 可视化
# 9.1 信号流图
pathways.show <- c("CXCL")
netVisual_aggregate(cellchat, signaling = pathways.show,
layout = "circle", vertex.label.cex = 0.8)

# 9.2 热图
netVisual_heatmap(cellchat, signaling = pathways.show)

# 9.3 气泡图
netVisual_bubble(cellchat, sources.use = c(1, 2),
targets.use = c(5, 6), remove.pvalues = FALSE)

# 9.4 信号流比较
netAnalysis_signalingRole_network(cellchat, width = 12, height = 5)

# Step 10: 信号角色分析
# 10.1 发送者、接收者、介导者、影响者
cellchat <- netAnalysis_signalingRole_scatter(cellchat)

# 10.2 信号路径贡献
cellchat <- netAnalysis_signalingRole_heatmap(cellchat)

# Step 11: 比较两个条件
# cellchat.cond1 <- ... (同上步骤处理条件1)
# cellchat.cond2 <- ... (同上步骤处理条件2)
# cellchat.list <- list(cond1 = cellchat.cond1, cond2 = cellchat.cond2)
# cellchat.merged <- mergeCellChat(cellchat.list, add.names = names(cellchat.list))
# netVisual_diffInteraction(cellchat.merged, weight.scale = T)
# netVisual_pairwiseDiffInteraction(cellchat.merged, weight.scale = T)

# Step 12: 保存
saveRDS(cellchat, file = "cellchat_results.rds")

20.4 LIANA

20.4.1 统一框架原理

LIANA(LIgand-receptor ANalysis frAmework)由Dimitrov等人于2022年提出,是一个元分析框架,整合了多种CCC方法的输出,提供共识性的通讯推断。

整合的方法:

LIANA整合了以下方法的评分:

  1. CellPhoneDB:几何平均 + 置换检验
  2. CellChat:通讯概率模型
  3. Connectome:标准化z-score
  4. logfc_mean:log fold change均值
  5. 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 li
import scanpy as sc
import matplotlib.pyplot as plt

# ============================================================
# LIANA完整代码流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad('annotated_data.h5ad')

# Step 2: 运行LIANA多方法整合
li.mt.rank_aggregate.by_sample(
adata,
groupby='cell_type',
resource_name='consensus', # 使用共识资源
expr_prop=0.1, # 最小表达比例
verbose=True
)

# Step 3: 查看结果
interactions = adata.uns['liana_res']
print(f"检测到的相互作用数: {len(interactions)}")
print(interactions.head(10))

# Step 4: 可视化
# 4.1 热图
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)
)

# 4.2 特定通路的相互作用
specific_lr = interactions[
interactions['ligand.complex'].str.contains('CXCL') |
interactions['receptor.complex'].str.contains('CXCR')
]
print("CXCL/CXCR相关相互作用:")
print(specific_lr)

# Step 5: 比较不同方法的一致性
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不仅推断配体-受体相互作用,还预测配体引起的下游靶基因表达变化。

核心创新:

  1. 信号传播建模:从配体→受体→信号通路→靶基因的完整传播链

  2. 先验网络:利用公共数据库构建配体-靶基因的先验调控网络

  3. 靶基因预测:给定一个”感兴趣的靶基因集合”,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
# NicheNet是R包
# devtools::install_github("saeyslab/nichenetr")

library(nichenetr)

# ============================================================
# NicheNet完整代码流程
# ============================================================

# Step 1: 加载数据和先验网络
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"
))

# Step 2: 定义感兴趣的靶基因集合
# 例如:在T细胞中与Macrophages相邻时上调的基因
geneset_oi <- c('IFNG', 'TNF', 'IL2', 'GZMB', 'PRF1', 'CXCR3')

# Step 3: 定义可能的配体(来自Macrophages)
ligands_oi <- c('IL1B', 'TNF', 'IL6', 'CXCL9', 'CXCL10', 'CCL5',
'TGFB1', 'B2M', 'CD80', 'CD86')

# Step 4: 预测配体-靶基因调控
ligand_activities <- predict_ligand_activities(
geneset = geneset_oi,
background_expressed_genes = background_genes,
ligand_target_matrix = ligand_target_matrix,
potential_ligands = ligands_oi
)

# Step 5: 排序配体
best_upstream_ligands <- ligand_activities %>%
arrange(-pearson) %>%
pull(test_ligand)

# Step 6: 可视化配体-靶基因热图
p <- visualize_ligand_target_regulatory_heatmap(
ligands = best_upstream_ligands[1:5],
targets = geneset_oi,
ligand_target_matrix = ligand_target_matrix
)
print(p)

# Step 7: 配体-受体对可视化
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方法基于不同的假设和评分策略,它们的输出往往不一致。推荐使用以下共识策略:

  1. 至少2种方法一致:只保留至少被2种独立方法检测到的相互作用
  2. LIANA整合排名:使用LIANA的排名聚合方法
  3. 实验验证优先级:对共识性高的相互作用优先进行实验验证
  4. 功能富集一致性:检测到的通路在不同方法间是否一致

20.7 12条完整注意事项

  1. 配体-受体数据库质量:CCC分析的基础是配体-受体数据库。不同方法使用不同的数据库,数据库的完整性直接影响结果。CellPhoneDB的数据库经过人工审校,是质量最高的选择之一。

  2. 表达阈值:大多数方法使用最低表达比例阈值(如0.1)过滤低表达基因。阈值过高会遗漏真实但低表达的相互作用,过低会增加假阳性。

  3. 细胞类型粒度:CCC分析的细胞类型粒度影响结果。过粗的粒度会掩盖特定细胞亚型间的通讯,过细的粒度会增加统计噪声。

  4. 空间信息缺失:scRNA-seq不保留空间信息,CCC分析假设所有细胞类型都可能相互通讯。实际中,只有空间上相邻的细胞才可能直接通讯。结合空间转录组数据可以提高可靠性。

  5. 配体-受体不等于功能通讯:配体和受体的共表达不等于它们之间存在功能性的信号传递。需要结合功能实验(如中和抗体、基因敲除)验证。

  6. 批量效应:不同样本/批次的细胞类型比例差异可能影响CCC推断。建议在同一批次内进行通讯分析。

  7. 单细胞vs批量:CCC分析在单细胞水平或细胞类型水平进行。细胞类型水平的分析更稳定,但可能遗漏细胞内的异质性。

  8. 逆向信号:某些表面蛋白既是配体也是受体(如EFN/EPH),可能导致双向信号。CellChat对此有特殊处理。

  9. 旁分泌vs自分泌:同一细胞类型内也可能存在自分泌信号(autocrine),不应被忽略。

  10. 时间动态:CCC是动态过程。静态的scRNA-seq快照只能提供某一时刻的通讯状态。如果可能,结合时间序列数据。

  11. NicheNet的先验偏差:NicheNet依赖先验调控网络,研究充分的通路更容易被预测。对研究不足的通路可能存在偏差。

  12. 多重检验校正:大量配体-受体对的统计检验需要多重检验校正。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区域)。

实验流程:

  1. 细胞分离:通过微流控(10x Genomics)或组合条形码(sci-ATAC-seq)分离单细胞
  2. 转座反应:Tn5转座酶切割开放染色质并插入接头序列
  3. PCR扩增:使用接头序列进行PCR扩增
  4. 测序:对扩增片段进行双端测序

数据特征:

  • 每个细胞检测到的fragments数通常为1,000-25,000(远少于scRNA-seq的reads数)
  • 数据极其稀疏(每个细胞中>95%的位点未检测到信号)
  • 数据是二值化的(某个位点被Tn5切割或未被切割)

与scRNA-seq的关键区别:

特征 scRNA-seq scATAC-seq
测量对象 mRNA丰度 染色质可及性
数据类型 连续(计数) 二值(开放/关闭)
稀疏性 高(~90%零) 极高(>95%零)
特征定义 基因 Peak/基因组区域
每细胞特征数 20K-30K基因 50K-500K peaks
生物学信息 转录状态 调控状态

21.2 特征定义策略

21.2.1 Peak calling

Peak calling是鉴定scATAC-seq数据中富集的开放染色质区域的标准方法。

方法:

  1. 伪批量Peak calling:将所有细胞的fragments合并,使用MACS2进行Peak calling
  2. 每簇Peak calling:按细胞类型分组,分别进行Peak calling,然后合并
  3. 迭代Peak calling:ArchR推荐的方法,先粗聚类→每簇Peak→合并→重新聚类→迭代
1
2
3
4
5
6
7
8
9
10
# MACS2 Peak calling(伪批量)
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,因为启动子的可及性信号更强)。

比较:

策略 优势 劣势
Peak calling 高分辨率、生物学意义明确 可能遗漏稀有细胞类型的Peaks
基因组分箱 全基因组覆盖、不依赖聚类 分辨率较低、包含大量噪声区域
Gene Activity 可直接与scRNA-seq基因对应 丢失远端增强子信息

21.3 数据极度稀疏问题

scATAC-seq数据的核心挑战是极度稀疏。一个典型的数据集中,每个细胞只有1,000-5,000个fragment,而基因组中有数百万个潜在的开放位点。这意味着对于任何给定位点,绝大多数细胞的测量值为0。

应对策略:

  1. 降维:使用LSPM(Latent Semantic Indexing)、LSI或PCA将稀疏矩阵映射到低维空间
  2. Imputation:使用MAGIC、cisTopic等方法填补缺失值
  3. 聚合:将Peaks聚合到更大的功能单元(如Gene Activity)
  4. 信息借用:通过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
# R代码
library(scDblFinder)
library(SingleCellExperiment)

# 从fragment文件创建SCE对象
sce <- read10xATAC("atac_fragments.tsv.gz")

# 运行scDblFinder
sce <- scDblFinder(sce, type="ATAC")

# 查看结果
table(sce$scDblFinder.class)
# singlet doublet
# 9500 500

AMULET:

AMULET是专门为scATAC-seq设计的双联体检测方法。其原理是:双联体细胞在某些基因组区域会异常地出现过量的fragments(因为两个细胞的信号叠加)。

1
2
3
4
5
6
# AMULET命令行运行
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 sc
import episcanpy as epi

# 计算TSS富集
# 需要基因注释文件和fragment文件
tss_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 sc
import episcanpy as epi
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# scATAC-seq QC完整代码流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad('raw_atac_data.h5ad')

# Step 2: 基本QC指标
# 2.1 Fragment计数
adata.obs['log_fragments'] = np.log10(adata.obs['n_fragments'])

# 2.2 TSS富集
# (需要预计算或使用ArchR/Signac)
# adata.obs['tss_enrichment'] = ...

# 2.3 核小体信号
# adata.obs['nucleosome_signal'] = ...

# 2.4 FRiP(Fraction of Reads in Peaks)
adata.obs['frip'] = adata.obs['fragments_in_peaks'] / adata.obs['n_fragments']

# Step 3: QC过滤
print(f"过滤前细胞数: {adata.n_obs}")

# 过滤条件
adata = adata[
(adata.obs['n_fragments'] > 1000) & # 最小fragment数
(adata.obs['n_fragments'] < 100000) & # 最大fragment数(去除双联体)
(adata.obs['tss_enrichment'] > 6) & # TSS富集阈值
(adata.obs['nucleosome_signal'] < 4) & # 核小体信号阈值
(adata.obs['frip'] > 0.15) # FRiP阈值
].copy()

print(f"过滤后细胞数: {adata.n_obs}")

# Step 4: 双联体检测
# 使用scDblFinder(R)或手动过滤
# 简化方法:基于fragment数的异常值
q99 = np.percentile(adata.obs['n_fragments'], 99)
adata = adata[adata.obs['n_fragments'] < q99].copy()
print(f"双联体过滤后细胞数: {adata.n_obs}")

# Step 5: 特征创建(Gene Activity)
# 使用ArchR或Signac的gene activity计算
# 简化版:
# adata = epi.pp.geneactivity(adata, gtf='gencode.v38.annotation.gtf')

# Step 6: LSI降维
# Term Frequency - Inverse Document Frequency (TF-IDF)变换
from sklearn.feature_extraction.text import TfidfTransformer

# 构建Peak×Cell矩阵
tfidf = TfidfTransformer(norm='l1', sublinear_tf=True)
matrix_tfidf = tfidf.fit_transform(adata.X.T).T

# SVD降维
from sklearn.decomposition import TruncatedSVD
lsi = TruncatedSVD(n_components=50, random_state=42)
adata.obsm['X_lsi'] = lsi.fit_transform(matrix_tfidf.T)

# 排除第一LSI成分(通常与测序深度相关)
adata.obsm['X_lsi'] = adata.obsm['X_lsi'][:, 1:]

# Step 7: 邻居图和聚类
sc.pp.neighbors(adata, use_rep='X_lsi', n_neighbors=30)
sc.tl.umap(adata)
sc.tl.leiden(adata, resolution=0.5)

# Step 8: 可视化
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数据,推断转录因子到靶基因的调控关系。

核心步骤:

  1. DORC评分(Degrees of Regulatory Chromatin):计算每个基因的调控染色质程度

    $$\text{DORC}g = \sum{p \in \text{peaks near } g} \text{correlation}(p, g)$$

    即基因 $g$ 附近所有Peaks与该基因表达的相关性之和。

  2. TF motif活性推断:使用chromVAR计算每个细胞中每个TF motif的偏差分数(deviation score)

  3. 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
# FigR是R包
# devtools::install_github("caleblareau/FigR")

library(FigR)
library(Signac)
library(Seurat)

# ============================================================
# FigR完整代码流程
# ============================================================

# Step 1: 加载配对的scATAC-seq和scRNA-seq数据
rna <- readRDS("rna_processed.rds")
atac <- readRDS("atac_processed.rds")

# Step 2: 计算Peak-Gene相关性
# 需要共享的细胞barcodes
shared_cells <- intersect(colnames(rna), colnames(atac))
rna <- rna[, shared_cells]
atac <- atac[, shared_cells]

# 运行FigR
figr_res <- FigR::runFigR(
rna = rna,
atac = atac,
genome = "hg38",
doregulon = TRUE,
numCores = 16
)

# Step 3: 查看DORC基因
dorc_genes <- FigR::getDORCs(figr_res, top_n = 50)
print("Top DORC基因:")
print(head(dorc_genes, 20))

# Step 4: TF-Peak-Gene调控网络
grn <- FigR::getGRN(figr_res)
print("TF-靶基因调控关系数:")
print(nrow(grn))

# Step 5: 可视化
# 5.1 DORC热图
FigR::plotDORCHeatmap(figr_res, top_n = 30)

# 5.2 TF活性UMAP
FigR::plotTFActivity(figr_res, tf = "SOX2", seurat_atac = atac)

# Step 6: 保存结果
saveRDS(figr_res, file = "figr_results.rds")

21.6 注意事项

  1. scATAC-seq的极度稀疏性是所有分析步骤的核心挑战。任何分析都需要先解决稀疏性问题。

  2. TSS富集是最重要的QC指标。TSS富集低的细胞应该被坚决过滤。

  3. Peak calling策略选择:推荐使用ArchR的迭代Peak calling方法。

  4. LSI vs PCA:scATAC-seq数据推荐使用LSI(Latent Semantic Indexing)降维,而非PCA,因为数据极度稀疏。

  5. 与scRNA-seq整合:scATAC-seq的生物学解读通常需要与scRNA-seq数据整合。推荐使用WNN(Weighted Nearest Neighbor)或GLUE方法。

  6. 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条形码的珠阵列上

技术对比:

特征 Visium MERFISH Slide-seq
分辨率 55 μm 亚细胞 10 μm
基因覆盖 全转录组 目标基因 全转录组
单细胞 否 是 接近
成本 中等 高 中等
适用组织 冰冻/FFPE 冰冻 冰冻

22.2 邻域分析(Ch28)

22.2.1 原理

邻域分析(Neighborhood Analysis)旨在识别空间上共定位的细胞类型或基因表达模式。其核心思想是:在空间组织中,特定功能的细胞倾向于聚集在一起形成”邻域”(neighborhood)。

方法:

  1. 邻域组成分析:对每个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$ 的细胞类型组成向量。

  2. 邻域聚类:对邻域组成向量进行聚类,识别不同的组织微环境

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 sc
import squidpy as sq
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# 邻域分析完整代码
# ============================================================

# Step 1: 加载空间数据
adata = sc.read_visium("visium_output/")
adata.var_names_make_unique()

# Step 2: 预处理
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)

# Step 3: 计算空间邻居图
sq.gr.spatial_neighbors(adata, n_neighs=6, coord_type='generic')

# Step 4: 空间自相关分析
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))

# Step 5: 邻域富集分析
sq.gr.nhood_enrichment(adata, cluster_key='cell_type')
sq.pl.nhood_enrichment(adata, cluster_key='cell_type',
method='heatmap', save='nhood_enrichment.png')

# Step 6: 共现分析
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')

# Step 7: 空间可变基因可视化
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
# BayesSpace (R)
library(BayesSpace)
library(SpatialExperiment)

# 创建SpatialExperiment对象
spe <- read10xVisium("visium_output/")
spe <- scran::computeSumFactors(spe)
spe <- logNormCounts(spe)

# PCA降维
spe <- runPCA(spe)

# BayesSpace聚类
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)整合基因表达和空间信息进行聚类。

算法:

  1. 构建空间-表达联合图:$A = \alpha A_{spatial} + (1-\alpha) A_{expression}$
  2. 使用GCN编码节点表示:$H^{(l+1)} = \sigma(\hat{A} H^{(l)} W^{(l)})$
  3. 对编码表示进行聚类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import SpaGCN as spg

# 创建SpaGCN对象
spg_obj = spg.SpaGCN()

# 设置参数
spg_obj.set_l(p=0.5) # 空间权重参数

# 运行聚类
spg_obj.train(adata, init="louvain")
y_pred, prob = spg_obj.predict()

# 添加到AnnData
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 SpatialDE

# 准备数据
counts = 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']]

# 运行SpatialDE
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种空间核函数:

  1. Gaussian: $K(d) = \exp(-d^2 / 2l^2)$
  2. Exponential: $K(d) = \exp(-d/l)$
  3. Periodic: $K(d) = \exp(-2\sin^2(\pi d / p) / l^2)$
  4. Matern 1/2: $K(d) = (1 + \sqrt{3}d/l)\exp(-\sqrt{3}d/l)$
  5. Matern 3/2: 类似但不同参数
1
2
3
4
5
6
7
8
9
10
11
12
# SPARK (R)
library(SPARK)

# 创建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 cell2location
import scanpy as sc

# Step 1: 准备参考签名
# 从scRNA-seq数据计算每个细胞类型的平均表达
ref_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']

# Step 2: 运行cell2location
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, # 每个spot估计的细胞数
detection_alpha=20
)
model.train(max_epochs=30000)

# Step 3: 提取结果
sp_adata = model.export_posterior(sp_adata)
cell_abundance = sp_adata.obsm['q05_cell_abundance_w_sf']

# Step 4: 可视化
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
# RCTD (R)
library(spacexr)

# 创建参考对象
reference <- Reference(
counts = ref_counts,
cell_types = ref_celltypes,
n_UMI = ref_nUMI
)

# 创建空间对象
spatial <- SpatialRNA(
coords = spatial_coords,
counts = spatial_counts
)

# 运行RCTD
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 tg
import scanpy as sc

# Step 1: 加载数据
ad_sc = sc.read_h5ad('reference_scrna.h5ad') # scRNA-seq参考
ad_sp = sc.read_visium('visium_output/') # 空间数据

# Step 2: 预处理
tg.pp_adatas(ad_sc, ad_sp, genes=None) # 自动选择共有基因

# Step 3: 运行Tangram映射
ad_map = tg.map_cells_to_space(
ad_sc,
ad_sp,
mode='cells', # 'cells'或'clusters'
density_prior='uniform',
num_epochs=500,
device='cuda:0' # GPU加速
)

# Step 4: 投影基因到空间
ad_ge = tg.project_gene_embeddings(ad_map, ad_sp)

# Step 5: 可视化插补基因
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')

# Step 6: 细胞类型映射
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 sc
import squidpy as sq
import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# 空间转录组完整分析流程
# ============================================================

# Step 1: 加载数据
adata = sc.read_visium("visium_output/")
adata.var_names_make_unique()

# Step 2: 预处理
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)

# Step 3: 空间邻居图
sq.gr.spatial_neighbors(adata, n_neighs=6)

# Step 4: 空间自相关
sq.gr.spatial_autocorr(adata, mode='moran')
top_svg = adata.uns['moranI'].head(20).index.tolist()

# Step 5: 空间域识别(使用Leiden on spatial graph)
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)

# Step 6: 可视化
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()

# Step 7: 保存
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)

  1. 抗体偶联:将特异性抗体与独特的DNA寡核苷酸标签(ADN)偶联
  2. 抗体孵育:将偶联抗体的混合物与细胞孵育,抗体结合表面抗原
  3. 单细胞捕获:使用10x Genomics微流控系统捕获单细胞
  4. 联合测序:同时测序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 sc
import muon as mu
import numpy as np

# ============================================================
# CITE-seq QC代码
# ============================================================

# Step 1: 加载多模态数据
mdata = mu.read_10x_h5("citeseq_filtered_feature_bc_matrix.h5")
# mdata包含两个modality: 'rna' 和 'prot'

rna = mdata.mod['rna']
prot = mdata.mod['prot']

# Step 2: RNA QC
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)

# Step 3: ADN/蛋白QC
# 3.1 检查ADN信号分布
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)

# 3.2 ADN vs RNA信号分离
# 高ADN低RNA = 可能是空液滴或ADN背景
# 高RNA低ADN = 可能是ADN捕获失败
import matplotlib.pyplot as plt
plt.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()

# Step 4: KNN双联体检测
# 使用RNA和蛋白信号联合检测双联体
# 双联体通常具有异常高的ADN信号
from scipy.stats import median_abs_deviation
prot_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背景信号进行归一化和噪声去除。

原理:

  1. 从空液滴中估计每种抗体的背景分布
  2. 对每个细胞的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 mu
import scanpy as sc

# DSB归一化
# 需要空液滴的ADN计数作为背景
mu.prot.pp.dsb(
mdata,
empty_droplets='empty_droplet_adata' # 空液滴AnnData
)
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
# CLR归一化
mu.prot.pp.clr(mdata, inplace=True)

DSB vs CLR对比:

特征 DSB CLR
背景校正 使用空液滴 无背景校正
噪声去除 显式去噪 不去噪
输入要求 需要空液滴数据 只需细胞数据
负值 可能产生 无
推荐度 高(如果空液滴可用) 一般

23.4 双联体检测(Ch35)

CITE-seq的双联体可以通过RNA和蛋白信号的异常组合来检测:

1
2
3
4
5
6
7
8
9
10
11
import scvi

# 使用totalVI检测双联体
# totalVI在训练过程中会学习每个细胞的混合权重
model = 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
# RNA PCA
sc.tl.pca(mdata.mod['rna'])

# 蛋白PCA
sc.tl.pca(mdata.mod['prot'])

# WNN(Weighted Nearest Neighbor)整合
mu.pp.neighbors(mdata, key_added='wnn')
mu.tl.umap(mdata, key_added='wnn')

23.6 批次校正(Ch37)

1
2
3
4
5
# 使用totalVI进行批次校正
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
# 利用蛋白标记辅助细胞类型注释
# CITE-seq的蛋白标记比RNA标记更可靠
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 ir
import scanpy as sc
import numpy as np

# ============================================================
# 克隆型分析完整代码
# ============================================================

# Step 1: 加载配对的AIRR+GEX数据
adata = sc.read_h5ad('paired_tcr_gex.h5ad')

# Step 2: 定义克隆型
# 基于CDR3序列相似性定义
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')

# Step 3: 克隆型频率分析
clonotype_counts = adata.obs['clonotype_id'].value_counts()
print("克隆型频率分布:")
print(clonotype_counts.describe())

# Step 4: 克隆扩增分析
ir.tl.clonal_expansion(adata, expansion_threshold=2)
sc.pl.umap(adata, color='clonal_expansion', title='Clonal Expansion')

# Step 5: 克隆型网络可视化
ir.pl.clonotype_network(
adata, color='clonotype_id', basis='umap',
layout='force', min_size=5
)

24.3 特异性分析(Ch41)

1
2
3
4
5
6
# Step 1: 基于公共数据库匹配TCR特异性
# 使用VDJdb等数据库
ir.tl.ir_query(adata, vdjdb_path='vdjdb.tsv')

# Step 2: 抗原特异性分析
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 ir
import scanpy as sc

# ============================================================
# scirpy完整整合分析
# ============================================================

# Step 1: 加载数据
adata = sc.read_h5ad('paired_airr_gex.h5ad')

# Step 2: TCR/BCR QC
ir.tl.chain_qc(adata)
ir.pl.chain_qc(adata)

# Step 3: 克隆型定义
ir.pp.ir_dist(adata, metric='alignment', sequence='aa', cutoff=15)
ir.tl.define_clonotypes(adata, receptor_arms='all')

# Step 4: 克隆型与转录状态关联
ir.tl.clonotype_modularity(adata, fdr_threshold=0.05)
ir.pl.clonotype_modularity(adata)

# Step 5: 克隆型间差异表达
ir.tl.clonotype_imbalance(adata, replicate_col='sample')
ir.pl.clonotype_imbalance(adata)

# Step 6: 保存
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 scvi
import muon as mu

# totalVI完整代码
mdata = 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 sc
latent_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 scvi

# MultiVI完整代码
adata = 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)作为”桥梁”连接不同模态:

  1. 模态特异性编码器:每个模态有独立的VAE编码器
  2. GRN图:将RNA基因与ATAC Peaks通过调控关系连接
  3. 图神经网络:在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 scglue

# GLUE完整代码

# Step 1: 预处理每个模态
rna = sc.read_h5ad('rna_data.h5ad')
atac = sc.read_h5ad('atac_data.h5ad')

# Step 2: 构建引导图(Guidance Graph)
# 基于基因-TF调控关系和Peak-gene链接
guidance = scglue.genomics.genomic_guidance_graph(rna, atac)

# Step 3: 训练GLUE模型
scglue.models.train_glue(
rna, atac, guidance,
model_class='GLUE',
max_epochs=500
)

# Step 4: 获取整合嵌入
rna.obsm['X_glue'] = scglue.models.encode(rna, 'GLUE')
atac.obsm['X_glue'] = scglue.models.encode(atac, 'GLUE')

# Step 5: 联合可视化
import anndata
combined = 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 scvi

# 加载预训练参考模型
ref_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 多模态评估指标

指标 描述 方向
ARI(Adjusted Rand Index) 聚类与真实标签的一致性 越高越好
NMI(Normalized Mutual Information) 聚类与真实标签的互信息 越高越好
ASW(Average Silhouette Width) 聚类的紧密度和分离度 越高越好
LISI(Local Inverse Simpson Index) 批次混合度和细胞类型分离度 cLISI↓, iLISI↑
Graph Connectivity 细胞类型在图上的连通性 越高越好
kBET(k-nearest neighbor Batch Effect Test) 批次效应强度 越低越好

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 scvi
import muon as mu
import scanpy as sc
import numpy as np

# ============================================================
# 多模态整合完整流程
# ============================================================

# Step 1: 加载数据
mdata = mu.read_10x_h5("multiome_data.h5")

# Step 2: 预处理各模态
# RNA
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
atac = mdata.mod['atac']
# ATAC预处理...

# Step 3: 选择整合方法
# 配对数据 → MultiVI
scvi.model.MULTIVI.setup_mudata(mdata, batch_key='batch')
model = scvi.model.MULTIVI(mdata)
model.train(max_epochs=500)

# Step 4: 获取整合表示
mdata.obsm['X_MultiVI'] = model.get_latent_representation()

# Step 5: 下游分析
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)

# Step 6: 可视化
sc.pl.umap(latent_adata, color=['cell_type', 'batch', 'leiden'])

# Step 7: 保存
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 五大未来方向

  1. 空间多组学整合:将空间转录组与空间蛋白质组、空间表观遗传组等数据整合,构建完整的空间细胞图谱。代表性技术如Spatial-ATAC-RNA-seq、Spatial-CITE-seq等。

  2. 单细胞长读长测序:利用PacBio和Oxford Nanopore平台进行单细胞全长转录本测序,解决可变剪接、融合基因、等位基因特异性表达等问题。

  3. 时间序列单细胞分析:结合谱系追踪和时间序列采样,构建动态细胞图谱,理解发育和疾病的时空动态。

  4. 基础模型(Foundation Models):基于大规模预训练的单细胞基础模型(如scGPT、Geneformer、scBERT),实现零样本和少样本学习,减少对标注数据的依赖。

  5. 因果推断:从观测数据中推断因果关系,超越相关性分析。结合干预实验和计算因果推断方法,建立基因调控的因果网络。

26.2 核心术语速查表

术语 英文 定义
单细胞RNA测序 scRNA-seq 在单细胞水平上测量mRNA丰度的技术
唯一分子标识符 UMI 标记每个原始分子的短序列,用于去除PCR重复
拟时序 Pseudotime 从基因表达推断的细胞进程顺序
RNA速率 RNA Velocity 基于未剪接/已剪接mRNA推断的细胞未来转录方向
差异表达 DE (Differential Expression) 不同条件/细胞类型间基因表达的统计学差异
批次效应 Batch Effect 技术因素导致的系统性偏差
高变基因 HVG (Highly Variable Gene) 在细胞间表达变异度高的基因
降维 Dimensionality Reduction 将高维数据映射到低维空间
聚类 Clustering 将相似细胞分组为簇的无监督方法
细胞类型注释 Cell Type Annotation 为每个细胞簇分配生物学标签
基因调控网络 GRN (Gene Regulatory Network) 描述TF与靶基因调控关系的网络
配体-受体 Ligand-Receptor 细胞间信号传递的分子对
染色质可及性 Chromatin Accessibility DNA区域对Tn5等酶的可接近程度
反卷积 Deconvolution 从混合信号中分解各成分的比例
变分自编码器 VAE (Variational Autoencoder) 生成式深度学习模型,用于降维和数据生成
流形 Manifold 高维空间中的低维结构
最近邻图 KNN Graph 基于K近邻构建的细胞关系图
图抽象 PAGA 将细粒度邻居图抽象为粗粒度连通图
谱系追踪 Lineage Tracing 追踪细胞分裂历史的实验和计算方法
多模态 Multimodal 同时测量同一细胞的多种分子特征
CITE-seq Cellular Indexing of Transcriptomes and Epitopes 同时测量转录组和表面蛋白
scATAC-seq single-cell ATAC-seq 单细胞染色质可及性测序
免疫受体 AIR (Adaptive Immune Receptor) TCR和BCR的总称
克隆型 Clonotype 具有相同免疫受体序列的细胞群体
CDR3 Complementarity Determining Region 3 免疫受体的抗原接触区域
TF-IDF Term Frequency-Inverse Document Frequency 文本挖掘中的加权方法,用于scATAC-seq
LSI Latent Semantic Indexing 基于SVD的降维方法
空间转录组 Spatial Transcriptomics 保留空间位置信息的转录组测序
反卷积 Deconvolution 从混合信号估计各细胞类型比例
插补 Imputation 填补缺失值的计算方法
双联体 Doublet 包含两个或多个细胞的液滴
标记基因 Marker Gene 在特定细胞类型中特异性高表达的基因
核小体 Nucleosome DNA缠绕组蛋白形成的结构
TSS Transcription Start Site 转录起始位点
FRiP Fraction of Reads in Peaks 落在Peak区域内的reads比例
Regulon Regulon 一个TF及其所有靶基因的集合
DORC Degrees of Regulatory Chromatin 基因附近调控染色质的程度指标
WNN Weighted Nearest Neighbor 多模态数据的加权最近邻方法

第八篇:工具链参考


第二十七章:Scanpy完整API参考

27.1 sc.pp预处理模块完整函数列表与参数

函数 描述 关键参数
sc.pp.filter_cells 过滤低质量细胞 min_genes, max_genes, min_counts, max_counts
sc.pp.filter_genes 过滤低表达基因 min_cells, max_cells, min_counts, max_counts
sc.pp.normalize_total 归一化总计数 target_sum, exclude_highly_expressed, max_fraction
sc.pp.log1p 对数变换 base, layer
sc.pp.highly_variable_genes 选择高变基因 n_top_genes, flavor, batch_key, subset
sc.pp.scale Z-score标准化 max_value, zero_center, layer
sc.pp.regress_out 回归去除协变量 keys, n_jobs
sc.pp.pca PCA降维 n_comps, svd_solver, random_state
sc.pp.neighbors 计算邻居图 n_neighbors, n_pcs, method, metric, use_rep
sc.pp.moments 计算一阶/二阶矩 n_pcs, n_neighbors
sc.pp.batch_correction 批次校正 多种方法
sc.pp.downsample_counts 下采样 counts_per_cell, total_counts
sc.pp.subsample 随机子采样 fraction, n_obs, random_state
sc.pp.recipe_zheng17 Zheng 2017预处理流程 n_top_genes
sc.pp.recipe_seurat Seurat风格预处理 n_top_genes
sc.pp.calculate_qc_metrics 计算QC指标 qc_vars, percent_top, log1p
sc.pp.combat Combat批次校正 key
sc.pp.remove_duplicate_cells 去除重复细胞 -
sc.pp.estimate_moments 估计矩 n_pcs, n_neighbors
sc.pp.filter_with_scrublet Scrublet双联体检测 batch_key, threshold

27.2 sc.tl工具模块完整函数列表与参数

函数 描述 关键参数
sc.tl.pca PCA降维 n_comps, svd_solver, random_state
sc.tl.tsne t-SNE降维 n_pcs, perplexity, learning_rate, n_iter
sc.tl.umap UMAP降维 min_dist, spread, n_components, init_pos
sc.tl.diffmap 扩散映射 n_comps, random_state
sc.tl.dpt 扩散拟时序 n_dcs, n_branchings, min_group_size
sc.tl.leiden Leiden聚类 resolution, flavor, n_iterations, key_added
sc.tl.louvain Louvain聚类 resolution, flavor, key_added
sc.tl.rank_genes_groups 差异表达分析 method, corr_method, pts, key_added
sc.tl.paga PAGA图抽象 groups, use_rna_velocity, model
sc.tl.ingest 查询映射 obs, embedding_method, label_key
sc.tl.score_genes 基因集评分 gene_list, ctrl_size, score_name
sc.tl.score_genes_cell_cycle 细胞周期评分 s_genes, g2m_genes
sc.tl.embedding_density 嵌入密度估计 basis, groupby
sc.tl.velocity RNA速率(需scVelo) mode
sc.tl.velocity_graph 速度图 n_jobs
sc.tl.velocity_pseudotime 速度伪时间 root_key
sc.tl.clustering 通用聚类接口 -

27.3 sc.pl可视化模块关键绘图函数

函数 描述 关键参数
sc.pl.umap UMAP散点图 color, use_raw, cmap, vmin/vmax, ncols
sc.pl.tsne t-SNE散点图 同上
sc.pl.pca PCA散点图 同上
sc.pl.diffmap 扩散映射图 同上
sc.pl.spatial 空间数据图 img_key, library_id, crop_coord
sc.pl.matrixplot 矩阵图 var_names, groupby, standard_scale
sc.pl.heatmap 热图 var_names, groupby, standard_scale, swap_axes
sc.pl.dotplot 点图 var_names, groupby, dot_max, color_map
sc.pl.violin 小提琴图 keys, groupby, stripplot, multi_panel
sc.pl.stacked_violin 堆叠小提琴图 var_names, groupby
sc.pl.tracksplot 轨道图 var_names, groupby
sc.pl.rank_genes_groups 差异基因图 n_genes, sharey, key
sc.pl.rank_genes_groups_heatmap 差异基因热图 n_genes, groupby, key
sc.pl.rank_genes_groups_dotplot 差异基因点图 同上
sc.pl.rank_genes_groups_violin 差异基因小提琴图 同上
sc.pl.rank_genes_groups_matrixplot 差异基因矩阵图 同上
sc.pl.rank_genes_groups_stacked_violin 差异基因堆叠小提琴图 同上
sc.pl.paga PAGA图 threshold, node_size_scale, edge_width_scale
sc.pl.paga_path PAGA路径图 nodes, keys
sc.pl.scatter 通用散点图 x, y, color, basis
sc.pl.embedding 通用嵌入图 basis, color
sc.pl.correlation_matrix 相关矩阵 groupby

27.4 使用原则

  1. 先pp后tl:预处理(pp)必须在工具(tl)之前
  2. 保存原始数据:使用adata.raw = adata保存归一化前的数据
  3. 检查中间结果:每步操作后检查输出是否合理
  4. 层(layer)管理:善用adata.layers存储不同处理阶段的数据
  5. 键(key)管理:使用key_added参数避免覆盖已有结果
  6. 可复现性:设置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对象
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)

# QC
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)

# PCA
sce <- runPCA(sce, subset.row = hvgs)

# UMAP
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()

# QC
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)

# 或使用Harmony
library(harmony)
sce <- RunHarmony(sce, group.by.vars = "batch")

第二十九章:Snakemake工作流引擎

29.1 核心概念:规则、输入输出、通配符

Snakemake是一个基于Python的工作流管理系统,灵感来源于GNU Make。

核心概念:

  1. 规则(Rule):定义一个处理步骤,包含输入、输出和执行命令

  2. 通配符(Wildcard):在文件名中使用{sample}等占位符,实现批量处理

  3. 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
# Snakefile
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
# 完整单细胞分析Snakefile

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 Seq
from Bio.SeqUtils import gc_fraction

# 创建序列
dna = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")

# 基本操作
print(f"长度: {len(dna)}")
print(f"GC含量: {gc_fraction(dna):.3f}")
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 SeqIO

# 读取FASTA文件
for record in SeqIO.parse("sequences.fasta", "fasta"):
print(f"ID: {record.id}")
print(f"描述: {record.description}")
print(f"序列长度: {len(record.seq)}")

# 读取GenBank文件
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]}")

# 写入FASTA
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 pairwise2
from Bio.pairwise2 import format_alignment

# 全局比对
alignments = pairwise2.align.globalms(
"ACCGT", "ACG",
2, -1, -0.5, -0.1 # match, mismatch, gap-open, gap-extend
)
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 Entrez

# 设置邮箱(NCBI要求)
Entrez.email = "your.email@example.com"

# 搜索NCBI
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 dc

# 数据加载
tasks, 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']:.3f}")
print(f"Test AUC: {test_score['mean-roc_auc_score']:.3f}")

31.2 分子表征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import deepchem as dc

# SMILES → 分子对象
smiles = "CC(=O)Oc1ccccc1C(=O)O" # 阿司匹林
mol = dc.utils.rdkit_utils.smiles_to_mol(smiles)

# ECFP指纹
featurizer = dc.feat.CircularFingerprint(size=2048, radius=4)
ecfp = featurizer.featurize([smiles])
print(f"ECFP维度: {ecfp.shape}")

# Mol2Vec
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 dc

# 加载数据
tasks, datasets, transformers = dc.molnet.load_hiv()
train, valid, test = datasets

# GraphConvModel
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
# 文件权限
# rwxrwxrwx = 所有者/组/其他
# 4=r, 2=w, 1=x

# 修改权限
chmod 755 script.sh # rwxr-xr-x
chmod +x script.sh # 添加执行权限

# 修改所有者
chown user:group file.txt

# 查看磁盘使用
df -h # 文件系统使用
du -sh /path/ # 目录大小

# 文件系统结构
# / 根目录
# /home 用户主目录
# /usr 系统程序
# /var 可变数据(日志等)
# /tmp 临时文件
# /opt 可选软件
# /dev 设备文件
# /proc 进程信息

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
# 基本Shell脚本

# 变量
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 -l

# 终止进程
kill PID
kill -9 PID # 强制终止

# 资源监控
top # 实时监控
htop # 增强版top
free -h # 内存使用
nvidia-smi # GPU使用

# 任务调度
# at (一次性)
echo "python script.py" | at 2:00 AM

# cron (周期性)
# crontab -e
# 0 2 * * * /path/to/script.sh # 每天凌晨2点

32.4 常用命令速查

命令 功能 示例
ls 列出文件 ls -lah
cd 切换目录 cd ~/data
cp 复制 cp -r dir1/ dir2/
mv 移动/重命名 mv old.txt new.txt
rm 删除 rm -rf temp/
mkdir 创建目录 mkdir -p a/b/c
cat 查看文件 cat file.txt
head 查看头部 head -n 20 file.txt
tail 查看尾部 tail -f log.txt
grep 搜索文本 grep -r "pattern" dir/
find 查找文件 find . -name "*.h5ad"
wc 计数 wc -l file.txt
sort 排序 sort -k2,2n file.txt
awk 文本处理 awk '{print $1}' file.txt
sed 流编辑 sed 's/old/new/g' file.txt
cut 提取列 cut -f1,3 file.tsv
paste 合并列 paste file1 file2
tar 打包 tar czf archive.tar.gz dir/
wget 下载 wget URL
curl 请求 curl -O URL
ssh 远程连接 ssh user@server
scp 远程复制 scp file user@server:path
rsync 同步 rsync -avz src/ dest/
conda 包管理 conda install scanpy
pip Python包管理 pip install scanpy
screen 终端复用 screen -S analysis
tmux 终端复用 tmux new -s analysis

第三十三章:算法基础(书籍摘要)

33.1 字符串/树/序列算法

字符串匹配算法:

  1. KMP算法:时间复杂度 $O(n+m)$,通过预处理模式串构建失配函数,避免重复比较

  2. 后缀树/后缀数组:用于高效字符串搜索、最长公共子串等问题

    • 后缀树构建:$O(n)$(Ukkonen算法)
    • 后缀数组构建:$O(n \log n)$
    • LCP数组:与后缀数组配合使用
  3. BWT(Burrows-Wheeler Transform):FM-index的核心,用于短读比对

    $$\text{BWT}(S) = {S[i-1] | S[i..] \in \text{sorted suffixes}}$$

树算法:

  1. 最小生成树:Kruskal $O(E \log E)$,Prim $O(E \log V)$
  2. 树遍历:前序、中序、后序、层序
  3. 最近公共祖先(LCA):倍增法 $O(n \log n)$ 预处理,$O(\log n)$ 查询

序列比对算法:

  1. Needleman-Wunsch:全局比对,$O(nm)$
  2. Smith-Waterman:局部比对,$O(nm)$
  3. BLAST:基于种子扩展的启发式比对,$O(n)$ 平均

33.2 生物信息学算法设计范式

  1. 动态规划(Dynamic Programming):

    • 序列比对
    • RNA二级结构预测
    • 隐马尔可夫模型Viterbi算法
  2. 图算法:

    • De Bruijn图用于基因组组装
    • STRING图用于转录本重构
    • A*算法用于读段比对
  3. 哈希与索引:

    • FM-index用于短读比对(BWA、Bowtie)
    • 哈希表用于k-mer计数
    • Bloom filter用于大规模集合查询
  4. 概率模型:

    • HMM用于基因预测
    • 贝叶斯网络用于调控网络推断
    • 期望最大化(EM)用于参数估计
  5. 蒙特卡洛与MCMC:

    • 系统发育树的贝叶斯推断
    • 群体遗传学参数估计

33.3 基因组规模算法设计

关键挑战:

  1. 内存效率:人类基因组3GB,需要内存高效的数据结构
  2. 并行化:利用多核CPU和GPU加速
  3. 流式处理:无法将所有数据载入内存时使用流式算法

代表性解决方案:

  1. FM-index:将3GB基因组压缩到~1GB内存,支持快速搜索
  2. Bloom filter:用少量内存判断k-mer是否存在于集合中
  3. MinHash:用于基因组距离估计,$O(1)$ 比较时间
  4. Cuckoo filter:Bloom filter的替代,支持删除操作
  5. 外部排序:当数据超过内存时使用磁盘排序

基因组索引构建:

1
2
3
4
5
6
7
8
9
# BWA索引构建(命令行)
# bwa index reference.fa

# Bowtie2索引构建
# bowtie2-build reference.fa reference_index

# k-mer计数(Jellyfish)
# jellyfish count -m 21 -s 5G -t 16 -C reads.fastq -o counts.jf
# jellyfish dump counts.jf > counts.txt

大基因组处理策略:

策略 描述 适用场景
索引压缩 FM-index, BWT 序列搜索
采样近似 MinHash, LSH 相似性估计
分布式计算 Spark, Dask 大规模分析
流式处理 逐条处理 数据格式转换
分块处理 区间划分 基因组区间操作
GPU加速 CUDA 比对、神经网络

全文完

本教材涵盖了单细胞转录组数据分析的第三部分核心内容,包括轨迹动态分析(拟时序、RNA速率、谱系追踪)、机制建模(基因调控网络、细胞通讯)、多模态与空间组学(scATAC-seq、空间转录组、CITE-seq、免疫受体、多模态整合)以及工具链参考(Scanpy、Bioconductor、Snakemake、Biopython、DeepChem、Linux、算法基础)。每个章节都提供了完整的理论解释、数学公式、可运行代码和详细注意事项,可作为单细胞生信分析的系统性教科书使用。

站内搜索

没有找到内容!