Как выбрать частичную строку в панде DataFrame?
Этот пост предназначен для читателей, которые хотят
- поиск подстроки в столбце строки (самый простой случай)
- поиск нескольких подстрок (аналогично
isin
)
- соответствует целому слову из текста (например, «синий» должен соответствовать «голубое небо», а не «синий»)
- сопоставить несколько целых слов
- Понять причину «ValueError: невозможно индексировать с вектором, содержащим значения NA / NaN»
... и хотел бы узнать больше о том, какие методы предпочтительнее других.
(PS: я видел много вопросов на подобные темы, я думал, что было бы хорошо оставить это здесь.)
Основной поиск подстроки
# setup
df1 = pd.DataFrame({'col': ['foo', 'foobar', 'bar', 'baz']})
df1
col
0 foo
1 foobar
2 bar
3 baz
str.contains
может использоваться для поиска по подстроке или по регулярному выражению. По умолчанию поиск выполняется на основе регулярных выражений, если вы явно не отключили его.
Вот пример поиска на основе регулярных выражений,
# find rows in `df1` which contain "foo" followed by something
df1[df1['col'].str.contains(r'foo(?!$)')]
col
1 foobar
Иногда поиск по регулярному выражению не требуется, поэтому укажите, regex=False
чтобы отключить его.
#select all rows containing "foo"
df1[df1['col'].str.contains('foo', regex=False)]
# same as df1[df1['col'].str.contains('foo')] but faster.
col
0 foo
1 foobar
По производительности, поиск по регулярным выражениям медленнее, чем поиск по подстроке:
df2 = pd.concat([df1] * 1000, ignore_index=True)
%timeit df2[df2['col'].str.contains('foo')]
%timeit df2[df2['col'].str.contains('foo', regex=False)]
6.31 ms ± 126 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.8 ms ± 241 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Избегайте использования поиска на основе регулярных выражений, если он вам не нужен.
Адресация ValueError
s
Иногда выполнение поиска и фильтрации подстроки по результату приводит к
ValueError: cannot index with vector containing NA / NaN values
Обычно это происходит из-за смешанных данных или NaN в столбце вашего объекта,
s = pd.Series(['foo', 'foobar', np.nan, 'bar', 'baz', 123])
s.str.contains('foo|bar')
0 True
1 True
2 NaN
3 True
4 False
5 NaN
dtype: object
s[s.str.contains('foo|bar')]
# ---------------------------------------------------------------------------
# ValueError Traceback (most recent call last)
На все, что не является строкой, не могут быть применены строковые методы, поэтому результатом будет NaN (естественно). В этом случае укажите na=False
игнорировать нестроковые данные,
s.str.contains('foo|bar', na=False)
0 True
1 True
2 False
3 True
4 False
5 False
dtype: bool
Поиск нескольких подстрок
Этого легче всего достичь с помощью поиска регулярных выражений с использованием регулярного выражения OR.
# Slightly modified example.
df4 = pd.DataFrame({'col': ['foo abc', 'foobar xyz', 'bar32', 'baz 45']})
df4
col
0 foo abc
1 foobar xyz
2 bar32
3 baz 45
df4[df4['col'].str.contains(r'foo|baz')]
col
0 foo abc
1 foobar xyz
3 baz 45
Вы также можете создать список терминов, а затем присоединиться к ним:
terms = ['foo', 'baz']
df4[df4['col'].str.contains('|'.join(terms))]
col
0 foo abc
1 foobar xyz
3 baz 45
Иногда разумно избегать ваших терминов, если в них есть символы, которые можно интерпретировать как метасимволы регулярных выражений . Если ваши термины содержат любой из следующих символов ...
. ^ $ * + ? { } [ ] \ | ( )
Затем вам нужно использовать, re.escape
чтобы избежать их:
import re
df4[df4['col'].str.contains('|'.join(map(re.escape, terms)))]
col
0 foo abc
1 foobar xyz
3 baz 45
re.escape
имеет эффект экранирования специальных символов, поэтому они трактуются буквально.
re.escape(r'.foo^')
# '\\.foo\\^'
Совпадение всего слова
По умолчанию поиск подстроки выполняет поиск указанной подстроки / шаблона независимо от того, является ли это полным словом или нет. Чтобы сопоставлять только полные слова, нам нужно будет использовать здесь регулярные выражения - в частности, наш шаблон должен будет указать границы слов ( \b
).
Например,
df3 = pd.DataFrame({'col': ['the sky is blue', 'bluejay by the window']})
df3
col
0 the sky is blue
1 bluejay by the window
Теперь рассмотрим,
df3[df3['col'].str.contains('blue')]
col
0 the sky is blue
1 bluejay by the window
V / S
df3[df3['col'].str.contains(r'\bblue\b')]
col
0 the sky is blue
Множественный поиск по всему слову
Аналогично приведенному выше, за исключением того, что мы добавляем слово border ( \b
) к объединенному шаблону.
p = r'\b(?:{})\b'.format('|'.join(map(re.escape, terms)))
df4[df4['col'].str.contains(p)]
col
0 foo abc
3 baz 45
Где это p
выглядит,
p
# '\\b(?:foo|baz)\\b'
Отличная альтернатива: используйте списки !
Потому что ты можешь! И ты должен! Они обычно немного быстрее строковых методов, потому что строковые методы трудно векторизовать и обычно имеют зацикленные реализации.
Вместо,
df1[df1['col'].str.contains('foo', regex=False)]
Используйте in
оператор внутри списка comp,
df1[['foo' in x for x in df1['col']]]
col
0 foo abc
1 foobar
Вместо,
regex_pattern = r'foo(?!$)'
df1[df1['col'].str.contains(regex_pattern)]
Используйте re.compile
(для кэширования вашего регулярного выражения) + Pattern.search
внутри списка comp,
p = re.compile(regex_pattern, flags=re.IGNORECASE)
df1[[bool(p.search(x)) for x in df1['col']]]
col
1 foobar
Если у «col» есть NaNs, то вместо
df1[df1['col'].str.contains(regex_pattern, na=False)]
Использование,
def try_search(p, x):
try:
return bool(p.search(x))
except TypeError:
return False
p = re.compile(regex_pattern)
df1[[try_search(p, x) for x in df1['col']]]
col
1 foobar
В дополнение к str.contains
списку и списку вы также можете использовать следующие альтернативы.
np.char.find
Поддерживает только поиск по подстроке (читай: без регулярных выражений).
df4[np.char.find(df4['col'].values.astype(str), 'foo') > -1]
col
0 foo abc
1 foobar xyz
np.vectorize
Это обертка вокруг цикла, но с меньшими накладными расходами, чем у большинства str
методов панд .
f = np.vectorize(lambda haystack, needle: needle in haystack)
f(df1['col'], 'foo')
# array([ True, True, False, False])
df1[f(df1['col'], 'foo')]
col
0 foo abc
1 foobar
Возможны решения Regex:
regex_pattern = r'foo(?!$)'
p = re.compile(regex_pattern)
f = np.vectorize(lambda x: pd.notna(x) and bool(p.search(x)))
df1[f(df1['col'])]
col
1 foobar
DataFrame.query
Поддерживает строковые методы через движок Python. Это не дает видимых преимуществ в производительности, но, тем не менее, полезно знать, нужно ли вам динамически генерировать ваши запросы.
df1.query('col.str.contains("foo")', engine='python')
col
0 foo
1 foobar
Более подробную информацию о семействе методов query
и их eval
семействе можно найти в разделе «Оценка динамических выражений в пандах» с помощью pd.eval () .
Рекомендуемое приоритетность использования
- (Во-первых)
str.contains
, за его простоту и удобство обработки NaN и смешанных данных
- Список понимания, для его производительности (особенно, если ваши данные являются чисто строками)
np.vectorize
- (Прошлой)
df.query