4

我需要修改一个 SQL 表来对稍微不匹配的名称进行分组,并为组中的所有元素分配一个标准化的名称。

例如,如果初始表如下所示:

Name
--------
Jon Q
John Q
Jonn Q
Mary W
Marie W
Matt H

我想创建一个新表或向现有表添加一个字段,如下所示:

Name     | StdName
--------------------
Jon Q    | Jon Q
John Q   | Jon Q
Jonn Q   | Jon Q
Mary W   | Mary W
Marie W  | Mary W
Matt H   | Matt H

在这种情况下,我选择了要分配的第一个名称作为“标准化名称”,但我实际上并不关心选择了哪个——最终最终的“标准化名称”将被散列为唯一的个人 ID。(我也对直接使用数字 ID 的替代解决方案持开放态度。)我也会匹配出生日期,因此实际上姓名匹配的准确性实际上并不需要那么精确。我已经对此进行了一些研究,并且可能会使用 Jaro-Winkler 算法(参见例如此处)。

如果我知道名字都是成对的,这将是一个相对容易的查询,但是可以有任意数量的同名。

我可以很容易地概念化如何用过程语言进行这个查询,但我对 SQL 不是很熟悉。不幸的是,我无法直接访问数据——它是敏感数据,因此其他人(官僚)必须为我运行实际查询。具体实现将是 SQL Server,但我更喜欢与实现无关的解决方案。

编辑:

在回应评论时,我想到了以下程序方法。它是用 Python 编写的,为了有一个工作代码示例,我用简单地匹配名称的第一个字母替换了 Jaro-Winkler。

nameList = ['Jon Q', 'John Q', 'Jonn Q', 'Mary W', 'Marie W', 'Larry H']
stdList = nameList[:]

# loop over all names
for i1, name1 in enumerate(stdList):

  # loop over later names in list to find matches
  for i2, name2 in enumerate(stdList[i1+1:]):

    # If there's a match, replace latter with former.
    if (name1[0] == name2[0]):
      stdList[i1+1+i2] = name1

print stdList

结果是['Jon Q', 'Jon Q', 'Jon Q', 'Mary W', 'Mary W', 'Larry H']

4

2 回答 2

6

假设您从 SSC 复制并粘贴jaro-winkler实现(需要注册),以下代码将起作用。我试图为它构建一个 SQLFiddle,但是当我构建架构时它一直在崩溃。

这个实现有一个作弊——我正在使用光标。通常,游标不利于性能,但在这种情况下,您需要能够将集合与自身进行比较。可能有一种优雅的数字/计数表方法来消除声明的游标。

DECLARE @SRC TABLE
(
    source_string varchar(50) NOT NULL
,   ref_id int identity(1,1) NOT NULL
);

-- Identify matches
DECLARE @WORK TABLE
(
    source_ref_id int NOT NULL
,   match_ref_id int NOT NULL
);

INSERT INTO
    @src
SELECT 'Jon Q'
UNION ALL SELECT 'John Q'
UNION ALL SELECT 'JOHN Q'
UNION ALL SELECT 'Jonn Q'
-- Oops on matching joan to jon
UNION ALL SELECT 'Joan Q'
UNION ALL SELECT 'june'
UNION ALL SELECT 'Mary W'
UNION ALL SELECT 'Marie W'
UNION ALL SELECT 'Matt H';

-- 2 problems to address
-- duplicates in our inbound set
-- duplicates against a reference set
--
-- Better matching will occur if names are split into ordinal entities
-- Splitting on whitespace is always questionable
--
-- Mat, Matt, Matthew 

DECLARE CSR CURSOR
READ_ONLY
FOR 
SELECT DISTINCT
    S1.source_string
,   S1.ref_id
FROM
    @SRC AS S1
ORDER BY
    S1.ref_id;

DECLARE @source_string varchar(50), @ref_id int
OPEN CSR

