news 2026/10/28 3:03:41

单细胞aggr合并矩阵按barcode后缀拆回样本

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单细胞aggr合并矩阵按barcode后缀拆回样本

前阵子帮某实验室的一位同学收拾数据,他手上只剩一份cellranger aggr产出的 merged 矩阵、一个aggr.csv,各样本原始的 counts 目录早被清理干净。偏偏他要做的是分样本比较,而 merged 那份矩阵里几万个 barcode 混在一块儿,谁来自哪个样本完全看不出来。我花了大半天把这份合并文件按gem_group拆回原样,过程中踩了几个不显眼、但会让你对不上数的坑,这里完整记一遍。

这篇东西讲的就是多个单细胞测序 aggr 合并文件的分解:一份 merged 的filtered_feature_bc_matrix或者h5ad,怎么还原成若干个独立的单样本矩阵,还原的时候依据是什么、有哪些参数会埋雷、拆完怎么验证是对的。它解决的是"数据被合并后失去样本标签"这个很具体的痛点——上游跑完 aggr 把几个样本拼成一张大表,下游突然要做分样本的差异分析、分样本的质控、或者只想拿其中一个样本画图,这时候没有原始的 per-sample 输出,就只能从合并文件里往回拆。

适合谁看:手上正拿着 aggr 输出、但又需要样本级信息的分析人员;做单细胞下游流程、需要兼容别人递来的"只剩 merged"数据的人;以及想搞清楚 barcode 后缀到底怎么生成、为什么拆出来数量对不上的同学。文中代码以 Python 为主,R 端给等价实现,两条路都走通。

1. aggr 到底把什么东西合在了一起

拆分这件事,本质上是把合并时加进去的信息再剥出来。所以在动手之前,得先弄明白合并动作到底做了什么、留下了什么痕迹。很多人拆不干净,不是代码写错,是没搞清楚合并时的信息流向,随手按顺序切一刀,结果样本边界切偏了。

1.1 输出目录里哪些文件带样本信息

aggr跑完之后,outs/目录大致是这么个结构(不同版本略有出入,但主干一致):

outs/ ├── filtered_feature_bc_matrix.h5 ├── filtered_feature_bc_matrix/ │ ├── barcodes.tsv.gz │ ├── features.tsv.gz │ └── matrix.mtx.gz ├── raw_feature_bc_matrix.h5 ├── raw_feature_bc_matrix/ ├── molecule_info.h5 # 部分流程会保留 ├── analysis/ │ ├── clustering/graphclust/clusters.csv │ ├── pca/10_components/projection.csv │ ├── tsne/2_components/projection.csv │ └── differential_expression/*.csv ├── metrics_summary.csv └── cloupe.cloupe

这里头真正带样本身份的只有两个地方:一是barcodes.tsv.gz里每个 barcode 尾巴上的-N后缀,二是一些分析产物(比如clusters.csv、projection.csv)里同样带着后缀的 barcode 列。features.tsv.gz是全体样本共用的,所有样本用的是同一套基因注释,这一列天然一致,不需要拆。matrix.mtx.gz是基因乘细胞的坐标格式,样本信息完全没写在里面,只能靠列号去和 barcodes 对齐。

换句话说,样本标签只存在于 barcodes 的字符串里。你拿到 merged 文件之后,如果 barcodes 那一列被某个工具自动去掉了后缀,那就等于把唯一的分组依据给删了,再想拆就只能靠细胞数比例去猜,非常难受。所以第一件要提醒的事是:任何读取步骤,都要确认后缀还在。

metrics_summary.csv也是好东西,它按 library 列了每个样本的细胞数、reads 数、中位基因数。拆分完成后拿这张表对账,比自己拿原始输出到处找要方便得多。

1.2 barcode 后缀是唯一可靠的拆分依据

aggr给 barcode 加后缀的规则并不复杂:按aggr.csv里的gem_group(没写这一列时,就按行序自动编号,从 1 开始)给每个细胞挂一个-<gem_group>。比如第一个 library 的所有 barcode 都变成AAACCTGAGAAACCAT-1,第二个是...-2,依此类推。

这里的顺序很关键:后缀数字对照的是gem_group,不是library_id的字母顺序,也不是文件名顺序。我在一次复盘里就见过有人按library_id排序去切,结果样本顺序和gem_group不一致,整份数据的样本标签全体错位,还照样跑出了"显著差异",非常危险。

