Как использовать pandas .replace () со списком регулярных выражений при соблюдении порядка следования списков? - PullRequest
8 голосов
/ 03 мая 2019

У меня есть 2 кадра данных: один (A) с некоторыми именами хостов из белого списка в форме регулярных выражений (т.е. (.*)microsoft.com, (*.)go.microsoft.com ...) и другой (B) с фактическими полными именами узлов сайтов.Я хочу добавить новый столбец к этому 2-му фрейму данных с текстом регулярного выражения для белого фрейма данных (1-й).Однако, похоже, что метод Pandas .replace() не заботится о том, какие элементы порядка находятся в его аргументах to_replace и value.

Мои данные выглядят так:

In [1] A
Out[1]: 
                                  wildcards  \
42   (.*)activation.playready.microsoft.com   
35    (.*)v10.vortex-win.data.microsoft.com   
40      (.*)settings-win.data.microsoft.com   
43            (.*)smartscreen.microsoft.com   
39             (.*).playready.microsoft.com   
38                     (.*)go.microsoft.com   
240                     (.*)i.microsoft.com   
238                       (.*)microsoft.com   
                                                 regex  
42   re.compile('^(.*)activation.playready.microsof...  
35   re.compile('^(.*)v10.vortex-win.data.microsoft...  
40   re.compile('^(.*)settings-win.data.microsoft.c...  
43       re.compile('^(.*)smartscreen.microsoft.com$')  
39        re.compile('^(.*).playready.microsoft.com$')  
38                re.compile('^(.*)go.microsoft.com$')  
240                re.compile('^(.*)i.microsoft.com$')  
238                  re.compile('^(.*)microsoft.com$')  


In [2] B.head()
Out[2]: 
                       server_hostname
146     mobile.pipe.aria.microsoft.com
205    settings-win.data.microsoft.com
341      nav.smartscreen.microsoft.com
406  v10.vortex-win.data.microsoft.com
667                  www.microsoft.com

Обратите внимание, что A имеет столбец скомпилированных регулярных выражений в форме, аналогичной столбцу wildcards.Я хочу добавить столбец wildcard к B следующим образом:

B.loc[:,'wildcards'] = B['server_hostname'].replace(A['regex'].tolist(), A['wildcards'].tolist())

Но проблема в том, что все значения подстановочных знаков B становятся (.*)microsoft.com.Это происходит независимо от порядка подстановочных значений A.Похоже, .replace() стремится использовать регулярное выражение to_replace по кратчайшему значению, а не по указанному порядку.

Как я могу предоставить список значений to_replace, чтобы в конечном итоге получить наиболее подробное значение имени хоста wildcards, связанное со B значениями server_hostname?

Ответы [ 7 ]

1 голос
/ 17 июля 2019

В большинстве ответов используется apply(), что, как известно, медленнее, чем решения для встроенных векторных функций.Я надеялся на использование .replace(), что это будет быстро, так как это такая встроенная векторная функция.Ответ @ vlemaistre был единственным, который не использовал .apply(), как мое решение здесь, которое вместо компиляции каждого подстановочного знака в регулярное выражение обрабатывает его как правую подстроку для использования логики: «Если server_hostname заканчивается на wildcard, то это совпадение ".До тех пор, пока я сортирую свои шаблоны по длине, это работает просто отлично.

Моя функция, которая делает это:

def match_to_whitelist(accepts_df, whitelist_df):
    """ Adds `whitelists` column to accepts_df showing which (if any) whitelist entry it matches with """
    accepts_df.loc[:, 'wildcards'] = None
    for wildcard in whitelist_df['wildcards']:
        accepts_df.loc[(accepts_df['wildcards'].isnull()) & (
            accepts_df['server_hostname'].str.endswith(wildcard)), 'wildcards'] = wildcard
    rows_matched = len(accepts_df['wildcards'].notnull())
matched {rows_matched}")
    return accepts_df

Здесь accepts_df похожа на B до, а whitelist_df похожа на A и раньше, но с двумя отличиями:

  1. нет regex столбец
  2. значения wildcards больше не имеют формат glob / regex (т. Е. "(. *) Microsoft.com" становится "microsoft.com)"

Для сравнения ответов на моем компьютере я буду использовать мой в качестве базового уровня, принимая 27 сек для обработки 100k accepts_df строк с 400 whitelist_df строками. Используя тот же набор данных, вотвремя для других решений (мне было лень: если они не выбежали за ворота, я не стал много отлаживать, чтобы узнать):

  • @ vlemaistre - Понимание списка с векторными функциями: 193сек
  • @ user214 - SequenceMatcher: 234сек
  • @ aws_apprentice - Сравнить длины результатов поиска RE: 24сек
  • @ fpersyn - первое совпадение (будет наилучшим совпадением, если отсортировано A): более 6 минут, так что выходите ...
  • @ Энди Хейден - lastgroup: не тестировал, потому что я не могу (быстро) построить длинную программу REчески.
  • @ capelastegui - Series.str.match(): Ошибка: «pandas.core.indexes.base.InvalidIndexError: Переиндексация действительна только для уникальных объектов индекса»

В конечном итоге, ни один из наших ответовскажем, как использовать .replace() по желанию, поэтому пока я оставлю этот вопрос без ответа на несколько недель, если кто-то сможет дать ответ, чтобы лучше использовать .replace() или хотя бы какое-нибудь другое быстрое векторное решение,До тех пор я буду придерживаться того, что имею, или, возможно, буду использовать aws_apprentice после проверки результатов.

РЕДАКТИРОВАТЬ Я улучшил свое сопоставление, добавив столбец «домен» к обоим DF, который состоит из двух последних частей каждого подстановочного знака / имени_хоста_сервера (т.е. www.microsoft.com становится "Microsoft".com ").Затем я использовал groupby('domain') на обоих DF, итерировал по группам белых списков доменов, выбрал одну и ту же группу доменов из DF-сервера имя_сервера (B) и выполнил сопоставление, просто используя подмножество подстановочных знаков / имен_хостов из каждой группы.Это сократило мое время обработки до половины.

0 голосов
/ 16 июля 2019

Самый чистый подход панд, который я смог найти, заключается в запуске Series.str.match() для B.server_hostname для каждого регулярного выражения, а затем в первом столбце каждого столбца с idxmax().

# Create input data
A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})

# Ensure B has a unique index
B = B.reset_index(drop=True)

# Check which regexes match each hostname
df_match = A.regex.apply(lambda x: B.server_hostname.str.match(x))
df_match.index= A.wildcards
df_match.columns=B.server_hostname

# Get first match for each hostname
df_first_match = df_match.idxmax().rename('wildcards').reset_index()

Вывод:

print(df_match)
print(df_first_match)

server_hostname                         v10.vortex-win.data.microsoft.com  www.microsoft.com
wildcards                                                                                   
(.*)activation.playready.microsoft.com                              False              False
(.*)v10.vortex-win.data.microsoft.com                                True              False
(.*)i.microsoft.com                                                 False              False
(.*)microsoft.com                                                    True               True

                     server_hostname                              wildcards
0  v10.vortex-win.data.microsoft.com  (.*)v10.vortex-win.data.microsoft.com
1                  www.microsoft.com                      (.*)microsoft.com

Тем не менее, это кажется немного медленнее, чем другие решения, опубликованные ранее.

0 голосов
/ 16 июля 2019

Альтернативным способом, который, к сожалению, все еще требует применения, является использование lastgroup.Это влечет за собой компиляцию одного регулярного выражения и затем поиск имени соответствующей группы (строки):

In [11]: regex = re.compile("|".join([f"(?P<i{i}>{regex})" for i, regex in s["wildcards"].items()]))

In [12]: regex
Out[12]:
re.compile(r'(?P<i42>(.*)activation.playready.microsoft.com)|(?P<i35>(.*)v10.vortex-win.data.microsoft.com)|(?P<i40>(.*)settings-win.data.microsoft.com)|(?P<i43>(.*)smartscreen.microsoft.com)|(?P<i39>(.*).playready.microsoft.com)|(?P<i38>(.*)go.microsoft.com)|(?P<i240>(.*)i.microsoft.com)|(?P<i238>(.*)microsoft.com)',
re.UNICODE)

In [13]: B.server_hostname.apply(lambda s: int(re.match(regex, s).lastgroup[1:]))
Out[13]:
146    238
205     40
341     43
406     35
667    238
Name: server_hostname, dtype: int64

In [14]: B.server_hostname.apply(lambda s: int(re.match(regex, s).lastgroup[1:])).map(s.wildcards)
Out[14]:
146                        (.*)microsoft.com
205      (.*)settings-win.data.microsoft.com
341            (.*)smartscreen.microsoft.com
406    (.*)v10.vortex-win.data.microsoft.com
667                        (.*)microsoft.com
Name: server_hostname, dtype: object

Этот атрибут не предоставляется пандами (но может быть возможно сделать что-то умное с внутренностями)) ...

0 голосов
/ 15 июля 2019

Документация pandas описывает метод .replace() следующим образом:

Значения DataFrame динамически заменяются другими значениями.Это отличается от обновления с помощью .loc или .iloc, которые требуют указания местоположения для обновления с некоторым значением.

Это означает, что метод будет перебирать все ячейки в кадре данных и заменять то, что ему нужно.может для каждого запроса, указанного в аргументе to_replace.Быстрый пример, демонстрирующий это:

df = pd.DataFrame({'A':['a','c'],'B':['b','d']})
df.replace(['a','b'],['b','c'])

Output:
    A   B
0   c   c
1   c   d

В вашем примере каждое правило регулярных выражений перезаписывает предыдущие замены при наличии нового совпадения, что позволяет получить вектор (.*)microsoft.com результатов.

Вместо этого вы можете использовать метод .apply().Например, сортируя ваш белый список (A) по убыванию по длине, итерируйте по каждой строке вашего значения DataFrame (B) и возвращайте каждое первое совпадение:

import pandas as pd
import re

# Using the definitions for A and B from your question, 
# where A is sorted descending by length.

def first_match(x):
    for index, row in A.iterrows():
        if bool(re.search(row['wildcards'], x['server_hostname'])) is True:
            return row['wildcards']
B['wildcards'] = B.apply(first_match, axis=1)
B

Output:
    server_hostname                     wildcards
0   mobile.pipe.aria.microsoft.com      (.*)microsoft.com
1   settings-win.data.microsoft.com     (.*)settings-win.data.microsoft.com
2   nav.smartscreen.microsoft.com       (.*)smartscreen.microsoft.com
3   v10.vortex-win.data.microsoft.com   (.*)v10.vortex-win.data.microsoft.com
4   www.microsoft.com                   (.*)microsoft.com

Возможно, также стоит прочитать на *Шаблон 1021 * split-apply-Объединить для более продвинутых стратегий.Я надеюсь, что это помогает.

0 голосов
/ 15 июля 2019

Вот еще один подход, использующий apply. Насколько я знаю, не существует чистого pandas способа сделать это. Я также позаимствовал данные, предоставленные @ vlemaistre .

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})

