Moyen efficace d'appliquer plusieurs filtres aux pandas DataFrame ou Series

148

J'ai un scénario dans lequel un utilisateur souhaite appliquer plusieurs filtres à un objet Pandas DataFrame ou Series. Essentiellement, je souhaite enchaîner efficacement un ensemble de filtrages (opérations de comparaison) qui sont spécifiés au moment de l'exécution par l'utilisateur.

Les filtres doivent être additifs (c'est-à-dire que chacun appliqué doit restreindre les résultats).

J'utilise actuellement reindex()mais cela crée un nouvel objet à chaque fois et copie les données sous-jacentes (si je comprends correctement la documentation). Donc, cela pourrait être vraiment inefficace lors du filtrage d'une grande série ou DataFrame.

Je pense que l'utilisation de apply(), map()ou quelque chose de similaire pourrait être mieux. Je suis assez nouveau sur Pandas, alors j'essaie toujours de comprendre tout.

TL; DR

Je veux prendre un dictionnaire de la forme suivante et appliquer chaque opération à un objet Series donné et renvoyer un objet Series «filtré».

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

Exemple long

Je vais commencer par un exemple de ce que j'ai actuellement et en filtrant simplement un seul objet Series. Voici la fonction que j'utilise actuellement:

   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

L'utilisateur fournit un dictionnaire avec les opérations qu'il souhaite effectuer:

>>> 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

Encore une fois, le «problème» avec mon approche ci-dessus est que je pense qu'il y a beaucoup de copies éventuellement inutiles des données pour les étapes intermédiaires.

En outre, je voudrais développer cela afin que le dictionnaire transmis puisse inclure les colonnes sur lesquelles l'opérateur et filtrer un DataFrame entier en fonction du dictionnaire d'entrée. Cependant, je suppose que tout ce qui fonctionne pour la série peut être facilement étendu à un DataFrame.

durden2.0
la source
De plus, je suis pleinement conscient que cette approche du problème pourrait être très éloignée. Il serait donc peut-être utile de repenser l’approche dans son ensemble. Je veux simplement permettre aux utilisateurs de spécifier un ensemble d'opérations de filtrage au moment de l'exécution et de les exécuter.
durden2.0
Je me demande si les pandas peuvent faire des choses similaires à data.table dans R: df [col1 <1 ,,] [col2> = 1]
xappppp
df.queryet pd.evalsemblent bien adaptés à votre cas d'utilisation. Pour plus d'informations sur la pd.eval()famille de fonctions, leurs caractéristiques et leurs cas d'utilisation, veuillez visiter Évaluation des expressions dynamiques dans les pandas à l'aide de pd.eval () .
cs95

Réponses:

245

Les pandas (et numpy) permettent une indexation booléenne , ce qui sera beaucoup plus efficace:

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

Si vous souhaitez écrire des fonctions d'assistance pour cela, envisagez quelque chose du genre:

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

Mise à jour: pandas 0.13 a une méthode de requête pour ce type de cas d'utilisation, en supposant que les noms de colonne sont des identificateurs valides, les travaux suivants (et peuvent être plus efficaces pour les grandes images car il utilise numexpr en arrière-plan):

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11
Andy Hayden
la source
1
Votre droit, booléen est plus efficace car il ne fait pas de copie des données. Cependant, mon scénario est un peu plus délicat que votre exemple. L'entrée que je reçois est un dictionnaire définissant les filtres à appliquer. Mon exemple pourrait faire quelque chose comme df[(ge(df['col1'], 1) & le(df['col1'], 1)]. Le problème pour moi est que le dictionnaire avec les filtres peut contenir de nombreux opérateurs et les enchaîner est compliqué. Peut-être que je pourrais ajouter chaque tableau booléen intermédiaire à un grand tableau, puis l'utiliser simplement mappour leur appliquer l' andopérateur?
durden2.0
@ durden2.0 J'ai ajouté une idée pour une fonction d'assistance, qui, je pense, est similaire à ce que vous recherchez :)
Andy Hayden
Cela ressemble beaucoup à ce que j'ai trouvé! Merci pour l'exemple. Pourquoi f()faut-il prendre *bau lieu de juste b? Est-ce que c'est pour que l'utilisateur f()puisse toujours utiliser le outparamètre facultatif pour logical_and()? Cela conduit à une autre petite question secondaire. Quel est l'avantage / le compromis en termes de performances de la transmission via la baie par out()rapport à l'utilisation de celle renvoyée logical_and()? Merci encore!
durden2.0
Peu importe, je n'ai pas regardé assez près. Le *best nécessaire parce que vous passez les deux tableaux b1et b2vous devez les décompresser lors de l' appel logical_and. Cependant, l'autre question se pose toujours. Y a-t-il un avantage en termes de performances à transmettre un tableau via un outparamètre à logical_and()vs en utilisant simplement sa valeur de retour?
durden2.0
2
@dwanderson vous pouvez transmettre une liste de conditions à np.logical_and.reduce pour plusieurs conditions. Exemple: np.logical_and.reduce ([df ['a'] == 3, df ['b']> 10, df ['c']. Isin (1,3,5)])
Kuzenbo
39

