TT Lab
开始
学习 学习路径 课程

数据流水线

血缘与可复现 — 跑两次,字节是否一致

在 TT Lab 中继续学习

目标

创建转换器 lineage.py,汇总每天投放的订单。把每个产出是由哪些输入(内容哈希)、哪份代码、哪些参数生成的,记录在清单里;用同样的输入运行两次,证明字节相同;并用实验确认某一列来自哪些输入列。

为什么重要

报告数字和会计对不上时,要回答的问题只有一个——这个数字是从哪里来的?靠文件名和修改时间是回答不了的。名字相同但被上游覆盖的情况很常见,而修改时间仅靠复制就会变。能作为依据的,是内容本身的哈希。 而“重新跑一遍看看”要行得通,相同的输入必须得到相同的输出。这不会自动成立。写进产出的当前时间、每次运行新生成的标识符、没有排序的文件列表和集合遍历、浮点数相加的顺序,都会悄悄破坏复现。本实验要把这四样逐个找出来并消除。 谱系也有粗细。数据集级只到“这张表来自那三个文件”,列级则到“这一列来自那几个输入列”。当上游通知说要改某一列时,能回答问题的粗细是后者。而且列谱系只要写下来就会很快过时,所以要用实验重新测量。 评分器不会相信你写出来的文字。它会在临时目录里摆好评分器生成的投放文件,真正运行你的转换器,把汇总值和清单与评分器亲自数出来的值相比对,运行两次比较字节,并逐个对输入列做扰动,确认你写出的列谱系是否相符。店铺名和金额每次运行都会变化。

步骤

  1. 创建并运行 /root/lineage/gen_drops.py,在 /root/lineage/drops 下生成按日期划分的三个投放文件。
  2. 在 /root/lineage/lineage.py 中实现 digest,输出内容哈希、大小和记录数。
  3. 加上 run,输出汇总产出 /root/lineage/out/shops.csv。
  4. 用 run --manifest 一并输出清单 /root/lineage/out/shops.manifest.json。
  5. 修改成运行两次字节相同,并把结果留在 /root/lineage/repro.json 中。
  6. 加上 columns,输出列级谱系,并留在 /root/lineage/columns.json 中。
  7. 加上 verify,按清单重新生成并对照。
  8. 加上 trace,并写出 /root/lineage/lineage_report.md。

参考

生成每天投放的文件

创建并运行 /root/lineage/gen_drops.py,在 /root/lineage/drops 下生成按日期划分的三个投放文件。表头是 order_id,shop,qty,amount,dropped_at,并且必须混有数量为 0 的行,以及同一单据仅金额改变后再次到来的行。

在现场,是先有文件的。这里由我们来生成这些文件。店铺至少要有四个,每个店铺至少要有三行会被接受的行,并且有一个单据号要出现在两个不同的文件里。dropped_at 是汇总不会用到的列,值有多种就行。

用内容而不是名字来判断相同

在 /root/lineage/lineage.py 中实现 digest <파일>(占位符为文件),以 JSON 输出 path、sha256、bytes、rows。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 这八个键。

输入是按名字排序的列表,包含 name、sha256、bytes、rows;code_sha256 是 lineage.py 自身的哈希。参数的默认值也要记下来——默认值以后会变。事后再补一定会漏,所以要在与产出相同的函数里生成。

运行两次,字节相同吗

修改成:用同样的输入运行两次时,产出的字节相同,并且清单除 created_at 之外的所有字段都相同。然后把两次运行的结果以 run_a、run_b、identical、manifest_diff_keys 留在 /root/lineage/repro.json 中。

现在这一版用随机数生成运行标识符,所以清单每次都不同。标识符要从内容派生——把代码哈希、参数和输出哈希连起来再做哈希,相同的运行就会有相同的名字。还要再检查一下文件列表和分组列表是否排过序。Python 每次运行的字符串哈希种子都不同,所以集合的遍历顺序在不同运行之间会变。

用实验测出某一列来自哪里

加上 columns,为每个输出列输出它所依赖的输入列的已排序列表,并把同样的内容留在 /root/lineage/columns.json 中。

不要只写下来就完事,要用实验确认——让一个输入列发生扰动后重新运行,看哪些输出列变了。改变店铺名,输出的行本身就会不同,所以所有列都依赖它。把单据号改成与另一行相同,那一行就会被当作重复而排除,因此记录数、数量和金额会一起变动。汇总完全不读取的列,哪里都不应该出现。

按清单重新生成并对照

加上 verify <매니페스트>(占位符为清单),把输入哈希和代码哈希与当前值比较,并用清单中的输入和参数重新生成,对照输出哈希。只要有一项对不上,退出码就是 4。

对输入,要把消失的和内容改变的分开报告——对调查的人来说,这是两件不同的事。必须用清单中记录的参数重新运行。如果用现在的默认值运行,即使得出了与当时不同的答案,也无法指出原因。

回答这个数字来自哪些输入

加上 trace --manifest <파일> --key <상호>(占位符依次为文件、店铺名),输出该店铺的记录数、数量和金额,以及真正为它贡献了行的输入文件的名字、内容哈希和行数。然后在 /root/lineage/lineage_report.md 中写成四节,标题是 ## 이 숫자는 어디서 왔나(韩文,意为“这个数字从哪里来”)、## 이름과 시각은 왜 근거가 못 되나(韩文,意为“名字和时间为什么不能作为依据”)、## 두 번 돌려 같았는가(韩文,意为“运行两次是否相同”)、## 칼럼 하나가 어디서 왔나(韩文,意为“某一列来自哪里”)。

sources 里只放真正为该店铺贡献了行的文件——读了但一行都没贡献的文件,不属于谱系。哈希直接用清单里记录的值。报告里要写数字,要让调查的人能在自己的文件里把那一行打开看到。