为了账号安全,请及时绑定邮箱和手机立即绑定

用Splink两分钟内去重700万条记录

使用DuckDB在混乱的数据中进行闪电般快速的实体解析任务

成群的鸭子正在处理杂乱无章的数据。由作者使用DALL·E 3生成的图片。

数据去重化就是在多个记录中匹配不同实体的问题。

以下是在不同地方收集到的有关同一个实体的记录示例

这些记录之间的差异和失误使得我们很难简单地判断它们是不是指的同一个人。

Splink 是一个免费的开源 Python 库,用于解决此类问题。本文将展示其重要功能之一:它运行速度非常快。

它展示了在使用默认的DuckDB后端对包含7百万行的数据集进行基准测试(benchmarking)时,Splink的结果。

据我所知,这些结果显示的是,它是最快的免费工具,能够准确地去除重复数据的大规模数据集——至少快一个数量级以上。

数据(待补充)

为了测试Splink的表现,从Wikidata查询服务收集了一组人的数据集。

每个记录都被修改,生成了0到35个重复记录,平均每个记录会产生大约8个重复。以下是一些典型的行。

这些行对应于, 维基数据ID Q101637549

Splink代码(注:Splink为特定术语,此处保留原文)

本文后面附有完整的、用于实际大规模链接任务的 Splink 脚本,如下所示。

  • 它使用多种复杂的模糊比较方法来检测记录间的相似度(例如 Josef 和 Joseph 或 1845–01–01 和 1845–01–13)
  • 该脚本不仅考虑字符串变量的差异,还建模出生地之间的地理距离
  • 它使用词频调整来确保不同名字的匹配得分不同(例如,匹配 John 的得分比匹配 Lucas 的得分低)

在这篇文章中,为了展示Splink的速度,我选择对每个输入记录进行大约100次比较,这意味着对于每个记录,任务会评估超过10亿次的两两比较。

在实际的记录匹配场景中,尤其是在我们有更多关于实体的数据列时,通常可以使用更精细的分组规则,从而每条输入记录的比较数量更少。

详细的结果分析

以下图表显示了运行时长(例如:估计模型参数、预测匹配对、利用图算法将匹配的对聚成实体等)根据不同类型的负载:

请注意对数刻度,也就是log scale

工作负载很容易实现并行处理,在更大的机器上运行时间大大减少。因此,运行时间大大减少。

我们还可以看到,即使在一台笔记本级别的机器(c6g.2xlarge = 8vCPU/16Gb 内存(RAM))上,最长的操作(即预测)只需大约 16 分钟。

来看看与其他库相比怎么样

三个其他流行的开源记录链接包的运行时间图表可以在随附的paper中找到,该论文伴随fastLink R 包。

图片来自 Enamorado Ted, Benjamin Fifield 和 Kosuke Imai. (2019). “利用概率模型帮助大规模行政记录整合.” American Political Science Review, 第113卷, 第2期(5月),第353–371页。

图表显示,在这三种方法中,fastLink(快速链接软件)表现最佳,在8核机器上,fastLink能在400分钟内处理三十万条记录的去除重复记录,并且其运行时间大约随着输入记录数量的增加而呈二次方增长。

Splink能够在不到一分钟的时间内在配置相当的机器上消除相同数量的记录。这表明它的速度至少快了两个数量级。

准确性

本文仅讨论运行时。准确性统计数据仅对用于基准测试的具体数据集有效,我没有其他库的相关准确性统计数据进行比较。

然而,一项独立的研究发现(例如,此项研究:https://scholar.googleusercontent.com/scholar?q=cache%3Ao4-c8w6DveYJ%3Ascholar.google.com%2F+splink&hl=en&as_sdt=0%2C5&as_ylo=2023),Splink在准确性方面与其它方法相比表现得相当不错。另外,从理论上讲(例如,这个理论观点:https://medium.com/me/stats/post/15a28c733e73),其他方法不太可能显著超越Splink

繁殖材料

性能基准测试是在多种 AWS EC2 实例上使用 pytest-benchmark 工具进行的。代码可以在以下两个代码库中找到:

更多阅读资料:

附录:完整的 Splink 脚本内容

    import pandas as pd  
    from splink.duckdb.blocking_rule_library import block_on  
    from splink.duckdb.comparison_library import (  
        distance_in_km_at_thresholds,  
        exact_match,  
        jaro_winkler_at_thresholds,  
        levenshtein_at_thresholds,  
    )  
    from splink.duckdb.linker import DuckDBLinker  

    df = pd.read_parquet("7m_prepareed.parquet")  

    splink_settings = {  
        "link_type": "dedupe_only",  
        "blocking_rules_to_generate_predictions": [  
            block_on(["last_name", "occupation"]),  
            block_on(["first_name", "last_name"]),  
            block_on(["first_name", "middle_name"]),  
            block_on(["middle_name", "last_name"]),  
            block_on(["first_name", "dob"]),  
            block_on(["first_name", "middle_name"]),  
            block_on(["last_name", "birth_lat"]),  
            block_on(["first_name", "birth_lng"]),  
            block_on(["middle_name", "occupation"]),  
        ],  
        "comparisons": [  
            jaro_winkler_at_thresholds(  
                "first_name", [0.9, 0.7], term_frequency_adjustments=True  
            ),  
            jaro_winkler_at_thresholds("middle_name", [0.9]),  
            jaro_winkler_at_thresholds(  
                "last_name", [0.9, 0.7], term_frequency_adjustments=True  
            ),  
            levenshtein_at_thresholds(  
                "dob", [1, 2, 4], term_frequency_adjustments=True  
            ),  
            distance_in_km_at_thresholds(  
                "birth_lat", "birth_lng", [10, 100]  
            ),  
            exact_match("occupation", term_frequency_adjustments=True),  
        ],  
        "retain_intermediate_calculation_columns": False,  
        "retain_matching_columns": False,  
        "max_iterations": 20,  
        "em_convergence": 0.001,  
    }  

    linker = DuckDBLinker("df", splink_settings)  

    linker.estimate_probability_two_random_records_match(  
        [  
            block_on(["first_name", "last_name", "dob"]),  
        ],  
        recall=0.8,  
    )  
    linker.estimate_u_using_random_sampling(max_pairs=1e9)  
    linker.estimate_parameters_using_expectation_maximisation(  
        block_on(  
            ["first_name", "last_name", "occupation"], salting_partitions=2  
        ),  
        estimate_without_term_frequencies=True,  
    )  
    linker.estimate_parameters_using_expectation_maximisation(  
        block_on(["dob", "middle_name"], salting_partitions=2),  
        estimate_without_term_frequencies=True,  
    )  

    df_predict = linker.predict(threshold_match_probability=0.9)  

    linker.cluster_pairwise_predictions_at_threshold(  
        df_predict=df_predict, threshold_match_probability=0.9  
    )
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

0 评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消