如何用data.table自身列修改列:统一组内姓名变体的唯一ID
问题描述
我有如下data.table数据:
dt <- data.table( group_id = c(1,1,1,2,2,2,3,3,3), team_id = c(1,1,2,3,4,4,5,5,6), person_id = c(1,2,3,4,4,5,6,7,8), person_name = c("Smith, J.", "Areta Franklin", "John Smith", "Robert Mitchum", "Robert Mitchum", "Cary Grant", "John Rambo", "Martin Sheen", "Rambo John") )
同一人在一个团队中仅出现一次,但在同一组中可能多次出现且姓名存在变体(如group_id=1中的"Smith, J."和"John Smith")。我希望创建新列uniq_person_id,使同组内同一人的所有记录拥有相同值。
我已创建函数simil_names(name1,name2),用于返回姓名相似度得分,完全匹配得分为0,name2可为字符串或字符串向量。
尝试的代码及问题
最初尝试的代码:
return_id <- function(pid,tid,gid) { group <- dt[group_id == gid,] person <- as.character(group[person_id == pid & team_id == tid,list(person_name)]) candidates <- group[uniq_person_id != 0,list(person_name,uniq_person_id)] new_id <- 0 if (nrow(candidates) > 0) { candidates$score <- simil_names(person, candidates$person_name) if (min(candidates$score) < 2) { # 得分低于2时,获取最佳匹配人员的ID new_id <- as.numeric(head(candidates[score == min(candidates$score), list(uniq_person_id)],1)) } } # 否则生成新的递增ID if(new_id == 0) { new_id <- max(group[group_id == gid & team_id == tid,]$uniq_person_id) + 1 } return(new_id) } dt$uniq_person_id <- 0 dt$uniq_person_id <- return_id(dt$person_id,dt$team_id, dt$group_id)
问题在于uniq_person_id列仅在函数执行完毕后才会修改,无法实时更新,导致逻辑失效。
后续尝试
先通过dt[, uniq_person_id := .N:1, by = .(group_id)]为每组每行定义不同值,再尝试以下代码:
return_id <- function(pid.v,tid.v,gid.v) { res.new_ids <- c() for(i in c(1:length(gid.v))) { gid <- as.numeric(gid.v[i]) tid <- as.numeric(tid.v[i]) pid <- as.numeric(pid.v[i]) group <- dt[group_id == gid,] person <- as.character(group[person_id == pid & team_id == tid,list(person_name)]) new_id <- as.numeric(dt[group_id == gid & team_id == tid & person_id == pid,list(uniq_person_id)]) candidates <- group[uniq_person_id != new_id,list(person_name,uniq_person_id)] if (nrow(candidates) > 0) { candidates$score <- simil_names(person, candidates$person_name) if (min(candidates$score) < 2) { # 得分低于2时,替换为最佳匹配候选的ID new_id <- as.numeric(head(candidates[score == min(candidates$score), list(uniq_person_id)],1)) } } res.new_ids <- append(res.new_ids,new_id) } return(res.new_ids) }
该方法仍无效,因为无法跟踪uniq_person_id列中已完成的修改。最终只能逐行遍历dt,用另一个data.table存储修改后的ID,希望得到更优的data.table风格解决方案。
解决方案
可以利用连通分量思路处理同组内相似姓名的匹配,结合data.table高效分组操作,实时维护ID映射,避免逐行遍历的低效:
library(data.table) library(igraph) # 用于计算连通分量,无需依赖可自行实现并查集逻辑 # 模拟simil_names函数(实际使用你自己的函数即可) simil_names <- function(name1, name2) { normalize_name <- function(x) tolower(gsub("[^a-zA-Z]", "", x)) stringdist::stringdist(normalize_name(name1), normalize_name(name2), method = "lv") } # 初始化临时ID dt[, temp_id := .I] # 按group_id分组生成uniq_person_id dt[, uniq_person_id := { grp_dt <- .SD # 生成组内所有姓名两两配对 name_pairs <- expand.grid(name1 = grp_dt$person_name, name2 = grp_dt$person_name, stringsAsFactors = FALSE) name_pairs <- name_pairs[name1 != name2, ] # 计算相似度得分 name_pairs$score <- mapply(simil_names, name_pairs$name1, name_pairs$name2) # 筛选得分<2的配对,视为同一人 matched_pairs <- name_pairs[score < 2, .(name1, name2)] # 构建图结构,用连通分量聚类相似姓名 if (nrow(matched_pairs) > 0) { g <- graph_from_data_frame(matched_pairs, directed = FALSE) cluster_membership <- components(g)$membership name_cluster <- data.table(person_name = names(cluster_membership), cluster_id = cluster_membership) } else { name_cluster <- data.table(person_name = grp_dt$person_name, cluster_id = seq_len(nrow(grp_dt))) } # 合并聚类结果到组内数据 grp_dt <- merge(grp_dt, name_cluster, by = "person_name", all.x = TRUE) # 为未参与配对的姓名分配独立ID grp_dt[is.na(cluster_id), cluster_id := seq_len(sum(is.na(cluster_id)))] # 转换为连续的组内唯一ID grp_dt[, cluster_id := match(cluster_id, unique(cluster_id))] grp_dt$cluster_id }, by = group_id] # 查看最终结果 print(dt)
关键说明:
- 利用图的连通分量,自动将所有相似姓名归为同一簇,生成统一的
uniq_person_id - 若不想依赖
igraph,可实现**并查集(Union-Find)**数据结构来合并相似ID,逻辑更轻量化 - 全程基于
data.table分组操作,避免逐行修改的性能瓶颈,适合处理大规模数据
内容的提问来源于stack exchange,提问作者Peyu
相关产品推荐
相关产品推荐