反过来,如果你拿不到aggr.csv,也可以从后缀反推样本数:把 barcodes 全部提出来,用正则抓最后一个-后面的数字,看有多少个不同的值,就是有多少个独立的分组。数量对得上metrics_summary.csv的行数,基本就能确认没拆错。

注意:取后缀一定要用右锚定正则,比如-(\d+)$。某些平台的自定义 barcode 或者加上样本前缀之后,字符串中间也可能出现-,用split("-")取最后一段虽然结果一样,但用正则更明确、更不容易被后续拼接破坏。

1.3 aggr.csv 与 gem_group 的对应关系

aggr.csv是个小文件,长这样:

library_id,molecule_h5,gem_group sample_A,/path/A/molecule_info.h5,1 sample_B,/path/B/molecule_info.h5,2 sample_C,/path/C/molecule_info.h5,3

gem_group这一列是可选的。如果三个样本各自跑了独立的芯片通道,那就三个不同的组,一一对应。但如果 sample_A 和 sample_B 是同一张芯片上两个不同的 lane,规范写法是两行都写gem_group=1,这样它们会被当成同一个 GEM well 处理,计数在相同 barcode 上合并。

这就引出一个非常重要的边界情况:同一个gem_group下的多个 library,拆不出来。因为它们的 barcode 加了同一个后缀,信息在合并阶段就已经被抹平了。这种情况只能回到molecule_info.h5或者各样本的原始 barcode 白名单去区分。所以在动手拆之前,第一件事是翻开aggr.csv看一眼gem_group列有没有重复值,有重复就提前死心,别浪费时间。

2. 三条拆分路线怎么选

拆分在技术上不难,难在选对入口。根据你手上的文件形态,大致有三条路:从 mtx 目录切、从 h5/h5ad 切、从 R 侧的 Seurat 对象切。三条路的结果应该完全一致,但内存占用、依赖环境和出错概率差别不小。

2.1 路线 A:直接在 mtx 坐标文件上切

matrix.mtx.gz是纯文本的坐标格式,第一行是注释,第二行是行数 列数 非零元素数,之后每行是基因行号 细胞列号 计数值。理论上完全可以按列号做流式过滤,内存占用极低,几个 G 的文件也能扛。

但真要在文本层面切,需要先算出每个细胞列属于哪个组,而这个映射来自 barcodes 的顺序,所以你还是得把 barcodes 读进内存(这一列很小,几十万行也就几 MB)。然后做一次列号到组号的映射表,再用脚本按行扫描、分流写出。麻烦在于 mtx 要求坐标按列升序排列,分流写入之后还得各自排序,手写容易出错。

我的实践是:文件不大(非零元素在五亿以内)就用scipy.io.mmread一路读进来切,简单可靠;文件特别大再考虑文本流式方案。别为了省那点内存把流程搞复杂,中途出错排查成本更高。

2.2 路线 B:读 h5 或 h5ad 再切

filtered_feature_bc_matrix.h5是 10x 的 HDF5 格式,scanpy.read_10x_h5一行就能读成 AnnData,barcode 落在obs_names上,后缀原样保留。这条路最舒服,因为切完之后直接write_h5ad就是标准下游输入,还能顺便做基因名去重、类型转换。

要注意read_10x_h5默认会执行var_names_make_unique,遇到重名基因会加-1、-2这种后缀,容易和 barcode 后缀混淆。我的习惯是读进来之后立刻把原始gene_ids存到var里,后续需要严格匹配再用 id,别用被改过的名字。

如果手上已经是h5ad(比如别人转好的),逻辑一样,只是 barcode 可能被处理过。先跑一行print(adata.obs_names[:5])看看尾巴还在不在,这是所有拆分动作的第一步。

2.3 路线 C:R 侧 Seurat 读入后 SplitObject

R 用户更习惯Read10X加SplitObject。这条路可行,但有两个版本差异要注意:Read10X有个strip.suffix参数,默认是FALSE,也就是保留-1后缀;某些旧版本或者某些封装函数会默认剥掉,剥掉之后就再也拆不出来了。所以我一般显式写Read10X(data.dir = ..., strip.suffix = FALSE),把意图钉死。

