Claude CodeでSnakemakeの解析パイプラインを組み再現性を確かめる
Snakemakeのルールと入出力をClaude Codeに書かせ、dry-runとハッシュ比較で再実行しても同じ結果になるかを確かめる手順です。
Snakemakeは、ルールごとに入力と出力を宣言して解析をつなぐワークフロー管理ツールです。Claude Codeにこれを書かせると、解析の手順がスクリプトの実行順ではなくファイルの依存関係として残ります。ただし、書けたことと、再実行しても同じ結果になることは別の話です。この記事では、小さな解析パイプラインをClaude Codeに組ませ、dry-run、出力を消しての再実行、ハッシュ比較の3段階で再現性を確かめます。手順は手元のSnakemake 9.27.0(Python 3.12、macOS)で実際に流したものです。
研究環境の仕組みとして再現性を扱った記事にはClaude Scienceの監査可能な成果物があります。こちらは、自分のコードで再現性を確かめる側の手順です。
「再現できた」を3つの確認に分ける
再現性という言葉は広すぎるので、確認できる単位に分けます。Snakemakeのコマンドと対応づけると次のとおりです。
| 確認したいこと | 使うもの | 分かること |
|---|---|---|
| 依存関係が宣言どおりか | 使うものsnakemake -n | 分かること何がなぜ実行されるか |
| 出力を消して作り直せるか | 使うもの--delete-all-output と再実行 | 分かること手作業の中間ファイルに頼っていないか |
| 結果が同じか | 使うものshasum -a 256 -c | 分かることバイト単位で一致するか |
3つ目が最も強い確認です。ファイルの更新日時ではなく中身を見るので、乱数やソフトウェアのバージョンが原因の差も拾えます。逆に言えば、1つ目と2つ目が通っても、3つ目が通るとは限りません。
最小のワークフローをClaude Codeに書かせる
題材は、乱数でデータを作り(simulate)、平均と標準偏差を出す(summarize)2段の解析です。Claudeには次のように頼みます。
Snakemakeで2段のワークフローを組んで。
simulateルールがconfig/config.yamlのseedとnからresults/raw.csvを作り、
summarizeルールがresults/summary.csvを作る。
最初にrule allを置き、スクリプトはscripts/に分けて入力に宣言すること。出てくる構成は次の形になります。これは手元で動かしたSnakefileです。
configfile: "config/config.yaml"
rule all:
input: "results/summary.csv"
rule simulate:
input: script="scripts/simulate.py"
output: "results/raw.csv"
params: seed=config["seed"], n=config["n"]
shell: "python {input.script} {params.seed} {params.n} {output}"
rule summarize:
input: data="results/raw.csv", script="scripts/summarize.py"
output: "results/summary.csv"
shell: "python {input.script} {input.data} {output}"見るべき点は3つです。
- 先頭の
rule allに、ほしい最終成果物だけを並べています。チュートリアルも、先頭に置くことをベストプラクティスとしています。ルールの並び順はDAG(実行順の依存グラフ)の作成に影響しません。 - 乱数のseedを
config/config.yamlに出し、paramsで渡しています。コードに埋め込まない形です。 - スクリプトを
inputに宣言しています。理由は後の節で実測の結果とあわせて説明します。
Claudeに任せると、スクリプトをshell:の文字列に直書きしたSnakefileが出てくることがあります。動きはしますが、その形だとスクリプトの編集が再実行のきっかけになりません。「スクリプトは入力に宣言すること」は、頼む側が最初から指定しておくほうが早い項目です。
dry-runで実行計画を読む
書けたら、まず何も実行せずに計画を出させます。
snakemake -n-nは--dry-runの短縮形で、公式は「何も実行せず、何が行われるかを表示する」と説明しています。初回の出力には、ジョブごとにreason:が付きます。手元では次のようになりました。
rule summarize:
input: results/raw.csv
output: results/summary.csv
reason: Missing output files: results/summary.csv; Input files updated by another job: results/raw.csv
...
Job stats:
job count
--------- -------
simulate 1
summarize 1
all 1
total 3見るのはJob statsの件数と、reasonの理由です。想定したジョブ数と一致しているか、「Missing output files」で動くべきジョブだけが動くかを確かめます。ここで想定外の件数が出るなら、入出力の宣言に漏れがあります。
Claudeには、この出力を自分で読ませます。「snakemake -nを実行して、Job statsの合計が3であることを確認してから次に進む」と指示しておくと、宣言の誤りをその場で直せます。出力を貼って判断を仰ぐ往復が減ります。
出力を消して作り直し、ハッシュを比べる
実行し、最初のハッシュを記録します。
snakemake --cores 1
shasum -a 256 results/*.csv > results.sha256--coresは並列に使うコア数です。省略すると利用可能なCPUコア数になります。この回の記録は次のとおりです。
0ba2b133a3393ae297ce944380f789bd370c1837a27bde574acfc1b27b107732 results/raw.csv
79badceef2f2ec33cbc38d4167e97e73c129033b296be1e662cf2c8dc4bcdde1 results/summary.csv次に、--delete-all-outputでワークフローが作った出力を消し、もう一度実行して照合します。この操作はscripts/verify.shにまとめました。
#!/bin/sh
set -eu
snakemake --cores 1 -q --delete-all-output
snakemake --cores 1 -q
shasum -a 256 -c results.sha256実行結果は次のとおりです。
results/raw.csv: OK
results/summary.csv: OK消して作り直しても、2つのファイルのハッシュが最初の実行と一致しました。この手元のワークフローでは、seed固定の再実行がバイト単位で一致したということです。
公式は--delete-all-outputについて、--dry-runと併せると実際に消さずに一覧を出せ、書き込み保護されたファイルは消さないと説明しています。Claudeに実行させる前に、snakemake --delete-all-output -nで消える対象を見ておくと安全です。
設定やスクリプトを変えると何が再実行されるか
再現性の確認は、変えたときに正しく作り直されるかの確認でもあります。手元で3つの変更を試しました。
| 変更 | snakemake -nの判定 |
|---|---|
config.yamlのseedを42から43に変更 | snakemake -nの判定simulateがParams have changedで再実行、下流も連鎖 |
scripts/simulate.pyを編集(入力に宣言済み) | snakemake -nの判定Updated input files: scripts/simulate.pyで再実行 |
Snakefileにinputを足す | snakemake -nの判定Set of input files has changedとCode has changedで再実行 |
どれもreasonに理由が出るので、Claudeに「なぜ再実行されるのか」を説明させる材料になります。
注意したいのは、スクリプトを入力に宣言しなかった場合です。最初にshell:へスクリプトのパスを直書きした版では、scripts/simulate.pyの末尾にコメントを1行足しても、snakemake -nはNothing to be doneでした。Snakemakeが見ているのはshellの文字列(コード)と宣言した入力、paramsなどで、shellに書かれたPythonファイルの中身までは追っていないためと考えられます。公式のCLI説明にも、再実行の契機はcode、input、mtime、params、software-envの5種類とあるだけで、外部スクリプトの中身を見るとは書かれていません。
したがって、Claudeに書かせるときは次の2点が必要です。
- スクリプトは
inputに宣言する(またはscript:ディレクティブを使う) - 手で書き換えたら、
-nのreasonに出ることを確かめる
契機は--rerun-triggersで絞れます。公式は、既定では全種類を使うことで「結果がワークフローのコードや設定と一貫することを保証する」と説明し、従来のように更新日時だけを見たいなら--rerun-triggers mtimeを使うとしています。再現性の確認では既定のままにします。
--list-changes paramsや--summaryも使えます。--summaryは、生成された各ファイルの更新日時、生成したルール、状態、次の実行で更新される計画を表にして出します。
seedを固定しないと何が起きるか
ハッシュ比較の価値は、失敗する場面で分かります。simulate.pyのseedを外し、random.Random()にして、-F(全ルールの強制実行)を2回続けて流した結果が次です。
fe2acbdd18c405304a1558158c263726383711697a5f5bd40b0445461bbe7782 results/raw.csv
4154c8a2918edac7de6b98b8276fe5f4faa477009ec78a79970433299fccb4e7 results/raw.csv同じSnakefile、同じ入力で、ハッシュが変わりました。Snakemakeは何も警告しません。依存関係は正しく宣言されているからです。-nも2回目の実行も成功します。差に気づけるのは、ハッシュの照合だけです。
この種の見落としは、Claudeが「動くコード」を書く過程で起きやすいものです。乱数を使うコードを書かせたら、seedを引数にしてconfigから渡すところまで指示に含めます。
環境の固定はSnakemakeの外側の話
ここまでの確認は、同じマシン、同じPythonでの再現でした。別のマシンで同じ結果になるかは、ソフトウェアの環境次第です。snakemake --lintは、そのことを教えてくれます。手元のワークフローに実行すると、ルールごとに次の2つが指摘されました。
logディレクティブが無い。出力が端末に混ざり、並列ジョブのエラーが読みにくくなるconda環境またはコンテナを指定していない。使うソフトウェアが文書化されず、前提なしに別のマシンで動かせない
公式のデプロイ説明では、ルールごとにconda:で環境ファイルを指定できます。ファイルの隣に<platform>.pin.txtがあれば、その内容でパッケージを決めるため、ビルド単位まで同じ環境にできます。ピン留めはsnakedeploy pin-conda-envs envs/ggplot.yamlのように生成します。コンテナを使うなら、ルールにcontainer:でdocker://のイメージを指定します。
環境を変えたときの再実行は、software-envの契機が担います。Claudeには、まず--lintの指摘を読ませ、logの追加と環境の宣言を直させるとよいでしょう。環境ファイルが増えれば、ハッシュ比較は別のマシンでも意味を持つようになります。
CLAUDE.mdに書く規約
検証の型が決まったので、Claudeに守らせるためのCLAUDE.mdを用意します。長さは200行以内を目安にするのが公式の勧めです。長くなると遵守率が下がります。
## Snakemakeワークフローの規約
- 完了条件は `sh scripts/verify.sh` が exit 0 で終わること
- Snakefile変更後は先に `snakemake -n` を実行し、Job statsの合計と reason を読む
- スクリプトは必ず rule の input に宣言する。shell 文字列に直書きしない
- 乱数は seed を config.yaml から受け取り、引数で渡す
- results.sha256 は人間がレビューした後にだけ更新する。Claudeは書き換えない
- 解析の出力は results/ に限る。手で作った中間ファイルを使わないポイントは最後から2番目の項目です。ハッシュの基準ファイルをClaudeが書き換えられるなら、照合が通らないときに基準のほうを作り直して通してしまう可能性があります。基準は人間が更新する運用にしておきます。
permissionsで実行の線引きを決める
.claude/settings.jsonで、検証に必要なコマンドだけを確認なしで通します。
{
"permissions": {
"allow": [
"Bash(snakemake -n)",
"Bash(snakemake --lint)",
"Bash(sh scripts/verify.sh)"
],
"ask": [
"Bash(snakemake --delete-all-output *)",
"Bash(snakemake --cleanup-metadata *)"
]
}
}公式によると、ルールはdeny、ask、allowの順に評価され、最初に一致したものが結果を決めます。*を含まないルールは完全一致なので、Bash(snakemake -n)はsnakemake -nだけを通します。--delete-all-outputはverify.shの中では確認なしで動きますが、単独で呼んだときは確認に回ります。
scripts/verify.shをallowに入れると、中の消去と再実行が確認なしで走ります。スクリプトの中身を変えさせたくないなら、そのファイルの編集を別途askに入れる運用が必要です。権限ルールの管理はClaude Codeの/permissionsコマンドにまとめています。
このやり方で確認できないこと
ハッシュが一致しても、保証されるのは「同じ環境で同じ入力から同じ出力が出た」ことまでです。
- 外部のデータベースやAPIを読む工程は、ソース側が変わると結果が変わる
- 浮動小数点の演算結果が、CPUやライブラリのバージョンで末尾の桁だけ違う場合は、バイト一致で失敗する。許容誤差で比べる検証に切り替える
- 並列実行でファイルの書き出し順が変わる工程は、出力がジョブの実行順に依存する
最後の2つは、判定基準の設計になります。Claudeに「許容誤差1e-9で比較する」とはっきり頼んでください。曖昧な指示だと、一致しない原因を探す前に基準を緩める方向で直されます。
同じ発想で依存を固定する例はClaude Codeでrenvを使いRの解析を進めるにあります。別言語から移した数値を検算する手順はMATLABからPythonへの移植で扱っています。
まとめ
再現性は、宣言(-n)、作り直し(--delete-all-output)、中身の照合(ハッシュ)の3段で確かめます。seedを外した実験が示すとおり、Snakemakeの成功表示は結果の一致を保証しません。Claudeには「ハッシュを通す」ことを完了条件として渡し、基準ファイルの更新は人間の側に残しておくのが、この構成の要になります。