规则详解:输入输出声明、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 就能得到当前匹配到的染色体名字。

一张图看清规则的结构

我把刚才讲的四个部分串起来,画了个流程图。你可能一眼就能看到数据怎么在规则间流转。

Snakemake 规则结构(输入输出 · shell/run · params) input: "raw/{sample}.csv" output: "clean/{sample}.csv" shell: "python" "{input}" "{output}" params: threshold=0.5 数据流从左到右 输入文件 → shell/run 处理 → 输出文件;params 提供配置参数

从图中你能看到,输入和输出是规则的“接口”,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: 声明依赖文件,支持通配符
input: "raw/{sample}.csv"
output: 声明生成文件,必须唯一
output: "clean/{sample}.csv"
shell: 简单命令或脚本调用
shell: "python script.py {input} {output}"
run: 需要 Python 逻辑或库调用
run: pd.read_csv(input[0])
params: 管理变动参数或动态计算
params: threshold = lambda w: 0.5 if w.sample == "ctrl" else 0.8

最后说一句:不要觉得这些基础概念简单就跳过。我见过太多因为输入输出去不写对导致整个 pipeline 跑崩的案例。把 rule 的骨架搭扎实了,后面加 log、threads、resources 都只是锦上添花的事。