SplitObject需要一个 metadata 列做分组依据,所以流程是先建对象、算出gem列、再拆。拆完记得把 barcode 上的后缀去掉并检查是否有跨样本重名,否则后面merge的时候会出问题。

2.4 三条路线的取舍对比

维度路线 A(mtx 直切)路线 B(h5/h5ad)路线 C(R/Seurat)
内存峰值低(可流式)中高(全量载入)高(对象开销大)
上手难度中,要自己管坐标排序低,几行代码低,但要熟悉 Seurat
输出格式标准 10x 三件套h5adrds
适合场景超大矩阵、只要矩阵下游走 Python 生态下游走 R 生态
主要风险排序错、列号偏移基因名被改写后缀被自动剥掉

实际选择上,我基本按"下游用什么就用什么入口"来定:下游是 Python 就选 B,是 R 就选 C,只有矩阵大到内存扛不住才退到 A。三条路交叉验证一次(比如 B 和 C 各跑一遍,比细胞数和总 counts),是最省心的正确性保障。

3. 从零开始的完整拆分实操

下面按 Python 主线走一遍完整流程,中间穿插 R 的等价写法。所有路径都假设你在outs/的上一级目录执行。

3.1 第一步:环境与前置校验

依赖就三个:numpy、pandas、scipy,走 h5 再加scanpy和h5py。先做三件校验:

  1. 确认aggr.csv里gem_group有没有重复值,有重复就没法按后缀拆。
  2. 确认barcodes.tsv.gz里的 barcode 是否带后缀。
  3. 确认metrics_summary.csv里每个 library 的细胞数,作为后面拆完的对账基线。
import gzip, shutil, re from pathlib import Path import numpy as np import pandas as pd import scipy.io import scipy.sparse as sp OUT = Path("outs") MAT = OUT / "filtered_feature_bc_matrix" # 1) 看 aggr.csv 的分组情况 aggr = pd.read_csv("aggr.csv") print(aggr) if "gem_group" in aggr.columns: dup = aggr["gem_group"].duplicated(keep=False) if dup.any(): print("[警告] 存在共用 gem_group 的 library,这些样本无法按后缀拆分:") print(aggr[dup]) # 2) 看 barcode 后缀 bcs = pd.read_csv(MAT / "barcodes.tsv.gz", header=None)[0].astype(str) gem = bcs.str.extract(r"-(\d+)$")[0] print("后缀分布:") print(gem.value_counts(dropna=False).sort_index()) print("无后缀细胞数:", gem.isna().sum())

gem.isna().sum()这个数如果大于零,说明有一部分 barcode 没有后缀。这种情况一般出现在混合来源上——比如有人手工拼过矩阵,或者用了非标准流程。处理策略后面第 5 节细说。

3.2 第二步:按 gem_group 切矩阵并回写 10x 目录

读取 mtx 时要注意方向:matrix.mtx是基因行 × 细胞列,所以切样本是切列。按列切片用 CSC 格式效率最高,别用 CSR。

def read_10x_mtx(d: Path): with gzip.open(d / "matrix.mtx.gz", "rb") as f: m = scipy.io.mmread(f).tocsc() # genes x cells bcs = pd.read_csv(d / "barcodes.tsv.gz", header=None)[0].astype(str).tolist() feats = pd.read_csv(d / "features.tsv.gz", header=None, sep="\t") assert m.shape == (feats.shape[0], len(bcs)), "维度对不上,先查文件是否配套" return m, bcs, feats

维度断言这一步很有必要。我遇到过有人把新版本的features.tsv.gz和旧版本的matrix.mtx.gz混着拷,肉眼看不出来,切片之后基因全错位,做出来的热图怎么看怎么怪。

回写函数要保证三件事:mtx 的 header 行格式符合 10x 要求、features.tsv.gz三列不丢、barcodes不带换行残留。

def write_10x_mtx(m, barcodes, feats, outdir: Path): outdir.mkdir(parents=True, exist_ok=True) tmp = outdir / "matrix.mtx" scipy.io.mmwrite(str(tmp), m, field="integer") with open(tmp, "rb") as fin, gzip.open(outdir / "matrix.mtx.gz", "wb") as fout: shutil.copyfileobj(fin, fout) tmp.unlink() with gzip.open(outdir / "barcodes.tsv.gz", "wt", encoding="utf-8") as f: f.write("\n".join(barcodes) + "\n") with gzip.open(outdir / "features.tsv.gz", "wt", encoding="utf-8") as f: for row in feats.itertuples(index=False): f.write("\t".join(str(x) for x in row) + "\n")

