数据加载与预处理:别着急分析,先把手弄脏

好,接下来我们真的要开始动手了。之前几章咱们聊了火山图的原理和表达矩阵的格式,现在轮到“数据加载与预处理”这个地基活儿。我个人习惯,拿到一个表达矩阵之后,绝对不会马上扔进统计测试 —— 你想想看,一个 CSV 或 TSV 文件里,可能有空单元格、行名不对、样品名里有奇怪的字符…… 我曾经因为没处理好缺失值,跑出来的差异基因一半是假阳性,后来花了整整两周复现才找到根源。所以,咱们先把数据洗干净再说。

4.1 读取表达矩阵:CSV 和 TSV 的读法

表达矩阵最常见的两种格式就是 CSV(逗号分隔)和 TSV(制表符分隔)。Python 里最顺手的工具当然是 pandas,我基本只用它来读表格数据。

小提示:很多 RNA-seq 工具输出的是 TSV,比如 featureCounts。如果你拿到的是 .txt 后缀但实际上是 TSV,别被名字骗了,用 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 —— 全删没了
警告:不要一上来就全局填充 0。log2 变换前,先考虑用 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 流程图)

下面这张图总结了我每次处理表达矩阵的标准步骤。你按照这个流程走,基本不会出大问题。

表达矩阵预处理流程 原始表达矩阵 pandas读取 (CSV/TSV) 检查缺失值 填充/删除缺失 数据概览 清洗后矩阵 几点注意 • 缺失率 > 50% 的基因直接删行 • 低缺失率用基因中位数填充 • 记得检查列名格式

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=',')
进阶技巧:如果数据量超大(比如 6 万基因 × 500 样本),可以考虑用 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,查了三天才发现是列类型没转换。所以,前处理花的每一分钟,后面都能省回来十倍。