Effiziente Möglichkeit, mehrere Filter auf Pandas DataFrame oder Series anzuwenden

148

Ich habe ein Szenario, in dem ein Benutzer mehrere Filter auf ein Pandas DataFrame- oder Serienobjekt anwenden möchte. Im Wesentlichen möchte ich eine Reihe von Filtern (Vergleichsoperationen) effizient miteinander verketten, die zur Laufzeit vom Benutzer angegeben werden.

Die Filter sollten additiv sein (auch bekannt als sollte jeder angewendete Filter die Ergebnisse einschränken).

Ich benutze gerade reindex() aber dies erstellt jedes Mal ein neues Objekt und kopiert die zugrunde liegenden Daten (wenn ich die Dokumentation richtig verstehe). Dies kann also beim Filtern einer großen Serie oder eines DataFrames sehr ineffizient sein.

Ich denke das mit apply() , map()oder etwas Ähnliches könnte besser sein. Ich bin ziemlich neu bei Pandas, versuche aber immer noch, meinen Kopf um alles zu wickeln.

TL; DR

Ich möchte ein Wörterbuch der folgenden Form nehmen und jede Operation auf ein bestimmtes Serienobjekt anwenden und ein 'gefiltertes' Serienobjekt zurückgeben.

relops = {'>=': [1], '<=': [1]}

Langes Beispiel

Ich beginne mit einem Beispiel für das, was ich derzeit habe, und filtere nur ein einzelnes Serienobjekt. Unten ist die Funktion, die ich gerade benutze:

   def apply_relops(series, relops):
        """
        Pass dictionary of relational operators to perform on given series object
        """
        for op, vals in relops.iteritems():
            op_func = ops[op]
            for val in vals:
                filtered = op_func(series, val)
                series = series.reindex(series[filtered])
        return series

Der Benutzer stellt ein Wörterbuch mit den Operationen bereit, die er ausführen möchte:

>>> df = pandas.DataFrame({'col1': [0, 1, 2], 'col2': [10, 11, 12]})
>>> print df
>>> print df
   col1  col2
0     0    10
1     1    11
2     2    12

>>> from operator import le, ge
>>> ops ={'>=': ge, '<=': le}
>>> apply_relops(df['col1'], {'>=': [1]})
col1
1       1
2       2
Name: col1
>>> apply_relops(df['col1'], relops = {'>=': [1], '<=': [1]})
col1
1       1
Name: col1

Wiederum besteht das "Problem" bei meinem obigen Ansatz darin, dass ich denke, dass es eine Menge möglicherweise unnötiger Kopien der Daten für die Zwischenschritte gibt.

Außerdem möchte ich dies erweitern, damit das übergebene Wörterbuch die zu operierenden Spalten enthalten und einen gesamten DataFrame basierend auf dem Eingabewörterbuch filtern kann. Ich gehe jedoch davon aus, dass alles, was für die Serie funktioniert, problemlos zu einem DataFrame erweitert werden kann.

durden2.0
quelle
Ich bin mir auch völlig bewusst, dass diese Herangehensweise an das Problem weit entfernt sein könnte. Vielleicht wäre es also nützlich, den gesamten Ansatz zu überdenken. Ich möchte nur zulassen, dass Benutzer zur Laufzeit eine Reihe von Filteroperationen angeben und diese ausführen.
Durden2.0
Ich frage mich, ob Pandas ähnliche Dinge tun können wie data.table in R: df [col1 <1 ,,] [col2> = 1]
xappppp
df.queryund pd.evalscheinen gut zu Ihrem Anwendungsfall zu passen. Informationen zur pd.eval()Funktionsfamilie, ihren Funktionen und Anwendungsfällen finden Sie unter Auswertung dynamischer Ausdrücke in Pandas mit pd.eval () .
CS95

Antworten:

244

Pandas (und Numpy) ermöglichen eine boolesche Indizierung , die viel effizienter ist:

In [11]: df.loc[df['col1'] >= 1, 'col1']
Out[11]: 
1    1
2    2
Name: col1

In [12]: df[df['col1'] >= 1]
Out[12]: 
   col1  col2
1     1    11
2     2    12