主流程:

m, bcs, feats = read_10x_mtx(MAT) bc_s = pd.Series(bcs, name="bc") gem = bc_s.str.extract(r"-(\d+)$")[0] assert gem.notna().all(), "存在无后缀 barcode,先处理再拆" total_before = int(m.sum()) split_dir = Path("split") report = [] for g in sorted(gem.unique(), key=int): cols = np.where(gem.values == g)[0] sub = m[:, cols].tocsc() sub_bcs = bc_s.iloc[cols].str.replace(r"-\d+$", "", regex=True).tolist() write_10x_mtx(sub, sub_bcs, feats, split_dir / f"gem{g}") report.append({"gem_group": g, "n_cells": len(sub_bcs), "n_counts": int(sub.sum())}) report = pd.DataFrame(report) print(report) print("总 counts 校验:", total_before, "->", report["n_counts"].sum())

这里有个刻意设计:拆出来的 barcode 去掉了后缀。理由是 10x 目录作为标准输入交给下游时,带-1后缀容易被某些工具当异常处理;但去掉之后跨样本必然出现 barcode 重名,所以每个 gem 单独一个目录,别急着合并。如果后面要重新合并,用add.cell.ids加样本前缀,别裸拼。

打印出来的total counts校验是必须做的:拆分前后总 counts 必须严格相等(前提是原始合并没做下采样,这一点第 5 节会讲)。数值不相等就说明切片有遗漏,最常见的是某个gem组没被遍历到。

3.3 第三步:走 h5 或 h5ad 的更短路径

如果矩阵不大,用 AnnData 更省事,还能顺手把基因名整理干净:

import scanpy as sc adata = sc.read_10x_h5("outs/filtered_feature_bc_matrix.h5") print(adata.shape) print(adata.obs_names[:3].tolist()) # 先确认后缀在 gem = pd.Index(adata.obs_names).str.extract(r"-(\d+)$")[0].values for g in sorted(set(gem), key=int): mask = gem == g sub = adata[mask].copy() sub.obs_names = [b[: -(len(str(g)) + 1)] for b in sub.obs_names] sub.obs["gem_group"] = str(g) sub.write_h5ad(f"split/gem{g}.h5ad") print(g, sub.shape)

注意sub = adata[mask].copy()里的.copy(),少了它后面改名会触发视图警告,甚至在旧版本里改不动。

3.4 第四步:R 端的等价实现

R 侧写法有版本差异,下面这段在 Seurat v4 和 v5 都能跑:

library(Seurat) library(Matrix) mat <- Read10X(data.dir = "outs/filtered_feature_bc_matrix", strip.suffix = FALSE) obj <- CreateSeuratObject(counts = mat, project = "merged", min.cells = 0, min.features = 0) bcs <- colnames(obj) obj$gem_group <- ifelse(grepl("-\\d+$", bcs), sub("^.*-(\\d+)$", "\\1", bcs), NA_character_) print(table(obj$gem_group, useNA = "ifany")) lst <- SplitObject(obj, split.by = "gem_group") dir.create("split", showWarnings = FALSE) for (nm in names(lst)) { x <- lst[[nm]] colnames(x) <- sub("-\\d+$", "", colnames(x)) saveRDS(x, file = file.path("split", paste0("gem", nm, ".rds"))) cat(nm, ncol(x), sum(x$nCount_RNA), "\n") }

min.cells = 0, min.features = 0这两项一定要显式写。默认值是min.cells = 0, min.features = 0,看着没问题,但有些人为了"顺手过滤一下"随手改成 3 或 200,拆完的细胞数就和原始对不上,还以为是拆分出错,其实是建对象时被过滤了。这类"顺手"造成的误差最难查。

4. 拆分结果的校验与对齐

拆完不校验,等于没拆。这一步看着枯燥,但它决定了你后面所有分析结论能不能站住。我给自己定的最低标准是三项对账:细胞数、总 counts、特征行数。

4.1 细胞数对账的三个层级

第一层是组内自洽:每个gem组的细胞数之和,必须等于 merged 的总细胞数。这一层最容易过,因为就是切片,除非漏组。

