第3章:Snakefile基础编写:配置文件、全局参数、规则依赖
说到Snakefile,有朋友觉得它就是个花哨的Makefile。
嗯,我第一次接触时也是这么想的。但真正上手药物分子预处理项目后,我才发现它的设计思维完全不同。
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]}"
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.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最厉害的地方,就是自动推导执行顺序。
你只需要声明每条规则的输入和输出,它就能画出整个依赖图。
咱们看个典型的分子预处理流程:
- 原始SDF → 统一CSV格式
- 统一CSV → 清洗(去重、去盐、标准化)
- 清洗后数据 → 计算分子描述符
- 描述符 → 合并为最终特征矩阵
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}"
3.5 单元测试:依赖关系的隐形质检
说到依赖,就不得不提一件事:你怎么确保依赖关系是对的?
我习惯写一个snakemake --dryrun测试:
snakemake --dryrun --configfile config.yaml data/final/feature_matrix.csv
这个命令不会真正执行,只会打印出执行计划。
看它能不能完整列出所有步骤、顺序对不对、有没有缺失的中间文件。
另一个检查是snakemake --dag,输出DOT格式的依赖图。你可以在终端里pipe到dot命令生成图,但我们这章不涉及网络图片,所以我只提一句:依赖关系对了,管道就稳了一半。
嗯,Snakefile基础的三块内容讲到这里。
配置文件管参数、全局参数管运行环境、规则依赖管执行顺序。
这三者合起来,就是一个可重复、可调试、可扩展的预处理管线。
snakemake --dryrun看依赖图。先用手跑顺这三元素,后面20多章的动力就全在这儿了。