数据加载与预处理:别着急分析,先把手弄脏
好,接下来我们真的要开始动手了。之前几章咱们聊了火山图的原理和表达矩阵的格式,现在轮到“数据加载与预处理”这个地基活儿。我个人习惯,拿到一个表达矩阵之后,绝对不会马上扔进统计测试 —— 你想想看,一个 CSV 或 TSV 文件里,可能有空单元格、行名不对、样品名里有奇怪的字符…… 我曾经因为没处理好缺失值,跑出来的差异基因一半是假阳性,后来花了整整两周复现才找到根源。所以,咱们先把数据洗干净再说。
4.1 读取表达矩阵:CSV 和 TSV 的读法
表达矩阵最常见的两种格式就是 CSV(逗号分隔)和 TSV(制表符分隔)。Python 里最顺手的工具当然是 pandas,我基本只用它来读表格数据。
sep='\t' 读就行。
我一般这样写:
import pandas as pd
# 读取 CSV
df_csv = pd.read_csv('expression_matrix.csv', index_col=0)
# 读取 TSV
df_tsv = pd.read_csv('expression_matrix.tsv', sep='\t', index_col=0)
index_col=0 的意思是第一列作为行名(通常是基因 ID)。有时候行名没有列标题,或者文件里带注释行,可以加上 comment='#' 跳过。我在项目里遇到过那种「前五行是实验描述」的奇葩格式,只好用 skiprows 跳过。
4.2 清洗缺失值:别让 NaN 捣乱
读取完成后,第一件事就是检查有没有缺失值。表达矩阵里出现 NaN 是常有的事 —— 可能是测序深度不足、某个基因在某个样本里没检测到,或者数据整合时拼接错了。我见过的坑包括:
- Excel 导出的 CSV 里明明没空单元格,但读进来变成 NaN(因为有个不可见字符)
- 用
fillna(0)直接填 0 导致后续 log2 变换报错(0 取 log 是无穷) - 某些人为了省事直接 dropna(),结果样本数从 30 变 2 —— 全删没了
df + 1 或 np.log2(df + 1) 这种伪计数方法。如果缺失值超过 50% 的基因,我个人建议直接扔掉。
查看缺失值的常用命令:
# 查看每个基因(行)的缺失比例
missing_ratio = df.isnull().mean(axis=1)
# 查看每个样本(列)的缺失比例
sample_missing = df.isnull().mean(axis=0)
# 统计整体缺失数
total_missing = df.isnull().sum().sum()
print(f"总共缺失 {total_missing} 个值")
如果缺失比例很低(< 5%),我倾向于用该基因在其他样本的中位数填充:
df_filled = df.apply(lambda col: col.fillna(col.median()), axis=0)
这里 axis=0 是按列(样本)填充中位数。注意:表达矩阵通常是「基因在行,样本在列」,所以 axis=0 是按样本列填充。
4.3 查看数据概览:先有个感觉再动手
清洗之后别急着做差异分析。我每次都会花 5 分钟看数据概览,就像检查食材新不新鲜一样。常用的几个函数:
# 基本信息
print(df.shape) # (基因数, 样本数)
print(df.columns.tolist()) # 样本名称列表
# 描述性统计
df.describe()
# 前几行看一眼
df.head()
# 检查数据类型
df.dtypes.value_counts()
这里最容易翻车的是「样本名里带空格或括号」—— 比如 "Sample_1 (Treated)" 这种,后面的分析如果需要用列名做分组,就得用 str.replace 清理。我记得有个项目,别人给的表达矩阵列名是 "Normal\t1"(制表符在里面),导致 pandas 直接解析出错,查了半天。
我还喜欢画一个简单的表达量分布直方图,大致看看数据范围:
import matplotlib.pyplot as plt
plt.hist(df.values.flatten(), bins=50, alpha=0.7)
plt.xlabel('Expression value')
plt.ylabel('Frequency')
plt.title('Global expression distribution')
plt.show()
如果看到左边拖一条长尾巴,说明有很多 low expression 的基因,后面差异分析可以考虑过滤掉。
4.4 完整的预处理流程(SVG 流程图)
下面这张图总结了我每次处理表达矩阵的标准步骤。你按照这个流程走,基本不会出大问题。
4.5 代码实战:一步到位
我把上面的步骤整合成一个函数,你直接复制就能用(改改文件名和参数)。
def load_and_clean_expression(filepath, sep=',', missing_thresh=0.5):
"""
加载并清洗表达矩阵。
filepath : 文件路径
sep : 分隔符,CSV用',',TSV用'\t'
missing_thresh: 基因缺失率超过此比例则删除该行
"""
# 1. 读取
df = pd.read_csv(filepath, sep=sep, index_col=0)
# 2. 检查缺失
missing_ratio = df.isnull().mean(axis=1)
genes_to_drop = missing_ratio[missing_ratio > missing_thresh].index
df_filtered = df.drop(genes_to_drop, axis=0)
# 3. 填充(按基因中位数)
df_filled = df_filtered.apply(lambda col: col.fillna(col.median()), axis=0)
# 4. 概览
print(f"原始基因数: {df.shape[0]}, 样本数: {df.shape[1]}")
print(f"删除 {len(genes_to_drop)} 个高缺失基因")
print(f"清洗后基因数: {df_filled.shape[0]}")
return df_filled
# 使用示例
df_clean = load_and_clean_expression('expression.csv', sep=',')
dask.dataframe 或者 pandas.read_csv(chunksize=...) 分块读取。我处理过一个 8 GB 的矩阵,单机内存不够,只好用 chunksize 分批并逐行检查缺失。
4.6 避坑清单(我亲手踩过的)
| 坑 | 现象 | 解决办法 |
|---|---|---|
| 列名中带空格或特殊字符 | df.columns 里看到 "Sample 1",后面分组时匹配不上 |
df.columns = df.columns.str.replace(' ', '_') |
| 行索引重复 | 两个基因有相同 ID,pandas 报错或合并 | df = df[~df.index.duplicated(keep='first')] |
| 文件编码问题 | Read 时报 UnicodeDecodeError | 加参数 encoding='utf-8' 或 'gbk' |
| 缺失值用 0 代替 | log2 变换后一片 -inf | 用伪计数 np.log2(df + 1) |
| 数值是字符串(如"1.2e3") | df.dtypes 显示 object |
df = df.astype(float) |
嗯,到这里数据就准备好了。你可能会问:为什么一定要做这些预处理?说白了,差异分析的统计方法(比如 t 检验、DESeq2 的负二项分布)对缺失值和非数值都很敏感。我曾经因为数据里有几个字符串,跑出来的 p-value 全部是 NaN,查了三天才发现是列类型没转换。所以,前处理花的每一分钟,后面都能省回来十倍。