GEO 数据挖掘 / R 与 Bioconductor

GEO 数据挖掘:下载、探针注释、limma 差异分析与常见坑

这页写给做 GEO 数据挖掘论文的医学研究生。全部代码在一个公开胰腺癌芯片数据集 GSE16515(52 个样本,GPL570)上跑通,每一步都给出实测数字,包括常见错误做法会让结果偏多少。下载接口和 TCGA 的状态于 2026-10-11 核实。

直接答案

芯片数据用 GEOquery 下载 Series Matrix(getGEO 加 destdir 和 getGPL = FALSE),先按 GEO2R 规则判断是否需要 log2(99% 分位数大于 100 就转换),再用注释包或 GPL 表把探针转成基因、每个基因保留一个探针,从 pData 的 characteristics 列提取分组并核对样本顺序,最后用 limma(设计矩阵加对比矩阵,eBayes)做差异分析,常用阈值为 adj.P < 0.05 且 |logFC| > 1。RNA-seq 数据用原始 counts 做 DESeq2、edgeR 或 limma-voom。合并多个 GSE 时,把批次放进 limma 的设计矩阵;removeBatchEffect 和 ComBat 的输出只用于作图。

先选对数据

GSE 是一个研究(Series),GSM 是其中一个样本,GPL 是芯片或测序平台,GDS 是 GEO 早年人工整理的数据集。做差异分析时,下载单位是 GSE,平台信息看 GPL。

NCBI 生成的 counts 的处理流程:HISAT2 比对到 GRCh38(GCA_000001405.15),featureCounts 计数,基因注释为 Annotation Release 109.20190905;比对率低于 50% 的 run 和单细胞数据不计数,同一 GSM 有多个 run 时 counts 相加。GEO2R 对 RNA-seq 用的就是这份 counts 和 DESeq2。

物种范围:官方页面(2026-07-08 修订)仍写小鼠数据“预计 2025 年提供”。本页 2026-10-11 用 E-utilities 检索 "rnaseq counts"[Filter],只含小鼠样本的 Series 为 0 条,带这一标记的记录约 2.77 万条,均为人类。

GDS 的现状:用 E-utilities 按年份统计,2014 年新增 279 个,2015 年 64 个,2016 年 2 个,2018 年起为 0。照旧教程用 getGEO("GDSxxxx") 只能拿到老数据。

文件内容什么时候用注意
Series Matrix(GSE…_series_matrix.txt.gz)提交者处理后的表达矩阵和样本信息芯片数据的默认选择,GEO2R 也用它值的尺度由提交者决定,先判断是否 log2;多平台 GSE 有多个文件
原始 CEL 文件(GSE…_RAW.tar)Affymetrix 扫描原始信号Series Matrix 值异常、缺失,或需要统一用 RMA 处理多个数据集时用 oligo 或 affy 包做 RMA;文件大,下载慢
NCBI 生成的 RNA-seq raw countsNCBI 用统一流程比对计数的基因 × 样本整数矩阵人类 bulk RNA-seq,提交者没有给 counts 或给的是 FPKM目前只有人类;可能与原文结果不一致;附带 FPKM、TPM 不能做差异分析
提交者补充文件(suppl)counts、FPKM、TPM 或其他格式需要和原文完全一致时格式不统一,读入前先看列名和数值是否为整数
GDS人工整理并分好组的数据集复现 2015 年前的老数据2016 年后基本不再新建,新数据没有 GDS

下载

下面的代码在本机首次下载 GSE16515 的 Series Matrix(6.4 MB)用时 8.3 秒。getGPL = TRUE 时会再下载 85.6 MB 的 GPL570 注释,网络差时多数失败出在这一步。

国内常用的镜像是生信技能树 AnnoProbe 包的 geoChina(),它从腾讯云服务器下载提交者处理好的 ExpressionSet(.Rdata)。本页 2026-10-11 测试该服务器仍可访问;它的 GSE 列表写在包内,仓库最后更新于 2022 年 11 月,之后公开的数据集不在镜像中。

GEOquery 版本差异:本页实测用 GEOquery 2.74.0(Bioconductor 3.20)。GitHub 上的开发版(2.81.x)计划把 getGEO 的返回类型从 ExpressionSet 改为 SummarizedExperiment,届时 exprs()、pData() 的写法要换成 assay()、colData()。

下载 Series Matrix 并缓存到固定目录r
library(GEOquery)
options(timeout = 600)                        # 默认 60 秒,大文件会中途断开
Sys.setenv(VROOM_CONNECTION_SIZE = 500000L)   # 避免 connection buffer 报错
dir.create("geo_cache", showWarnings = FALSE)

