web-dev-qa-db-fra.com

Méthode efficace pour appliquer plusieurs filtres à pandas DataFrame ou Series

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

Les filtres doivent être additifs (chaque filtre appliqué doit également limiter 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 bien la documentation). Cela pourrait donc être très inefficace lors du filtrage d'une grande série ou d'un cadre de données.

Je pense que l'utilisation de apply(), map(), ou de quelque chose de similaire pourrait être préférable. Je suis assez nouveau pour Pandas bien que j'essaie toujours de tout comprendre.

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 simplement filtrer un seul objet Series. Ci-dessous 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.

De plus, j'aimerais développer ceci afin que le dictionnaire transmis puisse inclure les colonnes à utiliser et filtrer un DataFrame complet en fonction du dictionnaire d'entrée. Cependant, je suppose que tout ce qui fonctionne pour la série peut facilement être étendu à un DataFrame.

110
durden2.0

Les pandas (et numpy) permettent 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 voulez écrire des fonctions d'assistance pour cela, envisagez quelque chose dans ce sens:

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 type de cas d'utilisation, en supposant que les noms de colonne sont des identificateurs valides des travaux suivants (et peuvent être plus efficaces pour les grands cadres car ils utilisent numexpr = dans les coulisses):

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

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

Notez qu'il y a encore des 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. Néanmoins, je m'attends à ce que cela soit suffisamment efficace pour de nombreuses applications et qu'il soit très lisible.

22
Gecko

La plus simple de toutes les solutions:

Utilisation:

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

Autre exemple, Pour filtrer le cadre de données en fonction des valeurs appartenant à février 2018, utilisez le code ci-dessous.

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

Depuis pandas 0.22 update , des options de comparaison sont disponibles, telles que:

  • gt (supérieur à)
  • lt (moins que)
  • 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
6
YOLO

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 performance) que le chaînage par l'opérateur. Vous pouvez bien sûr placer l’importation en haut du fichier.

1
Obol

vous pouvez également sélectionner des lignes en fonction des valeurs d’une colonne qui ne figurent pas dans une liste ou qui ne peuvent être itérées. Nous allons créer une variable booléenne comme avant, mais maintenant, nous allons nier la variable booléenne en plaçant ~ à l'avant.

Par exemple

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