pandas: filtrer les lignes de DataFrame avec le chaînage des opérateurs


329

La plupart des opérations en pandaspeut être accompli avec l' opérateur Enchaînement ( groupby, aggregate, apply, etc.), mais la seule façon que j'ai trouvé aux lignes de filtre se fait via l' indexation de support normale

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

C'est peu attrayant car cela nécessite que j'attribue dfune variable avant de pouvoir filtrer sur ses valeurs. Y a-t-il quelque chose de plus semblable au suivant?

df_filtered = df.mask(lambda x: x['column'] == value)

df.queryet pd.evalsemblent être de bons ajustements pour ce cas d'utilisation. Pour plus d'informations sur la pd.eval()famille de fonctions, leurs fonctionnalités et leurs cas d'utilisation, veuillez consulter Évaluation des expressions dynamiques dans les pandas à l'aide de pd.eval () .
cs95

Réponses:


384

Je ne suis pas tout à fait sûr de ce que vous voulez, et votre dernière ligne de code n'aide pas non plus, mais de toute façon:

Le filtrage "chaîné" se fait en "chaînant" les critères de l'index booléen.

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

Si vous souhaitez chaîner des méthodes, vous pouvez ajouter votre propre méthode de masque et l'utiliser.

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

2
Très bonne réponse! Alors (df.A == 1) & (df.D == 6), est-ce que le "&" est un opérateur surchargé dans Pandas?
Shawn


C'est une très bonne solution - je ne savais même pas que vous pouviez truquer des méthodes comme celle-ci en python. Une fonction comme celle-ci serait vraiment agréable à avoir dans Pandas lui-même.
naught101

Le seul problème que j'ai avec ceci est l'utilisation de pandas.. Tu devrais import pandas as pd.
Daisuke Aramaki

3
En effet, import pandas as pdc'est une pratique courante maintenant. Je doute que ce soit lorsque j'ai répondu à la question.
Wouter Overmeire

108

Les filtres peuvent être chaînés à l'aide d'une requête Pandas :

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

Les filtres peuvent également être combinés en une seule requête:

df_filtered = df.query('a > 0 and 0 < b < 2')

3
Si vous devez faire référence à des variables python dans votre requête, la documentation indique: "Vous pouvez faire référence à des variables dans l'environnement en les préfixant avec un caractère '@' comme @a + b". Notez que les éléments suivants sont valides: df.query('a in list([1,2])'), s = set([1,2]); df.query('a in @s').
user3780389

2
D'un autre côté, il semble que l'évaluation de la requête échouera si le nom de votre colonne a certains caractères spéciaux: par exemple "Place.Name".
user3780389

2
Le chaînage est le but de la requête.
piRSquared

66

La réponse de @lodagro est excellente. Je voudrais l'étendre en généralisant la fonction de masque comme:

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

Ensuite, vous pouvez faire des choses comme:

df.mask(lambda x: x[0] < 0).mask(lambda x: x[1] > 0)

8
Une généralisation utile! Je souhaite qu'il soit déjà intégré directement dans DataFrames!
duckworthd

24

Depuis la version 0.18.1, la .locméthode accepte un appelable pour la sélection. Avec les fonctions lambda, vous pouvez créer des filtres chaînables très flexibles:

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]

Si tout ce que vous faites est de filtrer, vous pouvez également omettre le .loc.


16

Je propose cela pour des exemples supplémentaires. Il s'agit de la même réponse que https://stackoverflow.com/a/28159296/

J'ajouterai d'autres modifications pour rendre ce post plus utile.

pandas.DataFrame.query
querya été fait exactement dans ce but. Tenez compte de la trame de donnéesdf

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

Utilisons querypour filtrer toutes les lignes oùD > 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

Que nous enchaînons

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

N'est-ce pas fondamentalement la même réponse que stackoverflow.com/a/28159296 Y a-t-il quelque chose qui manque dans cette réponse et que vous pensez qu'il faudrait clarifier?
bscan

9

J'avais la même question, sauf que je voulais combiner les critères dans une condition OU. Le format donné par Wouter Overmeire combine les critères en une condition ET telle que les deux doivent être satisfaites:

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

Mais j'ai trouvé que, si vous encapsulez chaque condition (... == True)et joignez les critères avec un tuyau, les critères sont combinés dans une condition OR, satisfait chaque fois que l'un d'eux est vrai:

df[((df.A==1) == True) | ((df.D==6) == True)]

12
Ne serait-ce pas df[(df.A==1) | (df.D==6)]suffisant pour ce que vous essayez d'accomplir?
eenblam

Non, ce ne serait pas le cas car cela donne des résultats bolléens (Vrai vs Faux) au lieu de comme c'est au-dessus de qui filtrent toutes les données qui satisfont la condition. J'espère que je l'ai dit clairement.
MGB.py

8

pandas propose deux alternatives à la réponse de Wouter Overmeire qui ne nécessitent aucune dérogation. On est .loc[.]avec un callable, comme dans

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

l'autre est .pipe(), comme dans

df_filtered = df.pipe(lambda x: x['column'] == value)

7

Ma réponse est similaire aux autres. Si vous ne souhaitez pas créer de nouvelle fonction, vous pouvez utiliser ce que les pandas ont déjà défini pour vous. Utilisez la méthode du tuyau.

df.pipe(lambda d: d[d['column'] == value])

CECI est ce que vous voulez si vous voulez enchaîner des commandes telles quea.join(b).pipe(lambda df: df[df.column_to_filter == 'VALUE'])
displayname

4

Si vous souhaitez appliquer tous les masques booléens courants ainsi qu'un masque à usage général, vous pouvez ranger les éléments suivants dans un fichier, puis les affecter simplement comme suit:

pd.DataFrame = apply_masks()

Usage:

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

C'est un peu hacky mais cela peut rendre les choses un peu plus propres si vous coupez et modifiez en continu des ensembles de données en fonction des filtres. Il y a aussi un filtre à usage général adapté de Daniel Velkov ci-dessus dans la fonction gen_mask que vous pouvez utiliser avec les fonctions lambda ou autrement si vous le souhaitez.

Fichier à enregistrer (j'utilise 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

3

Cette solution est plus hackeuse en terme d'implémentation, mais je la trouve beaucoup plus propre en terme d'utilisation, et elle est certainement plus générale que les autres proposées.

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

Vous n'avez pas besoin de télécharger l'intégralité du référentiel: enregistrer le fichier et faire

from where import where as W

devrait suffire. Ensuite, vous l'utilisez comme ceci:

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

Un exemple d'utilisation un peu moins stupide:

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

Soit dit en passant: même dans le cas où vous utilisez simplement des cols booléens,

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

peut être beaucoup plus efficace que

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

car il évalue cond2uniquement où cond1est True.

AVERTISSEMENT: J'ai d'abord donné cette réponse ailleurs parce que je ne l'avais pas vu.


2

Je veux juste ajouter une démonstration en utilisant locpour filtrer non seulement par lignes mais aussi par colonnes et certains mérites de l'opération chaînée.

Le code ci-dessous peut filtrer les lignes par valeur.

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

En le modifiant un peu, vous pouvez également filtrer les colonnes.

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

Alors pourquoi voulons-nous une méthode chaînée? La réponse est simple à lire si vous avez plusieurs opérations. Par exemple,

res =  df\
    .loc[df['station']=='USA', ['TEMP', 'RF']]\
    .groupby('year')\
    .agg(np.nanmean)

2

C'est peu attrayant car cela nécessite que j'attribue dfune variable avant de pouvoir filtrer sur ses valeurs.

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

semble fonctionner: vous pouvez également imbriquer l' []opérateur. Ils l'ont peut-être ajouté depuis que vous avez posé la question.


1
Cela dfn'a pas beaucoup de sens dans une chaîne car maintenant ne fait pas nécessairement référence à la sortie de la partie précédente de la chaîne.
Daan Luttik

@DaanLuttik: d'accord, ce n'est pas du chaînage, mais de l'emboîtement. Mieux pour toi?
serv-inc

1

Si vous définissez vos colonnes pour rechercher en tant qu'index, vous pouvez utiliser DataFrame.xs()pour prendre une coupe transversale. Ce n'est pas aussi polyvalent que les queryréponses, mais cela peut être utile dans certaines situations.

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

1

Vous pouvez également tirer parti de la bibliothèque numpy pour les opérations logiques. C'est assez rapide.

df[np.logical_and(df['A'] == 1 ,df['B'] == 6)]
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.