第二层是外部分组对账:和metrics_summary.csv里每个 library 的细胞数比。这一层是真正的问题探测器。数值对不上通常有三种可能——gem_group存在共用(多个 library 拼进同一组,该组细胞数偏大)、原始输出本身被过滤过、aggr时启用了下采样导致部分细胞被丢弃。

第三层是原始文件对账:如果各样本的outs/filtered_feature_bc_matrix/barcodes.tsv.gz还在,直接数行数。这一层最权威,但往往也是最缺的——数据都递到你手上了,原始目录大概率已经清了。

metrics = pd.read_csv("outs/metrics_summary.csv") print(metrics.columns.tolist()) # 常见列名:Library ID / Estimated Number of Cells chk = metrics[["Library ID", "Estimated Number of Cells"]].copy() chk.columns = ["library_id", "n_cells_expected"] chk["n_cells_expected"] = chk["n_cells_expected"].astype(int) # 按 aggr.csv 把 library_id 映射到 gem_group m = aggr[["library_id", "gem_group"]].merge( report, left_on="gem_group", right_on="gem_group", how="left") print(m.merge(chk, on="library_id"))

三个数一一对上,基本可以放心。对不上但差值很小(个位数级别),通常是边界细胞在合并阶段被去重或者被 QC 门槛卡掉,可以接受;差值大到百分之几,就要回去查gem_group和归一化参数了。

4.2 总 counts 与稀疏结构校验

细胞数对上,不代表计数对得上。我习惯再加两项:

import scipy.sparse as sp sub = scipy.io.mmread("split/gem1/matrix.mtx.gz").tocsc() print("非零元素:", sub.nnz) print("总 counts:", int(sub.sum())) print("每细胞中位 counts:", float(np.median(np.asarray(sub.sum(axis=0)).ravel()))) print("每细胞中位基因数:", float(np.median(np.asarray((sub > 0).sum(axis=0)).ravel())))

这两个中位数拿去和metrics_summary.csv里的Median Genes per Cell、Median UMI Counts per Cell比,是非常灵敏的探针。如果细胞数对得上但中位数明显偏小,八成是合并时做过深度归一化,计数被缩放了。

特征维度也要查:每个拆分目录的features.tsv.gz行数必须完全一致,且和 merged 的一致。不一致说明写回的时候丢了行,或者被某个工具做了基因过滤。

4.3 分析产物也要跟着拆

outs/analysis/下的东西同样是 merged 尺度的,不是拆完矩阵就完事了。clusters.csv、projection.csv、differential_expression/这些文件的 barcode 同样带后缀,可以按同样的规则拆:

tsne = pd.read_csv("outs/analysis/tsne/2_components/projection.csv") tsne["gem"] = tsne["Barcode"].str.extract(r"-(\d+)$")[0] for g, sub in tsne.groupby("gem", sort=True): sub.drop(columns="gem").to_csv(f"split/gem{g}_tsne.csv", index=False)

但要说清楚:这些是 merged 尺度下的降维和聚类结果,拆到单样本只对"看看每个样本在合并空间里落在哪"有用,不能当单样本自己的 UMAP 用。想拿单样本的降维图,得在拆分后的矩阵上重跑。同理,differential_expression/是拿library_id做分组算出来的组间差异,拆开后没有意义,直接忽略。

5. 踩过的坑与排查速查表

下面这几条都是我在实际项目里真踩过的,按"坑的隐蔽程度"排序,越靠前越容易被忽略。

5.1 后缀被悄悄剥掉

最典型的一次:某同学拿Read10X读完、建好对象、存在中间 rds 里准备第二天接着做,第二天用了另一个封装函数重新读 rds,结果那个函数内部调了类似make.names的处理,把-1变成了...1。再想拆的时候正则全失效,gem全变成NA。

这类问题的排查方法很简单:在任何中间产物落盘前后各打印一次head(barcodes)。发现后缀变形,优先怀疑读取函数的默认参数,而不是自己的正则。Read10X记得写strip.suffix = FALSE;Python 侧read_10x_h5不动obs_names,但要小心别自己跑obs_names_make_unique把重复 barcode 改成新的后缀。

5.2 归一化参数埋的雷

aggr有个--normalize参数,可选none(默认)、mapped、hm。走默认不重采样,拆出来的 counts 和原始一致;一旦选了mapped,每个 library 的 reads 会被下采样到同一水平,合并矩阵里的 counts 是缩过的整数,这时候拆出来的不是原始 counts,做绝对定量、做饱和度评估全都不准。

