【问题标题】:Vectorized column-wise regex matching in pandaspandas 中的向量化按列正则表达式匹配
【发布时间】:2020-04-26 05:41:36
【问题描述】:

第一部分

假设我有一个数据集 df,如下所示:

x   | y     
----|--------
foo | 1.foo-ya
bar | 2.bar-ga
baz | 3.ha-baz
qux | None

我想过滤 y 正好在中间包含 x 的行(不是开头也不是结尾,即匹配模式 '^.+\w+.+$',命中第 1 行和第 2 行),不包括 None/NaN:

x   | y
----|-----
foo | 1.foo-ya
bar | 2.bar-ga

这是一个典型的成对字符比较,在 SQL 中很容易:

select x, y from df where y like concat('^.+', x, '.+%');

或在 R 中:

library(dplyr)
library(stringr)
library(glue)
df %>% filter(str_detect(y, glue('^.+{x}.+$')))

但是由于我不是pandas专家,看来pandas中没有类似的简单“矢量化”正则表达式匹配方法?我应用了 lambda 方法:

import pandas as pd
import re
df.loc[df.apply(lambda row: bool(re.search(
                '^.+' + row.x + '.+$', row.y)) 
       if row.x and row.y else False, axis=1), :]

pandas 中有没有更优雅的方法来完成它?

第二部分

此外,我想提取第一部分产生的匹配记录中的前导数字(1、2、...):

x   | y        |  z
----|----------|---
foo | 1.foo-ya |  1
bar | 2.bar-ga |  2

在 R 中,我可以直接进行管道争吵:

df %>%
  filter(str_detect(y, glue('^.+{x}.+$'))) %>%
  mutate(z=str_replace(y, glue('^(\\d+)\\.{x}.+$'), '\\1') %>%
           as.numeric)

但在 pandas 中,我只知道 lambda 方法。有没有比它“更好”的方法?

a = df.loc[df.apply(lambda row: bool(
                re.search('^.+' + row.x + '.+$', row.y))
                if row.x and row.y else False, axis=1), 
       ['x', 'y']]
a['z'] = a.apply(lambda row: re.sub(
       r'^(\d+)\.' + row.x + '.+$', r'\1', row.y), axis=1).astype('int')
a

顺便说一句,assign 方法不起作用。

df.loc[df.apply(lambda row: bool(re.search(
                '^.+' + row.x + '.+$', row.y))
                if row.x and row.y else False, axis=1), 
       ['x', 'y']].assign(z=lambda row: re.sub(
                r'^(\d+)\.' + row.x + '.+$', r'\1', row.y))

谢谢!

【问题讨论】:

  • 您需要df[df['x'].eq(df['y'].str.split('\.|-').str[1])] 吗?您可以根据需要更改拆分的正则表达式
  • 尝试df.assign(foo=df['y'].str.extract('\d\.(\w+)-')).query('x == foo').drop( 'foo',axis=1).assign( num=df['y'].str.extract('(^\d)')) 获得一次性解决方案。
  • @Datanovice,谢谢。 “正好在中间”仅仅意味着匹配模式 r'^.+{column x}.+$'.
  • 上述方法有效吗?
  • @Datanovice,是的,两者都有效。谢谢。但是,如果模式在整个数据集中不一致怎么办?有没有一种矢量化的方法来首先识别列匹配?

标签: r regex pandas dplyr vectorization


【解决方案1】:

pandas 的字符串操作是建立在 python 的 string 和 re 模块之上的。试一试,看看是不是你想要的:

import re

#find out if values in column x are in column y
#according to the pattern u wrote in the question
pattern = [re.match(fr'^.+{a}.+$',b)
           for a,b 
           in zip(df.x.str.strip(),
                  df.y.str.strip())
          ]

match = [ent.group() if ent is not None else np.nan for ent in pattern]

#extract values for digit immediately preceding val in col x    
ext = [re.search(fr'\d(?=\.{a})', b) for a,b  in 
       zip(df.x.str.strip(),
           df.y.str.strip())]

extract = [ent.group() if ent is not None else np.nan for ent in ext]

df['match'], df['extract'] = match, extract

     x     y        match   extract
1   foo 1.foo-ya    1.foo-ya    1
2   bar 2.bar-ga    2.bar-ga    2
3   baz 3.ha-baz      NaN      NaN
4   qux    None       NaN      NaN

【讨论】:

    【解决方案2】:

    感谢所有鼓舞人心的回复。不得不说,虽然 Python 在很多方面都出类拔萃,但在这种矢量化操作方面,我更喜欢 R。所以我为这个案例重新发明了轮子。

    def str_detect(string: pd.Series, pattern: pd.Series) -> List[bool]:
        """mimic str_detect in R
        """
        if len(string) > len(pattern):
            pattern.extend([pattern[-1]] * (len(string)-len(pattern)))
        elif len(string) < len(pattern):
            pattern = pattern[1:len(string)]
    
        return [bool(re.match(y, x)) if x and y else False
                for x, y in zip(string, pattern)]
    
    def str_extract(string: pd.Series, pattern: pd.Series) -> List[str]:
        """mimic str_extract in R
        """
        if len(string) > len(pattern):
            pattern.extend([pattern[-1]] * (len(string)-len(pattern)))
        elif len(string) < len(pattern):
            pattern = pattern[1:len(string)]
        o = [re.search(y, x) if x and y else None
             for x, y in zip(string, pattern)]
    
        return [x.group() if x else np.nan for x in o]
    

    然后

    df.loc[str_detect(
        df['y'], '^.+' + df['x']+'.+$'), ['x', 'y']]
    (df
      .assign(z=str_extract(df['y'], r'^(\d+)(?=\.' + df['x'] + ')'))
      .dropna(subset=['z'])
      .loc[:, ['x', 'y', 'z']])
    

    【讨论】:

      【解决方案3】:

      这是你想要的方式吗?几乎复制了你在 R 中所做的事情:

      >>> from numpy import vectorize
      >>> from pipda import register_func
      >>> from datar.all import f, tribble, filter, grepl, paste0, mutate, sub, as_numeric
      [2021-06-24 17:27:16][datar][WARNING] Builtin name "filter" has been overriden by datar.
      >>> 
      >>> df = tribble(
      ...   f.x,   f.y,
      ...   "foo", "1.foo-ya",
      ...   "bar", "2.bar-ga",
      ...   "baz", "3.ha-baz",
      ...   "qux", None
      ... )
      >>> 
      >>> @register_func(None)
      ... @vectorize
      ... def str_detect(text, pattern):
      ...   return grepl(pattern, text)
      ... 
      >>> @register_func(None)
      ... @vectorize
      ... def str_replace(text, pattern, replacement):
      ...   return sub(pattern, replacement, text)
      ... 
      >>> df >> \
      ...   filter(str_detect(f.y, paste0('^.+', f.x, '.+$'))) >> \
      ...   mutate(z=as_numeric(str_replace(f.y, paste0(r'^(\d+)\.', f.x, '.+$'), r'\1')))
               x         y         z
        <object>  <object> <float64>
      0      foo  1.foo-ya       1.0
      1      bar  2.bar-ga       2.0
      

      免责声明:我是datar 包的作者。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-03-17
        • 2018-01-08
        • 2015-07-06
        • 1970-01-01
        • 1970-01-01
        • 2012-11-07
        • 2020-11-20
        • 2020-07-02
        相关资源
        最近更新 更多