第 1 步
读 filtered_feature_bc_matrix(或同名 .h5),它已去掉空液滴。raw_feature_bc_matrix 只在做 SoupX、CellBender 时作为输入。
# Seurat v5 / SeuratObject 5.4: replacements for v4 code
GetAssayData(obj, slot = "counts") # Error: The `slot` argument of `GetAssayData()` was deprecated in SeuratObject 5.0.0 and is now defunct.
obj@assays$RNA@counts # Error: no slot of name "counts" for this object of class "Assay5"
obj[["RNA"]]$counts # v5 accessors
LayerData(obj, assay = "RNA", layer = "data")
GetAssayData(obj, layer = "data") # works only while the assay has one "data" layer
# Old v4 object read into v5: convert the assay class when a tool needs Assay5 (e.g. BPCells, IntegrateLayers)
obj[["RNA"]] <- as(obj[["RNA"]], "Assay5")
# Tool that only accepts v4 assays: convert back (merge layers first)
obj <- JoinLayers(obj); obj[["RNA"]] <- as(obj[["RNA"]], "Assay")Cell Ranger 7 以后的数据基因数更高
Cell Ranger 7.0 起 count 的 --include-introns 默认为 true,内含子 reads 也计入表达,每个细胞检测到的基因数和 UMI 数整体上升。PBMC 3k 教程的 nFeature < 2500 上限来自 2016 年 Cell Ranger 1.1 生成的数据(本页实测中位基因数 817),照搬到新数据上会切掉正常细胞。阈值要按自己数据的分布重新定。
v5 对象的表达矩阵存在 layers 里
Seurat v5 的 Assay5 把 counts、data、scale.data 存为 layers,多样本 merge 后每个原对象各占一层(例如 counts.1、counts.2)。v4 教程中的 slot 参数和 @counts 写法在 SeuratObject 5.4 上直接报错,见下方代码块中的报错原文。
GetAssayData 只能取单层
assay 中有多个 data 层时,GetAssayData 报 GetAssayData doesn't work for multiple layers in v5 assay,FindMarkers 报 data layers are not joined. Please run JoinLayers。先 JoinLayers 再取矩阵或做差异分析。
基因名带下划线会被改写
CreateSeuratObject 把基因名中的下划线替换成短横线,并给出 Feature names cannot have underscores 警告。用基因名去匹配外部表格(例如 marker 列表、GTF 注释)时,先做同样的替换。
第 2 步
质控剔除三类细胞:几乎没测到东西的(空液滴或破损细胞),基因数和 UMI 异常高的(多为双细胞),线粒体比例高的(细胞膜破损、胞质 mRNA 流失)。阈值按每个样本的分布单独计算。
本页实测:macOS arm64(8 核、16 GB),Scanpy 1.12.4 / anndata 0.13.4 / Python 3.12,Seurat 5.5.1 / SeuratObject 5.4.0 / R 4.5,10x PBMC 3k(2,700 个细胞)。测试时同一台机器还在运行其他任务,耗时只用来看量级。
PBMC 3k 的线粒体比例中位数为 2.03%,第 95 百分位为 4.01%,第 99 百分位为 5.88%。教程固定阈值(200 < 基因数 < 2500 且线粒体 < 5%)保留 2,638 个细胞。MAD 法(UMI、基因数、前 20 基因占比各 5 MAD,线粒体 3 MAD 且 > 8%)剔除 104 个、保留 2,596 个;其中基因数的 MAD 区间是 366 到 1,820,UMI 是 708 到 6,811。
同一数据上线粒体 3 MAD 的上界只有 3.65%。去掉 8% 下限、只按 3 MAD 过滤时,被剔除的细胞从 104 个增加到 276 个,多出来的大部分是线粒体比例在 3.7%–5% 之间的正常 PBMC。这就是 MAD 法需要固定下限的原因。
MAD 法同样会删掉 RNA 含量低的真实细胞群。PBMC 3k 中 PPBP 高表达的血小板样细胞有 19 个,中位基因数只有 397,MAD 法剔除了其中 10 个,剩下的不足以单独成群,Scanpy 流程最终没有血小板群;固定阈值流程保留了这个 13 个细胞的群。关心血小板、红细胞前体、中性粒细胞等低 RNA 细胞时,先看被剔除细胞的 marker,再决定阈值。
多样本数据要逐个样本计算 MAD。把样本合并后统一算,深度高的样本会把深度低样本的正常细胞判成离群值。
| 指标 | 推荐判据 | 依据 | 注意 |
|---|---|---|---|
| UMI 数(nCount / total_counts) | log1p 后中位数 ±5 MAD | sc-best-practices 质控章节 | 5 MAD 是宽松阈值,目的是不误删小群细胞 |
| 基因数(nFeature / n_genes_by_counts) | log1p 后中位数 ±5 MAD | 同上 | 上限不能替代双细胞检测 |
| 前 20 个基因的计数占比 | 中位数 ±5 MAD | 同上 | 占比过高说明文库复杂度低 |
| 线粒体比例 | 中位数 +3 MAD,且同时高于固定下限 | sc-best-practices 用 8%;Osorio 与 Cai 2021 建议人 10%、小鼠 5% | 只用 3 MAD 时,线粒体本来就低的数据会被过度过滤 |
| 单核测序(snRNA-seq)线粒体比例 | 通常远低于单细胞,可设 1%–5% 作为上限 | 细胞核中没有线粒体,线粒体 reads 来自胞质污染 | 高线粒体核群可能是污染,不一定是低质量细胞 |
第 3 步
双细胞检测按捕获通道(10x 的每个 channel)分别跑,放在严格质控之前;环境 RNA 校正是可选步骤,影响最大的是 marker 和差异分析。
本页实测:同一份 PBMC 3k,scDblFinder(1.24.10)判为双细胞 124 个(4.6%),Scanpy 内置 Scrublet 只判 32 个(1.2%)。按每 1,000 个细胞 0.8% 估算,2,700 个细胞的期望双细胞数约 58 个,两种工具分布在期望值两侧。论文里报告用哪个工具、剔除了多少,并在 UMAP 上检查被判为双细胞的细胞是否集中在两群交界处。
被判为双细胞的细胞可以先标记、不删除,聚类后如果某个小群同时高表达两种谱系的 marker(例如 CD3E 和 CD14)且双细胞分数高,再整群去掉。sc-best-practices 也采用先保留标记的做法。
Janssen 等 2023 年在 Genome Biology 上用基因型混合样本比较了 CellBender、DecontX 和 SoupX:去背景对聚类和细胞分类的改善很小,对 marker 基因检测的改善最明显,CellBender 对背景水平的估计最准确。只做聚类和粗注释时可以跳过这一步;要做差异表达、报告某基因在某群中特异表达时,建议校正。
| 问题 | 工具 | 输入与时机 | 关键参数 | 何时需要 |
|---|---|---|---|---|
| 双细胞 | scDblFinder(R,Bioconductor) | 去掉空液滴后的原始 counts;按 samples= 分通道 | dbr.per1k 默认 0.008(每 1,000 个细胞 0.8%);10x HT 芯片用 0.004 | 每个 10x 样本都做;上样细胞多于 5,000 时双细胞比例超过 4% |
| 双细胞 | DoubletFinder(R) | 单个样本、已去除低质量群的 Seurat 对象 | pK 用 BCmvn 扫描;nExp 按上样密度估计并扣除同型双细胞 | 习惯 Seurat 时;不要在合并或整合后的对象上跑 |
| 双细胞 | Scrublet(Scanpy 内置 sc.pp.scrublet) | 原始 counts;多样本时 batch_key="sample" | expected_doublet_rate 默认 0.05 | Python 路线;自动阈值偏保守,建议看分数分布直方图 |
| 环境 RNA | SoupX(R) | raw 与 filtered 矩阵 + 粗聚类结果 | autoEstCont 自动估计污染比例 | 血红蛋白、免疫球蛋白等高表达基因出现在不该表达的细胞群里时 |
| 环境 RNA | CellBender(Python) | raw_feature_bc_matrix.h5 | v0.3.0 起 expected-cells 等参数自动估计 | 要求 marker 和差异分析准确时;官方要求用 GPU(--cuda) |
第 4 步
LogNormalize(Scanpy 的 normalize_total + log1p)是稳妥的默认选择。Ahlmann-Eltze 与 Huber 2023 年在 Nature Methods 上的比较显示,log(y/s + 1) 加 PCA 的效果与 SCTransform、Pearson 残差等更复杂的变换相当或更好。
# Scanpy 1.12 (Python >= 3.12): PBMC 3k from Cell Ranger output to annotated clusters
import numpy as np, scanpy as sc
from scipy.stats import median_abs_deviation
adata = sc.read_10x_mtx("filtered_gene_bc_matrices/hg19", var_names="gene_symbols")
# Cell Ranger >= 3: sc.read_10x_h5("filtered_feature_bc_matrix.h5")
adata.var_names_make_unique()
# QC metrics: mitochondrial / ribosomal / hemoglobin genes (mouse: "mt-", ("Rps","Rpl"), "^Hb[^(p)]")
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=[20], log1p=True, inplace=True)
# Doublets on raw counts, per capture (batch_key="sample" when several samples are concatenated)
sc.pp.scrublet(adata, random_state=0)
# MAD outliers: 5 MAD on counts / genes / top-20 share, mito 3 MAD AND above a fixed floor
def outlier(x, nmads, upper_only=False):
med, mad = np.median(x), median_abs_deviation(x)
return (x > med + nmads * mad) if upper_only else ((x < med - nmads * mad) | (x > med + nmads * mad))
o = adata.obs
bad = (outlier(o.log1p_total_counts, 5) | outlier(o.log1p_n_genes_by_counts, 5)
| outlier(o.pct_counts_in_top_20_genes, 5)
| (outlier(o.pct_counts_mt, 3, upper_only=True) & (o.pct_counts_mt > 8)))
adata = adata[~bad & ~adata.obs.predicted_doublet].copy()
sc.pp.filter_genes(adata, min_cells=3)
# Normalize, HVG, PCA
adata.layers["counts"] = adata.X.copy()
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor="seurat_v3", layer="counts") # add batch_key="sample" for multi-sample data
adata.raw = adata # keep all genes (log-normalized) for markers and plots
hv = adata[:, adata.var.highly_variable].copy() # scale only HVGs; scaling all genes makes X dense
sc.pp.scale(hv, max_value=10)
sc.tl.pca(hv, n_comps=50)
# Graph, clustering, UMAP
sc.pp.neighbors(hv, n_neighbors=15, n_pcs=30)
for r in (0.2, 0.4, 0.6, 0.8, 1.0):
sc.tl.leiden(hv, resolution=r, flavor="igraph", n_iterations=2, key_added=f"leiden_{r}")
sc.tl.umap(hv)
# Markers on log-normalized expression of all genes
adata.obs["leiden"] = hv.obs["leiden_0.6"].values
sc.tl.rank_genes_groups(adata, "leiden", method="wilcoxon", use_raw=False)
sc.pl.rank_genes_groups_dotplot(adata, n_genes=5)# Seurat v5: same workflow in R
library(Seurat)
counts <- Read10X("filtered_gene_bc_matrices/hg19") # Cell Ranger >= 3: Read10X_h5("filtered_feature_bc_matrix.h5")
obj <- CreateSeuratObject(counts, project = "pbmc3k", min.cells = 3, min.features = 200)
obj[["percent.mt"]] <- PercentageFeatureSet(obj, pattern = "^MT-") # mouse: "^mt-"
# Doublets: scDblFinder on the raw counts of each capture, before strict QC
library(scDblFinder); library(SingleCellExperiment)
set.seed(1)
sce <- scDblFinder(SingleCellExperiment(list(counts = LayerData(obj, layer = "counts"))))
obj$dbl_class <- sce$scDblFinder.class # several captures: scDblFinder(sce, samples = "sample")
# MAD thresholds instead of the 200 / 2500 / 5% values of the PBMC3k tutorial
mad_out <- function(x, n, upper = FALSE) {
m <- median(x); d <- mad(x)
if (upper) x > m + n * d else x < m - n * d | x > m + n * d
}
bad <- mad_out(log1p(obj$nCount_RNA), 5) | mad_out(log1p(obj$nFeature_RNA), 5) |
(mad_out(obj$percent.mt, 3, upper = TRUE) & obj$percent.mt > 8)
obj <- subset(obj, cells = colnames(obj)[!bad & obj$dbl_class == "singlet"])
obj <- NormalizeData(obj) # LogNormalize, scale.factor = 1e4
obj <- FindVariableFeatures(obj, nfeatures = 2000)
obj <- ScaleData(obj) # HVGs only by default
obj <- RunPCA(obj, npcs = 50)
ElbowPlot(obj, ndims = 50)
obj <- FindNeighbors(obj, dims = 1:20)
obj <- FindClusters(obj, resolution = c(0.2, 0.4, 0.6, 0.8, 1.0))
obj <- RunUMAP(obj, dims = 1:20)
library(clustree) # attach it; clustree::clustree() alone fails at ggsave
clustree(obj, prefix = "RNA_snn_res.")
Idents(obj) <- "RNA_snn_res.0.6"
markers <- FindAllMarkers(obj, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)SCTransform 要装 glmGamPoi
没有 glmGamPoi 时,SCTransform 提示 could not find glmGamPoi installed 并退回慢速实现。本页 2,638 个细胞用了 106.7 秒;细胞数到几万时差距更明显。用 BiocManager::install("glmGamPoi") 安装。
SCT 之后差异分析用 RNA 或 PrepSCTFindMarkers
SCT 残差只用于降维和聚类。多样本 SCT 后做 FindMarkers,要先运行 PrepSCTFindMarkers 校正各样本的测序深度,或者切回 RNA assay 用 LogNormalize 的 data 层。
高变基因数:2,000 是默认,3,000 是常用上限
Seurat 与 Scanpy 都默认 2,000。Scanpy 的 flavor="seurat_v3" 需要原始 counts(layer="counts");多样本时加 batch_key,按样本分别选再合并排序,可以避免批次特异基因被选为高变基因。Luecken 2022 的整合评测发现先选高变基因能提高整合效果。
PCA 维数:看拐点,宁多勿少
PBMC 3k 的 PC1 解释 2.9% 方差,第 7 个主成分后每个 PC 只占 0.18%–0.24%,曲线基本变平;前 10、30、50 个 PC 累计 7.6%、11.0%、14.2%。Seurat 教程称 7–12 个 PC 都说得过去。复杂组织或细胞数上万时用 20–50 个 PC;PC 太少会把稀有群并进大群,多取几个 PC 对结果影响较小。JackStraw 很慢,官方教程已不作为常规步骤。
| 方法 | 命令 | 适合 | 本页实测(PBMC 3k) |
|---|---|---|---|
| LogNormalize / normalize_total + log1p | NormalizeData();sc.pp.normalize_total(target_sum=1e4) + sc.pp.log1p | 默认;CellTypist 等工具要求这种输入 | Seurat 核心流程(归一化到 UMAP)15.9 秒 |
| SCTransform v2 | SCTransform(obj, vst.flavor = "v2") | 测序深度差异大、想省去 ScaleData 时;整合时各样本分别 SCT | 未装 glmGamPoi 时 106.7 秒;与 LogNormalize 聚类的 ARI 为 0.73 |
| Pearson 残差 | sc.experimental.pp.highly_variable_genes(flavor="pearson_residuals") + normalize_pearson_residuals | Scanpy 中替代 log 变换 | res 0.6 得到 10 群;与 log 流程的 ARI 为 0.63 |
第 5 步
先不做整合,把 UMAP 按样本着色。同一种细胞的不同样本已经混在一起时,不需要整合。整合只改变降维坐标(harmony、integrated.rpca 等 reduction),表达矩阵不变;差异分析始终用原始的 RNA counts 和 data。
本页实测(Seurat 5.5.1、harmony 2.0.5,把 PBMC 3k 随机分成两个“样本”):没有 split 直接运行 IntegrateLayers(method = HarmonyIntegration) 报 attempt to set an attribute on NULL;split 之后 Harmony 正常完成。整合后对亚群 subset,层已经合并,重新运行 IntegrateLayers 再次报同一错误;对子集重新 split 后通过。对 merge 得到的、已经按样本分层的对象再次 split,报 The following layers are already split。CSDN 作者报告,CCAIntegration 在未 split 时报 no applicable method for 'Assays' applied to an object of class "NULL"。
本页实测(Scanpy 1.12.4、harmonypy 2.0.2):sc.external.pp.harmony_integrate 报 ValueError: Value passed for key 'X_pca_harmony' is of incorrect shape。harmonypy 2.x 返回的 Z_corr 已经是细胞 × PC,Scanpy 的封装又转置了一次。下方代码改为直接调用 harmonypy。
# Seurat v5 multi-sample integration: split -> per-layer preprocessing -> IntegrateLayers -> JoinLayers
obj <- merge(s1, y = list(s2, s3), add.cell.ids = c("s1", "s2", "s3")) # already one layer per object: counts.1, counts.2, counts.3
# Object built from one matrix, or after JoinLayers: split by sample first
# obj[["RNA"]] <- split(obj[["RNA"]], f = obj$sample)
# Splitting a merged object again fails: The following layers are already split: 'counts.1', 'counts.2' Please join before splitting
obj <- NormalizeData(obj); obj <- FindVariableFeatures(obj); obj <- ScaleData(obj); obj <- RunPCA(obj)
# Unintegrated baseline first
obj <- FindNeighbors(obj, dims = 1:30, reduction = "pca")
obj <- FindClusters(obj, resolution = 0.5, cluster.name = "unintegrated_clusters")
obj <- RunUMAP(obj, dims = 1:30, reduction = "pca", reduction.name = "umap.unintegrated")
obj <- IntegrateLayers(obj, method = HarmonyIntegration, orig.reduction = "pca", new.reduction = "harmony")
# alternatives: method = RPCAIntegration (more conservative), CCAIntegration, FastMNNIntegration, scVIIntegration
obj <- FindNeighbors(obj, reduction = "harmony", dims = 1:30)
obj <- FindClusters(obj, resolution = 0.5, cluster.name = "harmony_clusters")
obj <- RunUMAP(obj, reduction = "harmony", dims = 1:30, reduction.name = "umap.harmony")
obj <- JoinLayers(obj) # before FindMarkers / FindAllMarkers / GetAssayData
# Re-clustering a subset: layers are joined now, so split again before IntegrateLayers
sub <- subset(obj, idents = c("0", "3"))
sub[["RNA"]] <- split(sub[["RNA"]], f = sub$sample)
sub <- NormalizeData(sub); sub <- FindVariableFeatures(sub); sub <- ScaleData(sub); sub <- RunPCA(sub)
sub <- IntegrateLayers(sub, method = HarmonyIntegration, orig.reduction = "pca", new.reduction = "harmony_sub")
sub <- FindNeighbors(sub, reduction = "harmony_sub", dims = 1:20); sub <- FindClusters(sub, resolution = 0.3)
sub <- JoinLayers(sub)# Scanpy: Harmony (harmonypy) and scVI on the same AnnData
import scanpy as sc
sc.pp.highly_variable_genes(adata, n_top_genes=2000, flavor="seurat_v3", layer="counts", batch_key="sample")
hv = adata[:, adata.var.highly_variable].copy()
sc.pp.scale(hv, max_value=10); sc.tl.pca(hv, n_comps=50)
# sc.external.pp.harmony_integrate(hv, key="sample") fails with harmonypy 2.x:
# ValueError: Value passed for key 'X_pca_harmony' is of incorrect shape ... (50,) while it should have had (n_cells,)
# harmonypy 2.x already returns Z_corr as cells x PCs, the Scanpy 1.12.4 wrapper transposes it again
import harmonypy
ho = harmonypy.run_harmony(hv.obsm["X_pca"], hv.obs, "sample")
Z = ho.Z_corr
hv.obsm["X_pca_harmony"] = Z if Z.shape[0] == hv.n_obs else Z.T
sc.pp.neighbors(hv, use_rep="X_pca_harmony", n_pcs=30)
sc.tl.leiden(hv, resolution=0.5, flavor="igraph", n_iterations=2)
# scVI: needs raw counts; a GPU shortens training, CPU works for tens of thousands of cells
import scvi
scvi.model.SCVI.setup_anndata(hv, layer="counts", batch_key="sample")
m = scvi.model.SCVI(hv, n_latent=30)
m.train() # max_epochs scales down automatically for large data
hv.obsm["X_scVI"] = m.get_latent_representation()
sc.pp.neighbors(hv, use_rep="X_scVI")过度整合的检查方法
整合前后各聚一次类,看三件事:每个群的 marker 是否仍然只属于一个谱系;预期只在某一条件出现的细胞状态是否还能单独成群;各样本在每个群中的比例是否被压成完全相同。Seurat 官方建议对比多种整合方法与未整合结果的聚类和 marker 表达。
v4 教程的 integrated assay 不能做差异分析
v4 的 IntegrateData 会生成 integrated assay,旧教程常在其上做 FindMarkers。整合后的值是校正后的低维重构,不适合检验基因表达差异。v5 的 IntegrateLayers 只生成 reduction,差异分析在 JoinLayers 后的 RNA assay 上做。
| 场景 | 首选 | 备选 | 选错的后果 |
|---|---|---|---|
| 同一实验、同一平台的几个样本,批次效应轻 | 不整合,或 Harmony | RPCA | 强行用 CCA 可能把样本间真实的组成差异抹平 |
| 对照与处理(疾病与正常)比较,期望出现条件特异的细胞状态 | Harmony 或 RPCA(Seurat 官方称 RPCA 更保守) | scVI | 过度整合会把处理组特有的状态并入对照组的群,后续差异分析找不到信号 |
| 多个实验室、多个平台或图谱级数据,批次多、细胞组成差异大 | scVI;有部分细胞标签时用 scANVI | Scanorama | Harmony 校正不足,同一细胞类型按来源分成多个群 |
| 几十万细胞以上 | Harmony(快、内存小)或 scVI(GPU) | Seurat sketch 后再整合 | CCA 在大数据上内存和时间开销大 |
第 6 步
分辨率决定群的粗细,没有唯一正确值。做法是扫一组分辨率,选群结构稳定、且每个群都有可解释 marker 的最低分辨率;需要更细的亚型时,提取大群后单独重新聚类。
在 PBMC 3k 上,res 1.5 时不同种子给出的群数在 16 到 18 之间波动,ARI 降到 0.75,说明多出来的群不稳定。30 个 PC、res 0.4 时 5 个种子结果几乎一致(ARI 0.99)。同一分辨率下,10 个 PC 比 30 个 PC 多分出 1–3 个群,PC 数与分辨率要一起报告。
clustree 图中,一个群同时接收来自上一层两个群的箭头时,说明分辨率已经高到开始重新划分细胞。本页的 PBMC 3k clustree 图里,这种交叉从 res 0.4 到 0.6 之间开始出现,与 Seurat 教程选用 0.5 一致。Seurat 官方给出的经验是约 3,000 个细胞时 res 0.4–1.2,细胞越多需要的分辨率越高。
clustree 报 Unknown guide: edge_colourbar
本页实测(clustree 0.5.1、ggraph 2.2.2、ggplot2 4.0.3):用 clustree::clustree() 而不加载包时,ggsave 或 print 阶段报 Unknown guide: edge_colourbar。先 library(clustree)(它会加载 ggraph)再画图即可。
Scanpy 的 Leiden 显式写 flavor
Scanpy 1.12.4 中不写 flavor 时仍使用 leidenalg,并给出 FutureWarning:The `igraph` implementation of leiden clustering is *orders of magnitude faster*。写 flavor="igraph", n_iterations=2,与官方教程一致。sc.tl.louvain 已弃用,环境中没有 louvain 包时直接报 ModuleNotFoundError。
提取亚群后要重新选高变基因
对 T 细胞等大群 subset 后,重新做 FindVariableFeatures、ScaleData、RunPCA 再聚类;沿用全体细胞的高变基因和 PC,区分亚型的基因可能不在其中。多样本数据还要按上一节重新 split 和整合。
| 设置 | res 0.2 | res 0.4 | res 0.6 | res 0.8 | res 1.0 | res 1.5 |
|---|---|---|---|---|---|---|
| Seurat,dims 1:10(Louvain) | 7 | 9 | 10 | 11 | 11 | 13 |
| Scanpy,10 个 PC(Leiden,5 个种子) | 6(ARI 0.96) | 8–9(0.85) | 9–10(0.89) | 11–12(0.83) | 12–13(0.85) | 16–18(0.75) |
| Scanpy,30 个 PC(Leiden,5 个种子) | 5(0.95) | 7(0.99) | 8(0.86) | 9(0.90) | 9(0.89) | 12–14(0.75) |
第 7 步
自动注释工具只能给出参考数据中存在的标签。流程是:先用自动工具给每个群一个候选标签,再用经典 marker 的点图逐群核对,冲突时以 marker 和组织背景为准。
本页实测(CellTypist 1.7.1,Immune_All_Low,majority_voting,over_clustering 用 Leiden res 0.6 的 8 个群):8 个群的投票标签与 marker 判断全部一致,依次为 Tcm/Naive helper T(1,165 个细胞)、Tem/Trm cytotoxic T(280)、B cells(332)、Classical monocytes(465)、CD16+ NK(157)、Non-classical monocytes(168)、DC(27)、Megakaryocytes/platelets(13)。逐细胞预测共给出 41 种标签,只看逐细胞结果会把一个群拆成许多细碎亚型。
这个分辨率下 CD4 初始 T 与记忆 T 没有分开(都在 1,165 个细胞的群中),CCR7、LEF1 只在部分细胞中高表达。要分开它们,需要对这个群单独重新聚类,或提高分辨率后用 CCR7、S100A4 核对。
# CellTypist on log1p data normalized to 10,000 counts per cell (adata after normalize_total + log1p)
import celltypist
from celltypist import models
models.download_models(model=["Immune_All_Low.pkl"])
pred = celltypist.annotate(adata, model="Immune_All_Low.pkl",
majority_voting=True, over_clustering=adata.obs["leiden"])
adata.obs["celltypist"] = pred.predicted_labels["majority_voting"]
adata.obs["celltypist_conf"] = pred.probability_matrix.max(axis=1) # low values = label not in the model# SingleR + celldex (Bioconductor). On macOS arm64 bioconda has no SingleR build:
# install with BiocManager::install(c("SingleR", "celldex")) inside the R environment
library(SingleR); library(celldex)
ref <- celldex::MonacoImmuneData() # immune; HumanPrimaryCellAtlasData() for broad tissue types
pred <- SingleR(test = LayerData(obj, layer = "data"), ref = ref,
labels = ref$label.fine, clusters = obj$seurat_clusters) # per-cluster mode
obj$singler <- pred$pruned.labels[match(obj$seurat_clusters, rownames(pred))]
plotScoreHeatmap(pred) # check that the best score clearly beats the secondmarker 基因的读法
Seurat FindAllMarkers 的 pct.1、pct.2 是该基因在本群和其他群中的表达细胞比例。好的 marker 是 avg_log2FC 高且 pct.1 远大于 pct.2;只看 log2FC 会挑出表达细胞很少的基因。本页 Seurat 结果中,B 细胞群 CD79A 的 pct.1 为 0.936、pct.2 为 0.041。
核糖体基因排在最前的群
Scanpy 结果中 T 细胞大群的前 5 个 marker 是 LDHB 和 4 个 RPS 基因。核糖体基因高是初始 T 细胞的特征之一,不能单独作为注释依据;用 CD3E、IL7R、CCR7 等核对。
FindAllMarkers 慢时装 presto
Seurat 未安装 presto 时用 R 实现的 Wilcoxon 检验,并提示安装 presto。Seurat 5.6.0 起 FindAllMarkers 可以在一次 presto 调用中完成所有群的比较。
| 工具 | 语言 | 参考与输入 | 优点 | 局限 |
|---|---|---|---|---|
| SingleR + celldex | R | celldex 参考(HumanPrimaryCellAtlas、Blueprint/ENCODE、Monaco 免疫等);log 归一化矩阵 | 可按群注释(clusters= 参数);给出 pruned.labels 和打分热图 | 参考多为纯化细胞的 bulk 数据,组织特异亚型和疾病状态缺失;macOS arm64 的 bioconda 没有 SingleR 包 |
| CellTypist | Python | 内置模型(默认 Immune_All_Low);log1p 且每个细胞归一到 10,000 的矩阵 | majority_voting 按群投票,结果稳定 | 模型以免疫细胞为主,非免疫组织需选对应器官模型 |
| Azimuth | R(Seurat)/ 网页 | Seurat 团队的参考图谱(PBMC、肺、肾、骨髓等),默认联网加载 | 多级标签,与 Seurat 对象直接衔接 | 只覆盖已有参考的组织;R 包仍为 v0.5.0 |
| 手工 marker | 两者 | 文献和 CellMarker 等数据库中的 marker | 能识别参考中没有的群 | 依赖经验;同一 marker 在不同组织含义不同 |
两条路线
两条路线可以完成同样的分析。细胞数在 10 万以上、或要用 scVI 等深度学习工具时,Python 路线内存占用更低;要用 SingleR、CellChat、Monocle 等 R 包时选 Seurat。
互转推荐 scverse 的 anndataR(Bioconductor 收录,GitHub 最新版本 v1.3.2,2026-10-07;本页用 bioconda 上的 1.2.2 测试)。SeuratDisk 的仓库自 2023 年 11 月起没有新提交,有 156 个未关闭 issue,常见问题包括 Convert 时 X 优先取 scale.data、原始 counts 没有转过去,以及读取 LZF 压缩的 h5ad 失败。
本页实测(anndataR 1.2.2):未 JoinLayers 的 v5 对象调用 write_h5ad 不会报错,只给出 Skipping Layer "counts.A" with unexpected dimensions 警告,写出的 h5ad 没有任何表达矩阵;JoinLayers 后写出的文件含 counts 和 data 两个 layer,X 为空,在 Python 中要手动指定 adata.X。反方向读取 Scanpy 输出的 h5ad 时,X 与 layers 都成为 Seurat 的 layers,obs 列和 X_pca、X_umap 都保留。anndataR 不转换 varp 以及 Seurat 的 Neighbors、Images。zellkonverter 经 SingleCellExperiment 中转,是另一条可用路线。
# R: anndataR 1.2 (Bioconductor). rhdf5 is a separate install, otherwise:
# Error: HDF5AnnData requires the rhdf5 package
# BiocManager::install(c("anndataR", "rhdf5"))
library(Seurat); library(anndataR)
obj <- read_h5ad("pbmc3k_processed.h5ad", as = "Seurat") # h5ad -> Seurat: X and layers become layers, obsm -> reductions
obj <- JoinLayers(obj) # split layers are skipped with a warning only
write_h5ad(obj, "from_seurat.h5ad") # Seurat -> h5ad: layers "counts" and "data", X left empty
# Python: X is empty after the Seurat -> h5ad direction, point it at a layer
import anndata as ad
adata = ad.read_h5ad("from_seurat.h5ad")
adata.X = adata.layers["counts"].copy() # or layers["data"] for log-normalized values| 步骤 | Seurat v5(R) | Scanpy 1.12(Python) |
|---|---|---|
| 读入 | Read10X / Read10X_h5 | sc.read_10x_mtx / sc.read_10x_h5 |
| 质控指标 | PercentageFeatureSet(pattern = "^MT-") | sc.pp.calculate_qc_metrics(qc_vars=["mt"]) |
| 双细胞 | scDblFinder、DoubletFinder | sc.pp.scrublet |
| 归一化 | NormalizeData / SCTransform | sc.pp.normalize_total + sc.pp.log1p |
| 高变基因 | FindVariableFeatures(nfeatures = 2000) | sc.pp.highly_variable_genes(n_top_genes=2000) |
| 整合 | IntegrateLayers(CCA、RPCA、Harmony、FastMNN、scVI) | harmonypy、scVI、Scanorama |
| 聚类 | FindNeighbors + FindClusters(默认 Louvain) | sc.pp.neighbors + sc.tl.leiden |
| marker | FindAllMarkers | sc.tl.rank_genes_groups |
| 自动注释 | SingleR、Azimuth | CellTypist |
| 大数据 | BPCells 存盘 + SketchData | read_lazy、backed 模式、Dask |
| 存储 | .rds / .qs | .h5ad / .zarr |
资源
单细胞分析的内存瓶颈是稠密矩阵。计数矩阵是稀疏的;ScaleData、sc.pp.scale 一旦作用于全部基因,就会生成细胞数 × 基因数的稠密矩阵。
本页实测:PBMC 3k 过滤后的稀疏矩阵占 16.8 MB,同样的矩阵转成稠密(2,607 × 13,607 × 8 字节)是 270.6 MB,相差 16 倍。按这个比例,10 万个细胞 × 2 万个基因在全基因缩放时需要约 16 GB 只存这一个矩阵。
内存不够时按顺序尝试:只对高变基因做 ScaleData(Seurat 默认如此,不要传 features = rownames(obj));保存前删除不再需要的 scale.data 和 SCT assay;Seurat 用 BPCells 把 counts 放在磁盘上,再用 SketchData 抽 5 万个细胞分析、ProjectData 投射回全体;Python 用 anndata 的 read_lazy 或 backed 模式,只把需要的子集读入内存。
R 中出现 vector memory limit of 16.0 Gb reached 时,macOS 可以在 ~/.Renviron 中设置 R_MAX_VSIZE=32Gb;Seurat 多线程时还会遇到 future.globals.maxSize 的限制,大数据集按官方教程设为 options(future.globals.maxSize = 4e9) 或更高。
# Seurat v5 + BPCells: counts stay on disk, analyze a 50,000-cell sketch, project back
library(Seurat); library(BPCells)
options(future.globals.maxSize = 4e9)
mat <- open_matrix_10x_hdf5("filtered_feature_bc_matrix.h5")
write_matrix_dir(mat, dir = "bpcells_counts") # one-time conversion to the on-disk format
obj <- CreateSeuratObject(open_matrix_dir("bpcells_counts"))
obj <- NormalizeData(obj); obj <- FindVariableFeatures(obj)
obj <- SketchData(obj, ncells = 50000, method = "LeverageScore", sketched.assay = "sketch")
DefaultAssay(obj) <- "sketch"
obj <- FindVariableFeatures(obj); obj <- ScaleData(obj); obj <- RunPCA(obj)
obj <- FindNeighbors(obj, dims = 1:50); obj <- FindClusters(obj, resolution = 1)
obj <- RunUMAP(obj, dims = 1:50, return.model = TRUE)
obj <- ProjectData(obj, assay = "RNA", full.reduction = "pca.full", sketched.assay = "sketch",
sketched.reduction = "pca", umap.model = "umap", dims = 1:50,
refdata = list(cluster_full = "seurat_clusters"))
# Python side: open .h5ad without loading X (anndata >= 0.12)
import anndata as ad
a = ad.experimental.read_lazy("big.h5ad") # lazy obs/var/X; subset first, then .to_memory()
a = ad.read_h5ad("big.h5ad", backed="r") # older backed mode: read-only X on disk| 规模 | 内存(实测或报告) | 来源 | 建议配置 |
|---|---|---|---|
| 2,600 个细胞(PBMC 3k) | Scanpy 全流程峰值 0.98 GB;Seurat 全流程(含 SCTransform、整合测试)峰值 3.86 GB,Seurat 对象 100 MB | 本页实测 | 笔记本即可 |
| 27,000 个细胞(PBMC 3k 复制 10 份) | Scanpy 只缩放高变基因峰值 2.64 GB;缩放全部 13,607 个基因峰值 4.03 GB | 本页实测(复制细胞只用于测内存) | 16 GB 内存 |
| 81,000 个细胞(复制 30 份) | Scanpy 只缩放高变基因峰值 4.42 GB | 本页实测 | 32 GB 内存 |
| 30 万个细胞,基础流程 | Seurat 约 40 GB,Scanpy 约 10 GB | Biomamba(CSDN)实测 | 64–128 GB,或改用 Scanpy、BPCells |
| 130 万个细胞(BPCells 存盘) | Seurat 对象约 596 MB,在 5 万个抽样细胞上分析 | Seurat sketch 官方教程 | 取决于抽样数,32–64 GB 可行 |
本页实测
本页实测:macOS arm64(8 核、16 GB),Scanpy 1.12.4 / anndata 0.13.4 / Python 3.12,Seurat 5.5.1 / SeuratObject 5.4.0 / R 4.5,10x PBMC 3k(2,700 个细胞)。测试时同一台机器还在运行其他任务,耗时只用来看量级。
| 项目 | Scanpy | Seurat |
|---|---|---|
| 读入 | 2,700 个细胞 × 32,738 个基因 | CreateSeuratObject(min.cells = 3, min.features = 200) 后 2,700 × 13,714 |
| 质控 | 固定阈值保留 2,638;MAD 法保留 2,596 | 固定阈值保留 2,638 |
| 双细胞 | Scrublet 32 个(1.2%) | scDblFinder 124 个(4.6%) |
| 进入聚类 | 2,607 个细胞 × 13,607 个基因(固定阈值 + 去 Scrublet 双细胞) | 2,638 个细胞 |
| 本页代码块(MAD + 去双细胞) | 2,567 个细胞,30 个 PC、res 0.6:7 群(无血小板群) | 2,564 个细胞,dims 1:20、res 0.6:9 群 |
| 聚类 | 30 个 PC、Leiden res 0.6:8 群 | dims 1:10、res 0.5:9 群 |
| 注释 | CellTypist 投票标签与 marker 一致(8/8 群) | FindAllMarkers 11.5 秒(未装 presto) |
| 耗时 | 全脚本 186 秒,其中 Scrublet 与质控 114 秒(含首次 numba 编译) | 核心流程 15.9 秒;SCTransform v2 106.7 秒 |
| 峰值内存 | 0.98 GB | 3.86 GB(含 SCTransform 与整合测试) |
- Seurat 5.5.1 上复现的报错:GetAssayData(slot = ) defunct;@counts 不存在;多层时 GetAssayData 与 FindMarkers 报错;未 split 的 IntegrateLayers 报 attempt to set an attribute on NULL;亚群上重新整合报同一错误。
- Seurat 5.5.1 的 RunUMAP 默认使用 R 的 uwot(余弦距离),与旧教程中通过 reticulate 调 Python umap-learn 的结果不会完全相同。
- SingleR 未在本机运行:macOS arm64 的 bioconda 没有 bioconductor-singler 包;代码只核对了函数与参数名。
- CellBender、scVI 需要 GPU 或较长 CPU 时间,BPCells 未在本机安装,这三部分代码只核对了参数名与文档。
- anndataR 1.2.2 双向转换已运行:需另装 rhdf5;未 JoinLayers 时写出的 h5ad 没有表达矩阵。
国内经验
以下来自 CSDN 和知乎专栏,只收录带报错原文或作者实测、且与官方文档或本页实测一致的内容。
亚群重新整合报 “NULL是不能有属性的”
CSDN 上至少两位作者(2024 年 7 月、8 月)记录了同一报错:中文环境下是“错误于names(groups) <- "group": NULL是不能有属性的”,英文是 attempt to set an attribute on NULL;一位是在提取免疫细胞亚群后用 Harmony 整合时遇到。原因是层已合并,解决办法是 Seurat GitHub Discussion #9045 给出的先 split。本页在 Seurat 5.5.1 上复现并验证了修正。
几个样本不一定需要去批次
CSDN 一位作者用 4 个小鼠前额叶样本比较了不整合、SCTransform 和 Harmony 三种流程,不整合时各样本已经混合良好,结论是先看数据再决定。这与 Seurat 官方建议先对比未整合结果一致。
30 万个细胞的内存实测
Biomamba 在 CSDN(2024 年 12 月)用 2.5 万到 30 万个细胞测了基础流程(归一化到 UMAP):30 万个细胞时 Seurat 约 40 GB,Scanpy 约 10 GB;同时提到 5 万个细胞的 monocle2 拟时序峰值内存可达 300 GB 以上。该文作者经营服务器租赁,数字有实测脚本支持,可作为量级参考。
稀疏矩阵节省的内存
知乎专栏《单细胞分析 | Seurat基础流程 | 保姆级教程》在 PBMC 3k 原始矩阵上测得稠密矩阵 709.6 MB、稀疏矩阵 29.9 MB,相差 23.7 倍。本页对过滤后的矩阵测得 16 倍。两者都说明内存问题主要出在把矩阵变稠密的步骤上。
交给 Agent
可以用一句话描述分析任务,由 Scientify 的科学智能体在云电脑中完成。
指令示例:“对 data/ 下 6 个 10x 样本(3 个对照、3 个处理)做单细胞分析:每个样本用 MAD 法质控并跑 scDblFinder,先出未整合 UMAP,再用 Harmony 和 scVI 整合并比较;用 clustree 和多种子 ARI 选分辨率;用 CellTypist 和 marker 点图注释,输出各群细胞比例和每群的处理组与对照组差异基因。”
智能体在预装 Scanpy 的云电脑中安装其余所需的包,按样本完成质控和双细胞检测,记录每一步剔除的细胞数;整合、聚类、注释后输出 UMAP、clustree 图、marker 点图、细胞比例表和差异基因表。需要 GPU 训练 scVI 时按任务自动租用。工作区保留脚本、参数、日志和 h5ad 文件,可以复现;智能体会对结果做对抗审阅,例如检查整合后是否有处理组特有的群消失、被注释的群是否有一致的 marker。关闭本机后任务继续运行。
你仍需要自己核对:质控阈值是否符合你的组织类型,整合是否抹掉了你关心的生物学差异,注释标签是否与组织背景和文献一致,差异分析是否按样本(而不是按细胞)计算重复。
资料来源
- Seurat:PBMC 3K guided tutorial — 固定质控阈值、PC 选择与 res 0.4–1.2 的经验范围、v5 layers 访问方式
- Seurat:Integrative analysis in Seurat v5 — split、IntegrateLayers 五种方法、JoinLayers;RPCA 更保守;与未整合结果对比
- Seurat:Sketch-based analysis in Seurat v5 — 130 万细胞的 BPCells 对象约 596 MB;SketchData 与 ProjectData 参数
- Seurat GitHub Releases — 5.2.0 起 RunLeiden 改用 leidenbase;5.5.0 BPCells 支持;5.6.0 FindAllMarkers 使用 presto
- SeuratObject NEWS — 5.0.0 起 slot 参数弃用,改为 layer
- Seurat GitHub Discussion #9045 — IntegrateLayers 报错的原因是未 split
- Scanpy:Preprocessing and clustering 教程 — mt/ribo/hb 前缀、scrublet、Leiden flavor="igraph"
- Scanpy release notes — 1.12.0 需要 Python 3.12 以上,louvain 弃用;当前版本 1.12.4
- Single-cell Best Practices:Quality control — 5 MAD / 3 MAD 与 8% 下限;SoupX 与 scDblFinder 按样本运行
- Osorio D, Cai JJ. Bioinformatics 2021;37(7):963 — 线粒体比例阈值:人 10%、小鼠 5%;各组织平均值
- scDblFinder vignette — dbr.per1k、samples 参数、在质控之前运行
- DoubletFinder README — v5 兼容后函数去掉 _v3 后缀;不在合并或整合对象上运行
- Janssen P 等. Genome Biology 2023;24:140 — CellBender、DecontX、SoupX 的背景噪声去除评测
- CellBender 文档:remove-background — 输入 raw h5、--cuda、v0.3.0 起参数自动估计
- Ahlmann-Eltze C, Huber W. Nature Methods 2023;20:665 — log 变换加 PCA 与复杂变换效果相当
- Luecken MD 等. Nature Methods 2022;19:41 — 整合方法评测:scANVI、Scanorama、scVI 适合复杂任务
- Cell Ranger 7.0 release notes — --include-introns 默认 true
- clustree 文档 — 多条入边表示过度聚类;sc3_stability
- CellTypist GitHub — 输入为 log1p 且归一到 10,000;majority voting
- Azimuth — v0.5.0;参考默认从网络加载
- anndataR:Read/write Seurat objects — read_h5ad(as = "Seurat") 与 write_h5ad;不转换的内容
- SeuratDisk GitHub — 最后推送 2023-11-04
- 经验帖:CSDN《单细胞IntegrateLayers报错(自备)》 — CCA 与 Harmony 报错原文及 split 解决办法
- 经验帖:CSDN《seurat V5 ... IntegrateLayers 遇到 "group": NULL是不能有属性的问题》 — 亚群重新整合时的中文报错原文
- 经验帖:CSDN《单细胞测序并不一定需要harmony去除批次效应》 — 4 个样本不整合、SCT、Harmony 对比
- 经验帖:CSDN《30w单细胞数据会吃掉多少内存?》 — 2.5 万–30 万细胞 Seurat 与 Scanpy 内存实测
- 经验帖:知乎《单细胞分析 | Seurat基础流程 | 保姆级教程》 — PBMC 3k 稀疏与稠密矩阵内存对比