Jak wybrać częściowy ciąg z pandy DataFrame?
Ten post jest przeznaczony dla czytelników, którzy chcą
- wyszukaj podciąg w kolumnie ciągu (najprostszy przypadek)
- wyszukaj wiele podciągów (podobne do
isin
)
- dopasuj całe słowo z tekstu (np. „niebieski” powinien pasować do „niebo jest niebieskie”, ale nie „bluejay”)
- dopasuj wiele całych słów
- Poznaj przyczynę błędu „ValueError: nie można zaindeksować wektorem zawierającym wartości NA / NaN”
... i chciałbym dowiedzieć się więcej o tym, jakie metody powinny być preferowane w stosunku do innych.
(PS: Widziałem wiele pytań na podobne tematy, pomyślałem, że dobrze byłoby zostawić to tutaj.)
Podstawowe wyszukiwanie podciągów
# setup
df1 = pd.DataFrame({'col': ['foo', 'foobar', 'bar', 'baz']})
df1
col
0 foo
1 foobar
2 bar
3 baz
str.contains
może być używany do wyszukiwania podłańcuchów lub wyszukiwania opartego na wyrażeniach regularnych. Domyślnie wyszukiwanie jest oparte na wyrażeniach regularnych, chyba że zostanie to jawnie wyłączone.
Oto przykład wyszukiwania opartego na wyrażeniach regularnych,
# find rows in `df1` which contain "foo" followed by something
df1[df1['col'].str.contains(r'foo(?!$)')]
col
1 foobar
Czasami wyszukiwanie wyrażeń regularnych nie jest wymagane, więc regex=False
należy je wyłączyć.
#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
Pod względem wydajności wyszukiwanie wyrażeń regularnych jest wolniejsze niż wyszukiwanie podciągów:
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)
Unikaj wyszukiwania opartego na wyrażeniach regularnych, jeśli go nie potrzebujesz.
Adresowanie ValueError
s
Czasami, wykonując podciąg przeszukiwanie i filtrowanie na wynik spowoduje
ValueError: cannot index with vector containing NA / NaN values
Wynika to zwykle z mieszanych danych lub NaN w kolumnie obiektu,
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)
Wszystko, co nie jest łańcuchem, nie może mieć na nim metod łańcuchowych, więc wynikiem jest NaN (naturalnie). W takim przypadku określ na=False
ignorowanie danych nieciągłych,
s.str.contains('foo|bar', na=False)
0 True
1 True
2 False
3 True
4 False
5 False
dtype: bool
Wyszukiwanie wielu podciągów
Najłatwiej to osiągnąć poprzez wyszukiwanie wyrażeń regularnych za pomocą potoku OR wyrażenia regularnego.
# 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
Możesz także utworzyć listę warunków, a następnie dołączyć do nich:
terms = ['foo', 'baz']
df4[df4['col'].str.contains('|'.join(terms))]
col
0 foo abc
1 foobar xyz
3 baz 45
Czasami rozsądnie jest uciec od terminów, na wypadek gdyby zawierały znaki, które można interpretować jako metaznaki regularne . Jeśli Twoje warunki zawierają jeden z następujących znaków ...
. ^ $ * + ? { } [ ] \ | ( )
Następnie musisz użyć ich, re.escape
aby uciec :
import re
df4[df4['col'].str.contains('|'.join(map(re.escape, terms)))]
col
0 foo abc
1 foobar xyz
3 baz 45
re.escape
powoduje ucieczkę od znaków specjalnych, więc są one traktowane dosłownie.
re.escape(r'.foo^')
# '\\.foo\\^'
Pasujące całe słowa
Domyślnie wyszukiwanie podciągów szuka określonego podciągu / wzorca bez względu na to, czy jest to pełne słowo, czy nie. Aby dopasować tylko pełne słowa, będziemy musieli użyć tutaj wyrażeń regularnych - w szczególności nasz wzorzec będzie musiał określać granice słów ( \b
).
Na przykład,
df3 = pd.DataFrame({'col': ['the sky is blue', 'bluejay by the window']})
df3
col
0 the sky is blue
1 bluejay by the window
Teraz rozważ
df3[df3['col'].str.contains('blue')]
col
0 the sky is blue
1 bluejay by the window
vs
df3[df3['col'].str.contains(r'\bblue\b')]
col
0 the sky is blue
Wyszukiwanie wielu słów
Podobnie jak powyżej, z tym wyjątkiem, że \b
do połączonego wzorca dodajemy słowo granica ( ).
p = r'\b(?:{})\b'.format('|'.join(map(re.escape, terms)))
df4[df4['col'].str.contains(p)]
col
0 foo abc
3 baz 45
Gdzie p
tak wygląda
p
# '\\b(?:foo|baz)\\b'
Świetna alternatywa: skorzystaj z listy !
Ponieważ możesz! I powinieneś! Zazwyczaj są one nieco szybsze niż metody łańcuchowe, ponieważ metody łańcuchowe są trudne do wektoryzacji i zwykle mają implementacje pętli.
Zamiast,
df1[df1['col'].str.contains('foo', regex=False)]
Użyj in
operatora wewnątrz listy,
df1[['foo' in x for x in df1['col']]]
col
0 foo abc
1 foobar
Zamiast,
regex_pattern = r'foo(?!$)'
df1[df1['col'].str.contains(regex_pattern)]
Użyj re.compile
(do buforowania wyrażenia regularnego) + Pattern.search
wewnątrz listy,
p = re.compile(regex_pattern, flags=re.IGNORECASE)
df1[[bool(p.search(x)) for x in df1['col']]]
col
1 foobar
Jeśli „col” ma NaN, to zamiast
df1[df1['col'].str.contains(regex_pattern, na=False)]
Posługiwać się,
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
Oprócz wyrażeń str.contains
i listy możesz także skorzystać z poniższych alternatyw.
np.char.find
Obsługuje tylko wyszukiwanie podciągów (odczyt: brak wyrażenia regularnego).
df4[np.char.find(df4['col'].values.astype(str), 'foo') > -1]
col
0 foo abc
1 foobar xyz
np.vectorize
Jest to opakowanie wokół pętli, ale z mniejszym narzutem niż większość str
metod pand .
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
Możliwe rozwiązania 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
Obsługuje metody łańcuchowe za pośrednictwem silnika python. Nie zapewnia to widocznych korzyści w zakresie wydajności, ale mimo to warto wiedzieć, czy trzeba dynamicznie generować zapytania.
df1.query('col.str.contains("foo")', engine='python')
col
0 foo
1 foobar
Więcej informacji na temat query
i eval
rodzina metod można znaleźć w Dynamiczna Expression Oceny w pand użyciu pd.eval () .
Zalecane użycie Priorytet
- (Po pierwsze)
str.contains
, ze względu na prostotę i łatwość obsługi NaN i danych mieszanych
- Wymień wyrażenia ze względu na ich wydajność (szczególnie jeśli Twoje dane są wyłącznie ciągami znaków)
np.vectorize
- (Ostatni, ubiegły, zeszły)
df.query