生信小白必看:手把手教你用R语言实现counts/FPKM/TPM互转(附代码)
生信入门实战R语言实现转录组表达量单位精准转换指南刚接触转录组数据分析时面对counts、FPKM、TPM这些专业术语很多初学者都会感到一头雾水。记得我第一次处理RNA-seq数据时就曾被这三种表达量单位搞得晕头转向——明明是同一样本为什么不同单位计算出来的数值差异这么大在差异表达分析时到底该用哪种格式这些问题不解决后续分析就无从谈起。1. 理解表达量单位的基础概念1.1 为什么需要不同表达量单位在RNA-seq数据分析中我们通常使用三种主要的表达量单位原始counts、FPKM和TPM。它们各有优缺点适用于不同的分析场景原始counts直接比对到每个基因的reads数优点最接近原始数据DESeq2等差异分析工具的首选输入缺点未考虑基因长度和测序深度的影响FPKM(Fragments Per Kilobase per Million mapped fragments)计算公式FPKM (基因reads数 × 10^9) / (基因长度 × 总reads数)优点消除了基因长度和测序深度的影响缺点样本间不可直接比较TPM(Transcripts Per Million)计算公式TPM (基因reads数/基因长度) / (Σ(基因reads数/基因长度)) × 10^6优点样本间可比性优于FPKM缺点计算过程更复杂注意虽然FPKM和TPM都进行了长度标准化但它们的标准化方式不同导致结果不可互换使用。1.2 单位间的关键差异对比下表总结了三种表达量单位的主要特点特征countsFPKMTPM标准化方式无长度测序深度长度相对比例样本间可比性差中等优适用分析差异表达基因表达量样本比较数值范围整数小数小数总和不固定不固定百万2. 准备工作与环境配置2.1 数据准备与R包安装在开始转换前我们需要准备以下数据基因表达矩阵counts数据基因长度信息通常从GTF文件获取样本信息可选首先安装必要的R包# 安装Bioconductor基础包 if (!require(BiocManager, quietly TRUE)) install.packages(BiocManager) # 安装其他必要包 BiocManager::install(c(DESeq2, edgeR, limma)) install.packages(tidyverse)2.2 数据导入与检查假设我们有一个counts矩阵gene_counts.csv和基因长度文件gene_lengths.csvlibrary(tidyverse) # 读取counts数据 counts_data - read_csv(gene_counts.csv) %% column_to_rownames(gene_id) # 读取基因长度数据 gene_length - read_csv(gene_lengths.csv) %% column_to_rownames(gene_id) %% pull(length) # 检查数据维度 dim(counts_data) length(gene_length) # 确保基因顺序一致 stopifnot(all(rownames(counts_data) names(gene_length)))3. counts转FPKM的完整实现3.1 基础计算方法FPKM的计算需要考虑三个要素基因的原始counts基因长度外显子总长度样本的总有效reads数counts_to_fpkm - function(counts, lengths) { # 确保输入数据格式正确 stopifnot(nrow(counts) length(lengths)) stopifnot(all(rownames(counts) names(lengths))) # 计算每百万reads的缩放因子 million_scale - colSums(counts) / 1e6 # 计算FPKM fpkm - counts / lengths fpkm - t(t(fpkm) / million_scale) return(fpkm) } # 使用示例 fpkm_data - counts_to_fpkm(counts_data, gene_length) head(fpkm_data)3.2 常见问题排查在实际操作中你可能会遇到以下问题基因长度为零会导致除以零错误解决方案过滤掉长度为零的基因valid_genes - gene_length 0 counts_data - counts_data[valid_genes, ] gene_length - gene_length[valid_genes]counts矩阵包含非整数可能来自某些预处理流程解决方案四舍五入或检查数据来源counts_data - round(counts_data)基因名称不匹配rownames不一致解决方案确保两个数据框的基因顺序一致counts_data - counts_data[names(gene_length), ]4. counts转TPM的进阶方法4.1 分步计算过程TPM的计算比FPKM多一个标准化步骤确保每个样本的TPM总和为百万counts_to_tpm - function(counts, lengths) { # 计算速率 (counts/kb) rate - counts / lengths # 计算每样本的缩放因子 scale_factor - colSums(rate) # 计算TPM tpm - t(t(rate) / scale_factor) * 1e6 return(tpm) } # 使用示例 tpm_data - counts_to_tpm(counts_data, gene_length) head(tpm_data)4.2 向量化高效实现对于大数据集我们可以使用矩阵运算加速counts_to_tpm_fast - function(counts, lengths) { # 确保长度是矩阵形式 length_matrix - matrix(lengths, nrow nrow(counts), ncol ncol(counts)) # 计算TPM tpm - (counts / length_matrix) / colSums(counts / length_matrix) * 1e6 return(tpm) }5. FPKM与TPM间的相互转换5.1 FPKM转TPM由于FPKM已经进行了长度标准化转换为TPM只需重新调整比例fpkm_to_tpm - function(fpkm) { # 计算每样本的缩放因子 scale_factor - colSums(fpkm) # 计算TPM tpm - t(t(fpkm) / scale_factor) * 1e6 return(tpm) } # 使用示例 tpm_from_fpkm - fpkm_to_tpm(fpkm_data)5.2 单位转换的数学原理理解这些转换背后的数学原理很重要FPKM公式FPKM (N × 10^9) / (L × T) 其中 N 基因counts L 基因长度(kb) T 样本总countsTPM公式TPM (N/L) / Σ(N/L) × 10^6转换关系TPM (FPKM / ΣFPKM) × 10^66. 差异表达分析前的预处理6.1 DESeq2的最佳实践DESeq2要求输入原始counts数据并进行自己的标准化library(DESeq2) # 创建DESeqDataSet对象 dds - DESeqDataSetFromMatrix( countData round(counts_data), colData sample_info, design ~ condition ) # 运行差异分析 dds - DESeq(dds) results - results(dds)6.2 limma处理FPKM数据如果需要用limma分析FPKM数据应先进行log2转换library(limma) # log2转换 log2_fpkm - log2(fpkm_data 1) # 构建设计矩阵 design - model.matrix(~ condition, data sample_info) # 线性模型拟合 fit - lmFit(log2_fpkm, design) fit - eBayes(fit) topTable(fit)6.3 三种单位在差异分析中的表现下表比较了不同分析工具对输入数据的要求工具推荐输入处理方式注意事项DESeq2raw counts内部标准化不接受非整数输入edgeRraw countsTMM标准化可处理复杂实验设计limmalog2(FPKM)线性模型需要先转换和过滤低表达基因limma-voomraw counts方差权重转换适合样本量大的研究7. 实战案例完整分析流程7.1 从原始数据到表达矩阵假设我们从fastq文件开始典型分析流程包括质量控制FastQC序列比对HISAT2/STAR生成counts矩阵featureCounts/HTSeq表达量单位转换本文介绍的内容差异表达分析功能富集分析7.2 自动化脚本示例以下是一个完整的R脚本封装了所有转换函数library(tidyverse) # 读取数据 read_expression_data - function(counts_file, lengths_file) { counts - read_csv(counts_file) %% column_to_rownames(gene_id) lengths - read_csv(lengths_file) %% column_to_rownames(gene_id) %% pull(length) list(counts counts, lengths lengths) } # counts转FPKM counts_to_fpkm - function(counts, lengths) { stopifnot(nrow(counts) length(lengths)) stopifnot(all(rownames(counts) names(lengths))) t(t(counts / lengths) / (colSums(counts) / 1e6)) } # counts转TPM counts_to_tpm - function(counts, lengths) { rate - counts / lengths t(t(rate) / colSums(rate)) * 1e6 } # FPKM转TPM fpkm_to_tpm - function(fpkm) { t(t(fpkm) / colSums(fpkm)) * 1e6 } # 主函数 main - function() { data - read_expression_data(counts.csv, lengths.csv) # 过滤低表达基因 keep - rowSums(data$counts) 10 data$counts - data$counts[keep, ] data$lengths - data$lengths[keep] # 转换 fpkm - counts_to_fpkm(data$counts, data$lengths) tpm - counts_to_tpm(data$counts, data$lengths) # 保存结果 write.csv(fpkm, fpkm_matrix.csv) write.csv(tpm, tpm_matrix.csv) } # 执行 main()在实际项目中我发现最常遇到的坑是基因长度信息的获取。很多初学者直接从UCSC下载的基因长度并不准确因为这些长度可能包含内含子区域。更准确的做法是从GTF文件计算外显子总长度或者使用TxDb等R包获取准确的转录本长度。