ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scissor算法实战:用bulk临床表型精准锁定单细胞关键亚群

Scissor算法实战:用bulk临床表型精准锁定单细胞关键亚群 前一段时间我把手头一个肺癌队列翻出来重新做单细胞数据是两年前跑的只有6个样本而bulk RNA-seq有三百多例生存、分期、化疗信息全都有。以前这两块数据是割裂的单细胞分辨率高但样本量太小生存分析根本统计不出来bulk数据量大、临床信息全但每个样本是一锅混合信号看不出是哪种细胞在驱动表型。后来用了Scissor算法把bulk表达量和临床表型作为监督信号反过来在单细胞里寻找与表型关联的细胞亚群整个分析链路才终于闭环。这篇文章把Scissor的原理、完整流程和踩坑经验整理出来。主要面向两类人一是手头单细胞样本少、但想用大队列bulk数据给自己结果增加说服力的研究者二是听说过Scissor但对它到底怎么用、结果怎么解读还比较模糊的朋友。我会尽量把参数选择、数据整理、结果验证这些环节讲透不绕弯子。1. 为什么拿单细胞数据做表型关联还需要绕一道弯1.1 单细胞和bulk数据的互补关系从数据形态来说bulk RNA-seq可以理解成把一堆细胞的RNA放在同一个管子里测得到的每个基因表达量是所有细胞的平均值。肿瘤组织里有肿瘤细胞、T细胞、巨噬细胞、成纤维细胞等等任何基因的bulk表达量都是这一堆细胞表达量的加权平均权重就是各细胞类型的比例。做去卷积的人天天在解这个方程但解出来的是“比例”不是真实的细胞状态。单细胞测序则把每个细胞单独分开可以直接看到某个CD8T细胞里PDCD1高表达还是低表达可以看到巨噬细胞处于M1还是M2表型。但问题也很现实一个样本能测的细胞数量看着多真正的生物学重复通常只有几个费用和样本获取难度摆在那里。我手里这6个10X样本做细胞类型比例差异也许还凑合想画PFS生存曲线样本量根本不允许。这里就出现一个很自然的诉求能不能用bulk大队列的临床表型做“外挂”指导我们在单细胞里找出真正跟临床相关的亚群Scissor就是干这件事的。它不要求你手动定义细胞类型也不要求单细胞数据自带临床信息而是把bulk层面的表型和细胞层面的转录特征桥接起来最终输出一个“表型相关细胞亚群”的名单。1.2 Scissor跟常规marker筛选有什么不同这里要澄清一个容易混淆的点。常规的单细胞分析路径是先聚类再做cluster之间的差异表达找出某个亚群的marker然后起名字比如“Treg”“M2巨噬细胞”。这条路从数据内部出发找到的每个细胞亚群都有分子特征但它没有和外部临床表型建立直接联系。哪怕我在某个罕见亚群里看到免疫抑制基因高表达也只是“推测它可能有临床意义”。Scissor的思路刚好反过来。它不要求你预先有亚群标签而是直接把bulk层面的表型作为监督信号在所有候选单细胞里挑选那些“最能解释表型差异”的细胞。听起来像在做特征选择一堆细胞就是一堆特征表型是标签选出来的细胞就是这个任务里的关键驱动者。这样做的好处是最终结果和临床问题直接挂钩。报告里写“Scissor亚群与较差预后相关”时这句话背后有明确的统计分析支撑而不是拍脑袋。把这一点讲清楚Scissor在分析流程中的定位就出来了它介于无监督细胞聚类和bulk生存分析之间承担的是“用宏观表型反推微观细胞”的角色。2. 核心原理拆解Scissor如何把bulk表型“投射”到单细胞上2.1 用相关系数把两种平台拉到同一个尺度Scissor的第一步不是直接比表达量而是计算每个单细胞和每个bulk样本的表达谱相关性。为什么要用相关系数因为单细胞和bulk的测序平台、文库构建、定量方式都不一样直接把某个基因在两个矩阵里的原始数值放在一起比较没有意义。但整个基因表达谱的形态也就是哪些基因高、哪些基因低是跨平台相对稳定的。Pearson或Spearman相关可以把这种形态相似性变成一个数字把平台差异尽量压下去。所以Scissor输入需要两个表达矩阵单细胞矩阵和bulk矩阵。实际计算时对每个单细胞用它的基因表达向量和每个bulk样本的基因表达向量做相关最后得到一个“细胞数 × bulk样本数”的相关矩阵R。R[i, j]大说明细胞i和bulk样本j整体表达谱像也就意味着这类细胞在这个样本里可能占比较高。这一步得到的相关矩阵是后续一切分析的基础。我实测下来不同相关算法对最终结果有一定影响Spearman秩相关会更稳一点尤其单细胞数据里有很多dropout零值和极端表达值的时候。但如果走Scissor官方的流程就保持它默认的处理方式不要自己在中间改算法否则后面参数的意义会变得不可控。2.2 Graph-guided稀疏回归怎么从一团细胞里“挑人”拿到相关矩阵R之后Scissor做的事情可以理解成一个带正则化的回归。响应变量是bulk样本的表型比如生存时间、药物敏感或者耐药特征是每个细胞的相关模式也就是R矩阵里的每一行。Scissor想找的是一组细胞它们的相关模式能最好地预测表型。如果你把成千上万个细胞全部塞进回归一定会过拟合因为细胞之间高度相似、数目远大于bulk样本数。Scissor用两个手段控制这个问题。一是在细胞之间建图利用细胞表达谱相似性让彼此像的细胞在回归里的系数尽量接近这就是graph-guided regularization二是加L1惩罚迫使绝大多数细胞的系数变成0只留下少数细胞作为被选中的亚群。这个设计很像在做带先验知识的特征选择。被选中的细胞回归系数为正的那部分叫Scissor和表型正向关联系数为负的叫作Scissor-和表型负向关联。假设你的表型是“是否对化疗敏感”Scissor就是和敏感组显著相关的一群细胞Scissor-就是和耐药组相关的一群细胞。整个过程既做了筛选又通过图约束把相似细胞聚成亚群。可以用一个生活化类比帮助理解你想在几千人的小区里找出哪些住户和“体检指标异常”有关但手里只有每个单元楼的平均体检数据没有个人体检数据。于是你让每个住户填一份生活习惯问卷再将问卷特征和楼栋均值做匹配最后选出最能解释楼间差异的关键住户。Scissor就是这套筛选框架的算法实现。2.3 和传统去卷积思路的差异很多人第一次接触Scissor会问这和CIBERSORT、MuSiC这些去卷积工具有什么区别两者目标确实有重叠都想回答“bulk组织里有什么细胞”但路径完全不同。去卷积需要预先知道参考细胞类型并假设每种类型有一个相对固定的表达签名然后估计每个bulk样本里这些类型的比例。它输出的是连续的比例值无法具体到某一个病人的某一群细胞。Scissor则不需要参考签名也不要求你预先定义细胞类型。它直接从你的单细胞数据里挑出具体细胞保留了细胞的异质性还能进一步对选出的细胞做差异表达、轨迹分析或者功能富集。换句话说去卷积适合回答“这个肿瘤里Treg占多少”Scissor适合回答“在所有T细胞里哪一群跟病人预后最相关、这群细胞长什么样”。我自己的习惯是两者结合用Scissor找出候选亚群和分子特征后再拿这个亚群的签名去做去卷积在更大的bulk队列里验证比例和预后的关系。这样既利用了单细胞的分辨率也利用了bulk大队列的可统计性分析链条会更完整。2.4 关键参数alpha和tau的实际含义说原理不说参数等于白说。Scissor里有两个参数我每次跑都会调alpha和tau。alpha控制图正则化的强度也就是“相似细胞之间回归系数要保持一致”这个约束的权重。alpha越大选出来的细胞越倾向于成片成块而不是零散分布alpha太小选出来的细胞可能碎成一片后面很难解释。tau在这类稀疏回归里可以理解成一个控制稀疏程度的阈值它的取值会直接影响最终选中的细胞数量。常见操作是先设tau0.1跑一版看Scissor和Scissor-各占多少细胞再根据结果微调。如果选出的细胞数太少小到几十个后续差异表达和可视化都会很虚如果选出来一大半细胞那等于没选亚群特异性就丢了。我的经验是选中细胞占总细胞数的5%到10%左右往往比较健康当然这个范围在不同数据里可以浮动。需要提醒的是Scissor不同版本的参数行为可能有差别官方源码也持续在更新。拿到一个新版本后先跑一个小数据集把alpha和tau都试一遍记录每种组合下的细胞数量再选一个生物学解释最顺的版本不要盲目相信某个固定默认值。3. 实战准备数据格式、环境与基因匹配3.1 需要准备的三类输入数据动手之前先把三类数据准备好。第一类是单细胞表达矩阵我习惯从Seurat对象里直接提取基因在行、细胞在列值取log-normalized的data slot。注意这里不需要先做聚类Scissor不关心细胞被分成几群它认的是单个细胞。第二类是bulk表达矩阵同样基因在行、样本在列最好也是log变换后的表达量不然平台差异会被放大。第三类是bulk样本对应的表型数据格式取决于你要做的问题。如果是生存分析需要两列时间列和事件列比如OS.time和OS.status其中事件是0/1编码。如果是二分类表型比如药物敏感/耐药、复发/不复发就给一个二值向量样本顺序必须和bulk矩阵的列一一对应。如果是连续表型比如肿瘤纯度、TMB值也可以用连续向量。格式错误是新手最容易卡住的地方。我自己就遇到过表型顺序和bulk矩阵列顺序对不上的情况跑出来的Scissor亚群完全解释不通排查了很久才发现是临床表型的行名顺序和表达矩阵列名顺序不一致导致的。这类问题不会报错但会让结果无声地变错非常危险。3.2 R环境和Scissor安装Scissor是一个R包推荐在R 4.x的环境里跑。安装方式走GitHub最方便一般用devtools::install_github()具体仓库地址以你当时搜索到的官方说明为准。我建议安装的同时把Seurat、Survival、Matrix这些常用包都更新到比较新的版本因为Scissor的依赖如果版本太旧运行时会报一些奇怪的内存错误或者函数找不到错误。如果是在服务器上跑最好建一个独立的conda环境或者单独指定一个R library路径别跟系统自带R包混在一起。我吃过亏系统里一个旧版glmnet和Scissor内部调用的函数冲突报错信息完全看不出原因最后重装到新环境才解决。还有一点Scissor部分依赖是Bioconductor的install.packages装不了得用BiocManager::install()。这个步骤对新手很容易卡一下提前知道能省不少时间。3.3 基因名匹配与低质量细胞过滤这是我实际跑下来踩得最深的坑。bulk表达矩阵很多来自TCGA或GEO行名往往是Ensembl ID或者已经注释成Symbol单细胞矩阵里行名一般是Symbol而且会有版本差异旧的Symbol和新的别名对不上。如果不统一Scissor计算相关矩阵时虽然不会直接报错但能匹配上的基因数量可能只剩一半结果自然一塌糊涂。我现在的习惯是先把bulk矩阵的行名统一转成Symbol用clusterProfiler或者biomaRt做ID转换再和单细胞矩阵取交集基因并且确保在两个矩阵里都稳定表达。低表达基因过滤也很重要单细胞数据里大量基因在多数细胞中表达量为0这些基因对相关性的贡献主要是拉低整体相似度不提供有效信息。我一般过滤掉在小于10%细胞中表达的基因同时去掉线粒体基因因为线粒体基因高表达往往代表细胞质量问题而不是真正的生物学信号。过滤完看一眼基因数量如果落在8000到12000这个范围后面跑起来通常比较顺。如果只剩两三千个基因信息量可能不够建议检查是不是过滤阈值太狠了或者ID转换时丢掉了太多基因。3.4 表型顺序与方向的最后检查这一步看起来不起眼但值得单独拿出来说。把bulk矩阵和临床表型放一起后我要求自己至少检查三件事第一表型的行名是否和bulk矩阵列名完全一致顺序是否一致不要靠肉眼扫用all.equal或identical判断第二生存状态列里0和1的编码方向是否符合分析预期通常1代表事件发生但在不同数据库里编码可能反过来第三二分类表型的两组样本量是否悬殊如果一组只有两三个样本Scissor基本不可能学到有效信号。关于表型方向还有一个很容易忽略的点Scissor和Scissor-的方向其实受表型编码影响。用同一个数据如果把0和1互换Scissor和Scissor-的生物学含义也会跟着换位。所以跑之前想清楚“我关注的到底是哪一端”别等结果出来了再猜。4. 核心实操跑通Scissor的完整流程4.1 从Seurat对象提取单细胞表达矩阵先说我的操作。假设已经有了一个Seurat对象seu并且做过NormalizeData直接这样提取library(Seurat) library(Scissor) # 提取log-normalized表达矩阵基因 x 细胞 log_norm - GetAssayData(seu, assay RNA, slot data) log_norm - as.matrix(log_norm) # 过滤低表达基因和线粒体基因 keep_genes - Matrix::rowSums(log_norm 0) 0.1 * ncol(log_norm) keep_genes - keep_genes !grepl(^MT-, rownames(log_norm)) log_norm - log_norm[keep_genes, ]这个矩阵就是后面Scissor的主要输入。如果你用的是SCTransform流程从SCT assay的data里取也没问题但要保证和bulk矩阵用同一套基因命名。整体逻辑是单细胞矩阵的质量越好、基因过滤越合理后面的相关矩阵就越干净Scissor结果也更稳定。4.2 表型数据组织和Scissor函数运行以生存分析为例。假设我读进一个临床表格clin里面有样本ID、OS.time和OS.status三列。构造表型数据时最安全的做法是先把bulk矩阵的列名和临床表的样本ID对齐再提取对应列bulk_mat - readRDS(bulk_log_mat.rds) # 基因 x 样本log2(TPM1)之类 clin - read.csv(clinical.csv, row.names 1) clin - clin[colnames(bulk_mat), , drop FALSE] # 按bulk列顺序重排 surv_time - clin$OS.time surv_status - clin$OS.status然后调Scissor函数。不同版本函数签名会有差别基本结构大致长这样set.seed(2024) scissor_res - Scissor( log_norm, # 单细胞表达矩阵 bulk_mat, # bulk表达矩阵 data.frame(time surv_time, status surv_status), family cox, # 生存数据用cox二分类用binomial alpha 0.05, tau 0.15 )如果你的版本里有tag、gamma之类的附加参数按官方文档说明设置即可。跑完后会返回一个列表里面一般有Scissor_pos和Scissor_neg分别是Scissor和Scissor-细胞的索引或细胞名没被选中的就是非Scissor细胞。跑的过程中控制台会打印进度如果长时间停在“calculating correlation”这一步说明它在算那个大相关矩阵这是耗时大头。如果是二分类表型把family换成binomial表型给一个0/1向量就行。连续表型的情况不同版本支持方式可能不一样建议直接去查你那个版本的帮助文档按文档格式来不要想当然。4.3 相关系数矩阵的加速计算与内存控制Scissor第一步计算相关矩阵是出了名的慢尤其是单细胞几万个、基因上万个的时候。我第一次跑的时候在办公室电脑上挂了三个小时没出结果后来发现那一步存在大量循环计算。如果你想在正式跑之前先快速估算一下结果或者干脆自己写流程可以用矩阵运算来算相关矩阵。思路是先对两个表达矩阵按基因做z-score标准化再一步矩阵乘法# A基因x细胞B基因x样本 az - t(scale(t(A))) # 对每行基因做z-score bz - t(scale(t(B))) cor_mat - t(az) %*% bz / (nrow(A) - 1) # 细胞 x 样本这样一个矩阵乘法就能算出所有细胞和所有bulk样本的相关系数。代价是内存占用。az本身是基因×细胞的大矩阵几万个细胞的规模就达到GB级别如果再同时保留原始矩阵和cor_mat内存很容易吃紧。我的经验是细胞超过5万就要警惕不要一次性把所有中间变量都留在环境里。如果细胞量特别大优先考虑用bigstatsr包的FBM内存映射矩阵来存中间结果或者分块计算每次取一部分细胞算相关写回磁盘清空内存再算下一批。不要硬扛等R跑到一半被OOM杀掉前面时间全白费。这个加速方法也可以用来做参数探索先用随机抽样的5000个细胞快速跑一版Scissor确认alpha和tau的行为再全量跑正式版能省下大量无谓的等待。5. 结果解读与可视化应用5.1 Scissor、Scissor-和非Scissor细胞的生物学含义Scissor的输出不是“细胞类型”而是“和表型相关的细胞成员”。这句话我每次都要强调。比如我用生存数据跑完Scissor不是免疫组化里那种阳性细胞它指的是回归里系数为正的那群细胞它们的存在和较差预后或你设定的表型方向相关。Scissor-则相反和较好预后相关。剩下那批非Scissor细胞不代表它们没意义只是在这个表型任务里没有被选中。判断Scissor和Scissor-到底是谁要回到单细胞的marker基因上去看。比如Scissor里高表达FOXP3、CTLA4那大概率是Treg如果高表达SPP1、TREM2可能是巨噬细胞里的某个功能状态如果高表达MKI67、STMN1可能是一群增殖性肿瘤细胞。解读时要结合具体疾病和组织类型不能机械地套“某某基因等于某某细胞”的公式。5.2 将Scissor结果映射到UMAP并找marker基因拿到结果后第一件事是把它塞回Seurat对象里方便画图和后续分析seu$scissor - Non_Scissor seu$scissor[scissor_res$Scissor_pos] - Scissor seu$scissor[scissor_res$Scissor_neg] - Scissor- seu$scissor - factor(seu$scissor, levels c(Scissor, Scissor-, Non_Scissor)) DimPlot(seu, group.by scissor, cols c(#d73027, #313695, grey80))这样能直观看到Scissor和Scissor-是不是分别聚成一块。如果两个群体在UMAP上完全重叠说明结果很可能不稳定后面要谨慎。如果Scissor聚成明显的一团或两三团就可以继续做差异表达看这群细胞到底有什么特征Idents(seu) - scissor markers - FindMarkers(seu, ident.1 Scissor, ident.2 Non_Scissor)实际分析中我还会额外做一步检查看Scissor细胞在原始无监督聚类中的cluster组成。如果Scissor主要落在某一个cluster里那这个cluster大概率就是关键亚群如果Scissor均匀散布在所有cluster里最好回头检查输入数据或表型定义。5.3 判断结果是否可靠的几条实证路径Scissor是计算筛选工具结论靠不靠谱需要交叉验证。我常用的检查思路有三个。第一看Scissor占比和bulk表型是否一致。比如Scissor对应预后差的细胞那理想情况下Scissor细胞占比高的样本预后应该更差可以用去卷积方法估计对应细胞类型的比例再做一次生存分析确认方向。第二用外部数据集验证。把训练数据里跑出来的Scissor群体marker基因做成一个签名放到另一个独立的bulk队列里打分看这个签名能不能复现预后关联。现在很多工具都能做基因签名打分核心思想是不能只在发现集里讲故事要在验证集里看到同样的趋势。第三回到基础生物学。Scissor找到的亚群marker基因是否有文献支持是否符合你对疾病机制的理解如果找出来一群完全没人报告过的细胞又缺乏功能线索那宁可多花时间验证也不要急着当新发现写进论文。这不是泼冷水而是我见过太多人把计算筛选结果直接当成最终结论最后湿实验没法验证整个故事崩掉。6. 常见问题、报错与避坑实录6.1 高频报错与排查对照表我在各种数据集上跑过很多次Scissor也帮人排查过不少问题整理一个速查表报错或现象可能原因解决方式安装失败缺依赖包部分Bioconductor依赖没装用BiocManager::install()补装后重装报错基因名不匹配单细胞和bulk基因名体系不一致统一转成Symbol取交集基因报错family或表型格式表型数据不是data.frame或顺序错位按colnames(bulk_mat)重排表型行名确认数值类型长时间卡在correlation计算细胞数太多、内存不够用矩阵乘法加速或分块计算先抽样测试结果全是Scissor几乎没有Scissor-表型方向太强或bulk样本组成混杂检查表型编码方向、样本批次、tau是否过小两次运行结果差异大缺少随机种子内部优化不稳定固定随机种子多次运行比较重叠率这个表覆盖了大部分情况。遇到新报错时先怀疑数据类型问题很多时候Scissor报错字面意思很清楚只是R的报错格式比较吓人把对象类型和维度检查一遍往往就能解决。6.2 tau的调节方法和结果稳定性判断tau是大家问得最多的参数。我的调节方法很朴素先固定alpha跑一组tau比如0.05、0.1、0.15、0.2、0.3把每次选中的细胞数记录下来画一条曲线。细胞数如果从8000掉到2000这种陡降区域往往就是合适的范围。在这个区间附近多试几个值看结果是不是稳定。稳定性还要看换种子之后的表现。Scissor内部如果涉及随机初始化或验证集划分结果会有波动。我一般固定种子跑一次主结果再用不同种子跑5次看Scissor和Scissor-细胞的重叠率。重叠率超过80%基本可以接受低于60%就要小心很可能数据里没有强信号越调参数越容易过拟合。另外Scissor对输入的单细胞质量非常敏感。如果数据里存在大量doublet或低质量细胞这些细胞因表达谱杂乱、基因数异常很容易被选成Scissor。所以跑Scissor之前先用DoubletFinder或者scDblFinder把doublet去掉再检查线粒体比例、UMI数这些QC指标。这一步省不得。6.3 实操经验小结最后分享几条我自己的操作习惯。第一跑Scissor之前永远先对齐样本顺序和基因名哪怕多花十分钟也比你跑三个小时出来一个解释不通的结果强。第二不要一次性上全量数据做参数调试先用随机抽样的5000个细胞加几十个bulk样本快速验证输入没问题再放开跑全量这能把整个流程从半天缩短到一两个小时。第三Scissor结果出来后一定要回到原始数据去看那些细胞的真实身份。把Scissor细胞的top marker基因做个热图和已知文献比对这一步能帮你过滤掉大部分假阳性。我实际跑下来最深的体会是Scissor真正难的部分不是算法本身而是前期的数据整理和后期的结果解读。只要你把bulk样本的临床信息和单细胞的表达谱对齐到位Scissor跑出来通常不会太离谱反而是一上来就急着调参、忽略数据质量的人很容易被结果的“看似合理”欺骗。做这种跨平台表型关联分析耐心和仔细比算法技巧重要得多这也是我想对后来者说的最重要的一句话。
返回列表