# destdir 固定缓存目录:默认存在 tempdir(),关掉 R 就没了,下次重新下载
gse <- getGEO("GSE16515", destdir = "geo_cache",
              GSEMatrix = TRUE, getGPL = FALSE)  # 不下载 85.6 MB 的 GPL570 注释
length(gse)        # 多平台 Series 返回多个 ExpressionSet,逐个检查
eset <- gse[[1]]
dim(eset)          # 54613 探针 x 52 样本
annotation(eset)   # "GPL570"
手动下载后本地读取(断点续传)bash
# FTP 路径规则:GSE 编号去掉后三位换成 nnn(GSE16515 -> GSE16nnn;GSE1000 以下为 GSEnnn)
# -c 支持断点续传,网络中断后重跑同一命令即可
wget -c https://ftp.ncbi.nlm.nih.gov/geo/series/GSE16nnn/GSE16515/matrix/GSE16515_series_matrix.txt.gz
# 平台注释(GPL570 小于 1000,目录为 GPLnnn)
wget -c https://ftp.ncbi.nlm.nih.gov/geo/platforms/GPLnnn/GPL570/annot/GPL570.annot.gz
# 原始 CEL 文件
wget -c https://ftp.ncbi.nlm.nih.gov/geo/series/GSE16nnn/GSE16515/suppl/GSE16515_RAW.tar

# R 中读本地文件,不再联网
Rscript -e 'library(GEOquery); e <- getGEO(filename = "GSE16515_series_matrix.txt.gz", getGPL = FALSE); print(dim(e))'
现象或报错原因处理
The downloaded content looks like an HTML page, not GEO data2026 年 8 月底起 NCBI 对 /geo/ 下的脚本请求返回 reCAPTCHA 验证页(HTTP 200),GEOquery 当作数据解析2026-09-02 起 NCBI 已对 SOFT 文本和带 acc 的文件下载链接开放;仍报错时用 getGPL = FALSE,或从 FTP 下载后用 filename 读取。FTP 不受影响
The size of the connection buffer (131072) was not large enoughvroom 读取超长行时默认缓冲区太小Sys.setenv(VROOM_CONNECTION_SIZE = 500000L),不够再加大
Timeout of 60 seconds was reached / 文件不完整R 默认下载超时 60 秒options(timeout = 600);或用 wget -c 断点续传
每次重开 R 都重新下载默认缓存目录是 tempdir()getGEO 加 destdir;之后显示 Using locally cached version 即为读缓存
无法与服务器建立连接下载方式与网络环境不兼容options('download.file.method.GEOquery' = 'libcurl') 后重试;仍失败就手动下载

预处理

limma 要求输入是 log 尺度。Series Matrix 里的值可能已经 log2,也可能是线性值,样本描述并不可靠:GSE16515 的 data_processing 写的是 GC-RMA(通常输出 log2),矩阵里的实际值却是线性尺度,最大值 65045。

GEO2R 的自动判断只看被分组的样本,对整个矩阵全做或全不做 log2。经验帖里常见的 log2(dat + 1) 写在模板里无条件执行,对已经 log2 的数据会把倍数压缩到几乎为 0。

数据集内含负值或大量 0 时(常见于 MAS5 减背景或 Illumina 未取对数的数据),直接 log2 会产生 NaN,判断后再决定是否换用原始文件重新处理。

GEO2R 的 log2 判断规则与分布检查r
library(limma)
ex <- exprs(eset)
# GEO2R 的判断规则
qx <- as.numeric(quantile(ex, c(0, .25, .5, .75, .99, 1), na.rm = TRUE))
LogC <- (qx[5] > 100) || (qx[6] - qx[1] > 50 && qx[2] > 0)
qx; LogC
# GSE16515: 2.28 7.11 18.17 86.87 3283.25 65045 -> TRUE
if (LogC) { ex[ex <= 0] <- NaN; ex <- log2(ex) }

# 看分布:各样本箱线图中位数应大致对齐
boxplot(ex, las = 2, outline = FALSE)
# 明显不齐时再做分位数归一化(GEO2R 的 Force normalization 也是这一步)
# ex <- normalizeBetweenArrays(ex, method = "quantile")
做法GSE16515 / GSE15471 实测结果(|logFC| > 1,adj.P < 0.05)
线性值先 log2 再 limma(正确)GSE16515:1136 个上调,399 个下调
线性值直接 limmaGSE16515:2804 个上调,3868 个下调,|logFC| 中位数 4.51
已是 log2 的数据再做 log2(x+1)GSE15471:0 个(正确处理时 2306 个上调,348 个下调)