FETCH NEXT FROM CSR INTO @source_string, @ref_id
WHILE (@@fetch_status <> -1)
BEGIN
    IF (@@fetch_status <> -2)
    BEGIN
        IF NOT EXISTS
        (
            SELECT * FROM @WORK W WHERE W.match_ref_id = @ref_id
        )
        BEGIN
            INSERT INTO
                @WORK
            SELECT
                @ref_id
            ,   S.ref_id
            FROM
                @src S
                -- If we have already matched the value, skip it
                LEFT OUTER JOIN
                    @WORK W
                    ON W.match_ref_id = S.ref_id
            WHERE
                -- Don't match yourself
                S.ref_id <> @ref_id
                -- arbitrary threshold, will need to examine this for sanity
                AND dbo.fn_calculateJaroWinkler(@source_string, S.source_string) > .95
        END
    END
    FETCH NEXT FROM CSR INTO @source_string, @ref_id
END

CLOSE CSR

DEALLOCATE CSR

-- Show me the list of all the unmatched rows 
-- plus the retained

;WITH MATCHES AS
(
    SELECT 
        S1.source_string
    ,   S1.ref_id
    ,   S2.source_string AS match_source_string
    ,   S2.ref_id AS match_ref_id
    FROM 
        @SRC S1
        INNER JOIN
            @WORK W
            ON W.source_ref_id = S1.ref_id
        INNER JOIN
            @SRC S2
            ON S2.ref_id = W.match_ref_id
)
, UNMATCHES AS
(
    SELECT 
        S1.source_string
    ,   S1.ref_id
    ,   NULL AS match_source_string
    ,   NULL AS match_ref_id
    FROM 
        @SRC S1
        LEFT OUTER JOIN
            @WORK W
            ON W.source_ref_id = S1.ref_id
        LEFT OUTER JOIN
            @WORK S2
            ON S2.match_ref_id = S1.ref_id
    WHERE
        W.source_ref_id IS NULL
        and s2.match_ref_id IS NULL
)
SELECT
    M.source_string
,   M.ref_id
,   M.match_source_string
,   M.match_ref_id
FROM
    MATCHES M
UNION ALL
SELECT
    M.source_string
,   M.ref_id
,   M.match_source_string
,   M.match_ref_id
FROM
    UNMATCHES M;

-- To specifically solve your request

SELECT
    S.source_string AS Name
,   COALESCE(S2.source_string, S.source_string) As StdName
FROM
    @SRC S
    LEFT OUTER JOIN
        @WORK W
        ON W.match_ref_id = S.ref_id
    LEFT OUTER JOIN
        @SRC S2
        ON S2.ref_id = W.source_ref_id

查询输出 1

source_string   ref_id  match_source_string match_ref_id
Jon Q   1   John Q  2
Jon Q   1   JOHN Q  3
Jon Q   1   Jonn Q  4
Jon Q   1   Joan Q  5
june    6   NULL    NULL
Mary W  7   NULL    NULL
Marie W 8   NULL    NULL
Matt H  9   NULL    NULL

查询输出 2

Name    StdName
Jon Q   Jon Q
John Q  Jon Q
JOHN Q  Jon Q
Jonn Q  Jon Q
Joan Q  Jon Q
june    june
Mary W  Mary W
Marie W Marie W
Matt H  Matt H

有龙

在 SuperUser 上,我谈到了我匹配人的经验。在本节中,我将列出一些需要注意的事项。

速度

作为匹配的一部分,万岁,因为你有一个生日来增加匹配过程。我实际上建议您首先仅根据出生日期生成匹配项。这是一个完全匹配,并且通过适当的索引,SQL Server 将能够快速包含/排除行。因为你会需要它。TSQL 实现很慢。我一直在对包含 28k 个姓名(已列为会议参加者的姓名)的数据集进行等效匹配。那里应该有一些很好的重叠,虽然我确实用数据填充了@src,但它是一个包含所有含义的表变量,但它现在已经运行了 15 分钟,但仍未完成。

由于多种原因,它很慢,但我突然想到的是函数中的所有循环和字符串操作。这不是 SQL Server 的亮点。如果您需要做很多这样的事情,最好将它们转换为 CLR 方法,这样至少您可以利用 .NET 库的优势进行一些操作。

我们曾经使用的匹配项之一是变音位,它会生成一对可能的名称语音解释。与其每次都计算,不如计算一次并将其存储在名称旁边。这将有助于加快一些匹配。不幸的是,看起来 JW 不适合那样分解它。

