寻求适用于百万级姓名数据集的相似姓名聚类算法方案
百万级相似姓名分组优化方案
原有的双重循环方案在百万级数据下完全不可行——时间复杂度为O(n²),数据量上升后计算量会直接爆炸。下面给出几个能处理大规模数据集的优化思路和实现方案,同时满足「优先归入匹配度最高的组,允许归入多组」的要求:
一、预过滤+N-Gram倒排索引(易落地、性价比高)
核心思路是避免每个姓名与所有组名全量比较,先通过N-Gram快速缩小候选范围,再对候选集计算编辑距离做精细匹配。
实现步骤:
- 构建N-Gram倒排索引:对每个姓名生成所有N元字符组(比如N=2时,
John生成jo、oh、hn),用字典存储每个N-Gram对应的姓名列表。 - 候选筛选:处理新姓名时,提取其N-Gram,从索引中找到共享足够多N-Gram的已分组姓名,作为候选组名。
- 精细匹配:计算候选组名与当前姓名的编辑距离,优先加入距离最小的组,同时可加入其他符合阈值的组。
代码示例(Python):
import Levenshtein from collections import defaultdict def build_ngram_index(names, n=2): ngram_index = defaultdict(list) for name in names: # 生成N-Gram,保留原始姓名用于后续匹配 grams = [name[i:i+n] for i in range(len(name)-n+1)] if len(name)>=n else [name] for gram in grams: ngram_index[gram].append(name) return ngram_index def group_similar_names_large_scale(names, threshold=2, n=2, min_shared_grams=1): groups = {} ngram_index = build_ngram_index(names) for name in names: # 从N-Gram索引中筛选候选组名 candidate_group_names = set() grams = [name[i:i+n] for i in range(len(name)-n+1)] if len(name)>=n else [name] for gram in grams: for candidate in ngram_index.get(gram, []): if candidate in groups: candidate_group_names.add(candidate) # 计算编辑距离并排序,优先匹配度最高的组 valid_pairs = [] for group_name in candidate_group_names: dist = Levenshtein.distance(name, group_name) if dist <= threshold: valid_pairs.append((dist, group_name)) if valid_pairs: # 按距离升序,先加入匹配度最高的组 valid_pairs.sort() min_dist, best_group = valid_pairs[0] groups[best_group].append(name) # 加入其他符合阈值的组(如果需要) for dist, group_name in valid_pairs[1:]: groups[group_name].append(name) else: groups[name] = [name] return groups.values()
二、向量近似搜索+聚类(超大规模场景适用)
如果数据量达到千万级,预过滤仍不够高效,可以将姓名转换为向量,用近似近邻搜索(ANN)工具快速定位相似目标,再完成分组。
实现步骤:
- 姓名向量化:用字符词袋模型(统计每个字符出现次数)或N-Gram向量将姓名转为数值向量。
- 构建ANN索引:用FAISS、Annoy等库构建向量索引,实现O(log n)级别的相似性搜索。
- 匹配分组:找到相似候选后,用编辑距离确认,优先加入匹配度最高的组,支持多组加入。
代码示例(Python+FAISS):
import Levenshtein import faiss import numpy as np from collections import defaultdict def name_to_vector(name, char_set): # 字符词袋向量:每个字符对应一个维度,值为出现次数 char_to_idx = {c:i for i,c in enumerate(char_set)} vec = np.zeros(len(char_set), dtype=np.float32) for c in name.lower(): if c in char_to_idx: vec[char_to_idx[c]] += 1 return vec def group_similar_names_faiss(names, threshold=2, top_k=5): # 收集所有出现过的字符 all_chars = set(''.join(names).lower()) char_list = list(all_chars) # 转换所有姓名为向量 vectors = np.array([name_to_vector(name, char_list) for name in names], dtype=np.float32) # 构建FAISS索引 index = faiss.IndexFlatL2(len(char_list)) index.add(vectors) groups = {name:[name] for name in names} name_to_idx = {name:i for i,name in enumerate(names)} for name in names: vec = name_to_vector(name, char_list).reshape(1, -1) # 搜索Top-K相似姓名(跳过自身) distances, indices = index.search(vec, top_k+1) best_dist = float('inf') best_group = None valid_candidates = [] for idx, dist in zip(indices[0], distances[0]): candidate_name = names[idx] if candidate_name == name: continue edit_dist = Levenshtein.distance(name, candidate_name) if edit_dist <= threshold: valid_candidates.append((edit_dist, candidate_name)) if edit_dist < best_dist: best_dist = edit_dist best_group = candidate_name if best_group: groups[best_group].append(name) # 加入其他符合条件的组 for edit_dist, group_name in valid_candidates: if group_name != best_group: groups[group_name].append(name) # 去重组内重复项 for group_key in groups: groups[group_key] = list(set(groups[group_key])) return groups.values()
三、规则预分组+组内精细匹配(轻量化方案)
如果不需要复杂算法,可先通过规则将姓名分成大组,再在组内做编辑距离比较,大幅减少计算量:
- 按姓名长度+首2字符分组:比如
John(长度4,首2字符jo)分到jo_4组,直接排除长度差超过阈值的姓名 - 提取核心姓名:去掉非字母字符并转小写,先按核心姓名分组,再处理核心相似的姓名(如
Jon和John)
代码示例:
import Levenshtein from collections import defaultdict def get_pre_group_key(name): # 预分组key:首2个小写字母 + 姓名长度 alpha_part = ''.join([c.lower() for c in name if c.isalpha()]) prefix = alpha_part[:2] if len(alpha_part)>=2 else alpha_part return f"{prefix}_{len(name)}" def group_similar_names_rule_based(names, threshold=2): # 规则预分组 pre_groups = defaultdict(list) for name in names: key = get_pre_group_key(name) pre_groups[key].append(name) final_groups = [] # 预分组内做精细匹配 for pre_group in pre_groups.values(): groups = {} for name in pre_group: best_dist = float('inf') best_group_name = None valid_pairs = [] for group_name in groups: dist = Levenshtein.distance(name, group_name) if dist <= threshold: valid_pairs.append((dist, group_name)) if dist < best_dist: best_dist = dist best_group_name = group_name if best_group_name: groups[best_group_name].append(name) for dist, group_name in valid_pairs: if group_name != best_group_name: groups[group_name].append(name) else: groups[name] = [name] final_groups.extend(groups.values()) return final_groups
内容的提问来源于stack exchange,提问作者K. Claesson
相关产品推荐
相关产品推荐

