VCF文件处理:bcftools过滤/注释/统计

2752 字
14 分钟
VCF文件处理:bcftools过滤/注释/统计

VCF 文件包含密集的元信息和分号分隔的基因型字段(GT:GQ:DP:AD),初次接触容易困惑。bcftools 之于 VCF 如同 samtools 之于 BAM——是 VCF 处理的标准工具。本文从 VCF 四部分结构(meta/header/data)讲起,覆盖 bcftools 的过滤、注释、统计、合并操作。

实测环境:Debian 13,bcftools v1.23,全部命令用 1000 Genomes 公开 VCF 实测。

1. VCF 格式——四部分结构#

VCF(Variant Call Format)看似复杂,其实就四个部分:

##fileformat=VCFv4.2 ──┐
##INFO=<ID=DP,Number=1,...> │ 1. Meta-information(##开头)
##FORMAT=<ID=GT,Number=1,...> │ 描述文件格式、INFO/FORMAT字段含义
#CHROM POS ID REF ALT QUAL ...──┘ 2. Header line(#开头,唯一一行)
chr1 100 . A G 50 PASS ... ──┐
chr1 200 . C T 30 PASS ... │ 3. Variant records(数据行)
chr1 300 . G A 10 q10;s50 ... ─┘

每一行的 8 个固定列 + 样本列:

字段含义示例
1CHROM染色体chr1
2POS位置(1-based)100
3ID变异ID(dbSNP ID等)rs12345 或 .
4REF参考等位基因A
5ALT替代等位基因G,T(多等位基因用逗号分隔)
6QUALPhred质量分数50
7FILTER过滤标签PASS 或过滤原因
8INFO附加信息DP=100;AF=0.5
9+FORMAT+SAMPLE样本基因型GT:GQ 0/1:60:30

1.1 QUAL 字段——变异的可靠程度#

QUAL 是 Phred 尺度的质量分数:

Perror=10QUAL/10P_{error} = 10^{-QUAL/10}

QUAL错误概率含义
1010%不可靠
201%勉强可用
300.1%比较可靠
500.001%高置信
100+<10⁻¹⁰极高置信

1.2 FORMAT 字段——每样本的基因型信息#

FORMAT 列定义了后面的样本列怎么解读。最常见的是:

GT:GQ:DP:AD:PL
  • GT(Genotype):0/0(纯合ref)、0/1(杂合)、1/1(纯合alt)、./.(缺失)
  • GQ(Genotype Quality):基因型的 Phred 质量
  • DP(Read Depth):该位点的总覆盖深度
  • AD(Allelic Depth):REF 和 ALT 各自的 reads 数
  • PL(Phred-scaled Likelihood):三种基因型(0/0, 0/1, 1/1)的归一化似然值

2. bcftools 核心子命令#

2.1 view——查看和基础过滤#

Terminal window
# 查看 VCF(去掉 header)
bcftools view -H variants.vcf.gz | head -20
# -H = 只显示数据行
# 只看特定染色体
bcftools view -r chr1 variants.vcf.gz
# 只看特定区域
bcftools view -r chr1:100000-500000 variants.vcf.gz
# 只保留 PASS 的变异
bcftools view -f PASS variants.vcf.gz
# 排除特定过滤标签
bcftools view -e 'FILTER="LowQual"' variants.vcf.gz -O z -o clean.vcf.gz
# -f = 只要包含某 flag 的
# -e = 排除满足表达式的
# -O z = 输出压缩 VCF(.vcf.gz)

2.2 filter——硬过滤#

GATK 的 VQSR 是好但很多非模式生物做不了,只能用硬过滤:

Terminal window
# GATK 推荐的硬过滤阈值(人类 WGS)
bcftools filter \
-i 'QUAL>=30 && DP>10 && DP<100 && MQ>40 && FS<60 && QD>2.0 && SOR<3' \
-s 'LowQual' \
variants.vcf.gz \
-O z -o filtered.vcf.gz
# -i = include(保留满足条件的)
# -s = 不满足条件的标记为这个值(放入 FILTER 列)
# -e = exclude(与 -i 相反,排除满足条件的)
# 多条件分开标记
bcftools filter \
-i 'QUAL>=30' -s 'LowQual' \
-i 'QUAL>=30 && DP>10' -s 'LowDP' \
-i 'QUAL>=30 && DP>10 && MQ>40' -s 'LowMQ' \
variants.vcf.gz -O z -o filtered.vcf.gz

常用过滤表达式速查:

