この数字はどこから来たのか — 系譜と再現
一言でいうと
出力物ごとに、どの入力(コンテンツハッシュ)と、どのコードのバージョンと、どのパラメーターで作られたかを横に書いておき、同じ入力でもう一度回してバイトが同じかで、その記録を証明します。
なぜ必要なのか
四半期レポートの売上が、経理側の数字と3,000万ウォンずれています。パイプラインはその日正常に終了し、ログもきれいです。問いは1つです。この数字はどこから来たのか。
答えようとすると、手元にあるものがほとんどありません。出力ファイル1つと、「その日回った」というログ1行。どのドロップファイルが入ったのか、そのファイルが今もそのときと同じ内容なのか、コードがその後に変わっていないか、しきい値のパラメーターがそのときいくつだったか。どれも残っていません。そのため、ほとんどの調査は、「もう一度回してみよう」に行きます。ところが、もう一度回した結果がそのときと違えば、何が変わって違うのかを、また知りません。
ファイル名と更新時刻は、根拠になりません。名前は同じなのに、上流がファイルを上書きした場合がよくあり、更新時刻は、コピー・移動・バックアップの復元だけでも変わります。逆に、内容が変わったのに時刻がそのままの場合もあります。根拠になるのは、内容そのもののハッシュだけです。
このコースとfde-dataが分かれるところ
fde-dataは、顧客が渡したファイル1つの塊を解きほぐす作業です。そのファイルは一度来るだけで、変なら、送る側に聞けばよいです。このコースは違います。毎日同じ場所に同じ名前でドロップが落ち、同じ変換が繰り返し回ります。そのため、問いは「このファイルをどう読むか」ではなく、「3か月前のあの実行は、どのファイルを読んだか」になります。繰り返しがある場所でだけ、リネージが問題になり、繰り返しがあるから、再現が可能になります。
マニフェストに何を書くか
出力物の横に、同じ名前のJSONを1つ残します。書くものは4つです。
- 入力: ファイル名とコンテンツハッシュとバイト数とレコード数。名前だけを書いても意味がありません。
- コード: 変換ツールのソースのハッシュ(またはコミット)。「バージョン1.2」のような、人が付けた名前は、嘘をつきます。
- パラメーター: その実行に渡した値のすべて。デフォルト値も書きます。デフォルト値は、あとで変わります。
- 出力: 出力物のコンテンツハッシュとレコード数。
{
"code_sha256": "9f2c...",
"params": {"min_qty": 1},
"inputs": [{"name": "orders-2026-03-01.csv", "sha256": "4a1e...", "rows": 12}],
"output": {"name": "shops.csv", "sha256": "7b30...", "rows": 4},
"run_id": "a41c9d02f7e3b118",
"created_at": "2026-03-04T02:11:00+00:00"
}
この1枚があれば、前の問いにすべて答えられるようになります。入力ファイルを今もう一度ハッシュしてみれば、そのときと同じかわかり、コードのハッシュを比べれば、その後に変わったかがわかり、もう一度回して出力のハッシュを突き合わせれば、記録が正しいかがわかります。
再現を壊すもの
「同じ入力なら同じ出力」は、自然には成り立ちません。静かに壊すものが4つあります。
1つ目は、現在時刻です。出力物の中に生成時刻を書き込むと、2回回した結果は、必ず違います。時刻が必要なら、出力物ではなくマニフェストに書き、再現の比較から除外される欄だと明記します。
2つ目は、乱数で作った識別子です。実行ごとにuuid4で作ったrun_idをマニフェストに入れると、同じ入力で回しても、マニフェストが違ってきます。識別子は、内容から導出すればよいです。コードのハッシュとパラメーターと出力のハッシュをつなげてハッシュすれば、同じ実行は同じ名前を持ち、違う実行は違う名前を持ちます。
3つ目は、順序が約束されていないものです。os.listdirの順序は決まっておらず、集合(set)の走査順序は、同じプログラムでも実行ごとに変わります(Pythonは、文字列のハッシュに、実行ごとに異なるシードを使います)。そのため、ファイルの一覧もグループの一覧も、ソートして書きます。
4つ目は、浮動小数点を足す順序です。(a + b) + cとa + (b + c)は、浮動小数点では同じ値ではありません。行の順序が揺れれば、合計の最後の桁が揺れ、そうするとバイトが変わります。金額のように、小数の桁が決まっている値は、整数(セント)に変えて足せば、この問題がそもそもなくなります。
データセット単位とカラム単位
リネージには、粒度があります。データセット単位は、「この表は、あの3つのファイルから来た」までです。作りやすく、障害が起きたときに影響範囲をたどるには、これで十分です。
カラム単位は、「この表のamount_centsカラムは、入力のamountとorder_idとshopから来た」まで下ります。上流がカラム1つを変えると知らせてきたときに、私たちの出力物のどの欄が揺れるかを答えられる粒度が、これです。
カラム単位のリネージは、書いておくだけで終わってはいけません。コードが変われば、そのドキュメントはすぐに古くなります。証明する方法は、実験です。入力カラム1つを揺らしてもう一度回し、どの出力カラムが変わるかを見ます。変われば依存があり、変わらなければありません。このように測れば、「使っていると思ったのに使っていないカラム」も、一緒に明らかになります。
現場での姿
- 同じ名前、違う内容。上流が昨日ファイルを直して再度アップロードしたのに、名前が同じで誰も気づきませんでした。コンテンツハッシュを残しておけば、次の実行ですぐに引っかかります。
- 「コードは変えていませんが」。ライブラリのバージョンが上がったか、デフォルトのパラメーターが変わった場合です。コードのハッシュとパラメーターを一緒に書いておけば、この会話は30秒で終わります。
- 再現できない再現スクリプト。調査のためにもう一度回したら、3回とも違う答えが出ます。たいてい、上の4つのうちのどれかです。
- リネージのドキュメントとコードの不一致。Wikiに書かれたリネージは、半年前のものです。実験で測り直さなければ、これに気づく方法がありません。
実務で本当に大切なこと
- 出力物とマニフェストを一緒に作ります。あとで付けると、必ず抜けます。
- 名前と時刻ではなく、コンテンツハッシュで同一性を判定します。
- 再現は、主張ではなく、2回回してバイトを突き合わせた結果で語ります。
- 再現を壊す欄(生成時刻)は1つにまとめ、その欄が比較から外れると書いておきます。
- カラムのリネージは、ドキュメントではなく実験でもう一度測ります。
次のラボですること
毎日落ちてくる注文ドロップを集計する変換ツールlineage.pyを1ステップずつ育てます。コンテンツハッシュを出すdigestから作って、名前と時刻がなぜ根拠にならないかを確認し、集計の出力物とマニフェストを一緒に出し、2回回してバイトが同じかを証明し、カラム単位のリネージを実験で確認し、マニフェストどおりに作り直して突き合わせるverifyと、「その数字がどの入力から来たか」に答えるtraceを付けます。採点ツールは、毎回異なる店舗と金額で自分のドロップを用意して、自分で作った変換ツールを実際に動かし、入力カラムを直接揺らして、自分で書いたカラムのリネージが正しいかを確認します。