In [13]: df[(df['col1'] >= 1) & (df['col1'] <=1 )]
Out[13]: 
   col1  col2
1     1    11

Wenn Sie dazu Hilfsfunktionen schreiben möchten, sollten Sie Folgendes berücksichtigen:

In [14]: def b(x, col, op, n): 
             return op(x[col],n)

In [15]: def f(x, *b):
             return x[(np.logical_and(*b))]

In [16]: b1 = b(df, 'col1', ge, 1)

In [17]: b2 = b(df, 'col1', le, 1)

In [18]: f(df, b1, b2)
Out[18]: 
   col1  col2
1     1    11

Update: pandas 0.13 verfügt über eine Abfragemethode für diese Art von Anwendungsfällen. Unter der Annahme, dass Spaltennamen gültige Bezeichner sind, funktioniert dies wie folgt (und kann für große Frames effizienter sein, da hinter den Kulissen numexpr verwendet wird ):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11
Andy Hayden
quelle
1
Ihr Recht, Boolescher Wert, ist effizienter, da keine Kopie der Daten erstellt wird. Mein Szenario ist jedoch etwas kniffliger als Ihr Beispiel. Die Eingabe, die ich erhalte, ist ein Wörterbuch, das definiert, welche Filter angewendet werden sollen. Mein Beispiel könnte so etwas tun df[(ge(df['col1'], 1) & le(df['col1'], 1)]. Das Problem für mich ist wirklich, dass das Wörterbuch mit den Filtern viele Operatoren enthalten kann und das Verketten dieser umständlich ist. Vielleicht könnte ich jedes boolesche Zwischenarray einem großen Array hinzufügen und dann einfach mapden andOperator auf sie anwenden ?
Durden2.0
@ durden2.0 Ich habe eine Idee für eine Hilfsfunktion hinzugefügt, die meiner Meinung nach ähnlich ist, wie Sie suchen :)
Andy Hayden
Das kommt dem sehr nahe, was ich mir ausgedacht habe! Danke für das Beispiel. Warum f()ergreifen müssen , um *bgerade statt b? Ist dies so, dass Benutzer von f()weiterhin den optionalen outParameter verwenden können logical_and()? Dies führt zu einer weiteren kleinen Nebenfrage. Was ist der Leistungsvorteil / Kompromiss bei der Übergabe des Arrays über die out()Verwendung des zurückgegebenen Arrays logical_and()? Danke noch einmal!
Durden2.0
Egal, ich habe nicht genau genug hingeschaut. Dies *bist erforderlich, da Sie die beiden Arrays übergeben b1und b2sie beim Aufruf entpacken müssen logical_and. Die andere Frage bleibt jedoch offen. Gibt es einen Leistungsvorteil bei der Übergabe eines Arrays über einen outParameter an logical_and()vs , wenn nur der Rückgabewert verwendet wird?
Durden2.0
2
@dwanderson Sie können eine Liste von Bedingungen für mehrere Bedingungen an np.logical_and.reduce übergeben. Beispiel: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo
38

Verkettungsbedingungen erzeugen lange Linien, von denen pep8 abhält. Die Verwendung der .query-Methode erzwingt die Verwendung von Zeichenfolgen, die leistungsstark, aber unpythonisch und nicht sehr dynamisch sind.

Sobald jeder der Filter vorhanden ist, ist ein Ansatz

import numpy as np
import functools
def conjunction(*conditions):
    return functools.reduce(np.logical_and, conditions)

c_1 = data.col1 == True
c_2 = data.col2 < 64
c_3 = data.col3 != 4

data_filtered = data[conjunction(c1,c2,c3)]

np.logical arbeitet weiter und ist schnell, akzeptiert jedoch nicht mehr als zwei Argumente, die von functools.reduce verarbeitet werden.

Beachten Sie, dass dies immer noch einige Redundanzen aufweist: a) Verknüpfungen treten nicht auf globaler Ebene auf. B) Jede der einzelnen Bedingungen wird für die gesamten Anfangsdaten ausgeführt. Trotzdem erwarte ich, dass dies für viele Anwendungen effizient genug ist und sehr gut lesbar ist.

