417

Most operations in pandas can be accomplished with operator chaining (groupby, aggregate, apply, etc), but the only way I've found to filter rows is via normal bracket indexing

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

This is unappealing as it requires I assign df to a variable before being able to filter on its values. Is there something more like the following?

df_filtered = df.mask(lambda x: x['column'] == value)
nick
  • 1,090
  • 1
  • 11
  • 24
duckworthd
  • 14,679
  • 16
  • 53
  • 68
  • `df.query` and `pd.eval` seem like good fits for this use case. For information on the `pd.eval()` family of functions, their features and use cases, please visit [Dynamic Expression Evaluation in pandas using pd.eval()](https://stackoverflow.com/questions/53779986/dynamic-expression-evaluation-in-pandas-using-pd-eval). – cs95 Dec 16 '18 at 04:54
  • dynamic expressions disallow any interpreter context help and are often a lower level of productivity/reliability. – WestCoastProjects Sep 13 '21 at 22:50

15 Answers15

467

I'm not entirely sure what you want, and your last line of code does not help either, but anyway:

"Chained" filtering is done by "chaining" the criteria in the boolean index.

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

If you want to chain methods, you can add your own mask method and use that one.

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
Andrew
  • 7,286
  • 3
  • 28
  • 38
Wouter Overmeire
  • 65,766
  • 10
  • 63
  • 43
154

Filters can be chained using a Pandas query:

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

Filters can also be combined in a single query:

df_filtered = df.query('a > 0 and 0 < b < 2')
bscan
  • 2,816
  • 1
  • 16
  • 16
  • 6
    If you need to refer to python variables in your query, the [documentation](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.query.html) says, "You can refer to variables in the environment by prefixing them with an ‘@’ character like @a + b". Note that the following are valid: `df.query('a in list([1,2])')`, `s = set([1,2]); df.query('a in @s')`. – teichert Nov 15 '16 at 16:14
  • 2
    On the other hand, it looks like the query evaluation will fail if your column name has certain special characters: e.g. "Place.Name". – teichert Nov 15 '16 at 16:17
  • 3
    Chaining is what query is designed for. – piRSquared Apr 10 '18 at 15:31
  • 1
    @teichert you can use backtick as described in this post(https://stackoverflow.com/questions/59167183/how-to-reference-a-pandas-column-that-has-a-dot-in-the-name) – KH Kim Aug 01 '21 at 18:45
  • 1
    @KHKim Nice! It looks like that support for dotted names in backticks was added in [v1.0.0](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.query.html). – teichert Aug 02 '21 at 22:13
75

The answer from @lodagro is great. I would extend it by generalizing the mask function as:

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

Then you can do stuff like:

df.mask(lambda x: x[0] < 0).mask(lambda x: x[1] > 0)
Daniel
  • 26,899
  • 12
  • 60
  • 88
39

Since version 0.18.1 the .loc method accepts a callable for selection. Together with lambda functions you can create very flexible chainable filters:

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]

If all you're doing is filtering, you can also omit the .loc.

Rafael Barbosa
  • 1,120
  • 12
  • 17
33

pandas provides two alternatives to Wouter Overmeire's answer which do not require any overriding. One is .loc[.] with a callable, as in

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

the other is .pipe(), as in

df_filtered = df.pipe(lambda x: x.loc[x['column'] == value])
Pietro Battiston
  • 7,930
  • 3
  • 42
  • 45
  • 5
    This is the best answer I've found so far. This allows for easy chaining and it is completely independent of the dataframe name, while maintaining a minimal syntax check (unlike "query"). Really neat approach, thanks. – Lucas Lima Jul 08 '20 at 20:08
  • 1
    +1 This should really be the accepted answer. It's built-in to pandas and requires no monkey-patching, and is the most flexible. I would also add that you can have your callable return an iterable of indexes as well, not just a boolean series. – ecotner Feb 02 '21 at 17:41
  • 3
    Great answer, if anyone need with two columns, follows: pandasDF.loc[lambda n: (n['col1'] == 'value') | (n[col2']=='value')] – Jayron Soares Apr 22 '21 at 14:10
  • Thank you for a simple answer that works with method chaining and the additional comments on how to include multiple conditions to filter on! – veg2020 Feb 08 '23 at 19:10
18

I offer this for additional examples. This is the same answer as https://stackoverflow.com/a/28159296/

I'll add other edits to make this post more useful.

pandas.DataFrame.query
query was made for exactly this purpose. Consider the dataframe df

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

Let's use query to filter all rows where 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

Which we chain

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
piRSquared
  • 285,575
  • 57
  • 475
  • 624
  • Isn't this basically the same answer as https://stackoverflow.com/a/28159296/ Is there something missing from that answer that you think should be clarified? – bscan Apr 10 '18 at 15:28
11

My answer is similar to the others. If you do not want to create a new function you can use what pandas has defined for you already. Use the pipe method.

df.pipe(lambda d: d[d['column'] == value])
Stewbaca
  • 535
  • 7
  • 9
  • 1
    **THIS** is what you want if you want to chain commands such as `a.join(b).pipe(lambda df: df[df.column_to_filter == 'VALUE'])` – Stefan Falk Jun 14 '17 at 10:10
10

I had the same question except that I wanted to combine the criteria into an OR condition. The format given by Wouter Overmeire combines the criteria into an AND condition such that both must be satisfied:

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

But I found that, if you wrap each condition in (... == True) and join the criteria with a pipe, the criteria are combined in an OR condition, satisfied whenever either of them is true:

df[((df.A==1) == True) | ((df.D==6) == True)]
sharon
  • 4,406
  • 1
  • 17
  • 10
  • 12
    Wouldn't `df[(df.A==1) | (df.D==6)]` be sufficient for what you're trying to accomplish? – eenblam Jun 10 '16 at 17:14
  • No, it wouldn't because it give bollean results (True vs False) instead of as it is above which filter all data which satisfy the condition. Hope that I made it clear. – MGB.py Dec 19 '19 at 09:02
6

Just want to add a demonstration using loc to filter not only by rows but also by columns and some merits to the chained operation.

The code below can filter the rows by value.

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

By modifying it a bit you can filter the columns as well.

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

So why do we want a chained method? The answer is that it is simple to read if you have many operations. For example,

res =  df\
    .loc[df['station']=='USA', ['TEMP', 'RF']]\
    .groupby('year')\
    .agg(np.nanmean)
Ken T
  • 2,255
  • 1
  • 23
  • 30
4

If you would like to apply all of the common boolean masks as well as a general purpose mask you can chuck the following in a file and then simply assign them all as follows:

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

It's a little bit hacky but it can make things a little bit cleaner if you're continuously chopping and changing datasets according to filters. There's also a general purpose filter adapted from Daniel Velkov above in the gen_mask function which you can use with lambda functions or otherwise if desired.

File to be saved (I use 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
dantes_419
  • 189
  • 3
  • 14
3

This solution is more hackish in terms of implementation, but I find it much cleaner in terms of usage, and it is certainly more general than the others proposed.

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

You don't need to download the entire repo: saving the file and doing

from where import where as W

should suffice. Then you use it like this:

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

A slightly less stupid usage example:

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

By the way: even in the case in which you are just using boolean cols,

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

can be much more efficient than

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

because it evaluates cond2 only where cond1 is True.

DISCLAIMER: I first gave this answer elsewhere because I hadn't seen this.

Pietro Battiston
  • 7,930
  • 3
  • 42
  • 45
3

This is unappealing as it requires I assign df to a variable before being able to filter on its values.

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

seems to work: you can nest the [] operator as well. Maybe they added it since you asked the question.

serv-inc
  • 35,772
  • 9
  • 166
  • 188
3

So the way I see it is that you do two things when sub-setting your data ready for analysis.

  • get rows
  • get columns

Pandas has a number of ways of doing each of these and some techniques that help get rows and columns. For new Pandas users it can be confusing as there is so much choice.

Do you use iloc, loc, brackets, query, isin, np.where, mask etc...

Method chaining

Now method chaining is a great way to work when data wrangling. In R they have a simple way of doing it, you select() columns and you filter() rows.

So if we want to keep things simple in Pandas why not use the filter() for columns and the query() for rows. These both return dataframes and so no need to mess-around with boolean indexing, no need to add df[ ] round the return value.

So what does that look like:-

df.filter(['col1', 'col2', 'col3']).query("col1 == 'sometext'")

You can then chain on any other methods like groupby, dropna(), sort_values(), reset_index() etc etc.

By being consistent and using filter() to get your columns and query() to get your rows it will be easier to read your code when coming back to it after a time.

But filter can select rows?

Yes this is true but by default query() get rows and filter() get columns. So if you stick with the default there is no need to use the axis= parameter.

query()

query() can be used with both and/or &/| you can also use comparison operators > , < , >= , <=, ==, !=. You can also use Python in, not in.

You can pass a list to query using @my_list

Some examples of using query to get rows

df.query('A > B')

df.query('a not in b')

df.query("series == '2206'")

df.query("col1 == @mylist")

df.query('Salary_in_1000 >= 100 & Age < 60 & FT_Team.str.startswith("S").values')

filter()

So filter is basicly like using bracket df[] or df[[]] in that it uses the labels to select columns. But it does more than the bracket notation.

filter has like= param so as to help select columns with partial names.

df.filter(like='partial_name',)

filter also has regex to help with selection

df.filter(regex='reg_string')

So to sum up this way of working might not work for ever situation e.g. if you want to use indexing/slicing then iloc is the way to go. But this does seem to be a solid way of working and can simplify your workflow and code.

Cam
  • 1,263
  • 13
  • 22
2

You can also leverage the numpy library for logical operations. Its pretty fast.

df[np.logical_and(df['A'] == 1 ,df['B'] == 6)]
Akash Basudevan
  • 820
  • 5
  • 15
1

If you set your columns to search as indexes, then you can use DataFrame.xs() to take a cross section. This is not as versatile as the query answers, but it might be useful in some 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
naught101
  • 18,687
  • 19
  • 90
  • 138