注释

芯片平台对应的 Bioconductor 注释包比 GPL 表更新。在 GPL570 上实测,两者对 41039 个单基因探针的注释有 3874 个不一致,多数是基因改名,例如 FAM122C 现名 PABIR3,CCDC11 现名 CFAP53。

探针转基因:去掉多基因探针,每个基因保留一个探针r
library(hgu133plus2.db)   # GPL570 对应的注释包;其他平台见下表
pk <- AnnotationDbi::select(hgu133plus2.db, keys = rownames(ex),
                            columns = "SYMBOL", keytype = "PROBEID")
multi <- names(which(table(pk$PROBEID) > 1))          # 一个探针对应多个基因:1528 个
pk <- pk[!is.na(pk$SYMBOL) & !pk$PROBEID %in% multi, ] # 去掉无注释和多基因探针
ex <- ex[pk$PROBEID, ]; sym <- pk$SYMBOL               # 剩 43112 个探针、20826 个基因

# 一个基因多个探针:保留平均表达最高的探针(11041 个基因有多个探针)
o <- order(sym, -rowMeans(ex))
keep <- o[!duplicated(sym[o])]
ex_gene <- ex[keep, ]; rownames(ex_gene) <- sym[keep]
# 另一种是取平均:ex_gene <- avereps(ex, ID = sym)

# 没有注释包的平台用 GPL 表(getGEO("GPLxxx") 或 FTP 上的 .annot.gz)
# gpl  <- getGEO("GPL570", destdir = "geo_cache")
# anno <- Table(gpl)[, c("ID", "Gene Symbol")]
# anno <- anno[anno$`Gene Symbol` != "" & !grepl("///", anno$`Gene Symbol`), ]

多探针处理方式会改变差异基因数

本页在 GSE16515 上用同一阈值比较:保留平均表达最高的探针得到 1136 个上调、399 个下调;avereps 取平均得到 834 个上调、294 个下调;保留四分位距最大的探针得到 1237 个上调、431 个下调。有 450 个基因只在最大均值法中显著,43 个只在平均法中显著,差别主要来自同一基因中低表达探针把均值拉低。方法部分要写明用了哪一种。

推荐做法

保留平均表达最高的探针(AnnoProbe 的 filterEM 用中位数最高,效果相近)。平均法会混入不表达或不特异的探针,使差异变小。探针水平先做 limma 再合并到基因的做法会得到 1771 个基因,比直接按基因分析多,因为同一基因的任一探针显著就会计入。

一个探针对应多个基因

hgu133plus2.db 中有 1528 个这样的探针,GPL 表中含 " /// " 的有 2796 个。经验帖常保留第一个基因名,AnnoProbe 源码也是保留一个并在注释中承认这样处理有问题。差异分析时去掉这些探针更稳妥。

注释前后各数一次

54613 个探针去掉无注释和多基因探针后剩 43112 个,对应 20826 个基因,其中 11041 个基因有多个探针,最多一个基因有 15 个探针。数字和这个数量级差得多时,通常是平台选错或矩阵行名不是探针号。

平台注释包备注
GPL570 HG-U133 Plus 2.0hgu133plus2.db54675 个探针,最常见
GPL96 HG-U133Ahgu133a.db22283 个探针
GPL571 HG-U133A_2hgu133a2.db
GPL6244 HuGene 1.0 SThugene10sttranscriptcluster.db按 transcript cluster 注释
GPL6947 / GPL10558 Illumina HumanHT-12illuminaHumanv3.db / illuminaHumanv4.db矩阵行名是 ILMN_ 探针号
Agilent 等无注释包的平台GPL 表或 FTP 上的 .annot.gzGene Symbol 列可能含 " /// ",表示一个探针对应多个基因

分组

getGEO 返回的 ExpressionSet 中,pData 行顺序与表达矩阵列顺序一致。错误出现在把 pData 排序、和外部临床表合并或手写分组向量之后,R 不会报错,结果却会完全改变。

样本数和原文对不上时,先查 GSE 是否包含多个平台(length(gse) 大于 1)、是否混有细胞系或技术重复。GSE15471 的 78 个样本对应 36 位患者:标题带 _rep 的 6 个样本是 3 位患者正常和肿瘤组织的重复芯片。把重复芯片当独立样本会高估样本量,可用 avereps 按患者和组织合并,或用 duplicateCorrelation 把患者作为 block。

提取分组并做三道核对r
pd <- pData(eset)
grep(":ch1$", colnames(pd), value = TRUE)   # 分组通常在 xxx:ch1 列
table(pd[["tissue:ch1"]])
# Normal Tissue in Pancreatic Cancer Sample 16 / Tumor Tissue ... 36

