第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)
  • 字符串不加引号也能解析,但如果路径里含冒号,最好用双引号包起来
小技巧:YAML里可以用#写注释。我经常在配置顶部写一段「这个配置用于什么项目、最后一次修改日期」,非常管用。

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。

踩坑记录:我曾经犯过一个错误——在YAML里写了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的配置可以来自多个地方:

  1. 默认配置:在Snakefile里写config = {}预定义默认值
  2. 配置文件:通过--configfile加载YAML
  3. 命令行参数:通过--config key=value直接传

优先级:命令行 > 配置文件 > 默认值。我个人经常在调试的时候用命令行覆盖某个参数,比如临时改样本数:

snakemake --configfile config.yaml --config threads=2 --dry-run

这样不用改文件,方便又安全。

最佳实践:我建议把所有可变参数放在YAML里,把固定逻辑留在Snakefile里。这样你做版本控制时可以清晰看到「这次跑的是不同配置还是不同代码」。

4.6 整个流程:从配置到运行

下面这张图总结了本章的核心逻辑:

参数化工作流:配置文件驱动流水线 YAML配置文件 样本、参考、线程、输出目录 Snakemake 读取 --configfile + config[] Snakefile 通用规则,不写死任何值 运行实例:一份代码,多套配置 项目A:人类 config_human.yaml 项目B:小鼠 config_mouse.yaml 项目C:大豆 config_soybean.yaml 同一份 Snakefile 规则不变,参数由配置决定 输出到 results/ 每个项目独立目录

你看,整个流程就是「配置分离→统一读取→参数化运行」。每次新项目,你只需要新建一个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里读取
  • 理解配置覆盖优先级

嗯,这章就到这。下一步你可以把目光放到「如何处理更复杂的配置——比如多层嵌套、动态样本表」,不过那已经是另一章的话题了。