Python 串联多个子串的Pandas滤波

Python 串联多个子串的Pandas滤波,python,string,pandas,dataframe,series,Python,String,Pandas,Dataframe,Series,我需要过滤pandasdataframe中的行,以便特定的字符串列至少包含所提供的子字符串列表中的一个。子字符串可能具有不寻常的/regex字符。比较不应涉及regex,并且不区分大小写 例如: lst = ['kdSj;af-!?', 'aBC+dsfa?\-', 'sdKaJg|dksaf-*'] 我当前应用的遮罩如下所示: mask = np.logical_or.reduce([df[col].str.contains(i, regex=False, case=False) for i

我需要过滤
pandas
dataframe中的行,以便特定的字符串列至少包含所提供的子字符串列表中的一个。子字符串可能具有不寻常的/regex字符。比较不应涉及regex,并且不区分大小写

例如:

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
中的第一项,我们不必测试该行的任何后续字符串。

如果您坚持使用纯熊猫,出于性能和实用性考虑,我认为您应该使用regex来完成此任务。但是,您需要首先正确转义子字符串中的任何特殊字符,以确保它们按字面意思匹配(而不是用作正则表达式元字符)

这很容易通过以下方式实现:

然后可以使用正则表达式管道将这些转义的子字符串连接起来。可以对照字符串检查每个子字符串,直到其中一个匹配为止(或者它们都经过测试)

然后,掩蔽阶段成为通过行的单个低级循环:

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秒(因此对于一百万行,可能需要20秒):

使用相同的输入数据,问题中的方法大约花费了5秒钟


值得注意的是,这些时间是“最坏情况”,因为没有匹配项(因此所有子字符串都已检查)。如果有比赛的话,比赛的时间会更好

如果你坚持使用纯熊猫,为了性能和实用性,我认为你应该使用正则表达式来完成这项任务。但是,您需要首先正确转义子字符串中的任何特殊字符,以确保它们按字面意思匹配(而不是用作正则表达式元字符)

这很容易通过以下方式实现:

然后可以使用正则表达式管道将这些转义的子字符串连接起来。可以对照字符串检查每个子字符串,直到其中一个匹配为止(或者它们都经过测试)

然后,掩蔽阶段成为通过行的单个低级循环:

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秒(因此对于一百万行,可能需要20秒):

使用相同的输入数据,问题中的方法大约花费了5秒钟

值得注意的是,这些时间是“最坏情况”,因为没有匹配项(因此所有子字符串都已检查)。如果有比赛的话,比赛的时间会更好

您可以尝试使用。在平均情况下,它是
O(n+m+p)
,其中
n
是搜索字符串的长度,
m
是搜索文本的长度,
p
是输出匹配数

Aho Corasick算法是在输入文本(草堆)中找到多个模式(针)

是一个围绕该算法的C实现的Python包装器


让我们比较一下它的速度和一些替代方案。以下是一个基准 使用_aho_corasick显示
比原始方法快30倍以上
(如问题所示)在50K行数据帧测试用例上:

|                    |     speed factor | ms per loop |
|                    | compared to orig |             |
|--------------------+------------------+-------------|
| using_aho_corasick |            30.7x |         140 |
| using_regex        |             2.7x |        1580 |
| orig               |             1.0x |        4300 |


这里是用于基准测试的设置。它还验证输出是否与
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):
    """https://stackoverflow.com/a/48590850/190597 (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))
将numpy导入为np
随机输入
作为pd进口熊猫
进口病
进口稀土
随机种子(321)
def orig(col,lst):
mask=np.logical_或.reduce([col.str.contains(i,regex=False,case=False)
对于i(在lst中])
返回掩码
使用正则表达式(列、列)的def:
"""https://stackoverflow.com/a/48590850/190597 (亚历克斯·莱利)“”
esc_lst=[关于lst中s的转义]
模式=“|”。连接(esc|lst)
掩码=col.str.contains(模式,大小写=False)
返回掩码
def使用_Ahocarasick(col,lst):
A=ahocarasick.Automaton(ahocarasick.STORE\u INTS)
对于lst中的单词:
A.add_单词(word.lower())
A.制造自动化设备()
col=col.str.lower()
掩码=列应用(lambda x:bool(列表(A.iter(x)))
返回掩码
N=50000
#100个5个字符的子字符串
lst=[''.join([chr(random.randint(0256))表示范围内(5)])表示范围内(100)]
#N个20个字符的字符串
strings=[''.join([chr(random.randint(0,256))表示范围内的(20)])表示范围内的(N)]
#使大约10%的字符串与lst中的字符串匹配;这有助于检查我们的方法是否有效
strings=[[if random.randint(0,99)<10 else+random.choice(lst)for in strings]
col=pd.系列(字符串)
预期值=原始值(col,lst)
对于名称,结果为[('using_regex',using_regex(col,lst)),
('using_ahocarasick',using_ahocarasick(col,lst)):
状态='pass'如果np.allclose(预期,结果)或'fail'
打印(“{}:{}”。格式(名称、状态))
您可以尝试使用。在平均情况下,它是
O(n+m+p)
,其中
n
是搜索字符串的长度,
m
是搜索文本的长度,
p
是输出匹配数

Aho Corasick算法是在输入文本(草堆)中找到多个模式(针)

是一个围绕该算法的C实现的Python包装器


让我们比较一下它的速度和一些替代方案。以下是一个基准 使用_aho_corasick
显示
比原始方法快30倍以上
(如问题所示)在50K行数据帧测试用例上:

|                    |     speed factor | ms per loop |
|                    | compared to orig |             |
|--------------------+------------------+-------------|
| using_aho_corasick |            30.7x |         140 |
| using_regex        |             2.7x |        1580 |
| orig               |             1.0x |        4300 |


这里是用于基准测试的设置。它还验证输出是否与
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):
    """https://stackoverflow.com/a/48590850/190597 (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))
将numpy导入为np
随机输入
作为pd进口熊猫
进口病
进口稀土
随机种子(321)
def orig(col,lst):
mask=np.logical_或.reduce([col.str.contains(i,regex=False,case=False)
import re
v=pd.Series(['cAt','dog','the rat','mouse','froG'])

[Out]:

0        cAt
1        dog
2    the rat
3      mouse
4       froG
pattern='at|Og'
v_binary=[1]*len(v)
s=v.str.contains(pattern, flags=re.IGNORECASE, regex=True)
v_binary*s

[Out]

0    1
1    1
2    1
3    0
4    1