group <- factor(ifelse(grepl("Tumor", pd[["tissue:ch1"]]), "Tumor", "Normal"),
                levels = c("Normal", "Tumor"))   # 第一个水平是对照
# 三道核对:样本名一致、交叉表无错配、组内样本数与原文一致
stopifnot(identical(rownames(pd), colnames(ex_gene)))
table(group, pd[["tissue:ch1"]])
patient <- sub("^Pancreatic Sample ?([0-9]*)-.*$", "\\1", pd$title)  # 配对信息,后面用
错误实测后果
按排过序的 pData 生成分组向量,再和原矩阵一起分析GSE16515 中 52 个样本有 18 个标签错位,差异基因从 1535 个变为 0 个
用 source_name 或 title 判断分组GSE15471 的 source_name 全是 pancreas,按它判断会把 78 个样本都归为肿瘤;分组实际在 sample:ch1 列
factor 水平按字母排序Normal 与 Tumor 恰好正确;换成 control 与 case 时 case 排在前面,logFC 方向相反
照搬 2020 年前的 GEO2R 结果GEO2R 自 2020 年 11 月起把先定义的组当作 test 组,之前相反,旧教程截图中的 logFC 符号可能与现在相反

差异分析

两组比较有两种等价写法:~0 + group 加 makeContrasts,或 ~group 后取 coef = 2。本页在 GSE15471 上验证两者 logFC 最大差 8.8 × 10⁻¹⁵,选一种自己读得懂的即可。多组比较时用 ~0 + group,每个对比写清楚。

阈值的依据:adj.P 用 BH 法控制假发现率,0.05 是 GEO2R 的默认值。|logFC| > 1 是惯例,没有统计学依据;limma 的 topTable 帮助文档写明,P 值和倍数相关性不高时先按 P 后按倍数筛选会使假发现率超过名义水平,需要倍数阈值时推荐 treat。论文里用惯例阈值时,写明阈值并在补充材料给出全部基因的结果表。

差异基因数受样本量影响很大:同一组织类型,GSE15471(78 个样本)得到 2306 个上调基因,GSE16515(52 个样本)得到 1136 个。上下调基因数不对称时,热图和富集要分别处理上调与下调。

limma 完整代码:两组比较、treat 与配对设计r
design <- model.matrix(~ 0 + group)
colnames(design) <- levels(group)                       # Normal, Tumor
contr <- makeContrasts(Tumor - Normal, levels = design) # logFC > 0 表示肿瘤中上调
fit  <- lmFit(ex_gene, design)
fit2 <- eBayes(contrasts.fit(fit, contr), trend = TRUE)
tt   <- topTable(fit2, number = Inf)                    # 默认 BH 校正
deg  <- subset(tt, adj.P.Val < 0.05 & abs(logFC) > 1)
table(sign(deg$logFC))                                  # 本页实测:-1 399,1 1136

# 把倍数阈值纳入检验(limma 推荐的做法)
tr <- topTreat(treat(contrasts.fit(fit, contr), lfc = 1, trend = TRUE), number = Inf)
sum(tr$adj.P.Val < 0.05)                                # 本页实测 166

# 配对样本:全部配对时把 patient 放进设计矩阵
# design <- model.matrix(~ patient + group)
# 部分配对(GSE16515 有 16 对加 20 个单独肿瘤):
corfit <- duplicateCorrelation(ex_gene, design, block = patient)
fitb <- lmFit(ex_gene, design, block = patient, correlation = corfit$consensus)
ttb  <- topTable(eBayes(contrasts.fit(fitb, contr), trend = TRUE), number = Inf)
阈值或设置(GSE16515)上调下调说明
adj.P < 0.05 且 |logFC| > 11136399文献中最常用
adj.P < 0.05 且 |logFC| > 0.585(1.5 倍)25651001样本少、效应小时常用
只用 adj.P < 0.0543395992样本量大时几乎所有基因都显著
treat(lfc = 1),adj.P < 0.05共 166检验倍数是否显著大于 2 倍
eBayes(trend = FALSE),阈值同第一行1134397芯片数据 trend 影响很小
部分配对 duplicateCorrelation,阈值同第一行1134403组内相关 0.136,16 对样本

RNA-seq

RNA-seq 的差异分析需要原始整数 counts。GEO 上只有 FPKM 或 TPM 时,先查 NCBI 是否生成了 raw counts;没有时只能从 SRA 下载 fastq 重新定量。

