第3章:Snakefile基础编写:配置文件、全局参数、规则依赖

说到Snakefile,有朋友觉得它就是个花哨的Makefile。
嗯,我第一次接触时也是这么想的。但真正上手药物分子预处理项目后,我才发现它的设计思维完全不同。
Snakefile的核心不是“怎么执行命令”,而是“声明你需要什么结果,系统自己找路”。
这章咱们就把地基打牢——配置文件、全局参数、规则依赖这三块,是后续所有复杂流程的基础。

一句话总结:Snakefile把“怎么做”的细节交给规则,把“要用什么”的参数交给配置文件,把“谁先谁后”的逻辑交给依赖声明。

3.1 配置文件:把硬编码赶出你的项目

我在早期做分子预处理时,习惯于把所有路径、参数直接写在Snakefile里。
比如这样:

rule convert_sdf_to_csv:
    input: "data/raw/compounds.sdf"
    output: "data/processed/compounds.csv"
    shell: "obabel {input} -O {output} --csv"

看着挺干净对吧?但三个月后项目扩到2000个化合物,输入文件变成多个分片SDF、输出格式要在CSV和Parquet之间切换、还要支持不同版本的OpenBabel参数。
这时候改Snakefile?改得你想砸键盘。
所以我把配置抽出去,单独放到config.yaml里:

# config.yaml
raw_data_dir: "data/raw"
processed_dir: "data/processed"
input_pattern: "compounds_{batch}.sdf"
output_format: "csv"
obabel_params: "--csv -d"

Snakefile里怎么用?直接用config字典:

configfile: "config.yaml"

rule convert_sdf_to_csv:
    input: f"{config['raw_data_dir']}/{config['input_pattern']}"
    output: f"{config['processed_dir']}/compounds.{config['output_format']}"
    shell: "obabel {input} -O {output} {config[obabel_params]}"
我的习惯:把config.yaml放在项目根目录,然后在Snakefile第一行用configfile声明。团队其他成员想调整参数,只需要改YAML,不碰管道逻辑。

配置文件支持嵌套结构,我通常把参数分组:

# config.yaml
paths:
  raw: "data/raw"
  intermediate: "data/intermediate"
  final: "data/final"

tools:
  obabel:
    executable: "/usr/local/bin/obabel"
    default_params: "--csv -d"
  rdkit:
    num_workers: 8

filters:
  mol_weight_min: 200
  mol_weight_max: 800
  logp_max: 5.0

在Snakefile里就用config['tools']['obabel']['executable']这样取。路径写法长了点,但结构清晰得多。我曾经接手过一个项目,所有参数散落在20多个规则里,改一个阈值要全文搜索……嗯,从那以后我强制团队用统一配置文件。

3.2 全局参数:给所有规则定个规矩

配置文件搞定的是“可变量”,但有些东西是全局的——比如日志级别、临时目录、线程数、是否启用调试模式。
这些我通常用Snakefile开头的全局变量来定义。

# Snakefile 开头部分
import os
import sys
from pathlib import Path

# 全局参数
THREADS = 8
LOG_LEVEL = "INFO"
VERBOSE = False
TEMP_DIR = Path("tmp")
DOCKER_IMAGE = "molprep:v1.2"

这些变量可以在任何规则里直接用。
比如我想让每个规则都能看到是否启用调试:

rule run_descriptor_calc:
    input: "data/processed/compounds.csv"
    output: "data/features/descriptors.csv"
    params:
        debug_flag = "--verbose" if VERBOSE else ""
    shell:
        "python scripts/calc_descriptors.py {input} {output} {params.debug_flag}"
注意:全局变量不要和配置文件重复。我个人习惯:配比变(比如路径、工具参数)用config;运比变(线程数、调试开关、临时目录前缀)用全局参数。

为什么这么分?
配置文件往往需要版本控制、不同环境差异(开发/测试/生产),而全局参数更多是当前运行环境的设置。你把线程数写到config.yaml里,换台机器跑就得改YAML,烦不烦?

我常用的另一个全局模式是路径帮助函数:

# 全局函数
def get_raw_path(batch):
    return f"data/raw/compounds_{batch}.sdf"

def get_processed_path(batch):
    return f"data/processed/{batch}_clean.csv"

这样规则里写input: get_raw_path("batch_01"),比手拼字符串靠谱得多。特别是当你数据集有10个批次、每个批次有多个输出类型时——这种函数能帮你减少90%的路径错误。我自己早期犯过把batch_01写成batch_1的蠢错,跑完一查全对不上……

