我每隔一段时间就要处理一批来源不太可靠的数据,几百万行的表格或者分隔符文件。这些年攒了一些固定动作,说不上高级,但能让我少返工。
动作一:先摸底,别急着洗
拿到文件我做的第一件事不是清洗,是给每一列出一张体检表:非空率、唯一值个数、最短最长长度、随机抽十个样本值。十几行代码,跑一遍心里就有数了。
这一步的价值在于它能改变你的清洗策略。有一次我准备写一套复杂的地址解析逻辑,摸底发现那一列 92% 是空的,剩下 8% 里有一半是同一个默认占位值。真正需要解析的只有三千多条,我直接改成人工规则加人工核对,半天搞定,省下了两天的开发。
动作二:读取时就把类型定死
让读取函数自己猜类型是个昂贵的坏习惯。一个 240 万行的文件,我不指定类型时占了 1.8GB 内存,指定之后降到 420MB。差别主要来自两处:
- 把低基数的字符串列声明成分类类型。一个只有 12 个取值的省份列,从存 240 万个字符串变成存 240 万个小整数加一张 12 项的字典。
- 把数值列声明成合适的位宽。默认可能给到 64 位,而很多列 32 位甚至 16 位就够。
还有一个坑是看起来像数字的字符串。编号列前面带零,被自动识别成整数之后前导零就没了,再也回不来。这种列必须显式声明成字符串,我因为这个重跑过整个流程。
动作三:日期解析一定要指定格式
不指定格式时,解析函数会逐行尝试推断,几十万行下来能跑好几分钟。指定了格式之后是几秒的事,我实测过一次差了将近三十倍。
更麻烦的是推断可能推错。日月和月日两种顺序在前十二天是无法区分的,如果数据前几行恰好都是小于 13 的日期,推断结果可能整批反过来。这种错误不会报错,只会让你的月度统计莫名其妙。
动作四:字符串清洗要讲顺序
我固定按这个顺序来,顺序反了会互相干扰:
- 先去掉不可见字符:零宽空格、字节序标记、各种控制符。它们看不见但会让相等判断失败,我曾经对着两个屏幕上一模一样的字符串比较返回 false 发呆了十分钟。
- 再做全角转半角。数字和字母的全角形式在人工录入的数据里非常常见。
- 然后去首尾空白。注意有些空白是不间断空格,普通的去空白函数不认它。
- 最后才是大小写归一、同义词映射这些业务层面的规整。
动作五:永远不要原地覆盖
清洗后的值写到新列,原始列保留。文件大就大点,磁盘比你的时间便宜。这样做的好处是当你发现某条规则写错了,可以只重跑那一步,而不是从头拉一遍原始数据。
相关的一条:每一步都记录影响了多少行。我的流程里每个清洗函数都返回处理结果和一个统计字典,最后汇总成一张表:这一步改了多少行、删了多少行、剩余多少行。
这张表比清洗后的数据本身更重要。有一次我看到某一步删掉了 18 万行,远超预期的几千行,一查发现是判空条件写反了。如果没有这张表,这 18 万行就无声无息地没了。
动作六:先定义什么叫重复
去重之前必须先回答:按哪几列判定重复?重复时保留哪一条?
我见过太多直接整行去重的,结果因为某个无关列(比如导出时间戳)有微小差异,一条都没去掉。也见过按主键去重、随便保留一条的,结果丢掉了更新更晚的那条。
我的习惯是:先按候选键分组统计,看看重复的分布——是有零星几组重复,还是有一组重了两万次。这两种情况的原因和处理完全不一样。第二种通常意味着数据源导出时出了问题,该做的是重新拿数据,而不是去重。
最后一条,也是最重要的
清洗脚本要能从头重跑。不要在交互式环境里改一步存一步,跑到第七步发现第二步错了,前面全白做。我现在一律写成脚本,中间结果落盘并且带步骤编号,跑坏了从任意一步接着来。
这条规矩是我在一个凌晨三点重跑了四个小时的流程之后立下的。