首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >4 万行客户表去重:精确匹配只清掉 37%,剩下全靠三层模糊|PRIVO AI智能操作助手 三合一·网页+桌面+IDE

4 万行客户表去重:精确匹配只清掉 37%,剩下全靠三层模糊|PRIVO AI智能操作助手 三合一·网页+桌面+IDE

原创
作者头像
用户12722577
发布于 2026-11-06 17:46:16
发布于 2026-11-06 17:46:16
890
举报

一份客户名单,41,806 行,300 多个销售录了三年。

拿到手第一件事是去重。按公司名完全相等跑一遍,删掉 6,412 行,剩下 35,394 行。我以为剩下的都能用了,直到把这三行并排放在一屏里:

北京某某科技有限公司

北京某某科技有限责任公司

北京某某科技(集团)有限公司

同一家公司,三个客户编号,三个销售各跟一段,回款记录散在三个地方。精确匹配只能证明两个名字完全相同,证明不了它们是两家公司。

脏数据的形态比想象中集中

抽 500 行人工看,重复的写法就那么几类:

形态

占比

例子

后缀不一致

41%

有限公司 / 有限责任公司 / 股份有限公司

括号注释

22%

(集团)、(华东)、【已停用】

空格与全半角

18%

「北京 某某」/「北京某某」

简称与全称

12%

某某科技 / 北京某某科技有限公司

错别字

7%

科技写成科枝

按这个分布,任何单一规则都清不干净。只能分层:一层比一层松,每层都留下"这一层合并了什么"的明细。模糊匹配的每一次合并都可能是错的,没有明细就没法回滚。

第一层:规范化之后精确比对

不涉及相似度,只做字符串归一:去空格、全角转半角、剥掉括号及内容、统一后缀。

代码语言:python
复制
import re

SUFFIX = r"(有限责任公司|股份有限公司|集团有限公司|有限公司|公司)$"
BRACKET = r"[((\[【][^))\]】]*[))\]】]"

def normalize(name: str) -> str:
    s = name.strip().replace(" ", "")
    s = s.translate(str.maketrans("()【】", "()[]"))
    s = re.sub(BRACKET, "", s)
    s = re.sub(r"\s+", "", s)
    return re.sub(SUFFIX, "", s)

这一层几乎不会出错,代价是它只认字面完全一致的——去掉后缀之后仍然不同的,一个都认不出来。

第二层:编辑距离,先分桶再比

规范化后仍不相等的,算相似度。直接全表两两比对是 35,394² ≈ 12.5 亿次,纯 Python 跑不完。实际做法是取规范化名字的前两个字做桶,只在桶内比,再加一道长度差过滤。

代码语言:python
复制
from difflib import SequenceMatcher
from collections import defaultdict

def bucket_key(s: str) -> str:
    return s[:2] if len(s) >= 2 else s

buckets = defaultdict(list)
for idx, row in enumerate(rows):
    buckets[bucket_key(row["norm"])].append(idx)

pairs = []
for idxs in buckets.values():
    for i in range(len(idxs)):
        for j in range(i + 1, len(idxs)):
            a, b = rows[idxs[i]]["norm"], rows[idxs[j]]["norm"]
            if abs(len(a) - len(b)) > 3:
                continue
            r = SequenceMatcher(None, a, b).ratio()
            if r >= 0.88:
                pairs.append((idxs[i], idxs[j], round(r, 3)))

0.88 这个阈值是试出来的。0.80 会把「北京宏远科技」和「北京宏达科技」并成一家;0.92 又漏掉「科枝」这类单字错。

第三层:拼音首字母,只召回不合并

「某某科技」规范化后 4 个字,对上「北京某某科技有限公司」的 8 个字,长度差超了 3,第二层直接跳过。这类简称只能靠拼音兜底。

代码语言:python
复制
from pypinyin import lazy_pinyin

def initials(s: str) -> str:
    return "".join(p[0] for p in lazy_pinyin(s) if p)

for idxs in buckets.values():
    sig = defaultdict(list)
    for i in idxs:
        sig[initials(rows[i]["norm"])].append(i)
    for k, group in sig.items():
        if len(group) > 1:
            print("待确认:", k, [rows[i]["name"] for i in group])

这一层我没有让它自动合并。bjhykj 既能是「北京宏远科技」也能是「北京华宇科技」,首字母的碰撞率太高。它只产出一个待确认清单,人工过。

结果

三层跑完,41,806 行合并成 33,102 个客户主体,减少 8,704 行,占 20.8%。

层

处理方式

合并组数

误合并

备注

精确

完全相等

6,412

0

最安全

规范化

后缀/括号/空格归一

2,109

0

吃掉剩余重复的 37%

编辑距离

0.88 + 分桶 + 长度过滤

4,187

6/200 抽样

必须人工抽检

拼音首字母

仅召回

341 组

—

不自动合并

两处我搞错的地方

第一处是阈值。一开始设 0.80,想着"宁可多并,回头再拆"。结果把「上海嘉禾贸易」和「上海嘉禾商贸」并了——这两家真是两家公司,一个做进出口一个做内贸,共用仓库但独立核算。业务方发现时已经并了三个月,拆分的工作量比当初去重还大。在主数据上,漏并的代价远小于错并。

第二处是顺序。我最初把拼音层放在编辑距离之前,大量简称提前进了候选池,编辑距离那层的桶被打散,跑出来比不做还乱。分层必须从严格到宽松,一层层收紧候选集,顺序错了等于白做。

交给工具之后

这套流程现在在 PRIVO 里是一步操作:表丢进去按三层跑,每层的合并明细单独出一份表,人工只审最后一层的清单。省掉的是写脚本、调阈值、对中间结果的时间;省不掉的是最后那道人工确认——主数据的误合并,没人能替你担。

局限

这套规则只对中文公司名成立。我拿同样的阈值跑过一批日本客户,误合并率 17%,后缀规则和分词方式完全不同,得整段重写。另外它解决的是"同一主体的不同写法",解决不了母子公司同名不同后缀的控股关系——那属于实体对齐,不是字符串相似度能覆盖的范围。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 脏数据的形态比想象中集中
  • 第一层:规范化之后精确比对
  • 第二层:编辑距离,先分桶再比
  • 第三层:拼音首字母,只召回不合并
  • 结果
  • 两处我搞错的地方
  • 交给工具之后
  • 局限
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档