为啥这两种变体之间的速度差异如此之大?
Posted
技术标签:
【中文标题】为啥这两种变体之间的速度差异如此之大?【英文标题】:Why is there so much speed difference between these two variants?为什么这两种变体之间的速度差异如此之大? 【发布时间】:2019-09-13 10:56:44 【问题描述】:版本 1:
import string, pandas as pd
def correct_contraction1(x, dic):
for word in dic.keys():
if word in x:
x = x.replace(word, " " + dic[word]+ " ")
return x
版本 2:
import string, pandas as pd
def correct_contraction2(x, dic):
for word in dic.keys():
if " " + word + " " in x:
x = x.replace(" " + word + " ", " " + dic[word]+ " ")
return x
我如何使用它们:
train['comment_text'] = train['comment_text'].apply(correct_contraction1,args=(contraction_mapping,))
#3 mins 40 sec without that space thing (version1)
train['comment_text'] = train['comment_text'].apply(correct_contraction2,args=(contraction_mapping,))
#5 mins 56 sec with that space thing (version2)
为什么会有如此大的速度差异,这不太可能是这种情况,其次是任何更好/隐藏的 pandas 技巧来进一步优化这一点? (代码已经在 Kaggle Kernels 上测试过多次)
train
是一个数据框,在这两种情况下都有 200 万行,也完全相同
contraction_mapping
是一个字典映射...(两种情况都一样)
希望有最新的熊猫。
编辑
数据来自Kaggle Comp,版本 1 更快!【问题讨论】:
是否可以创建一些数据样本进行测试? 当然,使用的数据来自这里的kaggle comp kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification/… 在版本 1 中,您为每个“for”创建并添加字符串,但在版本 2 中,您“仅当”时才这样做? 我认为第一个版本是两者中的更快? (1) 您的代码不必要地一遍又一遍地重新计算" " + word + " "
。您不妨将空格放在字典键中。 (2) 测试if word in x:
是多余的。无论如何,对replace()
的调用必须进行存在检查。
【参考方案1】:
很抱歉没有回答差异,但在任何情况下都可以轻松改进当前的方法。它对你来说很慢,因为你必须多次扫描所有句子(每个单词)。您甚至要检查每个单词两次,首先是否存在,然后替换它 - 您可以只替换。
这是进行文本替换时的关键一课,无论是使用正则表达式、简单的字符串替换,还是开发自己的算法:尝试只检查文本一次。无论您要替换多少个单词。正则表达式有很长的路要走,但根据实现需要在找不到命中时返回几个字符。感兴趣的人:查找 trie 数据结构。
尝试一个快速文本搜索的实现(aho-corasick)。我正在为此开发一个库,但在那之前,您可以使用flashtext
(它的作用略有不同):
import flashtext
# already considers word boundaries, so no need for " " + word " "
fl = flashtext.KeywordProcessor()
fl.add_keywords_from_dict(dic)
train['comment_text'] = train['comment_text'].apply(fl.replace_keywords)
如果你有很多词要替换,这会快几个数量级。
比较我能找到的第一个数据:
Words to replace: 8520
Sentences to replace in: 11230
Replacements made using flashtext: 1706
Replacements made using correct_contraction1: 25
flashtext: (considers word boundaries and ignores case)
39 ms ± 355 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
correct_contraction1: (does not consider case nor words at end of line)
11.9 s ± 194 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
<unannounced>
30 ms ± 366 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
所以我们说的是 300 倍的加速。这不会每天都发生;-)
作为参考,Jon Clements 添加了正则表达式:
pandas.str.replace + regex (1733 replacements)
3.02 s ± 82.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
在我测试时,我的新库将再减少 30%。我也看到了 2-3 倍于 flashtext 的改进,但更重要的是,作为用户,您可以拥有更多的控制权。它功能齐全,只需要清理它并添加更多文档即可。
答案到了我会更新的!
【讨论】:
不知道为什么,但稍后会引发错误“IndexError:字符串索引超出范围”;会尝试找出原因;感谢库\ 两个版本都会扫描两次,所以这并不能解释它们之间的速度差异。 @Aditya 确保不要尝试添加我猜的空字符串? 这里是集合,以防它帮助人们将来访问此查询gist.github.com/AdityaSoni19031997/… 真正有趣的是我创建了github.com/kootenpv/contractions 可以帮助你一点哈哈。虽然一旦我的新库出现,我将采用收缩的逻辑并以更快的方式实现它。我也犯了在那里多次扫描文本的错误!请继续关注。【参考方案2】:您最好在此处使用 Pandas 的 Series.str.replace
并根据查找表的内容为其提供编译的正则表达式。这意味着字符串替换操作可以比应用函数更快地在 Series 上工作,这也意味着您不会以字符串方式扫描,比您需要的时间要多得多......希望它可以将您的时间减少到几秒钟分钟。
import re
import pandas as pd
corrections =
"it's": "it is",
"can't": "can not",
"won't": "will not",
"haven't": "have not"
sample = pd.Series([
"Stays the same",
"it's horrible!",
"I hope I haven't got this wrong as that won't do",
"Cabbage"
])
然后构建您的正则表达式,以便它在字典中查找任何可能的匹配项,不区分大小写并尊重单词边界:
rx = re.compile(r'(?i)\b()\b'.format('|'.join(re.escape(c) for c in corrections)))
然后应用到您的列(例如将sample
更改为training['comment_text']
)str.replace
传递正则表达式和一个函数,该函数接受匹配并返回找到的键的匹配值:
corrected = sample.str.replace(rx, lambda m: corrections.get(m.group().lower()))
那么您将拥有corrected
作为一个包含:
['Stays the same',
'it is horrible!',
'I hope I have not got this wrong as that will not do',
'Cabbage']
注意It's
的大小写...它已被不区分大小写地提取并改为it is
... 有多种方法可以保留大小写,但它可能不是非常重要并且完全是一个不同的问题。
【讨论】:
非常感谢您的回答和提示;(即将将它们转换为 re.compile 然后 re.sub) 请注意,如果关键字可以以特殊的正则表达式元字符开头/结尾,\b
将不起作用。
@WiktorStribiżew 我是re.escape
'ing 所有的关键字,但我还有什么遗漏的吗?
\b\(
只会匹配a(
而不会匹配a (
。【参考方案3】:
第二个版本必须在每次循环中执行连接" " + word + " "
,当它找到匹配时,它会第二次执行替换。这让它变慢了。
您无法避免第一个串联(除非您修改 dic
以便键周围已经有空格)。但是您可以通过第一次将其保存在变量中来避免第二次串联。它仍然会比第一个版本慢,但不会慢很多。
def correct_contraction2(x, dic):
for word in dic.keys():
spaceword = " " + word + " "
if spaceword in x:
x = x.replace(spaceword, " " + dic[word]+ " ")
return x
似乎第二个版本可能无法在所有情况下正常工作。如果单词位于一行的开头或结尾,则不会被空格包围。最好使用带有\b
的正则表达式来匹配单词边界。
【讨论】:
非常感谢您的回答和提示;以上是关于为啥这两种变体之间的速度差异如此之大?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 Google Analytics 和 BigQuery 之间的独特事件差异如此之大?