【问题标题】:Pandas filtering for multiple substrings in seriesPandas 过滤多个串联子串
【发布时间】:2018-07-10 12:29:32
【问题描述】:

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

【问题讨论】:

    标签: python string pandas dataframe series


    【解决方案1】:

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

    使用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 秒。

    值得注意的是,这些时间是“最坏情况”,因为没有匹配项(因此检查了 所有 子字符串)。如果有比赛,那么时机将会改善。

    【讨论】:

      【解决方案2】:

      您可以尝试使用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):
          """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))
      

      【讨论】:

      • 非常有趣。是否可以在 pandas 数据帧中使用这个包或者会降低性能(因为我猜是循环)?
      • 上面显示的基准仍然适用。上面,A.iter 在对col.apply 的调用中被调用,其中col 是熊猫系列。这与您使用 pandas DataFrame 所做的事情并没有太大的不同(甚至可能完全相同)。使用 apply 的性能与简单的 Python 循环大致相同,但您仍然可以从使用 Aho-Corasick 算法中获益。
      【解决方案3】:

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

      过滤得到二元向量:

      我想查找pd.Seriesv 中包含“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)
      

      如果v的一个元素包含patternFalse如果它不包含它,我将获得一个布尔值s,即True

      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's solution 相比,我没有看到任何根本区别或好处,你能看到吗?
      • 你说的太对了!谢谢,我会编辑我的帖子并把它放在那里
      • 事实上,我遇到了问题。你能帮我解决这个问题吗: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
      • 如果我想要精确匹配怎么办?例如“老鼠”?
      • pattern='老鼠'
      猜你喜欢
      • 1970-01-01
      • 2021-09-21
      • 1970-01-01
      • 2019-08-30
      • 2012-07-06
      • 2020-06-07
      相关资源
      最近更新 更多