系譜と再現 — 二度動かしてバイトが同じか
目標
毎日落ちてくる注文ドロップを集計する変換ツールlineage.pyを作ります。出力物ごとに、どの入力(コンテンツハッシュ)と、どのコードと、どのパラメーターで作られたかをマニフェストに残し、同じ入力で2回回して、バイトが同じかを証明し、カラム1つがどの入力カラムから来たかを、実験で確認します。
なぜ重要なのか
レポートの数字が経理とずれたときに、答える必要がある問いは1つです。この数字はどこから来たのか。ファイル名と更新時刻では答えられません。名前は同じなのに上流が上書きした場合がよくあり、更新時刻は、コピーだけでも変わります。根拠になるのは、内容そのもののハッシュです。そして、「もう一度回してみよう」が通用するには、同じ入力が同じ出力を出す必要があります。それは自然には成り立ちません。出力物に書き込んだ現在時刻、実行ごとに新しく作った識別子、ソートしていないファイルの一覧と集合の走査、浮動小数点を足す順序が、静かに再現を壊します。このラボは、その4つを1つずつ見つけて取り除きます。リネージには、粒度もあります。データセット単位は、「この表は、あの3つのファイルから来た」までで、カラム単位は、「このカラムは、あの入力カラムから来た」までです。上流がカラム1つを変えると知らせてきたときに、答えられる粒度は、後者です。そして、カラムのリネージは、書いておくとすぐに古くなるため、実験でもう一度測ります。採点ツールは、提出された文言を信じません。一時ディレクトリに、採点ツールが作ったドロップを用意して、自分で作った変換ツールを実際に実行し、集計値とマニフェストを、採点ツールが直接数えた値と突き合わせ、2回回してバイトを比べ、入力カラムを1つずつ揺らして、自分で書いたカラムのリネージと合っているかを確認します。店舗と金額は、実行ごとに変わります。
ステップ
- /root/lineage/gen_drops.pyを作成して実行し、/root/lineage/dropsの下に、日付ごとのドロップを3つ作ってください。
- /root/lineage/lineage.pyに
digestを作り、コンテンツハッシュとサイズとレコード数を出力させてください。 runを追加して、集計の出力物/root/lineage/out/shops.csvを出力させてください。run --manifestで、マニフェスト/root/lineage/out/shops.manifest.jsonを一緒に出力させてください。- 2回回してバイトが同じになるように直し、その結果を/root/lineage/repro.jsonに残してください。
columnsを追加して、カラム単位のリネージを出力し、/root/lineage/columns.jsonに残してください。verifyを追加して、マニフェストどおりに作り直して、突き合わせさせてください。traceを追加して、/root/lineage/lineage_report.mdを書いてください。
参考
- 実行の契約:
python3 /root/lineage/lineage.py <명령> ...(プレースホルダーはコマンドです)。答えは、JSONの1つの塊として標準出力に出します。成功すれば終了コード0、ファイルがなければ3、verify・traceがずれれば4、使い方が間違っていれば2です。 - ドロップファイルの名前は
orders-<날짜>.csv(プレースホルダーは日付です)で、ヘッダーはorder_id,shop,qty,amount,dropped_atです。dropped_atは、集計が使わないカラムです。 - 集計ルール(順序が答えを変えます): ファイルを名前順に読み、同じ
order_idは最初に出た行だけを残し(重複排除を先に行います)、そのあとqtyがmin_qty未満の行を捨て、shopでまとめます。min_qtyのデフォルト値は1です。 - 出力物のヘッダーは
shop,orders,qty,amount_centsで、shopの昇順にソートします。行末はLFです。amount_centsは、amountを整数のセントに変えて足した値です。浮動小数点を使わなければ、足す順序が答えを変えられません。 digestの応答:{"path": 절대경로, "sha256": 문자열, "bytes": 정수, "rows": 정수}(プレースホルダーは、絶対パス、文字列、整数です)。rowsは、ヘッダーを除いたレコード数です。runの応答:{"out": 절대경로, "rows": 정수, "sha256": 문자열, "min_qty": 정수}(プレースホルダーは、絶対パス、整数、文字列です)で、マニフェストを出力したなら、manifestとrun_idが加わります。- マニフェストのキー:
tool・code_sha256・drops_dir・params・inputs・output・run_id・created_at。inputsは名前順の一覧で、項目ごとにname・sha256・bytes・rowsです。outputはname・path・sha256・rowsです。code_sha256は、lineage.py自身のコンテンツハッシュです。 - 再現の比較から外れる欄は、
created_atだけです。残りは、2回回しても同じである必要があります。 columnsの応答とcolumns.json: 出力カラム名をキーとし、そのカラムが依存する入力カラム名のソートされた一覧を値にします。判定の基準は実験です。その入力カラムを揺らしたときに、この出力カラムの値が変わるなら、依存があり、出力の行(キー)そのものが変わるなら、すべての出力カラムが、その入力カラムに依存します。verifyの応答:{"manifest": 절대경로, "ok": 참거짓, "input_missing": [이름], "input_changed": [이름], "code_changed": 참거짓, "output_changed": 참거짓}(プレースホルダーは、絶対パス、真偽値、名前です)。1つでもずれれば、終了コード4です。traceの応答:{"key": 상호, "orders": 정수, "qty": 정수, "amount_cents": 정수, "sources": [{"name": 이름, "sha256": 문자열, "rows": 정수}]}(プレースホルダーは、店舗、整数、名前、文字列です)。sourcesは、その店舗の行を実際に足したファイルだけを名前順に入れ、rowsは、そのファイルが足した行数です。ない店舗なら、終了コード4です。- 再現を壊すもの: 出力物の中の現在時刻、乱数で作った識別子、
os.listdirと集合(set)の走査順序、浮動小数点を足す順序。Pythonは、実行ごとに文字列のハッシュのシードが違うため、集合の走査順序が、実行の間で変わります。 - 公式ドキュメント: hashlib・csv・json・os.listdir・PYTHONHASHSEED
- よくある間違い: ファイル名と更新時刻で同一性を判定する、マニフェストをあとで付ける、デフォルト値のパラメーターを書かない、ソートなしで
os.listdirを使う。 - 2回回した結果を目で比較するには、
cmp -s 파일A 파일Bまたはsha256sum 파일A 파일B(どちらのプレースホルダーも、ファイルAとファイルBです)を使ってください。
毎日落ちてくるドロップを作る
/root/lineage/gen_drops.pyを作成して実行し、/root/lineage/dropsの下に、日付ごとのドロップを3つ作ってください。ヘッダーはorder_id,shop,qty,amount,dropped_atで、数量が0の行と、同じ伝票が金額だけ変わってもう一度来た行が、混ざっている必要があります。
現場では、ファイルが先にあります。ここでは、そのファイルを私たちが作ります。店舗は4つ以上、店舗ごとに受け入れられる行が3つ以上あり、伝票番号の1つは、異なる2つのファイルに出てくる必要があります。dropped_atは、集計が使わないカラムなので、値が複数種類あればよいです。
名前ではなく内容で同一性を判定する
/root/lineage/lineage.pyにdigest <파일>(プレースホルダーはファイルです)を作り、path・sha256・bytes・rowsをJSONで出力させてください。rowsは、ヘッダーを除いたレコード数です。
ファイルをバイナリで小分けにして読み、hashlib.sha256に入れます。同じ内容を別の名前でコピーしたり、touchで更新時刻だけを変えたりしてみて、ハッシュがそのままかを自分で確認してください。レコード数は、行数ではなく、csvで数える必要があります。
集計の出力物を出す
run --drops <디렉터리> --out <파일> [--min-qty N](プレースホルダーは、ディレクトリとファイルです)を追加して、/root/lineage/out/shops.csvを出力させてください。ファイルを名前順に読み、同じ伝票は最初に出た行だけを残し、そのあとqtyがmin_qty未満の行を捨て、shopでまとめて、shop,orders,qty,amount_centsを、店舗の昇順で書きます。
重複排除を数量のフィルターより先に行うという点が、答えを変えます。最初に出た行の数量が0なら、その伝票は丸ごと抜けます。金額は、文字列を整数のセントに変えて足してください。ファイルの一覧は、ソートして読みます。os.listdirの順序は、約束されたものではありません。
出力物とマニフェストを一緒に出す
run --manifest <파일>(プレースホルダーはファイルです)を追加して、/root/lineage/out/shops.manifest.jsonを一緒に出力させてください。tool・code_sha256・drops_dir・params・inputs・output・run_id・created_atの8つのキーを入れます。
入力は、名前順の一覧で、name・sha256・bytes・rowsを入れ、code_sha256は、lineage.py自身のハッシュです。パラメーターは、デフォルト値も書いてください。デフォルト値は、あとで変わります。あとで付けると必ず抜けるため、出力物と同じ関数の中で作ってください。
2回回してバイトが同じか
同じ入力で2回回したときに、出力物のバイトが同じで、マニフェストがcreated_atを除くすべての欄で同じになるように直してください。そして、2回回した結果を、/root/lineage/repro.jsonにrun_a・run_b・identical・manifest_diff_keysとして残してください。
今のバージョンは、実行の識別子を乱数で作っているため、マニフェストが毎回違います。識別子は、内容から導出してください。コードのハッシュとパラメーターと出力のハッシュをつなげてハッシュすれば、同じ実行は同じ名前を持ちます。ファイルの一覧とグループの一覧をソートしたかも、もう一度見てください。Pythonは、実行ごとに文字列のハッシュのシードが違うため、集合の走査順序が、実行の間で変わります。
カラム1つがどこから来たかを実験で測る
columnsを追加して、出力カラムごとに依存する入力カラムのソートされた一覧を出力させ、同じ内容を/root/lineage/columns.jsonに残してください。
書いておくだけで終わらせず、実験で確認してください。入力カラム1つを揺らしてもう一度回し、どの出力カラムが変わるかを見ます。店舗を変えると、出力の行そのものが変わるため、すべてのカラムがそこに依存します。伝票番号を、ほかの行と同じにすると、その行が重複として抜けるため、件数と数量と金額が一緒に動きます。集計がまったく読まないカラムは、どこにも出てこない必要があります。
マニフェストどおりに作り直して突き合わせる
verify <매니페스트>(プレースホルダーはマニフェストです)を追加して、入力ハッシュとコードのハッシュを現在の値と比べ、マニフェストの入力とパラメーターで作り直して、出力のハッシュを突き合わせさせてください。1つでもずれれば、終了コード4です。
入力は、なくなったものと、内容が変わったものを分けて報告してください。調査する人にとっては、別の話だからです。マニフェストに書かれたパラメーターで、もう一度回す必要があります。今のデフォルト値で回すと、そのときと違う答えが出ても、原因を指摘できません。
その数字がどの入力から来たかに答える
trace --manifest <파일> --key <상호>(プレースホルダーは、ファイルと店舗です)を追加して、その店舗の件数と数量と金額、そして行を実際に足した入力ファイルを、名前とコンテンツハッシュと行数で出力させてください。そして、/root/lineage/lineage_report.mdに、## 이 숫자는 어디서 왔나、## 이름과 시각은 왜 근거가 못 되나、## 두 번 돌려 같았는가、## 칼럼 하나가 어디서 왔나の4つの節で書いてください(韓国語の見出しで、順に「この数字はどこから来たか」「名前と時刻はなぜ根拠にならないか」「2回回して同じだったか」「カラム1つがどこから来たか」を意味します)。
sourcesには、その店舗の行を実際に足したファイルだけを入れます。読みはしたものの、1行も足さなかったファイルは、リネージではありません。ハッシュは、マニフェストに書かれた値をそのまま使ってください。レポートには、数字を書いてください。調査する人が、自分のファイルでその行を開いて見られる必要があります。