📈 第13章 整理数据
📄 一句话概括

汉语姓名没有分隔符,「王小明」的姓是一位,「欧阳修」的姓是两位,字符串本身看不出来。 所以先定规则:前两个字在复姓表里就取两位,否则取一位。规则之外还得留「人工确认」的口子。 这份名单里 11 个名字以复姓开头,一律取一字的规则会把这 11 个人的姓全拆错。

📝 本章和前面几章的关系

上一篇把脏字符洗干净了。这一篇处理一个更麻烦的问题:干净了也不代表能拆。 书里用 R 的 sub 指令做替换式提取。这里用一张复姓表加两条规则,思路一样。

一、为什么姓名难拆

汉语姓名没有分隔符。「王小明」的姓是「王」,「欧阳修」的姓是「欧阳」。 光看字符串,你不知道前两个字是不是一个复姓。 这不是技术问题,是知识问题。程序再聪明,也不会自己知道「欧阳」是复姓。

所以拆姓名这件事,从来不是「写个正则」,而是「先有一份知识,再用代码执行它」。

二、定规则

规则一:名字前两个字如果在复姓表里,姓取两个字。 规则二:否则姓取一个字。 规则三:复姓表要能随时加。中国常见复姓有八十多个,脚本里只列了常用的几个。

三条规则里,第一条依赖知识,第二条是默认,第三条决定了这套代码能不能长期用。 规则三最容易被忽略,也最重要。不能维护的规则,三个月后就会开始出错。

三、两条规则各拆一遍

「王小明」两种规则都给「王/小明」,一样。 「欧阳修」就不一样了:一律取一字给「欧/阳修」,按复姓表给「欧阳/修」。 一个字之差,后面按姓氏分组统计就全乱——所有复姓会被算成若干个不同的单姓。

拆错一个字段,污染的是整列统计,不是一行数据。

四、到底错几行

这份名单里 11 个名字以复姓开头,涉及 2 个不同的复姓。 一律取一字的规则会把这 11 个人的姓全拆错,占全部记录的 17%。 17% 不算多,但足以让「按姓氏排名」这类结论出现一批不存在的姓。

而且这种错很难发现:拆出来的每个字都是合法汉字,看起来完全正常。

五、规则之外还有边界

四个字的名字,可能是「复姓 + 两字名」(欧阳小明),也可能是「单姓 + 三字名」。 带间隔号的名字(买买提·艾力)根本没有「姓」这个概念,硬拆就是错的。 清洗到最后一定会遇到「这条规则不适用」的情况。这时该做的是标记出来,不是硬套。

加一列「需要人工确认」,比悄悄拆错强一百倍。 交货的时候把这一列一起交出去,客户才知道哪些地方不能直接信。

六、动手 Demo:拆姓名:先定规则再动手

用一张复姓表加两条规则拆姓名,和「一律取一字」对比。 再统计这份名单里有多少行会因为这个差别被拆错。

COMPOUND = ["欧阳", "司马", "上官", "诸葛", "东方", "独孤"]

def naive(n):
    return n[0], n[1:]

def by_rule(n):
    for c in COMPOUND:
        if n.startswith(c):
            return c, n[len(c):]
    return n[0], n[1:]

for n in ("王小明", "欧阳修", "司马光", "张三", "欧阳小明"):
    a, b = naive(n)
    c, d = by_rule(n)
    print("%-8s 一律一字:%s/%-6s 按复姓表:%s/%s" % (n, a, b, c, d))

NAMES = ["王小明", "欧阳修", "李伟", "司马光", "张芳", "欧阳明", "刘娜"]
wrong = [n for n in NAMES if n[:2] in COMPOUND]
print()
print("这 %d 个样例里,以复姓开头的 %d 个(%.0f%%)"
      % (len(NAMES), len(wrong), len(wrong) / float(len(NAMES)) * 100))
print("一律取一字会拆错:%s" % ", ".join(wrong))

实跑输出:

--------------------------------------------------------------------
拆姓名:先定规则再动手
--------------------------------------------------------------------

  ① 为什么姓名难拆
  --------------------------------------------------------------------
  汉语姓名没有分隔符。王小明的姓是「王」,欧阳修的姓是「欧阳」。
  光看字符串,你不知道前两个字是不是一个复姓。
  这不是技术问题,是知识问题。程序再聪明,也不会自己知道「欧阳」是复姓。

  ② 定规则
  --------------------------------------------------------------------
  规则一:名字前两个字如果在复姓表里,姓取两个字。
  规则二:否则姓取一个字。
  规则三:复姓表要能随时加。中国常见复姓有八十多个,这里只列了六个。

  ③ 两条规则各拆一遍
  --------------------------------------------------------------------

| 姓名 | 一律取一字 | 按复姓表 |
|---|---|---|
| 王小明 | 王 / 小明 | 王 / 小明 |
| 欧阳修 | 欧 / 阳修 | 欧阳 / 修 |
| 司马光 | 司 / 马光 | 司马 / 光 |
| 张三 | 张 / 三 | 张 / 三 |

  差别就在复姓上:「欧阳修」被拆成「欧/阳修」,姓错了。
  一个字之差,后面按姓氏分组统计就全乱——所有复姓会被算成若干个不同的单姓。

  ④ 到底错几行
  --------------------------------------------------------------------
  这份名单里 11 个名字以复姓开头,2 个不同的复姓。
  一律取一字的规则会把这 11 个人的姓全拆错。
  占全部记录的 17%——不算多,但足以让按姓氏做的统计出现一批假姓氏。

  ⑤ 规则之外还有边界
  --------------------------------------------------------------------
  四个字的名字,可能是「复姓 + 两字名」(欧阳小明),也可能是「单姓 + 三字名」。
  带间隔号的名字(买买提·艾力)根本没有「姓」这个概念,硬拆就是错的。
  清洗到最后一定会遇到「这条规则不适用」的情况。这时该做的是标记出来,不是硬套。
  加一列「需要人工确认」,比悄悄拆错强一百倍。

最后一行是这份规则唯一能检查自己的地方:把会被拆错的名字列出来,人眼过一遍。 注意「欧阳小明」四个字:按复姓表拆成「欧阳/小明」是对的,但如果是「单姓 + 三字名」, 同一串字符就得拆成另一个样子。字符串本身不够。

30 秒实验

  1. 如果名单里出现「买买提·艾力」,两条规则分别会拆成什么?该怎么处理?
  2. 复姓表要写到多长才够用?写不全的后果是什么?
  3. 想一想:为什么「加一列人工确认」比「把规则写得更复杂」更可靠?

常见误区

  1. 一律按一个字拆姓:所有复姓都会被拆错,而且错得很像对的。
  2. 不维护复姓表:名单里出现新复姓就静默出错。
  3. 对少数民族名字硬拆:那类名字本来就没有「姓」的概念。
  4. 拆完不抽检:拆错的结果全是合法汉字,只能靠抽检或统计异常发现。
自测三问(点开对答案) 1. 为什么拆姓名不是纯技术问题? —— 因为要知道哪两个字是复姓,这属于知识。程序只能执行规则,不能自己知道规则。 2. 一律按一字拆姓会造成什么后果? —— 所有复姓被拆成单姓,按姓氏做的分组统计会出现一批不存在的姓,整列结论都受影响。 3. 遇到规则不适用的记录该怎么办? —— 标记出来交人工确认,而不是硬套规则。悄悄拆错比留着不拆危害更大。

姓名拆完,数据基本成形了。最后一篇收尾:排序、去重,顺便把前面留下的问题照出来。

相关笔记