第4章 用YAML把参数「拎」出来
说实话,我第一次用Snakemake写流水线的时候,所有参数都硬编码在Snakefile里。样本名、基因组路径、阈值……改一个就得翻几十行。后来项目换了数据集,我对着屏幕改了半小时,改完一跑,报错——有个路径忘了改。那天晚上我就在想:能不能把配置从代码里拆出来?
答案就是YAML配置文件。
4.1 为什么非用配置文件不可
你想想看,流水线里哪些东西是「变」的?
- 样本列表(今天10个,明天100个)
- 参考数据路径(不同物种不一样)
- 工具参数(阈值、线程数、内存)
- 输出目录结构
把这些写在Snakefile里,每次跑新项目就得改代码;写在外面,改一行YAML就行。我个人的经验是:任何超过3个参数的工作流,都应该用配置文件。否则两周之后你自己都记不清哪个值是干嘛的。
| 对比维度 | 硬编码 | YAML配置 |
|---|---|---|
| 修改参数 | 改代码,可能影响逻辑 | 改数据,风险低 |
| 复用性 | 每次复制Snakefile再改 | 同一个Snakefile换配置即可 |
| 可读性 | 参数混在规则里,难找 | 配置集中,一目了然 |
| 版本控制 | 很难区分代码变更和参数变更 | 配置文件和代码分两个commit |
4.2 YAML:人类可读的数据格式
YAML的结构特别简单——缩进表示层级、冒号表示键值对、列表用短横线。举个例子,我常用的样本配置长这样:
# config/samples.yaml
samples:
- id: "SRR123456"
condition: "control"
fq1: "data/SRR123456_1.fastq"
fq2: "data/SRR123456_2.fastq"
- id: "SRR789012"
condition: "treatment"
fq1: "data/SRR789012_1.fastq"
fq2: "data/SRR789012_2.fastq"
ref: "/reference/genome.fa"
threads: 8
outdir: "results"
注意两点:
- 缩进必须一致(我习惯两个空格,不用Tab)
- 字符串不加引号也能解析,但如果路径里含冒号,最好用双引号包起来
#写注释。我经常在配置顶部写一段「这个配置用于什么项目、最后一次修改日期」,非常管用。
4.3 在Snakemake里读取YAML
Snakemake内置了config对象,只需要在命令行通过--configfile指定:
snakemake --configfile config/samples.yaml -j 8
然后在Snakefile里,所有配置项都能用config["key"]访问。比如:
# Snakefile
SAMPLES = config["samples"] # 列表
REF = config["ref"] # 字符串
THREADS = config.get("threads", 4) # 带默认值
OUTDIR = config["outdir"]
rule all:
input:
expand("{outdir}/{sample}.bam", outdir=OUTDIR, sample=[s["id"] for s in SAMPLES])
rule align:
input:
fq1=lambda wildcards: next(s["fq1"] for s in SAMPLES if s["id"] == wildcards.sample),
fq2=lambda wildcards: next(s["fq2"] for s in SAMPLES if s["id"] == wildcards.sample),
ref=REF
output:
"{outdir}/{sample}.bam"
params:
threads=THREADS
shell:
"bwa mem -t {params.threads} {input.ref} {input.fq1} {input.fq2} | samtools sort > {output}"
我个人习惯在Snakefile开头把所有配置解包成全局变量:
SAMPLES = config["samples"]
REF = config["ref"]
THREADS = config.get("threads", 4)
OUTDIR = config["outdir"]
这样后面写规则的时候更清晰,改配置只要看YAML,改逻辑只要看Snakefile。
threads: 08(八进制语法)。YAML会把08当作八进制数解析,导致报错。解决办法:要么写成threads: 8,要么加引号"08"。
4.4 参数化工作流:一份Snakefile,无限配置
所谓参数化,就是你写一次Snakefile,然后通过不同的YAML配置文件驱动不同的运行。
举个例子,我有两个项目:
- 项目A:人类全基因组分析,需要hg38参考
- 项目B:小鼠RNA-seq,需要mm10参考
我只需要写一个Snakefile,然后准备两个YAML文件:
# config_human.yaml
samples:
- id: "HG001"
fq1: "data/HG001_R1.fastq"
fq2: "data/HG001_R2.fastq"
ref: "/ref/hg38.fa"
threads: 16
# config_mouse.yaml
samples:
- id: "MOUSE01"
fq1: "data/MOUSE01_R1.fastq"
fq2: "data/MOUSE01_R2.fastq"
ref: "/ref/mm10.fa"
threads: 8
运行的时候分别指定:
# 人类项目
snakemake --configfile config_human.yaml -j 16 --cores 16
# 小鼠项目
snakemake --configfile config_mouse.yaml -j 8 --cores 8
同一个Snakefile,不同的输入数据、不同的参考基因组、甚至不同的并行数。这就是参数化的威力。
4.5 配置的层级覆盖:命令行参数最高
Snakemake的配置可以来自多个地方:
- 默认配置:在Snakefile里写
config = {}预定义默认值 - 配置文件:通过
--configfile加载YAML - 命令行参数:通过
--config key=value直接传
优先级:命令行 > 配置文件 > 默认值。我个人经常在调试的时候用命令行覆盖某个参数,比如临时改样本数:
snakemake --configfile config.yaml --config threads=2 --dry-run
这样不用改文件,方便又安全。
4.6 整个流程:从配置到运行
下面这张图总结了本章的核心逻辑:
你看,整个流程就是「配置分离→统一读取→参数化运行」。每次新项目,你只需要新建一个YAML文件,零代码改动。
4.7 实战小技巧:用--config直接传关键参数
有时候你不想为了一个小改动专门建YAML文件,Snakemake允许你在命令行直接传参数:
snakemake --config outdir=test_run threads=2 --dry-run
这种方式适合快速测试。不过我建议正式跑数据的时候还是用YAML,因为
- YAML可重用、可版本控制
- 命令行参数容易输错,还没语法高亮
config.yaml,然后加一行.gitignore忽略本地测试配置,但保留一个config.template.yaml作为模板给团队使用。
本章小结
把参数从代码里拎出来,放到YAML里,是Snakemake工作流迈向「可复现、可扩展」的第一步。你学会了
- 写结构清晰的YAML配置文件
- 用
--configfile加载配置 - 用
config["key"]在Snakefile里读取 - 理解配置覆盖优先级
嗯,这章就到这。下一步你可以把目光放到「如何处理更复杂的配置——比如多层嵌套、动态样本表」,不过那已经是另一章的话题了。