Pandas系列中多个子字符串的过滤

46

我需要在一个 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]

我的数据框很大(~1百万行),而lst的长度为100。有没有更有效的方法?例如,如果找到lst中的第一个项目,则不应再测试该行的任何后续字符串。

3个回答

53

如果您坚持使用纯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秒钟。

值得注意的是,这些时间是“最坏情况”,因为没有匹配(所以检查了所有子字符串)。如果存在匹配,则定时器将会提高。


48

你可以尝试使用Aho-Corasick算法。在平均情况下,它的时间复杂度为O(n+m+p),其中n是搜索字符串的长度,m是被搜索文本的长度,p是输出匹配项的数量。

Aho-Corasick算法通常用于在输入文本(“干草堆”)中查找多个模式(针)

pyahocorasick是该算法的C语言实现的Python包装器。


让我们比较一下使用其他方法的速度有多快。下面是一个基准测试,显示using_aho_corasick 在一个包含50,000行数据的DataFrame测试案例中比原始方法(问题中显示的方法)快30倍以上:

|                    |     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):
    """https://dev59.com/mVYM5IYBdhLWcg3wpRfa#48590850 (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))

非常有趣。能否在pandas数据框中使用此包,或者这会降低性能(因为循环的原因)? - ℕʘʘḆḽḘ
2
上面展示的基准仍然适用。在上面的代码中,A.iter 在调用 col.apply 时被调用,其中 col 是一个 pandas Series。这与使用 pandas DataFrame 做的事情并没有太大区别(甚至可能完全相同)。使用 apply 的性能与简单的 Python 循环大致相同,但您仍将获得使用 Aho-Corasick 算法的好处。 - unutbu

2

使用一个更简单的示例并忽略大小写(大写或小写)

过滤并获取二进制向量:

我想找到所有包含"at"或"Og"的pd.Series v元素。如果元素包含该模式,则获取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

我想找到所有包含“at”或“Og”的v元素。 也就是说,我可以将我的pattern定义为:
pattern='at|Og'

由于我希望如果项目包含模式,则返回一个包含1的向量,否则返回0。

我创建了一个与v相同长度的单位向量:

v_binary=[1]*len(v)

我得到了一个名为 s 的布尔值,如果 v 中的任何一个元素包含 pattern ,则为 True;如果不包含,则为 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

或者只需s.astype(int)而不是整个二进制向量逻辑。我没有看到与@AlexRiley的解决方案相比的基本差异或好处,你能看到吗? - jpp
你是对的!谢谢,我会编辑我的帖子并把它放在那里。 - pink.slash
实际上,我遇到了一个问题。你能帮我看一下这段代码吗:pattern='wiring | media | elect' v=pd.Series(['electricity fault']) s=v.str.contains(pattern, flags=re.IGNORECASE, regex=True) print(s)输出结果为:0 False dtype: bool - pink.slash
如果我想要一个精确匹配怎么办?例如,“the rat”? - Chadee Fouad
模式='老鼠' - pink.slash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接