怎么判断?看metrics_summary.csv里各 library 的Mean Reads per Cell是否被人为拉平,或者直接看拆分后每个样本的总 counts 是否和原始文件(如果还在)差一截。差得整齐,就是归一化过。

这种情况不是不能拆,而是要在结论里写清"counts 经过深度归一化,仅用于相对比较"。我一般会在项目 README 里加一行备注,免得半年后自己都忘了。

5.3 共用 gem_group:拆不开的死结

回到第 1.3 节那个问题。如果aggr.csv里两个 library 共用同一个gem_group,它们的 barcode 在合并时已经混成一锅,按后缀只能拆出"组",拆不出"样本"。这时候唯一可行的路子是:

  • 如果还留着各样本的molecule_info.h5,用分子级别的信息重建分样本矩阵;
  • 如果还留着各样本的barcodes.tsv.gz白名单(哪怕只有 barcode 列表),可以用它做交集反查;
  • 如果两者都没有,那就接受"这组样本无法区分"的事实,别硬拆。

顺便提一个反例:有人为了"拆得干净",手工改aggr.csv给每个 library 都写独立gem_group然后重跑合并,这是对的;但如果只是把 merged 的 barcode 后缀按比例重新编号,那就是纯粹的数据造假,做出来的样本间差异全是人为构造的,非常危险。

5.4 常见问题速查表

现象最可能原因排查动作处理
后缀解析全为 NA读取时后缀被剥离/变形打印 barcodes 头几行换读取方式,禁用剥后缀参数
拆分细胞数之和小于总数遍历漏组或正则不匹配检查gem.value_counts补齐分组,用右锚定正则
某组细胞数明显偏大多个 library 共用 gem_group查aggr.csv重复值回到分子文件重建,或接受
counts 总量对不上启用了 depth normalization比对 metrics 的中位 reads标注为归一化数据,仅做相对比较
基因数不一致features 版本混杂比对行数与基因 id统一参考版本后重做
R 中 barcode 变成...1中间函数做了名字规范化head(colnames(obj))关掉规范化,或从源重读
拆完后再 merge 报重复去后缀后跨样本重名检查交集大小merge 时加样本前缀
h5ad 读入后 obs 少一列写入时被覆盖对比shape与源文件重新拆分并保留分组列

5.5 一个容易被忽略的细节:空组与单细胞组

有些gem_group在 merged 里可能只有寥寥几个细胞,甚至是零。零细胞的情况意味着那个 library 在合并阶段就被全部过滤了。拆分脚本遍历sorted(gem.unique())时不会遍历到空组,所以split/下不会生成对应目录,这是正常的,但要在报告里显式标出来,否则业务方会以为你漏了一个样本。

细胞数极少的组(比如只有十几个)也要警惕:这类样本单独做 QC 和聚类基本没有统计意义,建议在报告里标注"细胞数不足,建议合并分析或不单独解读"。

6. 什么时候不该拆,以及拆完怎么收场

写到这里,拆分的技术路径已经完整了。但我更想说的是判断层面的东西:有些场景下,硬拆反而是错的。这一节没有什么代码,全是经验。

6.1 三种更适合"重做"而不是"回拆"的情况

第一种,原始molecule_info.h5还在。那最优解不是拆合并文件,而是直接用分子级别信息重建各样本矩阵,或者干脆按各自的molecule_info.h5重跑一遍定量。拆合并文件本质上是有损恢复,能不做就不做。

第二种,合并时做过深度归一化。拆出来的 counts 是缩过的,任何基于绝对值的指标都不能信。这种情况下我更倾向于找到原始数据重做,实在找不到,就在所有下游结论前加一句"基于归一化 counts"。

第三种,共用gem_group且没有其他线索。前面说过,这是死结。与其编一套看似合理的样本标签,不如老实告诉需求方"这几个样本合在一起了,分不开"。

还有一类特殊情况:你只是想看某个样本的 QC 指标,并不需要矩阵。这时候完全不用拆,metrics_summary.csv里已经有每个 library 的细胞数、reads、中位基因数,直接拿来看就行。别为了一个数字去切一个几 G 的矩阵。

6.2 拆完之后重新整合的正确姿势