Gecko
quelle
1
Gibt es eine Möglichkeit, dies für eine variable Anzahl von Bedingungen zu implementieren? Ich habe versucht , jede anhängen c_1, c_2, c_3, ... c_nin einer Liste, und dann vorbei , data[conjunction(conditions_list)]aber bekommen einen Fehler ValueError: Item wrong length 5 instead of 37.auch versucht , data[conjunction(*conditions_list)]aber ich habe ein anderes Ergebnis als data[conjunction(c_1, c_2, c_3, ... c_n )]nicht sicher , was los ist.
user5359531
An anderer Stelle eine Lösung für den Fehler gefunden. data[conjunction(*conditions_list)]funktioniert nach dem Packen der Datenrahmen in eine Liste und dem Entpacken der Liste an Ort
user5359531
1
Ich habe gerade einen Kommentar zu der obigen Antwort mit einer viel schlampigeren Version hinterlassen und dann Ihre Antwort bemerkt. Sehr sauber, ich mag es sehr!
Dwanderson
Das ist eine großartige Antwort!
Charlie Crown
1
Ich hatte verwendet: df[f_2 & f_3 & f_4 & f_5 ]mit f_2 = df["a"] >= 0usw. Keine Notwendigkeit für diese Funktion ... (nette Verwendung der Funktion höherer Ordnung obwohl ...)
A. Rabus
18

Einfachste aller Lösungen:

Verwenden:

filtered_df = df[(df['col1'] >= 1) & (df['col1'] <= 5)]

Ein weiteres Beispiel : Verwenden Sie den folgenden Code, um den Datenrahmen nach Werten zu filtern, die zum Februar 2018 gehören

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
Gil Baggio
quelle
Ich benutze Variable anstelle von Konstante. Fehler bekommen. df [df []] [df []] gibt eine Warnmeldung aus, gibt aber die richtige Antwort.
Nguai al
8

Seit dem Update von pandas 0.22 stehen Vergleichsoptionen zur Verfügung:

  • gt (größer als)
  • lt (kleiner als)
  • Gl. (gleich)
  • ne (nicht gleich)
  • ge (größer oder gleich)

und viele mehr. Diese Funktionen geben ein boolesches Array zurück. Mal sehen, wie wir sie nutzen können:

# sample data
df = pd.DataFrame({'col1': [0, 1, 2,3,4,5], 'col2': [10, 11, 12,13,14,15]})

# get values from col1 greater than or equals to 1
df.loc[df['col1'].ge(1),'col1']

1    1
2    2
3    3
4    4
5    5

# where co11 values is better 0 and 2
df.loc[df['col1'].between(0,2)]

 col1 col2
0   0   10
1   1   11
2   2   12

# where col1 > 1
df.loc[df['col1'].gt(1)]

 col1 col2
2   2   12
3   3   13
4   4   14
5   5   15
YOLO
quelle
2

Warum nicht?

def filt_spec(df, col, val, op):
    import operator
    ops = {'eq': operator.eq, 'neq': operator.ne, 'gt': operator.gt, 'ge': operator.ge, 'lt': operator.lt, 'le': operator.le}
    return df[ops[op](df[col], val)]
pandas.DataFrame.filt_spec = filt_spec

Demo:

df = pd.DataFrame({'a': [1,2,3,4,5], 'b':[5,4,3,2,1]})
df.filt_spec('a', 2, 'ge')

Ergebnis:

   a  b
 1  2  4
 2  3  3
 3  4  2
 4  5  1

Sie können sehen, dass die Spalte 'a' gefiltert wurde, wobei a> = 2 ist.

Dies ist etwas schneller (Eingabezeit, keine Leistung) als die Verkettung des Bedieners. Sie können den Import natürlich ganz oben in die Datei einfügen.

Obol
quelle
1

e kann auch Zeilen basierend auf Werten einer Spalte auswählen, die nicht in einer Liste enthalten sind oder iterierbar sind. Wir werden wie zuvor eine boolesche Variable erstellen, aber jetzt werden wir die boolesche Variable negieren, indem wir ~ vorne platzieren.

Beispielsweise

list = [1, 0]
df[df.col1.isin(list)]
Ram Prajapati
quelle