Поиск подстроки в строке в DataFrame pandas очень медленный
Изменить: я понял, выполняя это упражнение, что мне нужно извлекать целые слова, а не части слов. Я отредактировал исходный вопрос и свой ответ, чтобы сделать код более устойчивым к этому проекту, не меняя сути проблемы.
Мой поиск в Интернете и SO не дал результата, поэтому я обращаюсь к вам.
У меня есть DataFrame, который выглядит так:
import pandas as pd
rows = [
('chocolate', 'choco'),
('banana', pd.np.nan),
('hello world', 'world'),
('hello you', 'world'),
('hello you choco', 'world'),
('this is a very long sentence', 'very long')
]
data = pd.DataFrame.from_records(rows, columns=['origin', 'to_find'])
origin to_find
0 chocolate choco
1 banana NaN
2 hello world world
3 hello you world
4 hello you choco world
5 this is a very long sentence very long
Моя цель - найти строку второго столбца в первом столбце и удалить ее. Если я не найду подстрокуto_find
в origin
, Я заменяю to_find
с NaN
. Поскольку это строковая операция, которую нужно выполнять построчно, я выбралapply
путь. Это моя функция, которая * работает почти так, как ожидалось, и как яapply
Это:
def find_word(row):
# Handle the case where to_find is already NaN
if row.to_find is pd.np.nan:
return row
if row.to_find in row.origin:
row.origin = row.origin.replace(row.to_find, '').strip()
else:
row.to_find = pd.np.nan
return row
new_df = data.apply(find_word, axis=1)
* этот код возвращает два пробела вместо одного между this is a
а также sentence
, что нежелательно.
В new_df
ожидается, что это будет выглядеть так:
origin to_find
0 late choco
1 banana NaN
2 hello world
3 hello you NaN
4 hello you choco NaN
5 this is a sentence very long
Моя проблема в том, что мой оригинал df
имеет миллионы строк, и эта конкретная операция с огромным DataFrame занимает вечность. Есть ли у кого-нибудь более производительный, может быть, векторизованный способ решения этой проблемы?
(The .contains
похоже, работает только для поиска одной конкретной строки в векторе, а не попарно. Это был мой лучший ход, но он не работал.)
2 ответа
Обновить
Читая эту и эту ветку, мне удалось до смешного сократить время процесса, используя понимание списков. Сейчас начнетсяmethod_3
:
def method_3(df):
df["to_find"] = df["to_find"].fillna('')
df['temp_origin'] = df['origin'].copy()
df['origin'] = [' '.join([x for x in a.split() if x not in set(b.split())]) for a, b in zip(df['origin'], df['to_find'])]
df['temp_origin'] = [' '.join([x for x in a.split(' ') if x not in set(b.split(' '))]) for a, b in zip(df['temp_origin'], df['origin'])]
df['temp_origin'] = df['temp_origin'].replace('', pd.np.nan)
del df['to_find']
df.rename(columns={'temp_origin': 'to_find'}, inplace=True)
return df
Теперь с новыми таймингами:
Method 1 took 13.820100281387568 sec.
Method 2 took 2.89176794141531 sec.
Method 3 took 0.26977075077593327 sec.
Три подхода: O(n)
, но при использовании method_3
.
Исходный пост
Во многом вдохновленный ответом @sygneto, мне удалось улучшить скорость почти в 5 раз.
Два разных метода
Я поместил свой первый метод в функцию с именем method_1
а другой в method_2
:
def find_word(row):
if row.to_find is pd.np.nan:
return row
if row.to_find in row.origin:
row.origin = row.origin.replace(row.to_find, '').strip()
else:
row.to_find = pd.np.nan
return row
def method_1(df):
return df.apply(find_word, axis=1)
def method_2(df):
df = df.fillna('')
df['temp_origin'] = df['origin']
df["origin"] = df.apply(lambda x: x["origin"].replace(x["to_find"], ""), axis=1)
df["to_find"] = df.apply(lambda x: pd.np.nan if x["origin"] == (x["temp_origin"]) else x["to_find"], axis=1)
del df['temp_origin']
return df
Измерьте скорость для обоих методов
Чтобы сравнить затраченное время, я взял свой начальный DataFrame и concat
редактировал это 10000 раз:
from timeit import default_timer
df = pd.concat([data] * 10000)
t0 = default_timer()
new_df_1 = method_1(df)
t1 = default_timer()
df = pd.concat([data] * 10000)
t2 = default_timer()
new_df_2 = method_2(df)
t3 = default_timer()
print(f"Method 1 took {t1-t0} sec.")
print(f"Method 2 took {t3-t2} sec.")
который выводит:
Method 1 took 11.803373152390122 sec.
Method 2 took 2.362371975556016 sec.
Возможно, есть место для улучшений, но все же большой шаг сделан.
Это решение должно работать для обеих сторон, если вы хотите заменить origin
с участием to_find
. Используется оригинальная форма'origin'
столбец как temp_origin
но ваш ожидаемый результат не имеет смысла в последней строке, где to_find
нан.
rows = [
('chocolate', 'choco'),
('banana', np.nan),
('hello world', 'world'),
('hello you', 'world')
]
df = pd.DataFrame.from_records(rows, columns=['origin', 'to_find'])
df=df.fillna('')
df['temp_origin']=df['origin']
df["origin"] = df.apply(
lambda x: x["origin"].replace(x["to_find"], ""), axis=1
)
df["to_find"] = df.apply(
lambda x: x["to_find"].replace(x["temp_origin"], ""), axis=1
)
df=df.replace('',np.nan)
del df['temp_origin']
print(df)
origin to_find
0 late choco
1 banana NaN
2 hello world
3 hello you world