本页实测(2026-10-11):GEOquery 2.74.0 的 getRNASeqData("GSE164073") 报错 invalid 'row.names' length。原因是人类注释文件 Human.GRCh38.p13.annot.tsv.gz 的下载链接不带 acc 参数,仍被 reCAPTCHA 拦截,GEOquery 读到的是验证页 HTML。raw counts 矩阵本身能下载,用 org.Hs.eg.db 按 Entrez ID 注释即可,39376 个 GeneID 中有 37692 个映射到基因符号。

这份数据有 18 个样本(3 种眼表组织 × 感染/对照 × 3 个重复),设计矩阵 ~ tissue + infection。filterByExpr 后保留 16933 个基因,voom 得到 54 个上调、118 个下调基因(adj.P < 0.05,|logFC| > 1),用时 2.4 秒。

读 NCBI raw counts,用 limma-voom 做差异分析(GSE164073,本页实测)r
library(GEOquery); library(edgeR); library(limma); library(org.Hs.eg.db)
acc <- "GSE164073"
hasRNASeqQuantifications(acc)   # TRUE 表示有 NCBI 生成的 counts
# 直接下载 raw counts(这个链接格式在 2026-09-02 后不被 reCAPTCHA 拦截)
url <- paste0("https://www.ncbi.nlm.nih.gov/geo/download/?type=rnaseq_counts&acc=", acc,
              "&format=file&file=", acc, "_raw_counts_GRCh38.p13_NCBI.tsv.gz")
f <- file.path("geo_cache", paste0(acc, "_raw_counts.tsv.gz"))
if (!file.exists(f)) download.file(url, f, mode = "wb")
cnt <- as.matrix(read.delim(f, row.names = 1, check.names = FALSE))  # 行名是 Entrez Gene ID
sym <- mapIds(org.Hs.eg.db, rownames(cnt), "SYMBOL", "ENTREZID")     # 代替被拦截的注释文件

pd  <- pData(getGEO(acc, destdir = "geo_cache", getGPL = FALSE)[[1]])[colnames(cnt), ]
inf <- factor(ifelse(grepl("SARS", pd[["infection:ch1"]]), "CoV2", "mock"), levels = c("mock", "CoV2"))
tis <- factor(pd[["tissue:ch1"]])
design <- model.matrix(~ tis + inf)

y <- DGEList(cnt)
keep <- filterByExpr(y, design = design)          # 39376 -> 16933 个基因
y <- normLibSizes(y[keep, , keep.lib.sizes = FALSE])
v <- voom(y, design)
tt <- topTable(eBayes(lmFit(v, design)), coef = "infCoV2", number = Inf)
tt$SYMBOL <- sym[rownames(tt)]
数据方法说明
芯片(log2 强度)limmalmFit + eBayes
RNA-seq counts,每组 3–10 个样本DESeq2、edgeR 或 limma-voom三者结果高度重叠;GEO2R 用 DESeq2
RNA-seq counts,测序深度差异大或有样本权重需求limma-voom(voomWithQualityWeights)运行快,设计矩阵写法与芯片一致
RNA-seq counts,每组几十个以上的人群样本Wilcoxon 秩和检验(edgeR 标准化后)Li 等 2022 年报告 DESeq2、edgeR 在这类数据中假阳性偏多
只有 FPKM、TPM不做计数模型的差异分析log2(TPM + 1) 后用 limma-trend 只能作为探索;NCBI 页面也说明 FPKM、TPM 不适合跨样本定量比较

合并数据集

合并前先确认平台相同、各自已在 log2 尺度。批次效应通常比生物学差异大:GSE16515 与 GSE15471(均为 GPL570)合并后,PCA 第一主成分解释 48.9% 的方差,主要按数据集分开。

limma 的 removeBatchEffect 帮助文档写明,这个函数用于作图和数据探索,不用于给 lmFit 准备数据;需要线性模型时把批次放进模型,lmFit 才能正确估计标准误。先校正再分析会把残差自由度算多,P 值偏小。

ComBat 不带 mod 时,会把与批次重叠的分组差异一起去掉,本页平衡设计下少了 86 个上调基因。Nygaard 等 2016 年指出,批次和分组不平衡时,带 mod 的 ComBat 会让下游检验过于自信,假阳性增加。本页的不平衡场景差异不大,置换分组标签 5 次,ComBat 后得到 0 到 1 个假阳性基因。

一个数据集只有肿瘤、另一个只有正常时,批次与分组完全混杂,任何方法都无法分开两者。此时组间差异全部来自数据集差异,这类合并分析不应做;审稿人看到 table(batch, group) 就能发现。

