Wybierz według częściowego ciągu z pandy DataFrame


448

Mam DataFramez 4 kolumnami, z których 2 zawierają wartości ciągu. Zastanawiałem się, czy istnieje sposób na wybranie wierszy na podstawie częściowego dopasowania ciągu do konkretnej kolumny?

Innymi słowy, funkcja lub funkcja lambda, która zrobiłaby coś podobnego

re.search(pattern, cell_in_question) 

zwracanie wartości logicznej. Znam składnię, df[df['A'] == "hello world"]ale nie mogę znaleźć sposobu, aby zrobić to samo z powiedzeniem częściowego dopasowania ciągu 'hello'.

Czy ktoś byłby w stanie skierować mnie we właściwym kierunku?

Odpowiedzi:


786

Na podstawie numeru github nr 620 wygląda na to, że wkrótce będziesz w stanie wykonać następujące czynności:

df[df['A'].str.contains("hello")]

Aktualizacja: wektoryzowane metody ciągów (tj. Series.str) są dostępne w pandach 0.8.1 i nowszych .


1
Jak postępujemy w przypadku „Witaj” i „Wielkiej Brytanii”, jeśli chcę je znaleźć z warunkiem „LUB”.
LonelySoul

56
Ponieważ metody str. * Traktują wzorzec wejściowy jako wyrażenie regularne, możesz użyćdf[df['A'].str.contains("Hello|Britain")]
Garrett

7
Czy można przekonwertować .str.containsna .query()interfejs API ?
zyxue


3
df[df['value'].astype(str).str.contains('1234.+')]do odfiltrowywania kolumn nie łańcuchowych.
François Leblanc

213

Wypróbowałem powyższe rozwiązanie:

df[df["A"].str.contains("Hello|Britain")]

i dostał błąd:

ValueError: nie można maskować tablicą zawierającą wartości NA / NaN

możesz przekształcić wartości NA w Falsenastępujący sposób:

df[df["A"].str.contains("Hello|Britain", na=False)]

54
Lub możesz zrobić: df [df ['A']. Str. Contains ("Hello | Britain", na = False)]
joshlk

2
df[df['A'].astype(str).str.contains("Hello|Britain")]pracował również
Nagabhushan SN

108

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.containsmoż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=Falsenależ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 ValueErrors
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=Falseignorowanie 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.escapeaby 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 \bdo 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 ptak 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 inoperatora 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.searchwewną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

Więcej opcji dopasowanie częściowe wyrażenie: np.char.find, np.vectorize, DataFrame.query.

Oprócz wyrażeń str.containsi 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ść strmetod 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 queryi evalrodzina metod można znaleźć w Dynamiczna Expression Oceny w pand użyciu pd.eval () .


Zalecane użycie Priorytet

  1. (Po pierwsze) str.contains, ze względu na prostotę i łatwość obsługi NaN i danych mieszanych
  2. Wymień wyrażenia ze względu na ich wydajność (szczególnie jeśli Twoje dane są wyłącznie ciągami znaków)
  3. np.vectorize
  4. (Ostatni, ubiegły, zeszły) df.query

