规则详解:输入输出声明、shell命令执行、params参数、run块(Python代码)的使用
Snakemake 的玩法,说白了就是用一条条规则把任务串起来。而每条规则最重要的就是声明它要什么、给什么、怎么干。这仨东西弄明白了,再复杂的流程都不会乱。
我个人习惯先把输入输出写在最上面,就像写函数先写参数和返回值一样。这样后面调试时一眼就能看清楚数据流向。
输入输出声明:把数据流画出来
每个 rule 都需要告诉 Snakemake:“我要读哪些文件,我要生成哪些文件”。这个声明不仅是为了让你自己看清依赖,更是让 Snakemake 能自动构建 DAG(有向无环图)的关键。
rule clean_data:
input:
"raw/{sample}.csv"
output:
"clean/{sample}.csv"
shell:
"python scripts/clean.py {input} {output}"
这里 {sample} 是一个通配符。Snakemake 会根据最终需要的输出自动匹配通配符的值。比如你要生成 clean/A.csv,它就会去找 raw/A.csv。
我曾经遇到过一个坑:输出文件名里写死了路径,结果通配符一直匹配不上。后来发现是输出路径和规则名冲突了——规则名不能和输出目录同名,不然 Snakemake 会把它当成文件来处理。
output: 明确写出,不要依赖 touch() 或空文件。我曾经偷懒用了
output: touch("done.txt"),结果下游规则检查文件内容时全挂了——文件是空的。shell 命令执行:最直接的干活方式
如果处理逻辑不复杂,直接用 shell: 块扔给 bash 就行。Snakemake 会把 {input}、{output} 展开成对应的路径串。
rule align:
input:
"clean/{sample}.csv"
output:
"aligned/{sample}.bam"
shell:
"bwa mem ref.fa {input} > {output}"
注意这里 {input} 和 {output} 是 Snakemake 自动给的环境变量,跟 shell 里的 $1 不是一个东西。你想想看,如果写成 "bwa mem ref.fa $1 > $2",Snakemake 是不会帮你替换的——它只认大括号里的名字。
我自己的习惯是:只要命令行超过三行,就会写成 shell: 结合 params 来避免硬编码。比如参数模块路径、数据库路径这些容易改的东西,放到 params 里,别塞在命令字符串里。
params 参数:把配置抽离出来
params: 块可以定义一些名字,然后在 shell: 或 run: 里用 {params.xxx} 引用。这东西特别适合管理那些跟逻辑无关、但经常变化的配置项。
rule classify:
input:
"feats/{sample}.npy"
output:
"pred/{sample}.csv"
params:
model_path = "models/classifier.pkl",
threshold = 0.5
shell:
"python predict.py --model {params.model_path} --thresh {params.threshold} {input} {output}"
你还可以在 params 里放 lambda 函数,动态根据输入的名字生成参数。比如我有个规则需要根据样本名切换不同的参考基因组,就可以写成:
params:
ref = lambda wildcards: "ref_human.fa" if "human" in wildcards.sample else "ref_mouse.fa"
这样做的好处是参数和命令完全解耦。我曾在项目中遇到要同时跑 20 个不同参数组合的任务,把所有变动的参数全放进 params:,然后配合通配符生成不同的输出文件名,清爽得不得了。
run 块:用 Python 搞定复杂逻辑
当算法逻辑多到 shell 脚本写不下,或者需要调用 Python 库(比如 pandas、numpy)时,就轮到 run: 块上场了。在 run: 里你可以直接写 Python 代码,还能用 input、output、params 这些变量——它们已经是 Python 对象了。
rule summarize:
input:
"results/{group}.csv"
output:
"summary/{group}.json"
run:
import pandas as pd
import json
df = pd.read_csv(input[0])
stats = {
"mean": df["value"].mean(),
"std": df["value"].std(),
"n": len(df)
}
with open(output[0], "w") as f:
json.dump(stats, f)
注意:input 是一个列表(即使只有一个文件),所以要用 input[0] 取第一个。同样 output 也是列表。我刚开始写的时候总忘记加 [0],结果报错说找不到 read_csv 的参数类型不对——嗯,这个错我吃了一次就记住了。
run: 块里代码超过 30 行,建议把它抽成一个独立的 Python 文件,然后用 shell: 调用那个脚本。理由:一来更容易单测,二来
run: 里的全局变量容易污染,维护起来很头疼。还有个细节:run: 块里可以用 wildcards 对象拿到通配符的具体值。比如一个 rule 输入是 "data/{chrom}.bed",在 run: 里用 wildcards.chrom 就能得到当前匹配到的染色体名字。
一张图看清规则的结构
我把刚才讲的四个部分串起来,画了个流程图。你可能一眼就能看到数据怎么在规则间流转。
从图中你能看到,输入和输出是规则的“接口”,shell 或 run 是“实现”,params 是“配置”。这四个组合在一起,就是一个完整且可复用的任务单元。我个人觉得 Snakemake 的优雅之处就在这里:它把数据、逻辑、配置拆得清清楚楚,每个部分各司其职。
几个容易踩的坑(纯属个人教训)
- input 和 output 数量不匹配:shell 里如果用了
{input}和{output},但输入有多个文件,{input}会被展开成空格分隔的字符串。你需要用{input[0]}、{input[1]}或{input:q}来逐个引用。 - params 里不要放路径分隔符:有一次我把数据库路径放到 params 里,路径里带了空格,shell 执行时直接断开了。后来我用了
{params.db:q}才搞定——:q是 Snakemake 的 shell 转义标记,会自动加引号。 - run 块里别少写
import:每个run:块都是独立运行的 Python 进程,之前规则里的 import 不会被继承。我每次都乖乖在开头写import pandas as pd,虽然有点啰嗦,但不会出幺蛾子。
| 组件 | 适用场景 | 示例 |
|---|---|---|
input: |
声明依赖文件,支持通配符 | |
output: |
声明生成文件,必须唯一 | |
shell: |
简单命令或脚本调用 | |
run: |
需要 Python 逻辑或库调用 | |
params: |
管理变动参数或动态计算 | |
最后说一句:不要觉得这些基础概念简单就跳过。我见过太多因为输入输出去不写对导致整个 pipeline 跑崩的案例。把 rule 的骨架搭扎实了,后面加 log、threads、resources 都只是锦上添花的事。