3.3 规则依赖:让Snakemake自己排工期

Snakefile最厉害的地方,就是自动推导执行顺序。
你只需要声明每条规则的输入和输出,它就能画出整个依赖图。
咱们看个典型的分子预处理流程:

  1. 原始SDF → 统一CSV格式
  2. 统一CSV → 清洗(去重、去盐、标准化)
  3. 清洗后数据 → 计算分子描述符
  4. 描述符 → 合并为最终特征矩阵

Snakefile里可以写成:

rule convert_sdf_to_csv:
    input: "data/raw/compounds.sdf"
    output: "data/processed/compounds_raw.csv"
    shell: "obabel {input} -O {output} --csv"

rule clean_compounds:
    input: "data/processed/compounds_raw.csv"
    output: "data/processed/compounds_clean.csv"
    shell: "python scripts/clean.py {input} {output}"

rule calc_descriptors:
    input: "data/processed/compounds_clean.csv"
    output: "data/features/descriptors.csv"
    shell: "python scripts/calc_descriptors.py {input} {output}"

rule merge_features:
    input: "data/features/descriptors.csv"
    output: "data/final/feature_matrix.csv"
    shell: "python scripts/merge.py {input} {output}"

你想想看,我只需要运行snakemake data/final/feature_matrix.csv,它就会从最后一步反向推导:
merge_features需要descriptors.csv → calc_descriptors需要clean.csv → clean_compounds需要raw.csv → convert_sdf_to_csv需要原始SDF。
全自动排好序,缺哪步补哪步,哪步缓存没过期就不重跑。

我曾经在一个200万分子的项目里,因为改了最后一步的参数,Snakemake自动只重跑了最后两步——省了整整11小时的重复计算。

3.4 依赖的三种形式

依赖不止是“一个规则的输出是另一个规则的输入”这么简单。
实际项目中还有几种常见情况:

依赖类型写法适用场景
显式文件依赖直接写input: "path/to/file"大部分情况
通配符依赖{wildcards.batch}/clean.csv批量处理
函数式依赖input: lambda wildcards: func(wildcards)需要动态计算的输入

函数式依赖在药物分子预处理里特别有用。
比如你想根据化合物批次号,找到对应的参考数据:

def get_reference(wildcards):
    batch = wildcards.batch
    if batch.startswith("A"):
        return "ref/db_v1.csv"
    elif batch.startswith("B"):
        return "ref/db_v2.csv"
    else:
        return "ref/db_default.csv"

rule process_batch:
    input:
        compound = "data/raw/{batch}.sdf",
        reference = get_reference
    output: "data/processed/{batch}_annotated.csv"
    shell: "python scripts/annotate.py {input.compound} {input.reference} {output}"
避坑指南:我曾经在函数里写了个耗时的API调用,结果每次依赖检查都卡死。记住——Snakemake里输入函数在构建DAG时就被调用,不是运行时。所以函数里不要放太重的逻辑,最多做条件判断或字符串拼接。

3.5 单元测试:依赖关系的隐形质检

说到依赖,就不得不提一件事:你怎么确保依赖关系是对的?
我习惯写一个snakemake --dryrun测试:

snakemake --dryrun --configfile config.yaml data/final/feature_matrix.csv

这个命令不会真正执行,只会打印出执行计划。
看它能不能完整列出所有步骤、顺序对不对、有没有缺失的中间文件。

另一个检查是snakemake --dag,输出DOT格式的依赖图。你可以在终端里pipe到dot命令生成图,但我们这章不涉及网络图片,所以我只提一句:依赖关系对了,管道就稳了一半。

嗯,Snakefile基础的三块内容讲到这里。
配置文件管参数、全局参数管运行环境、规则依赖管执行顺序。
这三者合起来,就是一个可重复、可调试、可扩展的预处理管线。

Snakefile 核心三要素结构图 Snakefile 核心三要素 配置文件 config.yaml / .json 全局参数 THREADS, VERBOSE ... 规则依赖 input → output Snakefile 你只需要声明需求 自动推导执行顺序 & 增量更新 缺哪步补哪步,改哪步重跑哪步 核心思想 配置文件管变量 · 全局参数管环境 · 规则依赖管流程
趁热打铁:打开你的终端,创建一个空目录,写一个最简的Snakefile + config.yaml,然后跑snakemake --dryrun看依赖图。先用手跑顺这三元素,后面20多章的动力就全在这儿了。