平均值有两个 — 用规则定义「缺失」与「相同」
目标
构建工具 nulls.py:按类型统计“没有”的各种写法,用数字展示把缺失值填成 0 时平均值如何变化,区分完全重复、键重复和只在人眼里相同的情况,并通过规范化、分块和判定规则把同一个人归为一组。
为什么重要
看着同一个文件却算出两个平均值,是常有的事。因为一方把没有值的行排除在外来计算,而另一方把它填成 0 来计算。两者都不会报错,所以没有人察觉。因此说平均值时必须同时说分母,而要做到这一点,就得先按写法统计条数。 0 和没有是不同的事实。金额为 0 韩元的客户是“还没买过”,空白的客户是“不知道”。文件里如果写的是 0,光看文件无法知道是哪一种,所以要把条数另行统计并报告,并去问业务方。 重复也不止一种。一字不差的行只保留一条即可,但标识符相同而内容不同的行无法自动修复,只是写法不同的同一个对象则必须靠规则来判定。这里最常见的事故,是仅仅因为规范化后的值变得相同就合并。 评分器不会相信你的文字。它在临时目录里摆好评分器生成的名单,真实运行你的工具,并把条数、平均值和归成一组的分组与它自己数出来的值进行核对。姓名和金额每次运行都会变。
步骤
- 创建并运行 /root/miss/gen_customers.py,生成 /root/miss/raw/customers.csv。其中包含缺失值的写法和三种重复。
- 在 /root/miss/nulls.py 中实现
census <파일> <칼럼>(占位符依次为文件、列名),让它按类型统计“没有”的各种写法。 - 增加
stats <파일> <칼럼>(占位符依次为文件、列名),让它并排给出把缺失值填成 0 的平均值、去掉缺失值的平均值,以及连 0 也去掉的平均值,并把结果写入 /root/miss/stats.json。 - 增加
aging <파일>(占位符为文件),测量哨兵日期如何破坏平均处理周期,并把结果写入 /root/miss/aging.json。 - 增加
dupes <파일>(占位符为文件),让它区分统计完全重复和键重复。 - 增加
blocks <파일>(占位符为文件),让它规范化之后按电话号码后四位分块,并给出需要比较的候选配对减少到了多少个。 - 增加
match <파일>(占位符为文件),让它用判定规则把同一个人归为一组,并测量分块漏掉的配对。规则写入 /root/miss/rules.json。 - 一次性处理全部内容,生成 /root/miss/miss_report.json 和 /root/miss/miss_report.md。
参考
- 运行契约:
python3 /root/miss/nulls.py <명령> <파일> [칼럼](占位符依次为命令、文件、列名)。census 和 stats 还要再接收一个列名,aging、dupes、blocks、match 只接收文件。成功时退出码为 0,文件不存在时为 3,命令或参数个数有误时为 2。 - 视为“没有”的写法是空白以及
NULL、NA、N/A、NONE、-、?,不区分大小写。哨兵日期是9999-12-31。0不算作“没有”,而是另行统计:因为光看文件无法知道它是真正的 0,还是被填进去的 0。这份清单是本实验的假设。 census的响应:{"column": 이름, "total": 정수, "blank": 정수, "tokens": {"NULL": 정수, ...}, "zero": 정수, "sentinel": 정수, "present": 정수}(占位符依次为名称、整数、整数、整数、整数、整数、整数)。tokens 的键统一成大写。present 是 total 减去 blank、sentinel 和 tokens 之和的值。stats的响应:column、n_all、sum_naive、mean_naive、n_known、sum_known、mean_known、n_nonzero、mean_nonzero、gap。平均值四舍五入到小数点后第二位。mean_naive是把缺失值填成 0 的结果,所以分母是 n_all;mean_known的分母是 n_known。gap 是 mean_known 减去 mean_naive 的值。aging的响应:n_all、n_closed、open_count、unknown、avg_days_naive、avg_days_closed。avg_days_naive是把哨兵日期原样当作日期接受后得出的平均值,avg_days_closed是去掉哨兵日期后的平均值。开始日期或结束日期为“没有”的,计入 unknown。dupes的响应:rows、exact、key_dupes、keys。exact 是同一行多次出现的超出部分的条数,key_dupes 是record_id出现两次以上的键的个数。- 规范化规则:姓名依次为去掉前后空格,去掉公司类型写法(
(주)、㈜、(유)、주식회사、유한회사;韩文,依次意为“(株)”“㈜”“(有)”“股份有限公司”“有限公司”),合并成一个空格,转为小写。邮箱去掉前后空格后转为小写。电话号码只保留数字。 - 块键是规范化后电话号码的后四位,不足四位时为
NONE。blocks的响应:rows、blocks、candidate_pairs、pairs_without_blocking。 - 判定规则:规范化后的邮箱不为空且相同,就是同一个人;或者规范化后的姓名和电话号码都不为空且相同,也是同一个人。
match的响应:rows、clusters、merged、groups、recall_loss。groups 只放归并了两行以上的组,每个组是一个数组,按升序放入去掉表头之后的行号(从 1 开始)。recall_loss 是用全量比较找到的配对数,减去在块内找到的配对数的值。 - 官方文档:python csv · python statistics · python datetime · python collections
- 常见错误:把缺失值填成 0,分母却仍用全部行数;把哨兵日期当作日期来计算;把邮箱为空的行归为一组;因为规范化后的值变得相同就合并;不测量分块的损失。
生成合并自三个系统的名单
创建并运行 /root/miss/gen_customers.py,生成 /root/miss/raw/customers.csv。其中同时包含五种“没有”的写法,以及完全重复、键重复和只是写法不同的同一个人。
先生成 36 个人,再把其中 12 人只改变商号写法和电话号码格式后重新放进去。然后加入把前面的行原样复制的完全重复,以及只有 record_id 重叠而人不同的行。金额和结束日期中混入空白、NULL、NA、连字符、0 和哨兵日期。
按类型统计“没有”的写法
在 /root/miss/nulls.py 中实现 census <파일> <칼럼>(占位符依次为文件、列名),让它给出 total、blank、tokens、zero、sentinel、present。0 不算作“没有”,而是另行统计。
只去掉值的前后空格并统一成大写,再与清单比较。不把 0 放进“没有”的原因,是这一步的关键:光看文件无法知道它是真正的 0 还是被填进去的 0,所以先统计下来再去问业务方才是对的。
填成 0 的平均值与去掉缺失值的平均值
增加 stats <파일> <칼럼>(占位符依次为文件、列名),让它并排给出三种平均值,并把针对 amount 列的结果保存到 /root/miss/stats.json。
三种平均值的差别在于分母。把缺失值填成 0,合计不变而分母变大,平均值就下降。连 0 也去掉的平均值,分母更小,平均值就上升。三个数字都是对的,错的是不说明统计了什么的报告。
9999-12-31 造成的几千年
增加 aging <파일>(占位符为文件),让它同时给出把哨兵日期原样当作日期的平均值和去掉哨兵日期的平均值,并把结果保存到 /root/miss/aging.json。
9999-12-31 不是日期,而是“还没结束”的标记。把它当作日期来计算,平均值就会变成几千年。看起来可笑,但只要混进几条,平均值就会悄悄地被抬高,而且不会引人注意。开始日期或结束日期为“没有”时,从计算中排除并计入 unknown。
区分完全重复和键重复
增加 dupes <파일>(占位符为文件),让它给出 rows、exact、key_dupes、keys。exact 是同一行多次出现的超出部分,key_dupes 是 record_id 出现两次以上的键的个数。
之所以要区分这两种,是因为处理方针不同。一字不差的行无需判断,只保留一条即可;而标识符相同、内容不同的行,机器不知道哪一个才是对的。这样的键要留成清单,交给业务方。
规范化并用分块缩小候选
增加 blocks <파일>(占位符为文件),让它给出 rows、blocks、candidate_pairs、pairs_without_blocking。块键是规范化后电话号码的后四位,不足四位时为 NONE。
全部配对比较,数量会随行数的平方增长。先用成本低的键分组,需要比较的配对就会大幅减少。规范化是为了比较,这一步并不合并任何东西:合并由下一步的规则来做。
把同一个人归为一组,以及分块漏掉的配对
增加 match <파일>(占位符为文件),让它给出 rows、clusters、merged、groups、recall_loss。规则以 block_key、normalize、match、not_match 写入 /root/miss/rules.json。
如果把邮箱为空的行按“邮箱相同”归为一组,几十个不同的客户就会变成一个人:规则中必须加上“不为空”。recall_loss 是用全量比较找到的配对,减去在块内找到的配对的值。这里是用数字留下“分块不是免费的”这一事实的地方。
用一份报告说明缺失与重复
一次性处理全部内容,在 /root/miss/miss_report.json 中写入 rows、missing、mean_naive、mean_known、exact、key_dupes、clusters、merged、recall_loss,并在 /root/miss/miss_report.md 中分 ## 없음은 몇 가지로 적혀 있었나 ## 0 으로 채우면 무엇이 달라지나 ## 무엇을 같은 것으로 봤나 ## 남은 위험 四节书写(韩文标题,依次意为“‘没有’有几种写法”“填成 0 会有什么不同”“把什么视为相同”“剩余风险”)。
missing 是以列名为键、其中放入 census 结果的对象。要放入 amount 和 closed_at 两列。报告里要把三个平均值连同分母一起写出来,并把分块漏掉的配对数写进剩余风险:那些配对没有人会去举报。