結論:結果と一緒に、実際に使った入力・条件・環境を記録する
後から解析をやり直せるようにするには、結果のファイル名だけでなく、その実行が何を使ったかを残す。入力の識別情報、実際に有効だった条件、コードの版、Pythonや主要ライブラリの版、実行日時を一つの記録へまとめる。ファイルの場所だけでは内容が上書きされたか分からないため、入力のハッシュも添えると取り違えを調べやすい。
ハッシュを残すことと入力を保存することは別である。SHA-256の値から元データを復元できるわけではないので、再解析には元のファイルや取得手順も必要になる。環境情報も同様で、バージョンの一覧を残しただけで自動的に環境が復元されるものではない。実行の記録と、入力・コード・環境の保存方法を組み合わせて考えよう。
そのまま動かせる例
例では小さなCSVを一時フォルダーへ作り、入力のSHA-256、条件、コードのハッシュ、環境情報をrun.jsonへ保存して読み戻す。最後に入力を一文字変更して、ハッシュが変わることも確認する。NumPy自体の計算は使わないが、そのインストール済みの配布バージョンを取得するためNumPyが導入された環境で実行する。
この例のファイルは一時的で、終了時に削除される。実運用では結果と同じ実行IDのフォルダーなど、後から対応が分かる場所へ保存してほしい。started_at_utcは実行のたびに変わるので、掲載出力は実際の時刻文字列ではなくUTCのオフセットを含むことを確認した結果を示している。入力名は相対的な名前だけを記録し、作業環境の絶対パスを公開しない。
from datetime import datetime, timezone
from importlib.metadata import version
from pathlib import Path
from tempfile import TemporaryDirectory
import hashlib
import json
import platform
def sha256_file(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(65536), b""):
digest.update(block)
return digest.hexdigest()
with TemporaryDirectory() as folder:
root = Path(folder)
source = root / "input.csv"
source.write_bytes(b"x,y\n1,2\n3,4\n")
cfg = {"method": "mean", "threshold": 0.25}
record = {
"schema_version": 1,
"run_id": "demo-run",
"started_at_utc": datetime.now(timezone.utc).isoformat(),
"input": {"name": source.name, "bytes": source.stat().st_size,
"sha256": sha256_file(source)},
"code_sha256": sha256_file(Path(__file__)),
"conditions": cfg,
"environment": {"python": platform.python_version(),
"system": platform.system(), "numpy": version("numpy")},
}
path = root / "run.json"
path.write_text(json.dumps(record, ensure_ascii=False, indent=2,
allow_nan=False), encoding="utf-8")
restored = json.loads(path.read_text(encoding="utf-8"))
assert restored == record
assert restored["input"]["sha256"] == hashlib.sha256(source.read_bytes()).hexdigest()
assert len(record["code_sha256"]) == 64
before = record["input"]["sha256"]
source.write_bytes(b"x,y\n1,2\n3,5\n")
assert sha256_file(source) != before
print("input SHA-256:", before)
print("conditions:", restored["conditions"])
print("environment:", restored["environment"])
print("UTC offset recorded:", restored["started_at_utc"].endswith("+00:00"))
print("JSON round trip and changed-input detection: True")
実行結果
input SHA-256: 2a2b86e74ffd5e6a9b75e52a105cf9d02920837179f8e8961aa15411d380f7a3
conditions: {'method': 'mean', 'threshold': 0.25}
environment: {'python': '3.12.14', 'system': 'Linux', 'numpy': '2.3.5'}
UTC offset recorded: True
JSON round trip and changed-input detection: True
ハッシュは内容の取り違えを見つける手掛かり
sha256_fileはファイルを小分けに読み、順番にハッシュへ加える。巨大ファイルでも全内容を一つのbytesにする必要はない。ただしハッシュを計算するには全バイトを読むため、入力が大きければ相応のI/O時間はかかる。途中で別の処理がファイルを書き換えれば、解析に使った内容と記録の対応が崩れる可能性があるので、入力を固定して扱いたい。
ハッシュはバイト列に対して計算される。数値として同じCSVでも改行コード、列順、文字コード、余分な空白などが変われば異なる値になる。それは取り違え確認には有用だが、意味的に同じデータかどうかの比較とは違う。またSHA-256の一致だけで出所の真正性まで証明できるわけではなく、信頼できる保存先や配布経路の管理も別に必要になる。
コードと有効な条件を残す
設定ファイルを保存していても、CLI引数や既定値で上書きされていたら実際の条件と違う場合がある。記録には、全てを統合して処理へ渡した最終的な値を残すとよい。乱数を使うならseedだけでなく生成方式や条件への系列割当てなども関係する。機密情報の入った設定を丸ごと保存せず、再現に必要な項目を明示的に選ぶことも大切である。
例のcode_sha256はこのスクリプト一つの内容を識別する。複数モジュールへ分かれた実際のプロジェクトでは、入口ファイルだけのハッシュでは不十分である。GitのコミットID、未コミット変更の有無、配布物の版など、使ったコード一式へたどれる情報を残そう。コードを更新した後でも当時の版を取得できる保存方針が必要となる。
環境情報と記録形式の限界
platformはPythonやOSの識別情報、importlib.metadataは配布パッケージの情報を取得できる。ただし同じバージョン番号でもOS、CPU、数値計算バックエンド、並列数などが結果へ影響する場合がある。目的に応じて環境を固定し、どこまでの再現性を求めるかを決める。この例の短い一覧を、あらゆる解析の完全な再現条件だと扱わないようにしたい。
記録形式にはschema_versionを付け、後で項目を変更した際に読み分けられるようにする。JSONの読み戻しで内容を確認し、日時・非有限値・NumPy値など保存できない型は明示的に変換する。長時間の処理では開始記録だけでなく、終了状態や出力の識別情報も追加すると、途中失敗した結果を完成品と間違えにくい。必要な記録を自動で残し、人の記憶へ依存しない運用にしていこう。
確認環境と参考資料
例はLinux・CPython 3.12.14・NumPy 2.3.5で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。
- Python公式:hashlib(2026年10月2日参照)
- Python公式:platform(2026年10月2日参照)
- Python公式:importlib.metadata(2026年10月2日参照)
関連項目:別のPCで環境を再現する:依存関係の記録と入れ直し / 乱数実験の再現と系列分割を両立する / TOML設定ファイルをtomllibで読み込み、入力ミスを早めに見つける / JSONで保存できない値をどう扱う?日時・数値・日本語の受け渡し
