Векторизованное регулярное выражение в столбцах в pandas - PullRequest
2 голосов
/ 26 апреля 2020

Часть I

Предположим, у меня есть набор данных 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? Я применил лямбда-подход:

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 какие-нибудь более изящные методы для достижения этой цели?

Часть II

Более того, я хочу извлечь ведущие числа (1, 2, ...) в сопоставленных записях, приведенных в части I:

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 я знаю только о лямбда-приближении. Есть ли "лучшие" подходы, чем он?

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))

Спасибо!

Ответы [ 2 ]

1 голос
/ 26 апреля 2020

pandas строковые операции строятся на строковом и повторном модуле python. Имейте go в этом и посмотрите, если это то, что вы хотите:

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
0 голосов
/ 26 апреля 2020

Спасибо за все вдохновляющие ответы. Я должен сказать, что хотя 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']])
...