211 votes

Un moyen efficace d'appliquer plusieurs filtres à un DataFrame ou une série pandas.

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

  • Les filtres doivent être additif (c'est-à-dire que chaque application devrait réduire les résultats).
  • J'utilise actuellement reindex() (comme ci-dessous) mais cela crée un nouvel objet à chaque fois et copie les données sous-jacentes (si je comprends bien la documentation). Je veux éviter cette copie inutile car elle serait vraiment inefficace lors du filtrage d'une grande série ou d'un DataFrame.
  • Je pense qu'en utilisant apply() , map() ou quelque chose de similaire pourrait être mieux. Je suis assez novice en matière de Pandas et j'essaie encore de m'y retrouver.
  • De plus, j'aimerais étendre cela pour que le dictionnaire transmis peut inclure les colonnes sur lesquelles opérer et filtrer un DataFrame entier sur la base du dictionnaire d'entrée. Cependant, je suppose que ce qui fonctionne pour une série peut facilement être étendu à un cadre de données.

TL;DR

Je veux prendre un dictionnaire de la forme suivante, 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 filtrer 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

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

326voto

Andy Hayden Points 38010

Pandas (et numpy) permettent de indexation booléenne 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 voulez écrire des fonctions d'aide pour cela, envisagez quelque chose de ce 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 d'interrogation pour ce genre de cas d'utilisation, en supposant que les noms de colonnes sont des identifiants valides, la méthode suivante fonctionne (et peut être plus efficace pour les grands cadres puisqu'elle utilise numexpr dans les coulisses) :

In [21]: df.query('col1 <= 1 & 1 <= col1')
Out[21]:
   col1  col2
1     1    11

58voto

Gecko Points 1133

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

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

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

Notez que cela comporte encore quelques redondances : a) le raccourcissement ne se produit pas à un niveau global b) chacune des conditions individuelles s'exécute sur l'ensemble des données initiales. Néanmoins, je pense que cette méthode est suffisamment efficace pour de nombreuses applications et elle est très facile à lire.

Vous pouvez également créer une disjonction (dans laquelle une seule des conditions doit être vraie) en utilisant la commande np.logical_or à la place :

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(c_1,c_2,c_3)]

34voto

Gil Baggio Points 2137

La plus simple de toutes les solutions :

Utilisez :

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

Un autre exemple Pour filtrer le dataframe pour les valeurs appartenant à Feb-2018, utilisez le code suivant

filtered_df = df[(df['year'] == 2018) & (df['month'] == 2)]

13voto

YOLO Points 5868

Desde mise à jour de pandas 0.22 des options de comparaison sont disponibles comme :

  • gt (plus grand que)
  • lt (moins que)
  • eq (égale à)
  • ne (non égal à)
  • ge (plus grand que ou égal à)

et bien d'autres encore. Ces fonctions renvoient un tableau de booléens. 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 between 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

5voto

Obol Points 81

Pourquoi ne pas le faire ?

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émonstration :

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 lorsque a >=2.

Cette méthode est légèrement plus rapide (en temps de frappe, pas en performance) que le chaînage d'opérateurs. Vous pourriez bien sûr placer l'importation en haut du fichier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X