pandy: filtruj wiersze DataFrame z łańcuchem operatorów

329

Większość operacji w pandasmoże być dokonany z operatorem łączenia ( groupby, aggregate, applyitp), ale jedynym sposobem mam stwierdzono rzędów filtrów odbywa się za pomocą zwykłego zamka indeksowania

df_filtered = df[df['column'] == value]

Jest to nieprzyjemne, ponieważ wymaga przypisania dfdo zmiennej, zanim będę mógł filtrować jej wartości. Czy jest coś podobnego do tego?

df_filtered = df.mask(lambda x: x['column'] == value)
duckworthd
źródło
df.queryi pd.evalwydają się pasować do tego przypadku użycia. Aby uzyskać informacje na temat pd.eval()rodziny funkcji, ich funkcji i przypadków użycia, odwiedź stronę Dynamic Expression Evaluation w pandach za pomocą pd.eval () .
cs95,

Odpowiedzi:

384

Nie jestem do końca pewien, czego chcesz, a twój ostatni wiersz kodu też nie pomaga, ale w każdym razie:

Filtrowanie „łańcuchowe” odbywa się poprzez „łańcuchowanie” kryteriów indeksu boolowskiego.

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [99]: df[(df.A == 1) & (df.D == 6)]
Out[99]:
   A  B  C  D
d  1  3  9  6

Jeśli chcesz połączyć metody, możesz dodać własną metodę maski i użyć tej.

In [90]: def mask(df, key, value):
   ....:     return df[df[key] == value]
   ....:

In [92]: pandas.DataFrame.mask = mask

In [93]: df = pandas.DataFrame(np.random.randint(0, 10, (4,4)), index=list('abcd'), columns=list('ABCD'))

In [95]: df.ix['d','A'] = df.ix['a', 'A']

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [97]: df.mask('A', 1)
Out[97]:
   A  B  C  D
a  1  4  9  1
d  1  3  9  6

In [98]: df.mask('A', 1).mask('D', 6)
Out[98]:
   A  B  C  D
d  1  3  9  6
Wouter Overmeire
źródło
2
Świetna odpowiedź! Tak więc (df.A == 1) & (df.D == 6), czy „&” jest przeciążonym operatorem w Pandas?
Shawn
2
w rzeczy samej
Wouter Overmeire,
To naprawdę miłe rozwiązanie - nawet nie zdawałem sobie sprawy z tego, że można zastosować metody takie przy użyciu Pythona. Taka funkcja byłaby naprawdę miło mieć w samej Pandas.
naught101
Jedyny problem, jaki mam z tym, to użycie pandas.. Zalecana import pandas as pd.
Daisuke Aramaki
3
Rzeczywiście import pandas as pdjest teraz powszechną praktyką. Wątpię, żeby to było, kiedy odpowiedziałem na pytanie.
Wouter Overmeire,
108

Filtry można połączyć w łańcuch za pomocą zapytania Pandas :

df = pd.DataFrame(np.random.randn(30, 3), columns=['a','b','c'])
df_filtered = df.query('a > 0').query('0 < b < 2')

Filtry można również łączyć w jednym zapytaniu:

df_filtered = df.query('a > 0 and 0 < b < 2')
bscan
źródło
3
Jeśli potrzebujesz odwoływać się do zmiennych python w zapytaniu, dokumentacja mówi: „Możesz odwoływać się do zmiennych w środowisku, poprzedzając je znakiem„ @ ”, takim jak @a + b”. Uwaga: obowiązują następujące zasady: df.query('a in list([1,2])'), s = set([1,2]); df.query('a in @s').
user3780389,
2
Z drugiej strony wygląda na to, że ocena zapytania zakończy się niepowodzeniem, jeśli nazwa kolumny zawiera określone znaki specjalne: np. „Place.Name”.
user3780389,
2
Łańcuch jest tym, do czego przeznaczone jest zapytanie.
piRSquared
66

Odpowiedź od @lodagro jest świetna. Rozszerzyłbym to, uogólniając funkcję maski jako:

def mask(df, f):
  return df[f(df)]

Następnie możesz robić takie rzeczy jak:

df.mask(lambda x: x[0] < 0).mask(lambda x: x[1] > 0)
Daniel Velkov
źródło
8
Przydatne uogólnienie! Chciałbym, żeby było już zintegrowane bezpośrednio z DataFrames!
duckworthd
24

Od wersji 0.18.1.loc metoda akceptuje wywoływalnym do wyboru. Wraz z funkcjami lambda możesz stworzyć bardzo elastyczne filtry łańcuchowe:

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.randint(0,100,size=(100, 4)), columns=list('ABCD'))
df.loc[lambda df: df.A == 80]  # equivalent to df[df.A == 80] but chainable

df.sort_values('A').loc[lambda df: df.A > 80].loc[lambda df: df.B > df.A]

Jeśli wszystko, co robisz, to filtrowanie, możesz także pominąć .loc.