批次的正确用法:差异分析与作图分开r
library(sva)
# m:两个同平台 GSE 合并后的基因矩阵(各自已确认 log2);batch、group 为因子
table(batch, group)          # 先看设计:每个批次是否都有两组

# 差异分析:batch 放进设计矩阵
design <- model.matrix(~ group + batch)
fit <- eBayes(lmFit(m, design), trend = TRUE)
tt  <- topTable(fit, coef = "groupTumor", number = Inf)

# 画 PCA、热图用的校正矩阵(不再拿去做 limma)
m_plot <- removeBatchEffect(m, batch = batch, design = model.matrix(~ group))
# 一定要用 ComBat 时,带上 mod 保护分组效应
m_cb <- ComBat(m, batch = batch, mod = model.matrix(~ group))
做法平衡设计:两个数据集都有正常和肿瘤不平衡设计:GSE15471 只保留肿瘤
不校正1174 / 6213441 / 296
batch 放进设计矩阵(推荐)1149 / 2351117 / 404
ComBat(mod = ~group) 后 limma1144 / 2321108 / 417
removeBatchEffect 后 limma1149 / 236未测
ComBat 不带 mod 后 limma1063 / 220未测
本页实测,数字为上调 / 下调基因数(adj.P < 0.05,|logFC| > 1)。

作图

火山图的纵轴和筛选用同一个 P 值(本页都用 adj.P)。热图的样本按分组排列,基因按行标准化。

火山图与热图(GSE16515,作图用时 1.9 秒)r
library(ggplot2); library(ggrepel); library(pheatmap)
v <- tt; v$gene <- rownames(v)
v$change <- with(v, ifelse(adj.P.Val < 0.05 & logFC > 1, "Up",
                    ifelse(adj.P.Val < 0.05 & logFC < -1, "Down", "NS")))
lab <- head(v[v$change != "NS", ][order(v$adj.P.Val[v$change != "NS"]), ], 10)
p <- ggplot(v, aes(logFC, -log10(adj.P.Val), colour = change)) +
  geom_point(size = 0.8, alpha = 0.6) +
  scale_colour_manual(values = c(Down = "#2b6cb0", NS = "grey75", Up = "#c53030")) +
  geom_vline(xintercept = c(-1, 1), linetype = 2) +
  geom_hline(yintercept = -log10(0.05), linetype = 2) +   # 纵轴与筛选用同一个 P
  geom_text_repel(data = lab, aes(label = gene), size = 3, colour = "black") +
  labs(x = "log2 fold change (Tumor vs Normal)", y = "-log10 adjusted P") + theme_bw()
ggsave("volcano.png", p, width = 6, height = 5, dpi = 300)

# 热图:上调、下调各取 adj.P 最小的 25 个
up <- head(rownames(v)[v$change == "Up"][order(v$adj.P.Val[v$change == "Up"])], 25)
dn <- head(rownames(v)[v$change == "Down"][order(v$adj.P.Val[v$change == "Down"])], 25)
o  <- order(group)
ann <- data.frame(group = group[o], row.names = colnames(ex_gene)[o])
pheatmap(ex_gene[c(up, dn), o], scale = "row", cluster_cols = FALSE,
         annotation_col = ann, show_colnames = FALSE, fontsize_row = 6,
         breaks = seq(-2, 2, length.out = 101),   # 截断 z 值,少数极端值不压缩色阶
         filename = "heatmap.png", width = 7, height = 8)

热图的基因要上下调分开取

本页按 adj.P 取前 50 个差异基因画热图,50 个全是上调基因,因为这个数据中上调基因多、显著性也更高。上调、下调各取 25 个,图才能展示两个方向。

截断 z 值

scale = "row" 后,个别样本的 z 值可达 4 以上,会把色阶压缩,大部分格子颜色接近。breaks 设为 -2 到 2,超出部分按最深色显示。

火山图标注基因

只标 adj.P 最小的 10 个,用 ggrepel 避免重叠。本页前几位是 LAMB3、SLC2A1、TMPRSS4、MET、S100P,与胰腺癌已知上调基因一致,可作为结果合理性的初步核对。

列聚类

cluster_cols = TRUE 时,用差异基因画的热图几乎总能把两组分开,这不能作为分组可靠的证据。说明组间差异时用全部基因的 PCA。

TCGA 对照

GEO 是各实验室提交的分散研究,平台和处理方式各不相同;TCGA 是统一流程处理的癌症队列,带完整临床和生存信息,但正常对照很少。