pats = set(A.regex)

def max_match(hostname):
    d = {}
    for pat in pats:
        maybe_result = pat.search(hostname)
        if maybe_result:
            p = pat.pattern
            d[len(p)] = p
    return d.get(max([*d]))

B['wildcards'] = B['server_hostname'].apply(max_match)

                     server_hostname                                wildcards
0  v10.vortex-win.data.microsoft.com  ^(.*)v10.vortex-win.data.microsoft.com$
1                  www.microsoft.com                      ^(.*)microsoft.com$
0 голосов
/ 13 июля 2019

Один альтернативный способ - использовать SequenceMatcher и re.match .

Взятые данные из ответа @ vlemaistre

from difflib import SequenceMatcher
import pandas as pd
import re

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com', 'www.i.microsoft.com']})

def regex_match(x):
    match = None
    ratio = 0
    for w, r in A[['wildcards', 'regex']].to_numpy():
        if re.match(r, x) is not None:
            pct = SequenceMatcher(None, w, x).ratio()
            if ratio < pct: ratio = pct; match = w
    return match

B['wildcards'] = B.server_hostname.apply(regex_match)

# print(B.wildcards)
0    (.*)v10.vortex-win.data.microsoft.com
1                        (.*)microsoft.com
2                      (.*)i.microsoft.com
Name: server_hostname, dtype: object
0 голосов
/ 10 июля 2019

Вот способ сделать это, используя понимание двойного списка и функцию re.sub():

import re

A = pd.DataFrame({'wildcards' : ['(.*)activation.playready.microsoft.com',
                                 '(.*)v10.vortex-win.data.microsoft.com',
                                 '(.*)i.microsoft.com', '(.*)microsoft.com'],
                  'regex' : [re.compile('^(.*)activation.playready.microsoft.com$'),
                             re.compile('^(.*)v10.vortex-win.data.microsoft.com$'), 
                             re.compile('^(.*)i.microsoft.com$'), 
                             re.compile('^(.*)microsoft.com$')]})

B = pd.DataFrame({'server_hostname' : ['v10.vortex-win.data.microsoft.com',
                                       'www.microsoft.com']})
# For each server_hostname we try each regex and keep the longest matching one
B['wildcards'] = [max([re.sub(to_replace, value, x) for to_replace, value
                       in A[['regex', 'wildcards']].values
                       if re.sub(to_replace, value, x)!=x], key=len) 
                  for x in B['server_hostname']]

Output : 
                     server_hostname                              wildcards
0  v10.vortex-win.data.microsoft.com  (.*)v10.vortex-win.data.microsoft.com
1                  www.microsoft.com                      (.*)microsoft.com
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...