Rafael Barbosa
źródło
16

Oferuję to dla dodatkowych przykładów. To jest ta sama odpowiedź, co https://stackoverflow.com/a/28159296/

Dodam inne zmiany, aby ten post był bardziej przydatny.

pandas.DataFrame.query
queryzostał stworzony właśnie do tego celu. Rozważ ramkę danychdf

import pandas as pd
import numpy as np

np.random.seed([3,1415])
df = pd.DataFrame(
    np.random.randint(10, size=(10, 5)),
    columns=list('ABCDE')
)

df

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
2  0  2  0  4  9
3  7  3  2  4  3
4  3  6  7  7  4
5  5  3  7  5  9
6  8  7  6  4  7
7  6  2  6  6  5
8  2  8  7  5  8
9  4  7  6  1  5

Użyjmy querydo filtrowania wszystkich wierszy gdzieD > B

df.query('D > B')

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
2  0  2  0  4  9
3  7  3  2  4  3
4  3  6  7  7  4
5  5  3  7  5  9
7  6  2  6  6  5

Które łączymy

df.query('D > B').query('C > B')
# equivalent to
# df.query('D > B and C > B')
# but defeats the purpose of demonstrating chaining

   A  B  C  D  E
0  0  2  7  3  8
1  7  0  6  8  6
4  3  6  7  7  4
5  5  3  7  5  9
7  6  2  6  6  5
piRSquared
źródło
Czy nie jest to w zasadzie ta sama odpowiedź, co stackoverflow.com/a/28159296 Czy w odpowiedzi brakuje czegoś, co Twoim zdaniem powinno zostać wyjaśnione?
bscan
9

Miałem to samo pytanie, z tym wyjątkiem, że chciałem połączyć kryteria w stan OR. Format podany przez Wouter Overmeire łączy kryteria w warunek AND, więc oba muszą być spełnione:

In [96]: df
Out[96]:
   A  B  C  D
a  1  4  9  1
b  4  5  0  2
c  5  5  1  0
d  1  3  9  6

In [99]: df[(df.A == 1) & (df.D == 6)]
Out[99]:
   A  B  C  D
d  1  3  9  6

Ale odkryłem, że jeśli owiniesz każdy warunek (... == True)i połączysz kryteria za pomocą potoku, kryteria zostaną połączone w warunku LUB, spełniane zawsze, gdy jedno z nich jest prawdziwe:

df[((df.A==1) == True) | ((df.D==6) == True)]
Sharon
źródło
12
Nie df[(df.A==1) | (df.D==6)]wystarczyłoby to, co próbujesz osiągnąć?
eenblam,
Nie, nie dałoby to, ponieważ daje bollean wyniki (prawda vs fałsz) zamiast, ponieważ powyżej filtruje wszystkie dane, które spełniają warunek. Mam nadzieję, że wyjaśniłem to.
MGB.py,
8

panda zapewnia dwie alternatywy dla odpowiedzi Woutera Overmeire'a, które nie wymagają przesłonięcia. Jeden jest .loc[.]na żądanie, jak w

df_filtered = df.loc[lambda x: x['column'] == value]

drugi jest .pipe(), jak w

df_filtered = df.pipe(lambda x: x['column'] == value)
Pietro Battiston
źródło
7

Moja odpowiedź jest podobna do pozostałych. Jeśli nie chcesz tworzyć nowej funkcji, możesz użyć tego, co panda już dla ciebie zdefiniowała. Użyj metody potoku.

df.pipe(lambda d: d[d['column'] == value])
Stewbaca
źródło
To jest to, czego chcesz, jeśli chcesz łańcuch poleceń, takich jaka.join(b).pipe(lambda df: df[df.column_to_filter == 'VALUE'])
displayname
4

Jeśli chcesz zastosować wszystkie typowe maski boolowskie, a także maskę ogólnego przeznaczenia, możesz włożyć do pliku następujące elementy, a następnie po prostu przypisać je wszystkie w następujący sposób:

pd.DataFrame = apply_masks()

Stosowanie:

A = pd.DataFrame(np.random.randn(4, 4), columns=["A", "B", "C", "D"])
A.le_mask("A", 0.7).ge_mask("B", 0.2)... (May be repeated as necessary

Jest to trochę zhackowane, ale może uczynić trochę bardziej czystym, jeśli ciągle siekasz i zmieniasz zestawy danych zgodnie z filtrami. Istnieje również filtr ogólnego przeznaczenia, dostosowany od Daniela Velkova powyżej w funkcji gen_mask, którego można używać z funkcjami lambda lub w razie potrzeby w inny sposób.

Plik do zapisania (używam masks.py):

import pandas as pd

def eq_mask(df, key, value):
    return df[df[key] == value]

def ge_mask(df, key, value):
    return df[df[key] >= value]

def gt_mask(df, key, value):
    return df[df[key] > value]

def le_mask(df, key, value):
    return df[df[key] <= value]

def lt_mask(df, key, value):
    return df[df[key] < value]

def ne_mask(df, key, value):
    return df[df[key] != value]

def gen_mask(df, f):
    return df[f(df)]

def apply_masks():

    pd.DataFrame.eq_mask = eq_mask
    pd.DataFrame.ge_mask = ge_mask
    pd.DataFrame.gt_mask = gt_mask
    pd.DataFrame.le_mask = le_mask
    pd.DataFrame.lt_mask = lt_mask
    pd.DataFrame.ne_mask = ne_mask
    pd.DataFrame.gen_mask = gen_mask

    return pd.DataFrame

if __name__ == '__main__':
    pass
dantes_419
źródło
3

To rozwiązanie jest bardziej hackerskie pod względem implementacji, ale uważam, że jest o wiele czystsze pod względem użytkowania, a na pewno jest bardziej ogólne niż inne proponowane.

https://github.com/toobaz/generic_utils/blob/master/generic_utils/pandas/where.py

Nie musisz pobierać całego repo: zapisywanie pliku i robienie

from where import where as W

powinno wystarczyć. Następnie używasz go w następujący sposób:

df = pd.DataFrame([[1, 2, True],
                   [3, 4, False], 
                   [5, 7, True]],
                  index=range(3), columns=['a', 'b', 'c'])
# On specific column:
print(df.loc[W['a'] > 2])
print(df.loc[-W['a'] == W['b']])
print(df.loc[~W['c']])
# On entire - or subset of a - DataFrame:
print(df.loc[W.sum(axis=1) > 3])
print(df.loc[W[['a', 'b']].diff(axis=1)['b'] > 1])

Nieco głupszy przykład użycia:

data = pd.read_csv('ugly_db.csv').loc[~(W == '$null$').any(axis=1)]

Nawiasem mówiąc: nawet w przypadku, gdy używasz tylko logicznej kolumny,

df.loc[W['cond1']].loc[W['cond2']]

może być znacznie bardziej wydajny niż

df.loc[W['cond1'] & W['cond2']]

ponieważ ocenia cond2tylko, gdzie cond1jest True.

ZASTRZEŻENIE: Najpierw udzieliłem tej odpowiedzi gdzie indziej, ponieważ jej nie widziałem.

Pietro Battiston
źródło
2

Po prostu chcę dodać demonstrację używającą locdo filtrowania nie tylko wierszy, ale także kolumn i niektórych zalet operacji łańcuchowej.

Poniższy kod może filtrować wiersze według wartości.

df_filtered = df.loc[df['column'] == value]

Modyfikując go nieco, możesz również filtrować kolumny.

df_filtered = df.loc[df['column'] == value, ['year', 'column']]

Dlaczego więc chcemy metody łańcuchowej? Odpowiedź jest prosta: jeśli masz wiele operacji, możesz je łatwo odczytać. Na przykład,

res =  df\
    .loc[df['station']=='USA', ['TEMP', 'RF']]\
    .groupby('year')\
    .agg(np.nanmean)
Ken T.
źródło
2

Jest to nieprzyjemne, ponieważ wymaga przypisania dfdo zmiennej, zanim będę mógł filtrować jej wartości.

df[df["column_name"] != 5].groupby("other_column_name")

wydaje się działać: możesz również zagnieżdżać []operatora. Może dodali to odkąd zadałeś pytanie.

serv-inc
źródło
1
To nie ma sensu w łańcuchu, ponieważ dfteraz niekoniecznie odwołuje się do wyników poprzedniej części łańcucha.
Daan Luttik
@DaanLuttik: zgodził się, to nie jest tworzenie łańcuchów, ale zagnieżdżanie. Lepiej dla ciebie?
serv-inc
1

Jeśli ustawisz wyszukiwanie kolumn jako indeksy, możesz użyć DataFrame.xs()przekroju. Nie jest to tak wszechstronne, jak queryodpowiedzi, ale może być przydatne w niektórych sytuacjach.

import pandas as pd
import numpy as np

np.random.seed([3,1415])
df = pd.DataFrame(
    np.random.randint(3, size=(10, 5)),
    columns=list('ABCDE')
)

df
# Out[55]: 
#    A  B  C  D  E
# 0  0  2  2  2  2
# 1  1  1  2  0  2
# 2  0  2  0  0  2
# 3  0  2  2  0  1
# 4  0  1  1  2  0
# 5  0  0  0  1  2
# 6  1  0  1  1  1
# 7  0  0  2  0  2
# 8  2  2  2  2  2
# 9  1  2  0  2  1

df.set_index(['A', 'D']).xs([0, 2]).reset_index()
# Out[57]: 
#    A  D  B  C  E
# 0  0  2  2  2  2
# 1  0  2  1  1  0
naught101
źródło
1

Możesz także wykorzystać bibliotekę numpy do operacji logicznych. Jest dość szybki.

df[np.logical_and(df['A'] == 1 ,df['B'] == 6)]
Akash Basudevan
źródło