看看迭代。我们会首先尝试我们知道速度很快的算法。'John' = 'John' 所以没有必要拿出大手笔,所以我们会尝试第一次直接名称检查。如果我们没有找到匹配项,我们会更加努力。希望通过在匹配中进行各种滑动,我们可以尽快获得低悬的果实,并担心以后更难的匹配。

名称

在我的 SU 答案和代码注释中,我提到了昵称。比尔和比利将匹配。比利、利亚姆和威廉绝对不会匹配,即使他们可能是同一个人。您可能希望查看这样的列表,以提供昵称和全名之间的翻译。在对提供的名称运行一组匹配之后,也许我们会尝试根据可能的根名称来寻找匹配。

显然,这种方法存在缺陷。例如,我的祖父是 Max。只是麦克斯。不是马克西米利安、马克西姆斯或任何其他你可能喜欢的东西。

您提供的名称看起来像是第一个和最后一个连接在一起的。未来的读者,如果您有机会捕捉名称的各个部分,请这样做。有些产品会拆分名称并尝试将它们与目录匹配,以尝试猜测某些东西是名字/中间名还是姓氏,但是你有像“Robar Mike”这样的人。如果你在那里看到这个名字,你会认为 Robar 是一个姓氏,你也会把它读成“强盗”。相反,Robar(用法国口音说)是他的名字,而迈克是他的姓氏。无论如何,我认为如果您可以先和后拆分到单独的字段并将各个部分匹配在一起,您将获得更好的匹配体验。精确的姓氏匹配加上部分名字的匹配可能就足够了,特别是在法律上他们是“Franklin Roosevelt”并且您有一个“F. Roosevelt”的候选人的情况下,也许您有一个首字母可以匹配的规则。或者你没有。

噪音 - 正如 JW 帖子和我的回答中所引用的,为了匹配目的,去掉废话(标点符号、停用词等)。还要注意尊称(phd、jd 等)和世代(II、III、JR、SR)。我们的规则是有/没有世代的候选人可以匹配相反状态的候选人(Bob Jones Jr == Bob Jones)或者可以完全匹配世代(Bob Jones Sr = Bob Jones Sr)但如果你永远不想匹配两个记录都提供了它们并且它们相互冲突(Bob Jones Sr != Bob Jones Jr)。

区分大小写,请始终检查您的数据库和 tempdb 以确保您没有进行区分大小写的匹配。如果你是这样,为了匹配,将所有内容转换为上或下,但永远不要扔掉提供的外壳。祝你好运,尝试确定latessa 是否应该是Latessa、LaTessa 或其他东西。

我的查询即将进行一个小时的处理,没有返回任何行,所以我要杀死它并上交。祝你好运,匹配愉快。

于 2013-05-09T03:29:15.910 回答
6

只是一个想法,但您也许可以使用该SOUNDEX()功能。这将为names相似的创建一个值。

如果你从这样的事情开始:

select name, soundex(name) snd,
  row_number() over(partition by soundex(name)
                    order by soundex(name)) rn
from yt;

请参阅SQL Fiddle with Demo。这将为与 a 相似的每一行提供一个结果,row_number()因此您只能返回每个组的第一个值。例如,上面的查询将返回:

|    NAME |  SND | RN |
-----------------------
|   Jon Q | J500 |  1 |
|  John Q | J500 |  2 |
|  Jonn Q | J500 |  3 |
|  Matt H | M300 |  1 |
|  Mary W | M600 |  1 |
| Marie W | M600 |  2 |

然后,您可以从此结果中选择row_number()等于 1 的所有行,然后在soundex(name)值上连接回您的主表:

select t1.name,
  t2.Stdname
from yt t1
inner join
(
  select name as stdName, snd, rn
  from
  (
    select name, soundex(name) snd,
      row_number() over(partition by soundex(name)
                        order by soundex(name)) rn
    from yt
  ) d
  where rn = 1
) t2
  on soundex(t1.name) = t2.snd;

请参阅SQL Fiddle with Demo。这给出了一个结果:

|    NAME | STDNAME |
---------------------
|   Jon Q |   Jon Q |
|  John Q |   Jon Q |
|  Jonn Q |   Jon Q |
|  Mary W |  Mary W |
| Marie W |  Mary W |
|  Matt H |  Matt H |
于 2013-05-08T19:12:39.857 回答