第3章 Pandas数据处理:从加载到聚合
数据处理的活儿,Pandas 基本就是 Python 界的瑞士军刀。做差异表达报告,第一步就是把实验数据搬进 DataFrame 里来。我平时只要拿到一个 CSV 或者 Excel 文件,第一反应就是 pd.read_csv() 甩上去,然后快速看看 .head() 和 .info()。为什么?因为数据长什么样,心里得有数。
3.1 读取CSV和Excel
把数据从文件里读进来,最常用的就是这两个函数:
import pandas as pd
# 读取 CSV
df = pd.read_csv('expression_data.csv')
# 读取 Excel(需要 openpyxl 或 xlrd 引擎)
df_excel = pd.read_excel('sample_info.xlsx', sheet_name='Sheet1')
# 看一眼结构
print(df.head())
print(df.info())
有些细节你得留意。CSV 的编码问题,我遇到过好多次,明明文件打开是中文,读进来变成乱码。加个 encoding='utf-8' 或者 encoding='gbk' 试试。还有分隔符不一定是逗号,可能是制表符,那就用 sep='\t'。
| 参数 | 作用 | 常用值 |
|---|---|---|
filepath_or_buffer | 文件路径或类文件对象 | 字符串 |
sep | 列分隔符 | ',' 或 '\t' |
encoding | 文件编码 | 'utf-8', 'gbk' |
header | 表头在哪一行 | 0 或 None |
usecols | 只读指定列 | ['gene', 'log2FC'] |
小技巧:文件很大时,加个 nrows=1000 先预览,确认没问题再全量读取。我曾经在读取 10GB 的 RNA-seq 表格时忘了设 dtype,内存直接炸了——后来学乖了,先 nrows 试读,再 dtype={'gene': 'str', 'FPKM': 'float32'} 节约内存。
3.2 数据清洗:脏数据无处可藏
读进来的数据,十有八九都带点“毛刺”。空值、重复行、类型不对……我习惯先做一波清洗,再往下走。清洗顺序一般这样:
- 检查缺失值
.isnull().sum() - 处理缺失值(删除或填充)
- 删除重复行
.drop_duplicates() - 转换数据类型
.astype()
举个实际的例子,差异表达结果文件里经常出现 NA 表示基因没表达量,这时候:
# 查看每列缺失数量
print(df.isnull().sum())
# 删除含有缺失值的行(谨慎使用)
df_clean = df.dropna(subset=['log2FC', 'pvalue'])
# 或者用均值/中位数填充
df_clean['FPKM'].fillna(df_clean['FPKM'].median(), inplace=True)
# 删除完全重复的行
df_clean = df_clean.drop_duplicates()
# 把基因名列转为字符串类型
df_clean['gene'] = df_clean['gene'].astype(str)
注意:别一上来就 dropna() 全删了,万一缺失值很多,样本量就减少了。我先看缺失比例,低于 5% 且随机出现时才删;否则用 fillna 更稳妥。
我最头疼的一种情况是——明明数值列,读进来变成 object 类型。为什么?因为 CSV 里混进了逗号(比如 "1,234")或者字母。这时候先检查,再 pd.to_numeric() 强制转换,带上 errors='coerce' 把非法值变成 NaN 再处理。
3.3 合并操作:把多张表粘在一起
差异表达分析里,你经常需要把表达量表格、样本注释表、差异结果表拼起来。Pandas 提供两个主力:pd.concat 和 pd.merge。
concat 擅长纵向堆叠(增加行)或横向拼接(增加列)。比如多个批次的数据:
df_batch1 = pd.read_csv('batch1.csv')
df_batch2 = pd.read_csv('batch2.csv')
# 纵向合并
df_all = pd.concat([df_batch1, df_batch2], ignore_index=True)
# 横向合并(按行索引对齐)
df_annot = pd.read_excel('sample_annot.xlsx')
df_combined = pd.concat([df_expr, df_annot], axis=1)
merge 更像 SQL 里的 JOIN,按指定键匹配。我经常用它把差异分析结果和基因注释表拼起来:
de_results = pd.read_csv('diff_exp.csv')
gene_info = pd.read_csv('gene_annotation.csv')
# 默认 inner join,按 'gene_id' 匹配
merged = pd.merge(de_results, gene_info, on='gene_id', how='left')
print(merged.shape)
| merge 参数 | 说明 | 常用值 |
|---|---|---|
how | 连接方式 | 'inner', 'left', 'right', 'outer' |
on | 双方都有的键列名 | 'gene_id' |
left_on/right_on | 左右表不同的键列名 | 'gene', 'GeneSymbol' |
suffixes | 列名冲突时添加后缀 | ('_left','_right') |
核心:合并之后一定要检查行数有没有爆炸或缩水。value_counts() 看看键是否唯一,不唯一就会产生笛卡尔积。我有一次 merge 完行数翻了 20 倍,原来基因名在注释表里有重复——加个 drop_duplicates('gene_id') 解决。
3.4 分组聚合:从样本到统计量
分组聚合是差异表达报告里的高频操作。比如你想知道每个基因在不同条件下的平均表达量,或者计算每个样本分组的标准差。
# 分组计算均值
df_grouped = df.groupby('condition')['expression'].mean().reset_index()
# 多列聚合
stats = df.groupby('gene_id').agg(
mean_expr=('expression', 'mean'),
std_expr=('expression', 'std'),
count=('sample', 'count')
).reset_index()
# 分组后做多种统计
detailed = df.groupby('gene_id')['expression'].agg(['mean', 'std', 'min', 'max'])
print(detailed.head())
这里要注意 .reset_index(),否则分组键会变成索引,后面操作不方便。我习惯在每个 groupby 链最后都显式 reset。
另一种场景:你想保留其他列,只对数值列做聚合,可以用 .groupby() 后选列,或者用 transform:
# 在原 DataFrame 中添加每组的均值列
df['group_mean'] = df.groupby('condition')['expression'].transform('mean')
避坑:分组聚合前,确认分组列的 dtype 是 str 或 category,数值被误判为 float 可能会导致意想不到的 groupby 结果。我遇到过一例,因为浮点精度问题,本来该分在一起的行被分成了两个组——后来用 pd.cut() 离散化解决。
最后,把本章的核心流程画出来,你大概就能理解数据从文件到分析输入的完整路径:
以上就是 Pandas 数据处理的四大步。记住,不管是多复杂的差异表达报告,底层都是这些操作的组合。代码写多了你就会发现,groupby 和 merge 其实比 Excel 透视表灵活得多,尤其是当你有 10 万行基因表达数据时,Excel 早卡死了,Pandas 几秒搞定。
我的习惯:每做完一个处理步骤,都 .shape 看一眼,再 .head(3) 扫一眼,确保没丢行没乱列。调试时用 .sample(5) 随机抽几行验证,比看全部省时间。
OK,这章就到这儿。你先把这些基础操作练熟,后面做交互式报告时才能游刃有余。