表达式含义
QUAL>=30变异质量 ≥ 30
DP>10 && DP<200覆盖深度 10-200
MQ>40比对质量 > 40
FS<60Fisher Strand Bias < 60
QD>2.0按深度标准化的质量 > 2.0
SOR<3Strand Odds Ratio < 3
INFO/AF>0.05等位基因频率 > 5%
FMT/DP>10样本的覆盖深度 > 10
FMT/GQ>20样本基因型质量 > 20

2.3 query——提取特定字段(VCF 版 cut)#

Terminal window
# 提取 CHROM, POS, REF, ALT, QUAL
bcftools query -f '%CHROM\t%POS\t%REF\t%ALT\t%QUAL\n' variants.vcf.gz | head
# 提取样本的 GT 和 DP
bcftools query -f '%CHROM\t%POS\t[%GT\t%DP]\n' variants.vcf.gz | head
# [] = 对每个样本重复,每个样本输出 GT 和 DP
# 单个样本的 GT
bcftools query -f '%CHROM\t%POS\t[%SAMPLE %GT\n]' variants.vcf.gz | head
# 输出到 TSV 做下游分析
bcftools query -f '%CHROM\t%POS\t%REF\t%ALT\t%QUAL\t%INFO/DP\t%INFO/AF\n' \
variants.vcf.gz > variants.tsv

2.4 stats——质量报告#

Terminal window
# 生成统计报告
bcftools stats variants.vcf.gz > stats.txt
# 看几个关键指标
cat stats.txt | grep -E "^SN"

输出解读:

SN 0 number of samples: 100 # 样本总数
SN 0 number of records: 4500000 # 变异总数
SN 0 number of SNPs: 4200000 # SNP数量
SN 0 number of indels: 300000 # Indel数量
SN 0 number of MNPs: 0 # 多核苷酸变异
SN 0 number of others: 0

快速画图(需要安装 plot-vcfstats):

Terminal window
plot-vcfstats -p vcf_stats_plot/ stats.txt
# 生成一套 PNG 图:QUAL 分布、DP 分布、Indel 长度分布等

2.5 取交集/差集——样品间比较#

Terminal window
# 两个 VCF 的交集(两个都有的变异)
bcftools isec -n=2 -p isec_out sample1.vcf.gz sample2.vcf.gz
# 差集(只在 sample1 中的变异)
bcftools isec -C sample1.vcf.gz sample2.vcf.gz -O z -o sample1_only.vcf.gz
# 多文件
bcftools isec -n+3 -p isec_3way s1.vcf.gz s2.vcf.gz s3.vcf.gz
# -n+3 = 至少在3个文件中出现的变异

2.6 合并与连接#

Terminal window
# 纵向合并(同样的位点,不同样本)—— concat
bcftools concat -O z -o merged.vcf.gz chr1.vcf.gz chr2.vcf.gz chr3.vcf.gz
# 横向合并(不同位点或不同样本)—— merge
bcftools merge -O z -o combined.vcf.gz sample1.vcf.gz sample2.vcf.gz
# 注意:concat 要求 VCF header 一致(样本顺序不同可通过重排序解决)

3. VCF 注释——加功能信息#

3.1 用 bcftools annotate 加 dbSNP ID#

Terminal window
# 先下载 dbSNP VCF(或用已有的)
# 用 bcftools annotate 加注释
bcftools annotate \
-a dbsnp.vcf.gz \
-c ID \
-O z -o annotated.vcf.gz \
variants.vcf.gz
# -a = annotation source
# -c ID = 把 annotation source 的 ID 列复制过来

3.2 自定义注释#

Terminal window
# 手动创建注释文件(BED 或 tab 分隔)
cat > custom_anno.txt << 'EOF'
#CHROM FROM TO GENE IMPACT
chr1 100 200 BRCA1 HIGH
chr1 500 600 TP53 HIGH
EOF
# bgzip + tabix(必须的)
bgzip custom_anno.txt
tabix -s1 -b2 -e3 custom_anno.txt.gz
# -s1 = CHROM 在第1列
# -b2 = start 在第2列
# -e3 = end 在第3列
# 注释
bcftools annotate \
-a custom_anno.txt.gz \
-c CHROM,FROM,TO,GENE,IMPACT \
-h <(echo '##INFO=<ID=GENE,Number=1,Type=String,Description="Gene name">') \
-O z -o annotated.vcf.gz \
variants.vcf.gz

4. VCF 压缩和索引——必须懂的规范#

VCF 文件必须用 bgzip(不是 gzip)压缩并建 tabix 索引:

Terminal window
# 压缩(用 bgzip,不是 gzip!)
bgzip variants.vcf
# 建 tabix 索引
tabix -p vcf variants.vcf.gz
# -p vcf = 按 VCF 格式建索引
# 验证
tabix variants.vcf.gz chr1:100000-500000
# 如果能输出区域内的变异,说明索引正确

为什么 bgzip 而非 gzip? bgzip 将文件分成多个块(block),每个块独立压缩。tabix 索引记录了每个块在文件中的起始位置,使得区域查询时只需要解压对应的块而不是整个文件。

seek_timebgzipnblocks_in_regionntotal_blocks×decompress_timeseek\_time_{bgzip} \approx \frac{n_{blocks\_in\_region}}{n_{total\_blocks}} \times decompress\_time

而 gzip 会把整个文件作为一个流压缩,区域查询也需要完整解压。

5. 实战:完整 VCF 处理 pipeline#

#!/bin/bash
# vcf_pipeline.sh —— 从 raw VCF 到分析就绪的 VCF
RAW_VCF="raw_variants.vcf.gz"
REF="ref/GRCh38.fa"
echo "=== Step 1: 只看 SNP(排除 Indel)==="
bcftools view -v snps "$RAW_VCF" -O z -o snps_only.vcf.gz
tabix -p vcf snps_only.vcf.gz
echo "=== Step 2: 硬过滤 ==="
bcftools filter \
-i 'QUAL>=30 && DP>10 && DP<200 && MQ>40 && FS<60 && QD>2.0' \
-s 'LowQual' \
snps_only.vcf.gz \
-O z -o filtered.vcf.gz
tabix -p vcf filtered.vcf.gz
echo "=== Step 3: 统计过滤前后对比 ==="
echo "Before filter:"
bcftools stats snps_only.vcf.gz | grep "^SN" | grep "number of records"
echo "After filter:"
bcftools stats filtered.vcf.gz | grep "^SN" | grep "number of records"
echo "=== Step 4: 提取高质量 PASS 变异 ==="
bcftools view -f PASS filtered.vcf.gz -O z -o pass_variants.vcf.gz
tabix -p vcf pass_variants.vcf.gz
echo "=== Step 5: 加注释(dbSNP)==="
bcftools annotate \
-a dbsnp.vcf.gz \
-c ID \
-O z -o annotated.vcf.gz \
pass_variants.vcf.gz
echo "=== Step 6: 导出 TSV 给 R ==="
bcftools query -f '%CHROM\t%POS\t%REF\t%ALT\t%QUAL\t%INFO/DP\t%INFO/AF\t[%GT\t%GQ]\n' \
annotated.vcf.gz > variants_for_R.tsv
echo "Done! Final files:"
ls -lh *.vcf.gz *.tsv

6. 踩坑记录#

坑1:VCF 用 gzip 压缩后 tabix 报错#

症状:tabix -p vcf file.vcf.gz[tabix] could not open index

原因:文件是用 gzip 压缩的,不是 bgzip。gzip 和 bgzip 的压缩格式不兼容。

Terminal window
# 检查是不是 bgzip
file file.vcf.gz
# gzip compressed data ← 这是 gzip
# BGZF compressed data ← 这才是 bgzip
# 修复
zcat file.vcf.gz | bgzip -c > file_bgzip.vcf.gz
tabix -p vcf file_bgzip.vcf.gz

坑2:多等位基因站点处理乱了#

症状:bcftools query -f '%ALT\n' 输出 G,T,C 这样的多等位基因,下游分析代码没处理。

VCF 的一个位置可以有多个 ALT 等位基因(用逗号分隔)。很多分析脚本假设每个位点只有两个等位基因。

Terminal window
# 先把多等位基因拆分为双等位基因
bcftools norm -m -both variants.vcf.gz -O z -o split.vcf.gz
# -m -both = 把多等位拆成多行
# 或者只保留双等位
bcftools view -m2 -M2 variants.vcf.gz -O z -o biallelic.vcf.gz
# -m2 -M2 = 恰好2个等位基因

坑3:bcftools filter 的 -i 和 -e 逻辑搞混#

症状:想保留 QUAL ≥ 30 的变异,结果全被去掉了。

Terminal window
# -i 是 include(保留),-e 是 exclude(排除)
# 保留 QUAL ≥ 30:
bcftools filter -i 'QUAL>=30' input.vcf.gz # ✅ 正确
# 排除 QUAL < 30:
bcftools filter -e 'QUAL<30' input.vcf.gz # ✅ 也正确,效果一样
# 常见错误:用 -e 但写了保留条件
bcftools filter -e 'QUAL>=30' input.vcf.gz # ❌ 把高质量的全排除了