GDC 的变化:Data Release 32(2022-03-29)起删除 HTSeq 计数,只保留 STAR - Counts;Legacy Archive 于 2023-05-04 退役,legacy = TRUE 和 hg19 的旧代码都不能再用。2026-10-11 查询 GDC API,当前为 Data Release 46.0(2026-08-10)。

TCGAbiolinks 的已知问题(GitHub issues,2025–2026 年仍未关闭):2.37.x 和 2.38.0 的 GDCprepare 在部分项目报 Column `disease_response` doesn't exist,用户反馈 2.34 版正常;GDCquery_clinic 在 2025 年初因 GDC 接口变化失效,维护者建议安装 GitHub devel 分支;GDCdownload 偶发 Error in if (ret == 1) break(issue #656 未解决),可改用 method = "client" 调用 GDC Data Transfer Tool 重试。下载失败时,UCSC Xena 提供整理好的 GDC TCGA 表达和临床矩阵,可作替代。

TCGA 正常样本少,常见做法是合并 GTEx 正常组织。两者测序和处理流程不同,合并后批次与分组完全混杂,原理同上一节;这类比较的结论要用 GEO 数据集或实验再验证。

TCGAbiolinks 当前写法(未在本页运行,GDC API 查询已核对)r
library(TCGAbiolinks)
q <- GDCquery(project = "TCGA-PAAD",
              data.category = "Transcriptome Profiling",
              data.type = "Gene Expression Quantification",   # 不写这一行会多出一倍文件
              workflow.type = "STAR - Counts")                # HTSeq - Counts 已不存在
GDCdownload(q, files.per.chunk = 20)   # 分块下载,失败后重跑会跳过已下载的文件
se  <- GDCprepare(q)
cnt <- SummarizedExperiment::assay(se, "unstranded")         # 做 DESeq2/edgeR 用这一层
table(se$sample_type)                  # TCGA-PAAD:Primary Tumor 178,Solid Tissue Normal 4
GEOTCGA(GDC)
数据来源数千个独立研究,芯片与测序并存33 种癌症,统一测序和处理
正常对照很多数据集有配对正常组织少:TCGA-PAAD 有 178 个原发肿瘤,正常组织 4 个
临床信息取决于提交者,常只有分组生存、分期、治疗等较完整
表达数据Series Matrix、CEL、countsSTAR - Counts(unstranded、TPM、FPKM 等多列)
常见用途发现差异基因、外部验证预后模型、生存分析、和 GEO 互相验证

审稿

GEO 数据挖掘论文被退回,多数原因集中在下面几条。能在投稿前补上的尽量补上。

质疑应对
样本量小写明每组样本数;用 limma 的经验贝叶斯方法(小样本下比 t 检验稳定);合并同平台数据集并在设计矩阵中加入批次;结论限定为候选基因
没有验证集另找一个独立 GSE 单独分析,报告差异基因重叠和方向一致性。本页 GSE16515 与 GSE15471 分别得到 1507 和 1534 个差异基因,重叠 787 个,方向全部一致
平台不同不能合并各数据集分别做差异分析再取交集,或用 RobustRankAggreg 做排序整合;同平台才合并矩阵
批次效应处理不清楚给出校正前后按批次着色的 PCA 图;写明批次放在设计矩阵中,removeBatchEffect 只用于作图
阈值随意写明 adj.P 校正方法和 |logFC| 阈值;补充 treat 结果或敏感性分析
只有生信分析关键基因用 qPCR、免疫组化或 TCGA、HPA 数据验证;预后模型做外部队列验证
结果无法复现提供 GSE 编号、平台、R 与包版本、完整脚本和全部基因的结果表

国内经验

下面几条来自 CSDN、简书和生信技能树的经验帖,都有报错原文、源码或本页复现支持。

connection buffer 报错

CSDN 帖子贴出 getGEO 读取 GSE94994 时的报错 The size of the connection buffer (131072) was not large enough,设置 Sys.setenv("VROOM_CONNECTION_SIZE" = 99999999) 后读取成功。报错原文中的 Using locally cached version 路径在 Rtmp 临时目录,说明作者没有设 destdir。

geoChina 镜像

生信技能树 AnnoProbe 包的 geoChina() 下载提交者处理好的 ExpressionSet,等价于 getGEO(gse, getGPL = FALSE)。本页测试镜像仍可访问;2022 年 11 月之后的 GSE 不在列表中,会提示 Your GSE may not be expression by array。

AnnoProbe 的探针处理规则

filterEM 对一个探针多个基因的情况只保留一个注释,源码注释自认这里有问题;一个基因多个探针时保留中位数最高的探针。用 idmap + filterEM 时,方法部分可写作“保留表达中位数最高的探针”。

GPL 表的读法