Czy możesz edytować poprawną metodę do użycia podczas wyszukiwania ciągu w dwóch lub więcej kolumnach? Zasadniczo: any(needle in haystack for needling in ['foo', 'bar'] and haystack in (df['col'], df['col2']))i odmiany próbowałem zadławić się (narzeka any()i słusznie tak ... Ale doktor jest błogo niejasny, jak zrobić takie zapytanie.
Denis de Bernardy,

@DenisdeBernardydf[['col1', 'col2']].apply(lambda x: x.str.contains('foo|bar')).any(axis=1)
cs95

@ cs95 Wyodrębnianie wierszy z podciągiem zawierającym białe znaki po + w pandach df Odpowiedź została wkrótce udzielona, ​​ale możesz chcieć rzucić na nią okiem.
ankii

@ankiiiiiii Wygląda na to, że przegapiłeś część mojej odpowiedzi, w której wspomniałem o metaznakach regularnych: „Czasami rozsądnie jest uciec od terminów, na wypadek gdyby zawierały znaki, które można interpretować jako metaznaki regularne”.
cs95

1
@ 00schneider r w tym przypadku jest używany do wskazania literału surowego łańcucha. Ułatwiają one pisanie ciągów wyrażeń regularnych. stackoverflow.com/q/2081640
cs95

53

Jeśli ktoś zastanawia się, jak wykonać podobny problem: „Wybierz kolumnę według ciągu częściowego”

Posługiwać się:

df.filter(like='hello')  # select columns which contain the word hello

Aby wybrać wiersze według częściowego dopasowania ciągu, przekaż axis=0do filtru:

# selects rows which contain the word hello in their index label
df.filter(like='hello', axis=0)  

6
Można to przedestylować do:df.loc[:, df.columns.str.contains('a')]
elPastor

18
który może być dalej destylowany dodf.filter(like='a')
Ted Petrou

to powinno być własne pytanie + odpowiedź, szukało go już 50 osób ...
PV8

1
@ PV8 pytanie już istnieje: stackoverflow.com/questions/31551412/... . Ale kiedy szukam w Google hasła „pandy Wybierz kolumnę według częściowego ciągu”, ten wątek pojawia się jako pierwszy
Philipp Schwarz

28

Szybka uwaga: jeśli chcesz dokonać wyboru na podstawie częściowego ciągu znaków zawartego w indeksie, spróbuj wykonać następujące czynności:

df['stridx']=df.index
df[df['stridx'].str.contains("Hello|Britain")]

5
Możesz po prostu df [df.index.to_series (). Str.contains ('LLChit')]
Yury Bayda

21

Powiedz, że masz następujące elementy DataFrame:

>>> df = pd.DataFrame([['hello', 'hello world'], ['abcd', 'defg']], columns=['a','b'])
>>> df
       a            b
0  hello  hello world
1   abcd         defg

Zawsze możesz użyć inoperatora w wyrażeniu lambda, aby utworzyć swój filtr.

>>> df.apply(lambda x: x['a'] in x['b'], axis=1)
0     True
1    False
dtype: bool

Sztuczka polega na tym, aby użyć axis=1opcji w applycelu przekazania elementów do funkcji lambda rząd po rzędzie, w przeciwieństwie do kolumn po kolumnie.


Jak zmodyfikować powyżej, aby powiedzieć, że x ['a'] istnieje tylko na początku x ['b']?
ComplexData

1
zastosowanie jest złym pomysłem pod względem wydajności i pamięci. Zobacz tę odpowiedź .
cs95,

8

Oto, co skończyłem dla częściowych dopasowań ciągów. Jeśli ktoś ma bardziej skuteczny sposób na zrobienie tego, proszę dać mi znać.

def stringSearchColumn_DataFrame(df, colName, regex):
    newdf = DataFrame()
    for idx, record in df[colName].iteritems():

        if re.search(regex, record):
            newdf = concat([df[df[colName] == record], newdf], ignore_index=True)

    return newdf

3
Powinien być 2x do 3x szybszy, jeśli skompilujesz regex przed pętlą: regex = re.compile (regex), a następnie jeśli regex.search (rekord)
MarkokraM

1
@MarkokraM docs.python.org/3.6/library/re.html#re.compile mówi, że najnowsze wyrażenia regularne są dla ciebie buforowane, więc nie musisz się kompilować.
Teepeemm,

Nie należy używać iteratów do iteracji w ramce danych. Zajmuje ostatnie miejsce pod względem pandorowalności i wydajności
cs95

5

Używanie zawiera nie działało dobrze dla mojego ciągu znaków specjalnych. Znajdź działało.

df[df['A'].str.find("hello") != -1]

2

Przed tym są odpowiedzi, które spełniają zadaną funkcję, w każdym razie chciałbym pokazać najbardziej ogólnie:

df.filter(regex=".*STRING_YOU_LOOK_FOR.*")

W ten sposób uzyskamy kolumnę, której szukasz, niezależnie od tego, jak jest napisane.

(Oczywiście, musisz napisać właściwe wyrażenie regularne dla każdego przypadku)


1
Filtruje nagłówki kolumn . To nie jest ogólne, to jest nieprawidłowe.
cs95

@MicheldeRuiter, który wciąż jest nieprawidłowy, zamiast tego filtrowałby etykiety indeksu!
cs95,

Nie odpowiada na pytanie. Ale nauczyłem się czegoś. :)
Michel de Ruiter,

2

Może chcesz wyszukać tekst we wszystkich kolumnach ramki danych Pandy, a nie tylko w ich podzbiorze. W takim przypadku pomoże następujący kod.

df[df.apply(lambda row: row.astype(str).str.contains('String To Find').any(), axis=1)]

Ostrzeżenie. Ta metoda jest stosunkowo powolna, aczkolwiek wygodna.


2

Jeśli konieczne jest wyszukiwanie łańcucha bez rozróżniania wielkości liter w kolumnie ramki danych pandy:

df[df['A'].str.contains("hello", case=False)]
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.