先选对数据
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 counts | NCBI 用统一流程比对计数的基因 × 样本整数矩阵 | 人类 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()。
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"# 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 data | 2026 年 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 enough | vroom 读取超长行时默认缓冲区太小 | 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,判断后再决定是否换用原始文件重新处理。
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 个下调 |
| 线性值直接 limma | GSE16515:2804 个上调,3868 个下调,|logFC| 中位数 4.51 |
| 已是 log2 的数据再做 log2(x+1) | GSE15471:0 个(正确处理时 2306 个上调,348 个下调) |
注释
芯片平台对应的 Bioconductor 注释包比 GPL 表更新。在 GPL570 上实测,两者对 41039 个单基因探针的注释有 3874 个不一致,多数是基因改名,例如 FAM122C 现名 PABIR3,CCDC11 现名 CFAP53。
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.0 | hgu133plus2.db | 54675 个探针,最常见 |
| GPL96 HG-U133A | hgu133a.db | 22283 个探针 |
| GPL571 HG-U133A_2 | hgu133a2.db | |
| GPL6244 HuGene 1.0 ST | hugene10sttranscriptcluster.db | 按 transcript cluster 注释 |
| GPL6947 / GPL10558 Illumina HumanHT-12 | illuminaHumanv3.db / illuminaHumanv4.db | 矩阵行名是 ILMN_ 探针号 |
| Agilent 等无注释包的平台 | GPL 表或 FTP 上的 .annot.gz | Gene Symbol 列可能含 " /// ",表示一个探针对应多个基因 |
分组
getGEO 返回的 ExpressionSet 中,pData 行顺序与表达矩阵列顺序一致。错误出现在把 pData 排序、和外部临床表合并或手写分组向量之后,R 不会报错,结果却会完全改变。
样本数和原文对不上时,先查 GSE 是否包含多个平台(length(gse) 大于 1)、是否混有细胞系或技术重复。GSE15471 的 78 个样本对应 36 位患者:标题带 _rep 的 6 个样本是 3 位患者正常和肿瘤组织的重复芯片。把重复芯片当独立样本会高估样本量,可用 avereps 按患者和组织合并,或用 duplicateCorrelation 把患者作为 block。
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 个。上下调基因数不对称时,热图和富集要分别处理上调与下调。
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| > 1 | 1136 | 399 | 文献中最常用 |
| adj.P < 0.05 且 |logFC| > 0.585(1.5 倍) | 2565 | 1001 | 样本少、效应小时常用 |
| 只用 adj.P < 0.05 | 4339 | 5992 | 样本量大时几乎所有基因都显著 |
| treat(lfc = 1),adj.P < 0.05 | 共 166 | 检验倍数是否显著大于 2 倍 | |
| eBayes(trend = FALSE),阈值同第一行 | 1134 | 397 | 芯片数据 trend 影响很小 |
| 部分配对 duplicateCorrelation,阈值同第一行 | 1134 | 403 | 组内相关 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 秒。
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 强度) | limma | lmFit + 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) 就能发现。
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 / 621 | 3441 / 296 |
| batch 放进设计矩阵(推荐) | 1149 / 235 | 1117 / 404 |
| ComBat(mod = ~group) 后 limma | 1144 / 232 | 1108 / 417 |
| removeBatchEffect 后 limma | 1149 / 236 | 未测 |
| ComBat 不带 mod 后 limma | 1063 / 220 | 未测 |
作图
火山图的纵轴和筛选用同一个 P 值(本页都用 adj.P)。热图的样本按分组排列,基因按行标准化。
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 数据集或实验再验证。
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| GEO | TCGA(GDC) | |
|---|---|---|
| 数据来源 | 数千个独立研究,芯片与测序并存 | 33 种癌症,统一测序和处理 |
| 正常对照 | 很多数据集有配对正常组织 | 少:TCGA-PAAD 有 178 个原发肿瘤,正常组织 4 个 |
| 临床信息 | 取决于提交者,常只有分组 | 生存、分期、治疗等较完整 |
| 表达数据 | Series Matrix、CEL、counts | STAR - 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 个基因的热图。」
- 01
下载与核对
用 GEOquery 下载 Series Matrix 并缓存,接口被拦截时改走 FTP;记录样本数、平台和 data_processing 描述。
- 02
预处理
按 GEO2R 规则判断 log2,画箱线图,完成探针注释,输出注释前后的探针数和基因数。
- 03
差异分析与作图
提取分组并核对样本顺序,运行 limma,输出全部基因结果表、火山图和热图。
- 04
合并与验证
合并数据集,批次放进设计矩阵;输出 PCA 和两个数据集差异基因的重叠表。
- 05
对抗审阅
检查分组是否与原文一致、logFC 方向是否正确、是否对已 log2 的数据再次取对数、热图是否同时包含上调和下调基因。
- 工作区保留:缓存的原始文件、R 脚本、sessionInfo、结果表和全部图。
- 你仍需核对:数据集是否符合研究问题、分组和样本排除是否与原文一致、阈值是否适合你的论文。
- qPCR、免疫组化等实验验证由你完成。
参考资料
- NCBI GEO: About GEO2R — log2 自动判断、分组顺序在 2020 年 11 月的变化、循环对比、10 分钟超时、RNA-seq 用 DESeq2
- NCBI GEO: NCBI-generated RNA-seq count data — counts 生成流程、物种范围、文件类型和局限
- GEOquery GitHub issue #230:NCBI reCAPTCHA blocks programmatic access — 2026 年 8 月起的拦截、受影响接口、NCBI 2026-09-02 的例外与 getGPL = FALSE 绕法
- GEOquery vignette: RNA-seq quantifications from GEO — hasRNASeqQuantifications 与 getRNASeqData 用法
- limma 帮助文档:topTable、treat、removeBatchEffect — 倍数阈值不推荐与 treat;removeBatchEffect 不用于 lmFit 前的数据准备
- Biostars: GEO2R script for analysing microarray data — GEO2R 脚本中的 log2 判断代码
- Nygaard V, Rødland EA, Hovig E. Methods that remove batch effects while retaining group differences may lead to exaggerated confidence in downstream analyses. Biostatistics, 2016 — 不平衡设计下 ComBat 的问题
- Li Y, et al. Exaggerated false positives by popular differential expression methods when analyzing human population samples. Genome Biol, 2022 — 大样本 RNA-seq 中 DESeq2、edgeR 的假阳性与 Wilcoxon 检验
- Law CW, et al. voom: precision weights unlock linear model analysis tools for RNA-seq read counts. Genome Biol, 2014 — limma-voom 方法
- GDC: Why did GDC remove HTSeq gene expression quantification — HTSeq 移除与 STAR - Counts
- GDC: Legacy Archive retires — Legacy Archive 2023-05-04 退役
- TCGAbiolinks GitHub issues #639、#656、#657 — GDCquery_clinic、GDCdownload、GDCprepare 的当前问题与绕法
- AnnoProbe 源码(geoChina.R、filterEM.R) — 经验帖:国内镜像地址与探针去重规则
- CSDN:getGEO 报 connection buffer 不够大的解决 — 经验帖:报错原文与 VROOM_CONNECTION_SIZE 设置,作者贴出运行结果
- CSDN(生信摆渡):GEO 芯片平台探针基因注释 — 经验帖:GPL 文件读取与 " /// " 处理
- CSDN 转载简书:用 limma 包进行多个分组的差异分析 — 经验帖:两种设计矩阵写法等价;模板中无条件 log2 的反例
- GSE16515(胰腺癌,GPL570) — 本页实测数据集
- GSE15471(胰腺癌,GPL570) — 本页合并与验证用数据集