生信摆渡的帖子以 GPL570 为例:GPL 文件以 # 开头的说明行要跳过再读表;Gene Symbol 含 " /// " 时帖子取第一个基因。有的平台网页只能在线查看、没有下载按钮,可从 FTP 的 annot 目录下载。

模板里的 log2(dat + 1)

流传很广的模板在 boxplot 之后直接写 dat <- log2(dat + 1)。原作者用的数据是线性值,换成已 log2 的数据照抄,本页实测差异基因变为 0。照抄时把这一行换成上文的判断规则。

交给 Agent

下载、注释、差异分析和作图的步骤固定,适合交给科学智能体执行;选数据集、定分组和解读结果仍由你负责。

一句话指令示例:「用 GSE16515 和 GSE15471 做胰腺癌肿瘤与正常组织的差异分析:分别判断是否需要 log2,用 hgu133plus2.db 注释并保留平均表达最高的探针,各自用 limma 分析,再合并矩阵并把批次放进设计矩阵;阈值 adj.P < 0.05、|logFC| > 1;输出两个数据集的差异基因交集、批次校正前后的 PCA、火山图和上下调各 25 个基因的热图。」

  1. 01

    下载与核对

    用 GEOquery 下载 Series Matrix 并缓存,接口被拦截时改走 FTP;记录样本数、平台和 data_processing 描述。

  2. 02

    预处理

    按 GEO2R 规则判断 log2,画箱线图,完成探针注释,输出注释前后的探针数和基因数。

  3. 03

    差异分析与作图

    提取分组并核对样本顺序,运行 limma,输出全部基因结果表、火山图和热图。

  4. 04

    合并与验证

    合并数据集,批次放进设计矩阵;输出 PCA 和两个数据集差异基因的重叠表。

  5. 05

    对抗审阅

    检查分组是否与原文一致、logFC 方向是否正确、是否对已 log2 的数据再次取对数、热图是否同时包含上调和下调基因。

  • 工作区保留:缓存的原始文件、R 脚本、sessionInfo、结果表和全部图。
  • 你仍需核对:数据集是否符合研究问题、分组和样本排除是否与原文一致、阈值是否适合你的论文。
  • qPCR、免疫组化等实验验证由你完成。

参考资料

常见问题

GEO 数据怎么判断是否需要 log2 转换?

用 GEO2R 的规则:计算表达矩阵的分位数,99% 分位数大于 100,或最大值减最小值大于 50 且 25% 分位数大于 0,就做 log2。log2 后的芯片数据通常在 2 到 16 之间。不要只看样本描述,GSE16515 写的是 GC-RMA,实际是线性值。

一个基因对应多个探针,取最大值还是平均值?

推荐保留平均表达最高的探针。本页实测平均法比最大均值法少约 400 个差异基因,原因是低表达探针拉低了均值。无论选哪种,都在方法部分写明。

getGEO 下载失败或很慢怎么办?

先设 options(timeout = 600) 和 destdir,并用 getGPL = FALSE 跳过大体积的平台注释;仍失败时从 ftp.ncbi.nlm.nih.gov 用 wget -c 下载 series_matrix.txt.gz,再用 getGEO(filename = ...) 读取。2026 年 8 月底起 NCBI 网页接口对脚本有 reCAPTCHA 拦截,FTP 不受影响。

多个 GEO 数据集合并时 ComBat 怎么用?

差异分析时把批次放进 limma 的设计矩阵(~ group + batch),不需要先 ComBat。ComBat 或 removeBatchEffect 校正后的矩阵用于 PCA 和热图;一定要用 ComBat 时带上 mod = model.matrix(~ group)。一个数据集只有一组样本时,批次与分组混杂,不能合并比较。

RNA-seq 数据能用 limma 吗?

能,用原始 counts 经 edgeR 标准化后做 limma-voom。FPKM、TPM 不适合做计数模型的差异分析。人类数据可以先用 hasRNASeqQuantifications() 查 GEO 是否有 NCBI 生成的 raw counts。

TCGAbiolinks 下载报错怎么办?

先确认 workflow.type 用的是 "STAR - Counts",并加上 data.type = "Gene Expression Quantification";HTSeq 和 legacy 参数已失效。GDCprepare 报 disease_response 列不存在时,可退回 2.34 版或改用 UCSC Xena 下载整理好的矩阵。

把 GEO 数据挖掘交给 Scientify

给出 GSE 编号、分组和阈值,科学智能体在隔离云电脑中完成下载、注释、limma 差异分析、批次处理和作图,保留全部脚本、结果表和日志。新注册用户免费获得 5 美元等值额度。