从GEO下载完数据集,打开表达矩阵一看,行名全是TC01000001.hg.1这种编号,用网上现成的工具查了半天,发现大部分ID根本不在常见数据库里——这是做Affymetrix芯片数据重分析时最让人头疼的一步,尤其是碰到GPL5175这个平台。很多人连"affymatrix"这个拼写都是错的(正确写法是Affymetrix,少个e),搜索资料就更费劲了。
GPL5175对应的是Affymetrix Human Gene 2.0 ST Array(转录本版本),和很多人熟悉的U133系列芯片不同——它的探针ID不是200000_s_at这种格式,而是按"转录本簇"(Transcript Cluster)编号。这意味着,你在做探针转换时,不能照搬网上的老教程,必须针对这个平台的注释设计转换逻辑。我在这篇文章里就把这个过程的原理、坑、实操代码一次性讲清楚,适合正在做GEO数据库挖掘、准备做meta分析或重分析公共数据的人直接参考。
1. 先搞懂GPL5175平台和它的探针ID,再谈转换
1.1 Affymetrix Human Gene 2.0 ST阵列是什么
Affymetrix Human Gene 2.0 ST Array是Affymetrix在2011年前后推出的全转录本表达芯片,GEO平台编号就是GPL5175。它设计的核心思路,是把探针铺设在基因的多个外显子上,尤其偏重转录本层面的检测。芯片覆盖超过28,000个编码基因和数千个非编码RNA位点,表达矩阵中呈现的探针集(probe set / transcript cluster)数量在53,000个以上。
和早期U133时代的3'端表达芯片相比,Gene 2.0 ST的探针布局完全不同,这直接决定了你拿到的ID格式完全不同。U133系列拿到的探针ID如201041_s_at,是Affymetrix自己定义的探针集编号;而Gene 2.0 ST拿到的是TC01000001.hg.1这种格式,TC代表Transcript Cluster,后面的数字串是染色体区段内的编号,hg.1表示该转录本簇在人类参考基因组上的注释版本。
很多第一次接触这个平台的人,会误以为TC01000001.hg.1就是某种Ensembl转录本ID,去Ensembl官网一查,查不到,然后卡住。实际上这只是Affymetrix内部的探针集编号,你需要通过注释文件建立它和Gene Symbol之间的联系,这就是"探针转换"这个说法的真正含义。
1.2 探针ID的命名规则与"转录簇"的来龙去脉
要理解TC01000001.hg.1,先得理解转录簇这个概念。Affymetrix在注释时,会把基因组上相互重叠、共享剪接位点的转录本聚成一个簇,每个簇分配一个TC编号。这样设计的优势是,测到的信号不是来自单一位点,而是整个转录本簇内的多个探针信号综合,一定程度上能区分转录本异构体的表达趋势。
这些TC编号在GEO的series matrix文件中,通常出现在ID_REF列。当你用GEO2R做在线差异分析时,输出表格里也会出现ID和Gene symbol两列,看起来很美好;可一旦你把原始表达矩阵下载到本地,用read.table读进来一瞅,只有一堆TC开头的ID,这就是你需要自己做转换的时刻。
这里有个容易忽略的细节:GPL5175在GEO上的全称后缀是[transcript (gene) version],这个"gene version"指的是注释版本是把转录本簇映射到基因层面。Affymetrix官方的注释体系里,还有一套直接按探针物理位置注释的版本,两者的映射规则不完全一样。你做基因层面转换时,直接用GPL5175的注释即可,不要自己拿探针序列去比对,那样既慢又容易出错。
1.3 为什么表达矩阵里会出现一堆"---"
做GPL5175探针转换时,最让人崩溃的现象是:注释表拿到了,去匹配Gene Symbol,发现一列值全是---。这不是你的代码写错了,而是Affymetrix注释体系的一个坑。
在GEO平台的soft/annot文件中,很多探针集的Gene Symbol列,官方就没填。这些转录本簇可能是低置信度的预测转录本、尚未命名的非编码RNA、或者位于基因间区的新转录本。对于这些探针集,如果你强行保留,下游的富集分析会因为基因标识缺失而出错;如果直接删掉,又会损失一部分表达信号。常规做法是:保留ENTREZ_GENE_ID有值的探针集,用Entrez ID做转换,因为部分未被命名Symbol的转录本,实际上在Entrez数据库里是有条目的;只有ENTREZ_GENE_ID也为空时,才彻底过滤掉。
2. 探针转基因符号的三种主流方案,按场景选型
2.1 方案A:Bioconductor注释包,最快但依赖软件环境
Bioconductor为几乎所有主流Affymetrix芯片提供了注释包,GPL5175对应的包是hugene20sttranscriptcluster.db。这个包是基于SQLite的AnnotationDbi对象,装好之后一行mapIds就能把探针ID映射到Gene Symbol。
library(hugene20sttranscriptcluster.db) probes <- c("TC01000001.hg.1", "TC01000002.hg.1") mapIds(hugene20sttranscriptcluster.db, keys = probes, keytype = "PROBEID", column = "SYMBOL")这个方法速度快、代码短,而且能很方便地映射到Entrez ID、基因名、染色体位置等多个字段。但它的缺点也很明显:
- 如果本地R版本和Bioconductor版本不匹配,安装过程会踩不少坑,需要处理
BiocManager::install的一系列依赖问题; - 注释包的数据版本通常滞后于Affymetrix官方最新注释,可能比官方NetAffx文件晚一两个版本;
- 对于只需要做一次转换的人来说,专门装一个几百MB的包,环境开销偏大。
2.2 方案B:GEO平台注释表格,通用且可控
GEO的每个平台都有一份对应的注释表,GPL5175的注释表可以通过GEOquery直接下载,也可以从GEO网站手动下载GPL5175.annot.gz。下载后,Table(gpl)拿到的数据框里,ID列是探针ID,Gene Symbol列是基因符号,ENTREZ_GENE_ID列是Entrez编号。
library(GEOquery) gpl <- getGEO("GPL5175", destdir = ".") names(Table(gpl))这个方案的好处是不用装额外的注释包,GEOquery本身就足够,而且对任何GPL平台都适用——只要你把平台号换掉,代码逻辑完全一样。它适合写通用脚本,比如批量处理多个不同芯片平台的数据集时,这个方法最省心。
缺点是需要确保网络能通GEO服务器。GEOquery偶尔会出现下载失败或超时的问题,这时需要翻出浏览器手动下载文件,后面我会专门说离线兜底方案。
2.3 方案C:Affymetrix官方NetAffx注释文件,版本最全
如果你对注释版本有严格要求,比如论文审稿人要求注明所用注释版本,或者你需要比较不同版本注释对结果的影响,方案C最合适。去Affymetrix(Thermo Fisher)官网搜索HuGene-2_0-st,下载HuGene-2_0-st-v1.naXX.hg19.transcript.csv文件,里面包含了最完整的注释信息。
这个CSV文件很大,列众多,核心是probeset_id和gene_assignment两列。其中gene_assignment是官方用管道符串接的一整个字符串,包含了转录本ID、Gene Symbol、基因描述、Entrez ID、染色体位置等多重信息,格式类似:
ENST00000415118 // ZNF253 // zinc finger protein 253 // 5627 // chr19 // NM_001256615读入这个文件后,你需要自己拆分gene_assignment列,提取第二段作为Gene Symbol,第四段作为Entrez ID。这个方法灵活度最高,但预处理代码也最啰嗦。我一般只在对注释版本有严格要求的场景下才用它。
2.4 三种方案对比与我的选择逻辑
| 维度 | Bioconductor注释包 | GEO平台注释表 | NetAffx官方文件 |
|---|---|---|---|
| 代码量 | 最少 | 中等 | 最多 |
| 安装依赖 | 需处理BiocManager依赖 | 只需GEOquery | 无额外依赖 |
| 版本新鲜度 | 较旧 | 随GEO更新 | 最新最全 |
| 离线可用性 | 装好后可离线 | 需先联网下载 | 需先联网下载 |
| 通用性 | 每个平台一个包,换芯片需换包 | 换平台号即可 | 每个芯片单独下载 |
我个人的习惯是:单次分析任务,直接用方案B,下载GEO注释表,因为它是数据源同一体系的东西,和原始矩阵格式天然匹配,出问题的概率最低。方案A适合已经装了注释包、不想反复下载数据的人。方案C则在正式发文章前,当你需要核对注释版本时再补做一次校验。
3. 实操:从GEO矩阵到基因Symbol表达矩阵的完整流程
3.1 下载表达矩阵与注释表
这一步有两条路。一条是通过GEOquery拉取ExpressionSet对象,然后从对象里提取表达矩阵;另一条是直接下载GEO页面的series_matrix.txt.gz文件,用read.table读入。两种方式读取到的ID列不同:GEOquery的exprs()拿到的矩阵,行名就是探针ID;而用read.table读series_matrix文件时,第一列通常叫ID_REF。
我用GEOquery演示,因为它的容错性更好,还能顺手获取样本分组信息:
library(GEOquery) # 下载并读取GSE数据 gse <- getGEO("GSE36200", destdir = ".") expr_matrix <- exprs(gse[[1]]) # 查看矩阵结构 dim(expr_matrix) head(rownames(expr_matrix))接着下载GPL5175的注释表:
gpl <- getGEO("GPL5175", destdir = ".") gpl_table <- Table(gpl) head(gpl_table[, c("ID", "Gene Symbol", "ENTREZ_GENE_ID")])这里有个细节要提醒:getGEO("GSE36200")返回的是一个list,每个元素对应一个平台的数据集,所以要用gse[[1]]。如果你的GSE包含多个平台的数据,要确认gse[[1]]是不是GPL5175,可以通过annotation(gse[[1]])查看。
3.2 构建ID到Symbol的映射关系
拿到注释表后,第一步是做两件事:去掉Gene Symbol为空的探针,去掉---占位符。然后从原始表达矩阵中,只保留能够映射到Symbol的探针行。
# 提取映射关系 mapping <- gpl_table[, c("ID", "Gene Symbol")] mapping <- mapping[!is.na(mapping$`Gene Symbol`) & mapping$`Gene Symbol` != "---", ] # 将表达矩阵转为数据框,并加入映射 expr_df <- as.data.frame(expr_matrix) expr_df$probe_id <- rownames(expr_df) expr_df <- merge(expr_df, mapping, by.x = "probe_id", by.y = "ID") # 检查匹配率 nrow(expr_df) / nrow(expr_matrix)常见情况是,原始5万多个探针中,大约有2万到3万个能映射到明确的Gene Symbol。匹配率在50%到70%之间都算正常,因为芯片上本身就有相当比例的非编码或未注释转录本簇。
merge的时候要注意探针ID是否完全一致。有时候series_matrix里的ID列是"TC01000001.hg.1",而GPL5175注释表里的ID列多了个引号或空格,这会导致大量匹配失败。我建议merge之前先做一次环境清理:
mapping$ID <- trimws(mapping$ID) expr_df$probe_id <- trimws(expr_df$probe_id)3.3 多探针合并策略:取均值、中位数还是最大值
一个基因对应多个探针是Affymetrix芯片的常态。GPL5175虽然是按转录本簇设计的,但同一个Gene Symbol仍然可能对应多个TC。合并策略直接影响下游差异分析结果,这里需要讲清楚不同方法的适用性。
- 取均值:最推荐,因为它综合了多个探针的信号,能降低单个探针的测量噪声。对于常规差异表达分析,用均值足够稳健。
- 取中位数:比均值更抗离群值。如果某个基因的多个探针中,有一个探针信号明显异常高,均值会被拉高,中位数能保持稳定。适合做样品质量检查或聚类分析前的预处理。
- 取最大值:在某些生存分析场景下,人们倾向于认为只要任意一个探针检测到高表达,就代表该基因有生物学功能,这时候取最大值能保留信号。但常规转录组分析中不太建议,因为最大值放大了噪声。
- 取最小值:很少使用,除非你要严格控制假阳性,比如做免疫组化芯片的严格阈值过滤。
实操代码(取均值):
library(dplyr) expr_final <- expr_df %>% group_by(`Gene Symbol`) %>% summarise(across(where(is.numeric), mean)) %>% as.data.frame() rownames(expr_final) <- expr_final$`Gene Symbol` expr_final <- expr_final[, -1]如果你用tidyverse的across不熟练,也可以用基础R的aggregate,效果一样:
expr_final <- aggregate(. ~ `Gene Symbol`, data = expr_df, mean)3.4 可直接复用的完整R脚本
下面是我整理好的完整流程,已经经过多个数据集的验证,直接替换gse_id和gpl_id就能用:
library(GEOquery) library(dplyr) probe2symbol <- function(gse_id, gpl_id = "GPL5175") { # 下载表达矩阵 gse <- getGEO(gse_id, destdir = ".") expr_matrix <- exprs(gse[[1]]) if (annotation(gse[[1]]) != gpl_id) { message("平台不匹配,请检查数据") return(NULL) } # 下载平台注释 gpl <- getGEO(gpl_id, destdir = ".") gpl_table <- Table(gpl) mapping <- data.frame( probe_id = trimws(gpl_table$ID), symbol = gpl_table$`Gene Symbol`, entrez = gpl_table$ENTREZ_GENE_ID, stringsAsFactors = FALSE ) mapping <- mapping[!is.na(mapping$symbol) & mapping$symbol != "---", ] # 表达矩阵转数据框并合并 expr_df <- as.data.frame(expr_matrix) expr_df$probe_id <- trimws(rownames(expr_df)) expr_df <- merge(expr_df, mapping, by = "probe_id") # 去重,取均值 expr_final <- expr_df %>% group_by(symbol) %>% summarise(across(where(is.numeric), mean, na.rm = TRUE)) %>% as.data.frame() rownames(expr_final) <- expr_final$symbol expr_final$symbol <- NULL return(expr_final) } # 使用示例 result <- probe2symbol("GSE36200", "GPL5175")这个脚本把注释下载、匹配、合并全封装了,批量跑多个数据集时,一个for循环就能搞定。有一点提醒:如果exprs()里包含非数值列,summarise会报错,建议在表达矩阵读入后先做一次数据类型检查。
4. 我踩过的坑:探针匹配失败与注释信息丢失
4.1 探针ID格式对不上,问题出在空白字符和大小写
有一次我从GSE页面直接下载series_matrix.txt.gz,读入后检查探针ID,肉眼看好好的,和GPL5175注释表里的ID一模一样,但merge之后匹配率只有3%。排查到最后,发现series_matrix文件中的ID_REF列前面有一个不可见字符,是文件编码带来的零宽空格。这个坑非常隐蔽,你用identical()逐个比较的时候,肉眼根本察觉不到。
所以我现在拿到任何表达矩阵,第一件事是跑一遍:
summary(nchar(rownames(expr_matrix)))看行名的字符长度分布是不是稳定。正常情况下,GPL5175的探针ID长度应该是17个字符左右(TC+ 8位数字 +.hg.+ 1位)。如果发现个别ID长度异常,基本就是格式污染。解决方案是正则清理:
clean_id <- function(x) { x <- gsub("[^ -~]", "", x) # 去除非ASCII字符 x <- gsub("^\\s+|\\s+$", "", x) # 去首尾空格 x }4.2 一个基因对应多个探针时,平均值不一定是对的
我之前处理一批GPL5175数据时,遇到一个特殊情况:某个基因对应了两个探针,但这两个探针的表达模式高度不一致——一个在所有样本中几乎不表达,另一个则高表达。取均值后,这个基因的表达量被拉到了中间水平,导致后续差异分析把这个基因误判成了不显著。
这种情况多见于基因注释边界发生变化、或者一个探针实际结合了非特异性序列。排查方法是做一次探针级的相关性检查:
# 对同一基因的多个探针做相关性分析 dup_symbols <- names(table(expr_df$symbol)[table(expr_df$symbol) > 1]) check <- expr_df[expr_df$symbol %in% dup_symbols[1], 2:5] cor(t(check))如果发现探针之间的相关性极低,建议人工查看这两个探针在注释表中的具体信息,必要时用median代替mean。批量操作时不用逐个检查,但至少要有这个意识。
4.3 GPL5175和GPL6244容易搞混,平台号千万别抄错
Affymetrix有多个Gene ST系列芯片,GPL5175是Human Gene 2.0 ST,GPL6244是Human Gene 1.0 ST。两个平台看似接近,但探针ID体系和基因覆盖差异很大。1.0 ST的探针ID也是TC开头,但后缀通常不同,而且覆盖的转录本数量少不少。
更麻烦的是,有些GEO数据集的系列矩阵文件,文件名标注的是GPL5175,但内部的实际注释版本是[probe version]而非[transcript (gene) version]。不算常见,但我确实碰到过一次。所以下载注释后要检查gpl_table的列结构,确认Gene Symbol列存在且非空。严谨起见,我会先跑一句:
table(is.na(gpl_table$`Gene Symbol`))如果Gene Symbol列有大量NA,就要怀疑是否下载错了文件版本。
4.4 GEOquery下载注释失败时的离线兜底方案
GEOquery连不上GEO服务器很常见,尤其是批量下载时容易被限流。这时候不要干等,直接用浏览器打开:
https://ftp.ncbi.nlm.nih.gov/geo/platforms/GPL5nnn/GPL5175/annot/GPL5175.annot.gz把.annot.gz文件下载到本地,然后用getGEO(filename = "GPL5175.annot.gz")读取:
gpl <- getGEO(filename = "GPL5175.annot.gz")注意,从本地读取时,getGEO会自动识别GPL注释文件的格式。如果你下载的是soft格式而不是annot格式,读取后Table()的结构会稍有不同,Gene Symbol列名可能变成Gene_symbol,建议读入后先colnames()查看一遍。
另一个兜底思路是,部分GSE页面有作者自己上传的注释后表达矩阵,通常以GSEXXXXXX_processed_data.txt.gz或GSEXXXXXX_non-normalized.txt.gz形式放在页面底部的Supplementary files里。下载后直接查看表头,如果已经包含Gene Symbol,就不用再做探针转换了。但作者注释可能使用非官方符号,建议只把它当参照,不要直接当最终结果。
5. 批量处理多个数据集时的效率技巧
5.1 封装核心函数,避免重复下载注释
如果你要同时处理十几个GSE数据,最忌讳的做法是每个数据集都调用一次getGEO("GPL5175")。GPL5175的注释表有几十MB,重复下载不仅慢,还容易被限流。
正确思路是:第一次下载后,把注释表保存成本地RData或CSV文件,后续直接用readRDS加载。
# 第一次运行时保存 gpl <- getGEO("GPL5175", destdir = ".") saveRDS(Table(gpl), "GPL5175_annotation.rds") # 后续运行时加载 gpl_table <- readRDS("GPL5175_annotation.rds") mapping <- gpl_table[, c("ID", "Gene Symbol", "ENTREZ_GENE_ID")]这一步能省掉最耗时的网络I/O。我实测过,直接下载注释表耗时10到30分钟不等(取决于网络状况),读本地RDS不到1秒。
5.2 批量循环时要记录每个数据集的匹配率
批量处理时,不要只顾着输出结果,一定要保存一份处理日志,记录每个GSE的探针总数、匹配成功数、最终基因数。原因很实际:不同批次的GPL5175数据,有的作者上传时已经过滤过一部分探针,有的包含全部探针,匹配率差异很大。如果直接把多个数据集的结果合并,平台注释批次差异可能被当成生物学差异,下游分析就失真了。
一个简单的日志方案:
gse_ids <- c("GSE36200", "GSE46261", "GSE5281") log_info <- data.frame() for (gse_id in gse_ids) { res <- probe2symbol(gse_id, "GPL5175") log_info <- rbind(log_info, data.frame( gse = gse_id, n_probe = nrow(expr_matrix), n_symbol = nrow(res), match_rate = nrow(res) / nrow(expr_matrix) )) } write.csv(log_info, "conversion_log.csv", row.names = FALSE)合并多个数据集的表达矩阵之前,先按intersect(rownames(res1), rownames(res2))取基因交集,这样可以进一步减少平台注释差异带来的批次效应。
5.3 不用写代码的临时替代方法
如果没有编程基础,临时只想看一个数据集,也可以用GEO2R的在线分析结果——它输出的表格里已经包含了Gene symbol列。但这个方案有两个限制:GEO2R一次只能分析一个数据集,而且它只给差异分析结果,不给标准化后的全表达矩阵。所以只要你后续要做自己定义的分组比较、生存分析或机器学习特征筛选,还是得回到本地自己转换探针。
另一个替代思路是使用UCSC Xena或GEPIA这类在线工具,它们已经把GPL5175的表达数据预处理成了基因Symbol矩阵。比如Xena上有TCGA和GTEx的数据,但GEO里的普通GSE数据不一定收录。适用场景有限,本地转换始终是最通用的手段。
探针转换这件事,说到底是"注释体系的翻译"——芯片厂商用自己的编号体系组织探针,而生物学分析需要公众通用的基因命名体系。GPL5175的设计已经相对友好,因为TC编号本身带有一定的基因组位置信息,比早期U133的_s_at后缀好理解得多。但你真正露一手的地方,是如何把格式污染、注释缺失、多探针合并这些细节处理干净,这决定了你下游的分析结果是否经得起推敲。按照上面这套流程,无论你是处理一个数据集还是几十个数据集,都能稳妥地拿到一份干干净净的基因Symbol表达矩阵。