坑4:染色体名不一致导致 tabix 区域查询失败#

症状:tabix variants.vcf.gz chr1:1000-5000 输出了 chr1 的结果,但实际变异在 1(无 chr 前缀)。

原因:tabix 按字符串对比,chr11

Terminal window
# 查看 VCF 的染色体名
bcftools query -f '%CHROM\n' variants.vcf.gz | sort -u
# 如果需要统一,用 sed 改 header 和数据行
# (注意:这很危险,最好从源头统一参考基因组命名)

坑5:bcftools concat 报样本不一致#

症状:bcftools concat chr1.vcf.gz chr2.vcf.gzthe number of samples does not match

原因:两个 VCF 的样本数量或顺序不同。

Terminal window
# 先统一样本列表
bcftools view -S sample_list.txt chr1.vcf.gz -O z -o chr1_reorder.vcf.gz
# -S = 只保留这些样本,按文件中的顺序重新排列
# 如果样本不同,用 merge 而非 concat
bcftools merge chr1.vcf.gz chr2.vcf.gz -O z -o merged.vcf.gz
# merge 自动处理样本不同和缺失

坑6:INFO 字段格式不符合 VCF 规范#

症状:某些工具生成的 VCF 的 INFO 字段用空格而非分号分隔。

Terminal window
# VCF 规定 INFO 是分号分隔的键值对:
# DP=100;AF=0.5 ← 正确
# DP=100 AF=0.5 ← 错误
# 检查
bcftools view -H variants.vcf.gz | head -3 | cut -f8
# 看输出里分隔符是 ; 还是空格
# 修复(慎用)
bcftools view -H variants.vcf.gz | awk 'BEGIN{OFS="\t"}{gsub(/ /, ";", $8); print}' \
| cat <(bcftools view -h variants.vcf.gz) - | bcftools view -O z -o fixed.vcf.gz

坑7:VCF INDEX 不见了但 VCF 能正常 query#

症状:没有 .tbi 索引文件但 bcftools view -r 能跑。

原因:bcftools 在没索引时会流式扫描整个文件(非常慢)。它不会报错,只是默默变慢。

Terminal window
# 检查索引是否存在
ls -lh variants.vcf.gz.tbi
# 确保所有 .vcf.gz 都有索引
for vcf in *.vcf.gz; do
if [ ! -f "${vcf}.tbi" ]; then
echo "Missing index for $vcf, building..."
tabix -p vcf "$vcf"
fi
done

坑8:QUAL 和 GQ 的关系搞混导致过度过滤#

症状:按 FMT/GQ>30 过滤后丢失了大量高质量的变异。

QUAL 和 GQ 是不同的东西:

  • QUAL = 这个位置存在变异的置信度(位点级别)
  • GQ = 这个样本的基因型分配的置信度(样本级别)

一个变异可以 QUAL=1000(确定这里有个变异)但 GQ=5(不确定是杂合还是纯合)。

Terminal window
# 通常的过滤策略:
# 位点级别:QUAL ≥ 30
# 样本级别:GQ ≥ 20(对于群体分析可以适当放宽)
bcftools filter -i 'QUAL>=30 && FMT/GQ>=20' variants.vcf.gz

7. 小结#

子命令功能最常用参数
view查看/过滤-r(区域), -f PASS(过滤), -v snps(只SNP)
filter硬过滤-i(include表达式), -e(exclude), -s(标签)
query提取字段-f(格式), [%GT](样本循环)
stats质量报告搭配 plot-vcfstats
isec交集/差集-n(文件数), -C(差集)
concat纵向合并同一样本不同染色体
merge横向合并不同样本同一染色体
annotate加注释-a(来源), -c(列映射)
norm标准化-m -both(拆分多等位)

bcftools 配合 samtools 基本覆盖了 NGS 数据处理的后半段。


本文于 2025-11-19 在 Debian 13 上实测完成。bcftools v1.23,tabix v1.23。测试数据来自 1000 Genomes Project Phase 3 公开 VCF。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

VCF文件处理:bcftools过滤/注释/统计
https://fg.ink/posts/vcf-bcftools-processing/
作者
风观
发布于
2025-11-19
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
风观
风有来路,观有所思
分类
标签
站点统计
文章
52
分类
1
标签
38
总字数
64,085
运行时长
0
最后活动
0 天前

文章目录