第3章 用Pandas读取VCF:INFO与FORMAT字段解析实战

说实话,VCF文件解析是我做生物信息时最常碰到的活儿。刚开始我也是用循环逐行读文件,后来发现——真没必要。Pandas一出手,这些表格化的数据就能收拾得服服帖帖。

这一章,我就带你把VCF里的INFO和FORMAT字段拆解成DataFrame。你可能会问:直接用文本工具不香吗?当然可以,但一旦数据量上去,批处理和统计就麻烦了。你看完后面的内容就明白了。

3.1 VCF文件结构快速回顾

在动手前,咱们得先搞清楚VCF长啥样。典型的VCF长这样:

##fileformat=VCFv4.2
#CHROM  POS  ID  REF  ALT  QUAL  FILTER  INFO  FORMAT  SAMPLE1  SAMPLE2
chr1  100  .  A  G  50.0  PASS  DP=100;AF=0.5  GT:AD:DP  0/1:50,50:100  1/1:0,100:100

关键部分我标一下:

  • INFO:用分号分隔的键值对,记录变异级别的信息(比如测序深度、等位基因频率)
  • FORMAT:冒号分隔的标签列表,定义样品级别的字段
  • 样品列:每个样品的数据,用冒号分隔,顺序和FORMAT一一对应

我最初做全外显子组数据时,看到几十兆的VCF文件——嗯,人工解析基本没戏。

3.2 为什么用Pandas而不是自己写循环?

你可能手写过这样的解析代码:

for line in open('input.vcf'):
    if line.startswith('#'):
        continue
    fields = line.strip().split('\t')
    info = dict(item.split('=') for item in fields[7].split(';'))
    # 然后用info['DP']、info['AF']去做统计……

这代码本身没问题,但可维护性差。数据量大、字段多时,索引容易乱。我习惯用Pandas,主要是因为它:

  • 处理大数据集时不卡顿(用底层C数组)
  • 能直接做分组、聚合、过滤——这些在生信里太常用
  • 导出为Excel、CSV一键搞定
我的习惯做法:先用Pandas读入基本列,再用apply函数把INFO和FORMAT拆成列。后面我会给代码示例。

3.3 读取VCF核心列

先跳过注释行。Pandas读不了“#”开头的行,所以得指定跳过注释行:

import pandas as pd

# 定义列名(VCF标准列)
col_names = ['CHROM', 'POS', 'ID', 'REF', 'ALT', 'QUAL', 'FILTER', 'INFO', 'FORMAT']
# 读取VCF,跳过注释行
df = pd.read_csv('input.vcf', sep='\t', comment='#', header=None, names=col_names)

print(df.head())

输出结果里,INFO和FORMAT还是字符串。别着急,下一步就是拆解它们。

我曾经踩过的坑:VCF有时包含“#”开头的额外信息行(像##fileformat),这些行read_csv会忽略掉。但你得确保你用的VCF文件格式规范——有些第三方软件会输出不标准的格式。

3.4 解析INFO字段

INFO字段由分号分隔的键值对构成。比如:

DP=100;AF=0.5;AN=200;AC=100

我们把它转为字典:

def parse_info(info_str):
    if pd.isna(info_str):
        return {}
    items = info_str.split(';')
    result = {}
    for item in items:
        if '=' in item:
            key, value = item.split('=', 1)
            # 尝试转数字
            try:
                value = float(value) if '.' in value else int(value)
            except:
                pass
            result[key] = value
        else:
            # 有的是标志位,没有值(比如END=300)
            result[item] = True
    return result

# 应用函数
df_info = df['INFO'].apply(parse_info).apply(pd.Series)
df = pd.concat([df.drop(columns=['INFO']), df_info], axis=1)

为什么我用split('=', 1)?我之前遇到过一个VCF,INFO里直接写成END=200;SOMETHING_NAME=value,只用split('=')会得到三个元素,然后索引就乱了。按第一个等号分最安全。

解析后的DataFrame变成这样:

CHROMPOSDPAFANAC
chr11001000.5200100
chr1200800.316048
核心点:INFO字段解析后,每一行代表一个变异,每一列代表一个信息字段。这样你就可以直接做df['DP'].mean()算平均深度了。

3.5 解析FORMAT字段

FORMAT字段稍微复杂点——它描述样品级别的数据。比如:

FORMAT:  GT:AD:DP  
SAMPLE1: 0/1:50,50:100

解析策略:对每个样品列,按冒号分割,再映射到FORMAT里定义的标签。

# 假设样品列从第10列开始(0-based索引)
# 先得到样品名
sample_cols = df.columns[9:].tolist()  # 或从原VCF读取的列

def parse_format(format_str, sample_str):
    tags = format_str.split(':')
    values = sample_str.split(':')
    if len(tags) != len(values):
        return {}
    result = {}
    for tag, val in zip(tags, values):
        try:
            val = float(val) if '.' in val else int(val)
        except:
            pass
        result[tag] = val
    return result

for sample in sample_cols:
    # 对每个样品,构建其解析后的列
    parsed = df.apply(lambda row: parse_format(row['FORMAT'], row[sample]), axis=1)
    parsed_df = parsed.apply(pd.Series)
    # 添加前缀避免列名冲突
    parsed_df.columns = [f"{sample}_{col}" for col in parsed_df.columns]
    df = pd.concat([df, parsed_df], axis=1)

解析后,你会看到类似这样的列:

sample1_GT  sample1_AD  sample1_DP  sample2_GT  sample2_AD  sample2_DP
0/1         50,50       100         1/1         0,100       100
注意!FORMAT字段的标签因VCF版本和分析工具不同会有差异。常用标签包括GT(基因型)、AD(等位基因深度)、DP(深度)、GQ(基因型质量)。我在处理GATK输出的VCF时遇到过PL标签,它是一个逗号分隔的整数列表——直接用float转换会报错,得单独处理。

3.6 整体流程展示

我把整个流程整合成一张图,你一看就明白:

VCF解析为DataFrame流程 原始VCF文件 Pandas读入基本列 解析INFO字段 解析FORMAT字段 分号分隔键值对 转为多列 冒号分隔,逐样品映射 拆分为样品×标签列 完整DataFrame 结果包含所有原始列+解析列 可直接用于统计分析

3.7 实战避坑指南

这里我说几个我亲自踩过的坑:

我曾经因为没处理缺失值导致整个脚本崩溃:有些VCF的FORMAT字段值为.(点),Pandas会把它当作字符串。float('.')会报错。解决方案是在try/except里加上if val == '.': val = None。
性能优化建议:如果你的VCF有几百MB或者几个GB,建议用分块读取:pd.read_csv(..., chunksize=1000),然后循环解析每个块,最后合并。我处理过2GB的VCF,不分块直接解析,内存飙到8GB——差点把服务器跑崩。

3.8 小结

通过这一章,你应该能熟练使用Pandas读取VCF,并把INFO和FORMAT字段拆成整齐的列。接下来你就可以对这些数据做各种统计,比如算一下每个位点的等位基因频率分布,或者筛选出深度大于30的位点。

我个人的心得是:解析这一步虽然看起来繁琐,但做好了一劳永逸。把解析函数封装好,以后不管多少VCF文件,一行代码就能搞定。这比每次手动改正则表达式靠谱多了。