这是一个常见的电子表格或数据库问题:
+-----+-------------------+|row|fullname|+-----+-------------------+|1|JohnF.Doe||2|Esquivel,Mara||3|Doe,JohnF||4|Whyte,Luke||5|Doe,JohnFrancis|+-----+-------------------+第1,3和5行可能指的是拼写和格式略有偏差的同一个人。在小型数据集中,可以手动清洁细胞。但是在庞大的数据集中呢?如何梳理成千上万的文本条目并将类似的实体分组?
理想情况下,有一种简单的方法来添加第三列,如下所示:
+-----+-------------------+---------------+|row|fullname|name_groups|+-----+-------------------+---------------+|1|JohnF.Doe|DoeJohnF||2|Esquivel,Mara|EsquivelMara||3|Doe,JohnF|DoeJohnF||4|Whyte,Luke|WhyteLuke||5|Doe,JohnFrancis|DoeJohnF|+-----+-------------------+---------------+好吧,那就是要做的事情。
TLDR:为此构建了一个工具。可以在此处安装Python模块。但是如果想了解这个工具背后的概念请继续阅读。
将讨论的主题:
在本教程中,将使用美国劳工部工资盗窃调查的这个数据集。它包含了从1984年到2018年由于最低工资或加班违规而对雇主进行的每次DOL调查。
数据包括一legal_name列,列出了被调查公司的名称。但是,输入格式变化很大:
+-----+----------------------+|row|legal_name|+-----+----------------------+|1|Wal-martInc||2|WalmartStoresInc.||3|Wal-martstoresInc||4|Wal-MartstoresInc.|+-----+----------------------+将对条目进行规范化和分组,legal_name然后使用组进行快速分析。
第一步:使用TF-IDF和N-Grams构建文档术语矩阵
在这里面临的最大挑战是,专栏中的每个条目都需要与其他条目进行比较。因此,一张400,000行的纸张需要400,0002的计算。
如果可以使用矩阵乘法进行同步计算会更快,可以使用文档术语矩阵,TF-IDF和N-Grams。
定义这些术语:
文件术语矩阵
文档术语矩阵本质上是BagofWords(BOW)概念的延伸,喜欢这个概念,因为它听起来就像是一个蒙面男子会在芝麻街偷窃的东西。
BOW涉及计算字符串中单词的频率。所以鉴于这句话:
“RhodeIslandisneitheraroadnorisitanisland.Discuss.”
可以生成这样的BOW表示:
+---------+-------+|term|count|+---------+-------+|rhode|1||island|2||is|2||neither|1||a|1||road|1||nor|1||it|1||an|1||discuss|1|+---------+-------+文档术语矩阵(DTM)将BOW扩展为多个字符串(或者在命名中,“多个文档”)。想象一下,有以下三个字符串:
DTM可能如下所示:
每个条目的值通过计算每个单词在每个字符串中出现的次数来确定。
上述方法的问题在于,诸如“the”,“is”和“if”之类的微不足道的词语往往比重要词语更频繁地出现,这可能会扭曲分析。
因此可以为它们分配TF-IDF分数,而不是计算单词,该分数评估每个单词对DTM的重要性。
TF-IDF
为了计算TF-IDF分数,将术语在单个文档中出现的次数(术语频率或TF)乘以术语对整个语料库的重要性(逆文档频率或IDF)-单词出现的文档越多在这个词中,人们认为这个词在区分文件方面的价值就越低。
重要的是,对于文档术语矩阵中的每个单词,如果用TF-IDF分数替换单词计数,可以在检查字符串相似性时更有效地权衡单词。
N元
最后将解决这个问题:
BurgerKing是两个字。BurgerKing应该是两个单词,但计算机会将其视为一个单词。因此,当计算文档术语矩阵时,这些术语将不匹配。
N-gram是一种将字符串分成较小块的方法,其中块N大小。所以如果设置N到3得到:
和:
它比原始字符串重叠得多。
因此当构建文档术语矩阵时,计算N-Grams的TF-IDF分数而不是单词。
最后一些代码:
以下是使用N-Grams构建文档术语矩阵作为列标题和值的TF-IDF分数的代码:
第10行从legal_name数据集的列中提取唯一值,并将它们放在一维NumPy数组中。
在第14行,编写了用于构建5个字符N-Grams的函数。使用正则表达式过滤掉一些字符。
第20行传递ngrams_analyzer给将用于构建矩阵的TF-IDF矢量化器。
最后在第23行,构建了文档术语矩阵。
稀疏与密集矩阵以及如何使计算机崩溃
上述代码的结果tfidf_matrix是压缩稀疏行(CSR)矩阵。
出于目的,要知道任何大多数零值的矩阵都是稀疏矩阵。这与大多数非零值的密集矩阵不同。
N-Grams矩阵有237,573行和389,905列。前10行和列如下所示:
这很稀疏。没有理由将所有这些零存储在内存中。如果这样做,就有可能耗尽RAM并触发一个MemoryError。
输入CSR矩阵,该矩阵仅存储矩阵的非零值和对其原始位置的引用。
重要的是CSR格式可以节省内存,同时仍允许快速行访问和矩阵乘法。
步骤二:使用余弦相似度计算字符串之间的接近度
余弦相似度是0和1之间的度量,用于确定类似字符串的长度,而不管它们的长度如何。
它测量多维空间中字符串之间角度的余弦。该值越接近1(余弦为0°),字符串相似度越高。
采取以下三个字符串:
并将它们放在文档术语矩阵中:
然后在多维空间上绘制此矩阵,其中每个维度对应于我们的四个术语之一。这可能看起来像这样:
如果看看点之间的距离,“Ilovedogs”和“Ihatecats”实际上比“Ilovedogs”和“Ilove…lovedogs”更接近彼此。
然而,如果看一下点线之间的角度-余弦距离-可以看到“Ilovedogs”和“Ilove…lovedogs”之间的角度远小于“Ilovedogs”之间的角度和“Ihatecats”。
因此字符串1和字符串2之间的余弦相似性将比字符串1和字符串3之间的余弦相似性更高(更接近1)。
这是一个更深入的解释。
在Python中计算余弦相似度
可以使用scikit-learn来计算余弦相似度。这将返回具有余弦相似度值的成对矩阵,如:
然后将通过相似性阈值(例如0.75或0.8)过滤此矩阵,以便对认为代表相同实体的字符串进行分组。
但是如果使用由INGBank的数据科学家构建的这个模块,可以在构建矩阵时按照相似性阈值进行过滤。该方法比scikit-learn更快,并返回内存密集度较低的CSR矩阵供使用。
所以在脚本中添加以下内容:
第三步:构建一个哈希表,将发现转换为电子表格中的“组”列
现在要构建一个Python字典,其中包含legal_name列中每个唯一字符串的键。
最快的方法是将CSR矩阵转换为坐标(COO)矩阵。COO矩阵是稀疏矩阵的另一种表示。
例如如果有这个稀疏矩阵:
+------------+|0,0,0,4||0,1,0,0||0,0,0,0||3,0,0,7|+------------+将其转换为COO矩阵,它会成为一个对象,具有三个属性-,,row-分别包含以下三个数组,:coldata
因此可以说值4(存储在matrix.data[0])的坐标是(0,3)(存储在(matrix.row[0],matrix.col[0])中。
构建COO矩阵并使用它来填充字典:
在第39-43行,遍历坐标矩阵,为非零值拉出行和列索引-记住它们都具有超过0.8的余弦相似性-然后将它们转换为它们的字符串值。
为了澄清,通过一个简单的示例进一步解开第39-43行。再次,取这个余弦矩阵:
如果使用awesome_cossim_topn阈值设置为0.8构建它,然后将其转换为COO矩阵,可以像这样表示:
继续这个例子,在所有的字符串通过之后add_pair_to_lookup,最终得到:
矢量化Panda
最后,可以在Pandas中使用矢量化功能,将每个legal_name值映射到GroupDataFrame中的新列并导出新的CSV。
由于Pandas函数可以同时对整个数组进行操作-而不是依次对各个值进行操作-因此这个过程非常快:
把它们放在一起:
剧透警报:这是沃尔玛。183项调查导致他们同意支付近4100万美元的拖欠工资。
最后一点
如果希望按两列或更多列而不是一列进行分组,则可以创建一个临时列,以便在DataFrame中对每个列连接成单个字符串的条目进行分组: