问题描述
场景
我有许多企业数据集,我必须找到它们之间缺失的链接,我用来查找潜在匹配项的方法之一是连接名字和姓氏。复杂的是,我们有很多人在一个数据集(员工记录)中使用他们的法定姓名,但他们在其他数据集中使用昵称或(更糟糕的是)他们的中间名(即 EAD、培训、PIV 卡、等等。)。我正在寻找一种方法来匹配各种数据集中这些可能不同的名称。
简化示例
这是我尝试做的一个过于简化的例子,但我认为它传达了我的思考过程。我从员工表开始:
员工表
employee_id | first_name | last_name |
---|---|---|
052451 | 罗伯特 | 阿姆斯登 |
442896 | 雅各 | 克拉克斯福德 |
054149 | 授予 | 基廷 |
025747 | 加布里埃尔 | 伦顿 |
071238 | 玛格丽特 | 塞芬马赫 |
并尝试从 PIV 卡片数据集中找到匹配的数据:
卡片表
card_id | first_name | last_name |
---|---|---|
1008571527 | 鲍比 | 阿姆斯登 |
1009599982 | 杰克 | 克拉克斯福德 |
1004786477 | 加比 | 伦顿 |
1000628540 | 玛姬 | 塞芬马赫 |
想要的结果
尝试在名字和姓氏上匹配这些数据集后,我想得到以下结果:
Employees_Cards 表
emp_employee_id | emp_first_name | emp_last_name | crd_card_id | crd_first_name | crd_last_name |
---|---|---|---|---|---|
052451 | 罗伯特 | 阿姆斯登 | 1008571527 | 鲍比 | 阿姆斯登 |
442896 | 雅各 | 克拉克斯福德 | 1009599982 | 杰克 | 克拉克斯福德 |
054149 | 授予 | 基廷 | NULL | NULL | NULL |
025747 | 加布里埃尔 | 伦顿 | 1004786477 | 加比 | 伦顿 |
071238 | 玛格丽特 | 塞芬马赫 | 1000628540 | 玛姬 | 塞芬马赫 |
如您所见,我想进行以下匹配:
加布里埃尔 -> 加比
雅各布 -> 雅各布
玛格丽特 -> 玛姬
罗伯特 -> 鲍比
Name_Aliases 表
name1 | name2 | name3 | name4 |
---|---|---|---|
加布里埃尔 | 加比 | NULL | NULL |
雅各 | 杰克 | NULL | NULL |
玛格丽特 | 玛姬 | 玛姬 | 梅格 |
迈克尔 | 迈克 | Mikey | 米克 |
罗伯特 | 鲍比 | 鲍勃 | 罗布 |
并在 JOIN 中使用类似的东西:
CREATE TABLE employee_cards AS
SELECT
employees.employee_id AS emp_employee_ID,employees.first_name AS emp_first_name,employees.last_name AS emp_last_name,cards.card_id AS crd_card_id,cards.first_name AS crd_first_name,cards.last_name AS crd_last_name
FROM employees
LEFT OUTER JOIN name_aliases
LEFT OUTER JOIN cards
ON employees.first_name IN (
nane_aliases.name1,nane_aliases.name2,nane_aliases.name3,nane_aliases.name4
)
AND employees.last_name = cards.last_name;
这就是我卡住的地方,因为我不知道如何将第一个 ON 条件的结果与卡片表中的名字联系起来。
一些问题
当我更深入地思考这个问题时,我知道我不是第一个遇到这种常见名称变体匹配需求的人。我最初的搜索向我指出了 fuzzysearch
和 soundex
之类的东西,但这些并不是我当前场景所需要的(尽管它们可能会派上用场)。鉴于此,我有几个问题要问社区:
可下载的通用名称变体数据集?
是否有人按照我上面的 name-aliases
表的内容编译或众包了一个综合名称变体数据集?我的搜索让我找到了几个似乎有此类数据的网站,但它们都无法下载以导入到我的本地数据库中。
我确实发现这个 SO 讨论已经有十多年的历史了,但它似乎不是最新的:Database of common name aliases / nicknames of people
另外,我无法为此支付任何费用,所以我希望github上可能有一个隐藏的人。
构造 Name_Aliases 表的更好方法?
既然 name_aliases 表中的每条记录都可以有两个或多个条目,那么有没有更好的方法来设置该表的结构,使其具有无限的灵活性?
匹配 Name_Aliases 表中的任何列?
如何设置 JOIN 查询以将员工.first_name 与 name_alises 中的任何列匹配,然后最终将其与 card.first_name 匹配?
更好的解决方案?
我是否采取了错误的方法来解决这个问题?有人使用 Postgresql 提出了更灵活、更优雅的方法吗?
解决方法
解决方案
这有点像一场战斗,但我的所有问题都得到了解答,我很高兴终于让这个项目的一切工作顺利进行。详情如下。
昵称数据集
我在 Github 上发现了几个很有前途的昵称数据集,这个看起来是维护最积极的:https://github.com/carltonnorthern/nickname-and-diminutive-names-lookup。我将names.csv 文件下载到我的计算机,并使用以下代码将其导入到我的数据库中:
导入脚本
DROP TABLE IF EXISTS names_aliases_temp;
CREATE TABLE names_aliases_temp
(
names_data text
);
COPY names_aliases_temp
FROM '~/Downloads/names.csv';
DROP TABLE IF EXISTS names_aliases;
CREATE TABLE names_aliases
(
id serial,nicknames text[]
);
INSERT INTO names_aliases (nicknames)
SELECT string_to_array(names_data,',') FROM names_aliases_temp;
DROP TABLE IF EXISTS names_aliases_temp;
CREATE INDEX idx_gin_names ON names_aliases USING GIN(nicknames);
Names_Aliases 数据集
以下是导入数据库后的示例:
id | 昵称 |
---|---|
1 | {aaron,erin,ronnie,ron} |
2 | {abbigail,nabby,abby,gail} |
3 | {abednego,bedney} |
4 | {abel,ebbie,ab,abe,eb} |
5 | {abiel,ab} |
6 | {abigail,gail} |
7 | {abijah,bige} |
8 | {abner,ab} |
9 | {abraham,abe} |
10 | {abram,abe} |
使用数组的注意事项
我对进入数组格式的难易感到兴奋,但更让我兴奋的是我能够在 JOIN 查询中使用数组格式!由于每行的条目数差异很大,我发现 array
数据类型使其非常适合此数据,这也使得使用 ILIKE ANY()
运算符匹配记录变得非常容易。
JOIN 查询脚本
DROP TABLE IF EXISTS employee_cards;
CREATE TABLE employee_cards AS
WITH joined_data AS (
SELECT
employees.employee_id AS emp_id,employees.first_name AS emp_first_name,employees.last_name AS emp_last_name,cards.first_name AS crd_first_name,cards.last_name AS crd_last_name,cards.card_id AS crd_id
FROM employees
-- Attempt to match first names with nicknames
LEFT JOIN names_aliases
ON employees.first_name ILIKE ANY(names_aliases.nicknames)
LEFT JOIN cards
-- First match records where name is the same between `employees` and `cards`
ON (employees.last_name ILIKE cards.last_name
AND employees.first_name ILIKE cards.first_name)
-- Then bring in nicknames where no matches are found
OR (employees.last_name ILIKE cards.last_name
AND cards.first_name ILIKE ANY(names_aliases.nicknames))
)
-- Put successful matches at the top for each employee and retain only the first row
SELECT DISTINCT ON (emp_id)
emp_id,emp_first_name,emp_last_name,crd_first_name,crd_last_name,crd_id
FROM joined_data
ORDER BY
emp_id,crd_id NULLS LAST;
使用的表格
为方便起见,本练习中使用的三个表格如下。
员工表
employee_id | first_name | last_name |
---|---|---|
052451 | 罗伯特 | 阿姆斯登 |
022448 | 迈克尔 | 棕色 |
442896 | 雅各 | 克拉克斯福德 |
054149 | 授予 | 基廷 |
025747 | 加布里埃尔 | 伦顿 |
425972 | Consorcia | 雷亚斯 |
071238 | 玛格丽特 | 塞芬马赫 |
insert into public.employees (employee_id,first_name,last_name)
values ('052451','Robert','Armsden'),('022448','Michael','Brown'),('442896','Jacob','Craxford'),('054149','Grant','Keeting'),('025747','Gabrielle','Renton'),('425972','Consorcia','Reyas'),('071238','Margaret','Seifenmacher');
卡片表
card_id | first_name | last_name |
---|---|---|
1008571527 | 鲍勃 | 阿姆斯登 |
1000594085 | 迈克尔 | 棕色 |
1009599982 | 杰克 | 克拉克斯福德 |
1004786477 | Gabby | 伦顿 |
1009481574 | Consorcia | 雷亚斯 |
1000628540 | 玛姬 | 塞芬马赫 |
insert into public.cards (card_id,last_name)
values ('1008571527','Bob',('1000594085',('1009599982','Jake',('1004786477','Gabby',('1009481574',('1000628540','Maggy','Seifenmacher');
员工卡(联接)表
emp_id | emp_first_name | emp_last_name | crd_first_name | crd_last_name | crd_id |
---|---|---|---|---|---|
052451 | 罗伯特 | 阿姆斯登 | 鲍勃 | 阿姆斯登 | 1008571527 |
022448 | 迈克尔 | 棕色 | 迈克尔 | 棕色 | 1000594085 |
442896 | 雅各 | 克拉克斯福德 | 杰克 | 克拉克斯福德 | 1009599982 |
054149 | 授予 | 基廷 | NULL | NULL | NULL |
025747 | 加布里埃尔 | 伦顿 | Gabby | 伦顿 | 1004786477 |
425972 | Consorcia | 雷亚斯 | Consorcia | 雷亚斯 | 1009481574 |
071238 | 玛格丽特 | 塞芬马赫 | 玛姬 | 塞芬马赫 | 1000628540 |
insert into public.employee_cards (emp_id,crd_id)
values ('052451','Armsden','1008571527'),'Brown','1000594085'),'Craxford','1009599982'),'Keeting',null,null),'Renton','1004786477'),'Reyas','1009481574'),'Seifenmacher','1000628540');
,
如何构造和查询别名表是一个有趣的问题。我建议成对组织它而不是更宽的行,因为您事先不知道在一组连接的名称中最终可能需要多少变体,而两列结构使您能够添加到给定的组无限期:
姓名1 | name2 |
---|---|
雅各 | 杰克 |
玛格丽特 | 玛姬 |
玛格丽特 | 玛姬 |
玛格丽特 | 梅格 |
玛姬 | 玛姬 |
玛姬 | 梅格 |
玛姬 | 梅格 |
然后您只需检查查询中每个 JOIN 中的两列,如下所示:
SELECT DISTINCT
employees.employee_id AS emp_employee_id,employees.first_name AS emp_first_name,employees.last_name AS emp_last_name,cards.card_id AS card_id,cards.first_name AS crd_first_name,cards.last_name AS crd_last_name
FROM
employees
INNER JOIN name_aliases ON
name_aliases.name1 = employees.first_name
OR name_aliases.name2 = employee.first_name
LEFT JOIN cards ON
(
cards.first_name = name_aliases.name1
OR cards.first_name = name_aliases.name2
)
AND cards.last_name = employees.last_name
这假设一些员工不在卡片表中,并且没有两个人有相同的名字。如果这些假设是正确的,您将需要 DISTINCT 来消除部分外连接上的重复项。
您可以通过在全外连接中配对名称来进一步简化查询逻辑:
姓名1 | name2 |
---|---|
雅各 | 雅各 |
雅各 | 杰克 |
杰克 | 雅各 |
杰克 | 杰克 |
玛格丽特 | 玛格丽特 |
玛格丽特 | 玛姬 |
玛姬 | 玛格丽特 |
玛姬 | 玛姬 |
梅格 | 梅格 |
玛格丽特 | 梅格 |
梅格 | 玛格丽特 |
玛姬 | 梅格 |
梅格 | 玛姬 |
依此类推,然后您可以像对待多对多连接表一样对待别名表:
SELECT DISTINCT
employees.employee_id AS emp_employee_id,cards.last_name AS crd_last_name
FROM
employees
INNER JOIN name_aliases ON name_aliases.name1 = employees.first_name
LEFT JOIN cards ON
cards.first_name = name_aliases.name2
AND cards.last_name = employees.last_name