拆完再合,是很多流程的必经之路(比如先分样本 QC、再统一整合)。这里有个高频翻车点:去掉后缀之后,不同样本的 barcode 会重名。10x 的 barcode 是 16bp 随机序列,几万个细胞里跨样本撞车几乎是必然的。

Python 侧用scanpy.concat时带上label和keys,它会自动加前缀:

import anndata as ad parts = [sc.read_h5ad(f"split/gem{i}.h5ad") for i in (1, 2, 3)] merged = ad.concat(parts, label="sample", keys=[f"gem{i}" for i in (1, 2, 3)], index_unique="-", join="outer") print(merged.obs["sample"].value_counts())

index_unique="-"是关键参数,它会把样本标签拼到 barcode 后面,避免重名。R 侧对应的是merge(x, y, add.cell.ids = c("gem1","gem2")),一样的作用。

整合完成之后,建议再跑一次总 counts 对账,确认拼接过程没有丢细胞。这一步很多人省掉,结果在整合后的对象上发现细胞数比拆分前少了几千个,回头一查是join参数写成了inner,把基因并集之外的东西全丢了。

我个人在实际操作中的体会是,拆分这件事的技术难度远低于它的判断难度。代码就是几十行,写一次能复用很久;真正花时间的是想清楚"这份合并文件里到底还残留多少样本信息"、"哪些结论在拆分后依然成立"。每次动手前先把aggr.csv和metrics_summary.csv翻一遍,把分组关系和细胞数基线记在纸上,后面所有的对账和排查都会顺很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 3:03:00

技能管理实战:从技能地图到刻意训练

不提环境变量&#xff0c;也不说潮流风向&#xff0c;就聊聊一件每天都在发生、但很多人长期没搞明白的事——skills。这个词近年在职场和培训里被反复提及&#xff0c;但多数人真正面对的问题是&#xff1a;技能学了不少&#xff0c;简历写得密密麻麻&#xff0c;可真到要用的…

作者头像 李华
网站建设 2026/10/28 3:02:57

昇腾CANN架构解析:AI计算优化与算子开发实战

1. CANN架构全景解析&#xff1a;AI计算的基石设计第一次接触昇腾AI处理器配套的CANN&#xff08;Compute Architecture for Neural Networks&#xff09;时&#xff0c;最让我震撼的是其算子库的设计理念。这个看似简单的组件库&#xff0c;实际上承载着从算法模型到芯片指令的…

作者头像 李华
网站建设 2026/10/28 3:02:39

YOLO26端到端目标检测:架构革新与工业实践

1. YOLO26&#xff1a;端到端目标检测的新里程碑第一次听说YOLO26时&#xff0c;我正在调试一个无人机巡检项目中的目标检测模型。当时使用的YOLOv5在复杂场景下总会出现漏检和误报&#xff0c;直到看到社区讨论这个号称"真正实现端到端预测"的新版本。经过两周的实测…

作者头像 李华
网站建设 2026/10/28 3:02:07

AI评估体系失效诊断与动态评估框架构建

1. AI评估体系失效的典型症状诊断当AI系统开始出现评估指标与实际业务表现脱节时&#xff0c;往往会在三个关键维度显现异常。我在金融风控和医疗影像诊断两个领域的AI落地实践中&#xff0c;总结出最具代表性的预警信号&#xff1a;1.1 指标漂移现象&#xff08;Metric Drift&…

作者头像 李华
网站建设 2026/10/28 3:01:59

基于MRI影像组学的肝内胆管癌分级研究

1. 项目背景与临床意义肝内肿块型胆管癌&#xff08;IMCC&#xff09;作为原发性肝癌中预后较差的亚型&#xff0c;其准确分级对临床治疗方案选择具有决定性作用。传统病理活检存在取样偏差、创伤性等局限&#xff0c;而MRI作为一种无创检查手段&#xff0c;能够提供肿瘤内部多…

作者头像 李华
网站建设 2026/10/28 3:00:07

金融科技AI智能体:架构设计与行业实践

1. 项目背景与行业定位金融科技领域正在经历一场由AI驱动的深刻变革。在这个大背景下&#xff0c;诸葛智能凭借其金融分析智能体解决方案&#xff0c;成功在细分赛道确立领先地位。作为从业者&#xff0c;我观察到这个领域的技术演进呈现出三个明显特征&#xff1a;数据处理从结…

作者头像 李华