SQL中的模糊分组

时间:2013-05-08 19:05:11

标签: sql sql-server

我需要修改一个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算法(参见例如here)。

如果我知道名称都是成对的,那么这将是一个相对简单的查询,但可以有任意数量的同名。

我可以很容易地概念化如何使用过程语言进行此查询,但我对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']

2 个答案:

答案 0 :(得分:6)

假设您从SSC复制并粘贴jaro-winkler实现(需要注册),以下代码将起作用。我尝试为它构建一个SQLFiddle,但是当我构建模式时,它仍然不知所措。

这个实现有一个骗子---我使用游标。通常,游标不利于性能,但在这种情况下,您需要能够将该集合与自身进行比较。可能有一种优雅的number/tally table方法来消除声明的光标。

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上,我谈到了我的experience matching people。在本节中,我将列出一些需要注意的事项。

速度

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

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

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

看看迭代。我们首先尝试我们知道快速的算法。 &#39;约翰&#39; =&#39;约翰&#39;因此,我们不需要拔出大枪,所以我们尝试直接进行直接姓名检查。如果我们没有找到匹配项,我们会更加努力。我们希望通过采取各种方式进行匹配,我们可以尽快获得低成果,并担心以后会有更难的比赛。

姓名

在我的SU答案和代码评论中,我提到了昵称。比尔和比利将要匹配。即使他们可能是同一个人,Billy,Liam和William绝对不会 匹配。您可能希望查看此类列表,以便在nickname and full name之间进行翻译。在提供的名称上运行一组匹配后,我们可能会尝试根据可能的根名称查找匹配项。

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

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

噪音 - 在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还是别的什么。

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

答案 1 :(得分:5)

只是一个想法,但您可以使用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。哪个会为每个与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 |