SQL中的模糊分组

abe*_*bop 4 sql sql-server

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

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

Name
--------
Jon Q
John Q
Jonn Q
Mary W
Marie W
Matt H
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我选择了第一个名称作为"标准化名称",但我实际上并不关心选择哪一个 - 最终将最终的"标准名称"转换为唯一的人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
Run Code Online (Sandbox Code Playgroud)

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

Tar*_*ryn 6

只是一个想法,但你可能能够使用该SOUNDEX()功能.这将为names相似的值创建一个值.

如果你开始这样的事情:

select name, soundex(name) snd,
  row_number() over(partition by soundex(name)
                    order by soundex(name)) rn
from yt;
Run Code Online (Sandbox Code Playgroud)

请参阅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 |
Run Code Online (Sandbox Code Playgroud)

然后,您可以从此结果中选择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;
Run Code Online (Sandbox Code Playgroud)

请参阅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 |
Run Code Online (Sandbox Code Playgroud)


bil*_*nkc 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
Run Code Online (Sandbox Code Playgroud)

查询输出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
Run Code Online (Sandbox Code Playgroud)

查询输出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
Run Code Online (Sandbox Code Playgroud)

有龙

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

速度

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

由于一些原因它很慢,但跳出来的东西都是函数中的循环和字符串操作.这不是SQL Server闪耀的地方.如果您需要做很多这样的事情,那么将它们转换为CLR方法可能是个好主意,因此至少可以利用.NET库的强度进行一些操作.

我们过去使用的其中一个匹配是Double Metaphone,它会产生一对可能的名称语音解释.不是每次计算,而是计算一次并将其与名称一起存储.这将有助于加速一些匹配.不幸的是,看起来JW并不像这样打破它.

看看迭代.我们首先尝试我们知道快速的algs.'John'='John'所以没有必要拔出大枪所以我们会尝试第一次直接的名字检查.如果我们找不到比赛,我们会更加努力.我们希望通过对比进行各种滑动,我们可以尽快获得低悬的成果,并担心以后会有更难的比赛.

名称

在我的SU答案和代码评论中,我提到了昵称.比尔和比利将要匹配.比利,利亚姆和威廉绝对不会匹配,即使他们可能是同一个人.您可能希望查看这样的列表,以提供昵称和全名之间的转换.在提供的名称上运行一组匹配后,我们可能会尝试根据可能的根名称查找匹配项.

显然,这种方法有缺点.例如,我的祖父是马克斯.Just Max.不是Maximilian,Maximus或您可能做的任何其他事情.

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

噪音 - 正如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还是别的什么.

我的查询正在进行一个小时的处理,没有返回任何行,所以我要杀了它然后上交.祝你好运,快乐匹配.