Pandas 筛选串联的多个子串
Pandas filtering for multiple substrings in series
我需要过滤 pandas
数据框中的行,以便特定字符串列至少包含所提供子字符串列表中的一个。子字符串可能包含不寻常的/正则表达式字符。比较不应涉及正则表达式且不区分大小写。
例如:
lst = ['kdSj;af-!?', 'aBC+dsfa?\-', 'sdKaJg|dksaf-*']
我目前是这样敷面膜的:
mask = np.logical_or.reduce([df[col].str.contains(i, regex=False, case=False) for i in lst])
df = df[mask]
我的数据框很大(~1mio 行)并且 lst
的长度为 100。有没有更有效的方法?例如,如果找到 lst
中的第一项,我们就不必测试该行的任何后续字符串。
如果您坚持使用 pure-pandas,为了性能和实用性,我认为您应该使用正则表达式来完成这项任务。但是,您需要首先正确转义子字符串中的任何特殊字符,以确保它们按字面匹配(而不是用作正则表达式元字符)。
这很容易使用 re.escape
:
>>> import re
>>> esc_lst = [re.escape(s) for s in lst]
然后可以使用正则表达式管道连接这些转义子字符串 |
。可以针对字符串检查每个子字符串,直到有一个匹配(或者它们都已经过测试)。
>>> pattern = '|'.join(esc_lst)
屏蔽阶段随后变成一个遍历行的低级循环:
df[col].str.contains(pattern, case=False)
这里有一个简单的设置来感受性能:
from random import randint, seed
seed(321)
# 100 substrings of 5 characters
lst = [''.join([chr(randint(0, 256)) for _ in range(5)]) for _ in range(100)]
# 50000 strings of 20 characters
strings = [''.join([chr(randint(0, 256)) for _ in range(20)]) for _ in range(50000)]
col = pd.Series(strings)
esc_lst = [re.escape(s) for s in lst]
pattern = '|'.join(esc_lst)
建议的方法大约需要 1 秒(因此对于 100 万行可能最多需要 20 秒):
%timeit col.str.contains(pattern, case=False)
1 loop, best of 3: 981 ms per loop
问题中的方法使用相同的输入数据大约需要 5 秒。
值得注意的是,这些时间是 'worst case',因为没有匹配项(所以 所有 个子字符串都被检查了)。如果有比赛,时间会有所改善。
您可以尝试使用 Aho-Corasick algorithm。在一般情况下,它是 O(n+m+p)
,其中 n
是搜索字符串的长度,m
是搜索文本的长度,p
是输出匹配的数量。
Aho-Corasick 算法 often used 在输入文本(大海捞针)中找到多个模式(针)。
pyahocorasick 是算法的 C 实现的 Python 包装器。
让我们比较一下它与一些替代方案的速度。下面是一个基准
显示 using_aho_corasick
比原始方法快 30 倍以上
(在问题中显示)在 50K 行 DataFrame 测试用例上:
| | speed factor | ms per loop |
| | compared to orig | |
|--------------------+------------------+-------------|
| using_aho_corasick | 30.7x | 140 |
| using_regex | 2.7x | 1580 |
| orig | 1.0x | 4300 |
In [89]: %timeit using_ahocorasick(col, lst)
10 loops, best of 3: 140 ms per loop
In [88]: %timeit using_regex(col, lst)
1 loop, best of 3: 1.58 s per loop
In [91]: %timeit orig(col, lst)
1 loop, best of 3: 4.3 s per loop
这里是用于基准测试的设置。它还验证输出是否与 orig
:
返回的结果匹配
import numpy as np
import random
import pandas as pd
import ahocorasick
import re
random.seed(321)
def orig(col, lst):
mask = np.logical_or.reduce([col.str.contains(i, regex=False, case=False)
for i in lst])
return mask
def using_regex(col, lst):
""" (Alex Riley)"""
esc_lst = [re.escape(s) for s in lst]
pattern = '|'.join(esc_lst)
mask = col.str.contains(pattern, case=False)
return mask
def using_ahocorasick(col, lst):
A = ahocorasick.Automaton(ahocorasick.STORE_INTS)
for word in lst:
A.add_word(word.lower())
A.make_automaton()
col = col.str.lower()
mask = col.apply(lambda x: bool(list(A.iter(x))))
return mask
N = 50000
# 100 substrings of 5 characters
lst = [''.join([chr(random.randint(0, 256)) for _ in range(5)]) for _ in range(100)]
# N strings of 20 characters
strings = [''.join([chr(random.randint(0, 256)) for _ in range(20)]) for _ in range(N)]
# make about 10% of the strings match a string from lst; this helps check that our method works
strings = [_ if random.randint(0, 99) < 10 else _+random.choice(lst) for _ in strings]
col = pd.Series(strings)
expected = orig(col, lst)
for name, result in [('using_regex', using_regex(col, lst)),
('using_ahocorasick', using_ahocorasick(col, lst))]:
status = 'pass' if np.allclose(expected, result) else 'fail'
print('{}: {}'.format(name, status))
使用更简单的示例并忽略大小写(大写或小写)
过滤并获取二元向量:
我想找到 pd.Series
、v
中包含 "at" 或 "Og" 的所有元素。如果元素包含模式,则为 1,否则为 0。
我将使用 re
:
import re
我的矢量:
v=pd.Series(['cAt','dog','the rat','mouse','froG'])
[Out]:
0 cAt
1 dog
2 the rat
3 mouse
4 froG
我想找到 v 中包含 "at" 或 "Og" 的所有元素。
也就是说,我可以将 pattern
定义为:
pattern='at|Og'
因为我想要一个向量,如果项目包含模式则为 1,否则为 0。
我创建了一个与 v 长度相同的酉向量:
v_binary=[1]*len(v)
我得到一个布尔值 s
,即 True
如果 v
的一个元素包含 pattern
或 False
如果不包含它.
s=v.str.contains(pattern, flags=re.IGNORECASE, regex=True)
为了获得二进制向量,我乘以 v_binary
*s
:
v_binary*s
[Out]
0 1
1 1
2 1
3 0
4 1
我需要过滤 pandas
数据框中的行,以便特定字符串列至少包含所提供子字符串列表中的一个。子字符串可能包含不寻常的/正则表达式字符。比较不应涉及正则表达式且不区分大小写。
例如:
lst = ['kdSj;af-!?', 'aBC+dsfa?\-', 'sdKaJg|dksaf-*']
我目前是这样敷面膜的:
mask = np.logical_or.reduce([df[col].str.contains(i, regex=False, case=False) for i in lst])
df = df[mask]
我的数据框很大(~1mio 行)并且 lst
的长度为 100。有没有更有效的方法?例如,如果找到 lst
中的第一项,我们就不必测试该行的任何后续字符串。
如果您坚持使用 pure-pandas,为了性能和实用性,我认为您应该使用正则表达式来完成这项任务。但是,您需要首先正确转义子字符串中的任何特殊字符,以确保它们按字面匹配(而不是用作正则表达式元字符)。
这很容易使用 re.escape
:
>>> import re
>>> esc_lst = [re.escape(s) for s in lst]
然后可以使用正则表达式管道连接这些转义子字符串 |
。可以针对字符串检查每个子字符串,直到有一个匹配(或者它们都已经过测试)。
>>> pattern = '|'.join(esc_lst)
屏蔽阶段随后变成一个遍历行的低级循环:
df[col].str.contains(pattern, case=False)
这里有一个简单的设置来感受性能:
from random import randint, seed
seed(321)
# 100 substrings of 5 characters
lst = [''.join([chr(randint(0, 256)) for _ in range(5)]) for _ in range(100)]
# 50000 strings of 20 characters
strings = [''.join([chr(randint(0, 256)) for _ in range(20)]) for _ in range(50000)]
col = pd.Series(strings)
esc_lst = [re.escape(s) for s in lst]
pattern = '|'.join(esc_lst)
建议的方法大约需要 1 秒(因此对于 100 万行可能最多需要 20 秒):
%timeit col.str.contains(pattern, case=False)
1 loop, best of 3: 981 ms per loop
问题中的方法使用相同的输入数据大约需要 5 秒。
值得注意的是,这些时间是 'worst case',因为没有匹配项(所以 所有 个子字符串都被检查了)。如果有比赛,时间会有所改善。
您可以尝试使用 Aho-Corasick algorithm。在一般情况下,它是 O(n+m+p)
,其中 n
是搜索字符串的长度,m
是搜索文本的长度,p
是输出匹配的数量。
Aho-Corasick 算法 often used 在输入文本(大海捞针)中找到多个模式(针)。
pyahocorasick 是算法的 C 实现的 Python 包装器。
让我们比较一下它与一些替代方案的速度。下面是一个基准
显示 using_aho_corasick
比原始方法快 30 倍以上
(在问题中显示)在 50K 行 DataFrame 测试用例上:
| | speed factor | ms per loop |
| | compared to orig | |
|--------------------+------------------+-------------|
| using_aho_corasick | 30.7x | 140 |
| using_regex | 2.7x | 1580 |
| orig | 1.0x | 4300 |
In [89]: %timeit using_ahocorasick(col, lst)
10 loops, best of 3: 140 ms per loop
In [88]: %timeit using_regex(col, lst)
1 loop, best of 3: 1.58 s per loop
In [91]: %timeit orig(col, lst)
1 loop, best of 3: 4.3 s per loop
这里是用于基准测试的设置。它还验证输出是否与 orig
:
import numpy as np
import random
import pandas as pd
import ahocorasick
import re
random.seed(321)
def orig(col, lst):
mask = np.logical_or.reduce([col.str.contains(i, regex=False, case=False)
for i in lst])
return mask
def using_regex(col, lst):
""" (Alex Riley)"""
esc_lst = [re.escape(s) for s in lst]
pattern = '|'.join(esc_lst)
mask = col.str.contains(pattern, case=False)
return mask
def using_ahocorasick(col, lst):
A = ahocorasick.Automaton(ahocorasick.STORE_INTS)
for word in lst:
A.add_word(word.lower())
A.make_automaton()
col = col.str.lower()
mask = col.apply(lambda x: bool(list(A.iter(x))))
return mask
N = 50000
# 100 substrings of 5 characters
lst = [''.join([chr(random.randint(0, 256)) for _ in range(5)]) for _ in range(100)]
# N strings of 20 characters
strings = [''.join([chr(random.randint(0, 256)) for _ in range(20)]) for _ in range(N)]
# make about 10% of the strings match a string from lst; this helps check that our method works
strings = [_ if random.randint(0, 99) < 10 else _+random.choice(lst) for _ in strings]
col = pd.Series(strings)
expected = orig(col, lst)
for name, result in [('using_regex', using_regex(col, lst)),
('using_ahocorasick', using_ahocorasick(col, lst))]:
status = 'pass' if np.allclose(expected, result) else 'fail'
print('{}: {}'.format(name, status))
使用更简单的示例并忽略大小写(大写或小写)
过滤并获取二元向量:
我想找到 pd.Series
、v
中包含 "at" 或 "Og" 的所有元素。如果元素包含模式,则为 1,否则为 0。
re
:
import re
我的矢量:
v=pd.Series(['cAt','dog','the rat','mouse','froG'])
[Out]:
0 cAt
1 dog
2 the rat
3 mouse
4 froG
我想找到 v 中包含 "at" 或 "Og" 的所有元素。
也就是说,我可以将 pattern
定义为:
pattern='at|Og'
因为我想要一个向量,如果项目包含模式则为 1,否则为 0。
我创建了一个与 v 长度相同的酉向量:
v_binary=[1]*len(v)
我得到一个布尔值 s
,即 True
如果 v
的一个元素包含 pattern
或 False
如果不包含它.
s=v.str.contains(pattern, flags=re.IGNORECASE, regex=True)
为了获得二进制向量,我乘以 v_binary
*s
:
v_binary*s
[Out]
0 1
1 1
2 1
3 0
4 1