Les conditions d'enchaînement créent de longues lignes, qui sont découragées par pep8. L'utilisation de la méthode .query oblige à utiliser des chaînes, ce qui est puissant mais non rythmique et peu dynamique.

Une fois que chacun des filtres est en place, une approche est

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 fonctionne et est rapide, mais ne prend pas plus de deux arguments, ce qui est géré par functools.reduce.

Notez que cela a encore quelques redondances: a) les raccourcis ne se produisent pas au niveau global b) Chacune des conditions individuelles s'exécute sur l'ensemble des données initiales. Pourtant, je m'attends à ce que ce soit assez efficace pour de nombreuses applications et il est très lisible.

Vous pouvez également effectuer une disjonction (dans laquelle une seule des conditions doit être vraie) en utilisant à la np.logical_orplace:

import numpy as np
import functools
def disjunction(*conditions):
    return functools.reduce(np.logical_or, conditions)

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

data_filtered = data[disjunction(c1,c2,c3)]
Gecko
la source
1
Existe-t-il un moyen d'implémenter cela pour un nombre variable de conditions? J'ai essayé chaque annexant c_1, c_2, c_3... c_ndans une liste, et en faisant passer data[conjunction(conditions_list)]mais je reçois une erreur ValueError: Item wrong length 5 instead of 37.aussi essayé data[conjunction(*conditions_list)]mais j'obtenir un autre résultat que data[conjunction(c_1, c_2, c_3, ... c_n )], pas sûr de ce qui se passe.
user5359531
J'ai trouvé une solution à l'erreur ailleurs. data[conjunction(*conditions_list)]fonctionne après avoir emballé les dataframes dans une liste et décompressé la liste en place
user5359531
1
Je viens de laisser un commentaire sur la réponse ci-dessus avec une version beaucoup plus bâclée, puis j'ai remarqué votre réponse. Très propre, je l'aime beaucoup!
dwanderson le
C'est une excellente réponse!
Charlie Crown
1
je l' avais utilisé: df[f_2 & f_3 & f_4 & f_5 ]avec f_2 = df["a"] >= 0etc Pas besoin de cette fonction ... (belle utilisation de la fonction d'ordre supérieur mais ...)
A. Rabus
19

La plus simple de toutes les solutions:

Utilisation:

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

Un autre exemple , pour filtrer le dataframe pour les valeurs appartenant à février-2018, utilisez le code ci-dessous

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]
Gil Baggio
la source
J'utilise la variable au lieu de la constante. obtenir une erreur. df [df []] [df []] donne un message d'avertissement mais donne une réponse correcte.
Nguai al
8

Depuis la mise à jour de pandas 0.22 , des options de comparaison sont disponibles comme:

  • gt (supérieur à)
  • lt (inférieur à)
  • eq (égal à)
  • ne (pas égal à)
  • ge (supérieur ou égal à)

et beaucoup plus. Ces fonctions renvoient un tableau booléen. Voyons comment nous pouvons les utiliser:

# 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
la source
2

Pourquoi ne pas faire ça?

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

Démo:

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

Résultat:

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

Vous pouvez voir que la colonne 'a' a été filtrée où a> = 2.

C'est légèrement plus rapide (temps de frappe, pas de performances) que l'enchaînement d'opérateurs. Vous pouvez bien sûr placer l'importation en haut du fichier.

Obole
la source
1

Vous pouvez également sélectionner des lignes en fonction des valeurs d'une colonne qui ne sont pas dans une liste ou qui ne sont pas itérables. Nous allons créer une variable booléenne comme avant, mais maintenant nous allons nier la variable booléenne en plaçant ~ devant.

Par exemple

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