Mathew K Analytics

Lesson 9 · Pandas Full Course

Filter Pandas DataFrames with Conditions: Boolean Indexing Made Easy | Pandas Course #9

In this lesson you'll be able to: Build a True/False mask and use it to keep matching rows Combine conditions with &, | and ~ Match lists, ranges and text…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Pandas 09: Conditional Selection and Boolean Indexing#

In this lesson you'll be able to:

  • Build a True/False mask and use it to keep matching rows
  • Combine conditions with &, | and ~
  • Match lists, ranges and text with isin, between and str.contains
  • Handle missing values inside conditions
  • Update only the matching rows with loc Dataset: New York taxi trips, March 2019 (data/pandas/taxis.csv)

Warm-up: select by label and position#

Lesson 08 selected rows and columns with loc, iloc and nlargest.

import pandas as pd
taxis = pd.read_csv("data/pandas/taxis.csv")
print(taxis.shape)
taxis.loc[:2, ["pickup_zone", "dropoff_zone", "fare"]]
(6433, 14)
pickup_zone dropoff_zone fare
0 Lenox Hill West UN/Turtle Bay South 7.0
1 Upper West Side South Upper West Side South 5.0
2 Alphabet City West Village 7.5
cols = ["pickup_zone", "dropoff_zone", "distance", "fare"]
taxis.nlargest(3, "fare")[cols]
pickup_zone dropoff_zone distance fare
5364 JFK Airport JFK Airport 36.70 150.0
5648 East Flushing NaN 33.46 150.0
4050 LaGuardia Airport NaN 33.76 143.5

Keep rows that pass one test#

Problem: select rows by what they contain, not where they sit.

no_tip = taxis["tip"] == 0
no_tip.head()
0    False
1     True
2    False
3    False
4    False
Name: tip, dtype: bool
print(no_tip.sum())
print(no_tip.mean().round(3))
2311
0.359
zero = taxis[no_tip]
zero[["payment", "fare", "tip"]].head()
payment fare tip
1 cash 5.0 0.0
7 NaN 8.5 0.0
10 cash 17.0 0.0
13 cash 10.5 0.0
14 cash 11.5 0.0
zero["payment"].value_counts(dropna=False)
payment
cash           1812
credit card     455
NaN              44
Name: count, dtype: int64

Quick check: how many trips were longer than 10 miles?

long = taxis["distance"] > 10
print(long.sum())
394

Common mistake: comparing a number column with text

taxis[taxis["passengers"] > "2"]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 taxis[taxis["passengers"] > "2"]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\common.py:85, in _unpack_zerodim_and_defer.<locals>.new_method(self, other)
     82     other = sanitize_array(other, None)
     83     other = ensure_wrapped_if_datetimelike(other)
---> 85 return method(self, other)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\arraylike.py:58, in OpsMixin.__gt__(self, other)
     56 @unpack_zerodim_and_defer("__gt__")
     57 def __gt__(self, other):
---> 58     return self._cmp_method(other, operator.gt)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\series.py:6735, in Series._cmp_method(self, other, op)
   6732 lvalues = self._values
   6733 rvalues = extract_array(other, extract_numpy=True, extract_range=True)
-> 6735 res_values = ops.comparison_op(lvalues, rvalues, op)
   6737 return self._construct_result(res_values, name=res_name, other=other)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\array_ops.py:353, in comparison_op(left, right, op)
    349         res_values = np.zeros(lvalues.shape, dtype=bool)
    351 elif is_numeric_v_string_like(lvalues, rvalues):
    352     # GH#36377 going through the numexpr path would incorrectly raise
--> 353     return invalid_comparison(lvalues, rvalues, op)
    355 elif lvalues.dtype == object or isinstance(rvalues, str):
    356     res_values = comp_method_OBJECT_ARRAY(op, lvalues, rvalues)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\invalid.py:55, in invalid_comparison(left, right, op)
     53 else:
     54     typ = type(right).__name__
---> 55     raise TypeError(f"Invalid comparison between dtype={left.dtype} and {typ}")
     56 return res_values

TypeError: Invalid comparison between dtype=int64 and str
crowded = taxis[taxis["passengers"] > 2]
print(len(crowded))
783
print((taxis["fare"] > 0).all())
print((taxis["distance"] == 0).any())
print((taxis["distance"] == 0).sum())
True
True
51

Combine conditions with &, | and ~#

Problem: real questions have more than one rule.

big_fare = taxis["fare"] > 50
big_no_tip = big_fare & no_tip
print(big_no_tip.sum())
taxis[big_no_tip][["payment", "fare", "pickup_zone"]].head()
74
payment fare pickup_zone
56 credit card 52.0 JFK Airport
302 cash 52.0 JFK Airport
850 credit card 57.0 East New York
1384 cash 67.5 JFK Airport
1406 cash 52.0 JFK Airport
group_or_long = (taxis["passengers"] >= 4) | (taxis["distance"] > 15)
print(group_or_long.sum())
707
not_yellow = ~(taxis["color"] == "yellow")
print(not_yellow.sum())
taxis.loc[not_yellow, "color"].value_counts()
982
color
green    982
Name: count, dtype: int64

Common mistake: using Python's and instead of &

taxis[(taxis["fare"] > 50) and (taxis["tip"] == 0)]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_2500\1583402158.py in ?()
----> 1 taxis[(taxis["fare"] > 50) and (taxis["tip"] == 0)]

~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\generic.py in ?(self)
   1499     @final
   1500     def __bool__(self) -> NoReturn:
-> 1501         raise ValueError(
   1502             f"The truth value of a {type(self).__name__} is ambiguous. "
   1503             "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
   1504         )

ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
fixed = taxis[(taxis["fare"] > 50) & (taxis["tip"] == 0)]
print(len(fixed))
74

Common mistake: forgetting the brackets around each test

taxis[taxis["fare"] > 50 & taxis["tip"] == 0]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\array_ops.py:374, in na_logical_op(x, y, op)
    365 try:
    366     # For exposition, write:
    367     #  yarr = isinstance(y, np.ndarray)
   (...)
    372     # Then Cases where this goes through without raising include:
    373     #  (xint or xbool) and (yint or bool)
--> 374     result = op(x, y)
    375 except TypeError:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\roperator.py:55, in rand_(left, right)
     54 def rand_(left, right):
---> 55     return operator.and_(right, left)

TypeError: ufunc 'bitwise_and' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\array_ops.py:388, in na_logical_op(x, y, op)
    387 try:
--> 388     result = libops.scalar_binop(x, y, op)
    389 except (
    390     TypeError,
    391     ValueError,
   (...)
    394     NotImplementedError,
    395 ) as err:

File pandas/_libs/ops.pyx:178, in pandas._libs.ops.scalar_binop()

ValueError: Buffer dtype mismatch, expected 'Python object' but got 'double'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[17], line 1
----> 1 taxis[taxis["fare"] > 50 & taxis["tip"] == 0]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\common.py:85, in _unpack_zerodim_and_defer.<locals>.new_method(self, other)
     82     other = sanitize_array(other, None)
     83     other = ensure_wrapped_if_datetimelike(other)
---> 85 return method(self, other)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\arraylike.py:76, in OpsMixin.__rand__(self, other)
     74 @unpack_zerodim_and_defer("__rand__")
     75 def __rand__(self, other):
---> 76     return self._logical_method(other, roperator.rand_)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\series.py:6746, in Series._logical_method(self, other, op)
   6743 lvalues = self._values
   6744 rvalues = extract_array(other, extract_numpy=True, extract_range=True)
-> 6746 res_values = ops.logical_op(lvalues, rvalues, op)
   6747 return self._construct_result(res_values, name=res_name, other=other)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\array_ops.py:464, in logical_op(left, right, op)
    460 else:
    461     # i.e. scalar
    462     is_other_int_dtype = lib.is_integer(rvalues)
--> 464 res_values = na_logical_op(lvalues, rvalues, op)
    466 # For int vs int `^`, `|`, `&` are bitwise operators and return
    467 #   integer dtypes.  Otherwise these are boolean ops
    468 if not (left.dtype.kind in "iu" and is_other_int_dtype):

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\ops\array_ops.py:397, in na_logical_op(x, y, op)
    389         except (
    390             TypeError,
    391             ValueError,
   (...)
    394             NotImplementedError,
    395         ) as err:
    396             typ = type(y).__name__
--> 397             raise TypeError(
    398                 f"Cannot perform '{op.__name__}' with a dtyped [{x.dtype}] array "
    399                 f"and scalar of type [{typ}]"
    400             ) from err
    402 return result.reshape(x.shape)

TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]

Quick check: the ampersand runs before > and ==. Which part did pandas try to calculate first?

rule = (taxis["fare"] > 50) & (taxis["tip"] == 0)
print(rule.sum())
74

Your turn#

Of the yellow-cab rides over 20 miles, how many had no tip?

far = (taxis["color"] == "yellow") & (taxis["distance"] > 20)
print(far.sum())
print((far & no_tip).sum())
31
13

Match lists, ranges and text#

Problem: avoid long chains of == and | for common tests.

outer = ["Queens", "Brooklyn", "Bronx"]
from_outer = taxis["pickup_borough"].isin(outer)
print(from_outer.sum())
1139
short = taxis["distance"].between(1, 2)
print(short.sum())
print(taxis.loc[short, "fare"].median())
2176
8.0
zone = taxis["pickup_zone"]
airport_pick = zone.str.contains("Airport", na=False)
print(airport_pick.sum())
zone[airport_pick].value_counts()
297
pickup_zone
JFK Airport          151
LaGuardia Airport    146
Name: count, dtype: int64
dz = taxis["dropoff_zone"]
print(dz.str.contains("park", na=False).sum())
print(dz.str.contains("park", case=False, na=False).sum())
0
265

Common mistake: passing a single string to isin

taxis[taxis["payment"].isin("cash")]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[24], line 1
----> 1 taxis[taxis["payment"].isin("cash")]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\series.py:6076, in Series.isin(self, values)
   6002 def isin(self, values) -> Series:
   6003     """
   6004     Whether elements in Series are contained in `values`.
   6005 
   (...)
   6074     dtype: bool
   6075     """
-> 6076     result = algorithms.isin(self._values, values)
   6077     return self._constructor(result, index=self.index, copy=False).__finalize__(
   6078         self, method="isin"
   6079     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\algorithms.py:513, in isin(comps, values)
    508     raise TypeError(
    509         "only list-like objects are allowed to be passed "
    510         f"to isin(), you passed a `{type(comps).__name__}`"
    511     )
    512 if not is_list_like(values):
--> 513     raise TypeError(
    514         "only list-like objects are allowed to be passed "
    515         f"to isin(), you passed a `{type(values).__name__}`"
    516     )
    518 if not isinstance(values, (ABCIndex, ABCSeries, ABCExtensionArray, np.ndarray)):
    519     orig_values = list(values)

TypeError: only list-like objects are allowed to be passed to isin(), you passed a `str`
cash = taxis[taxis["payment"].isin(["cash"])]
print(len(cash))
1812

Your turn#

Count the green-cab rides from Brooklyn or the Bronx with a fare between 10 and 20.

green = taxis["color"] == "green"
place = taxis["pickup_borough"].isin(["Brooklyn", "Bronx"])
price = taxis["fare"].between(10, 20)
print((green & place & price).sum())
148

Handle missing values in conditions#

Problem: a missing value is never equal to anything.

missing_pay = taxis["payment"].isna()
print(missing_pay.sum())
taxis.loc[missing_pay, ["fare", "tip", "payment"]].head(3)
44
fare tip payment
7 8.5 0.0 NaN
445 5.5 0.0 NaN
491 2.5 0.0 NaN

Quick check: how many rows does payment == None find?

print((taxis["payment"] == None).sum())
0
not_cash = taxis["payment"] != "cash"
print(not_cash.sum())
print((taxis["payment"] == "credit card").sum())
4621
4577
card_only = not_cash & taxis["payment"].notna()
print(card_only.sum())
4577
pay = taxis["payment"].fillna("unknown")
pay.value_counts()
payment
credit card    4577
cash           1812
unknown          44
Name: count, dtype: int64

Your turn#

How many rides are missing the pickup zone, the dropoff zone, or both? And how many are missing both?

no_pick = taxis["pickup_zone"].isna()
no_drop = taxis["dropoff_zone"].isna()
print((no_pick | no_drop).sum())
print((no_pick & no_drop).sum())
50
21

Select columns and update rows with loc#

Problem: pick columns for the matching rows, or change just those rows.

rows = taxis["tip"] > 10
taxis.loc[rows, ["fare", "tip", "dropoff_zone"]].head()
fare tip dropoff_zone
42 80.0 20.08 NaN
128 36.0 11.50 Clinton East
162 52.0 13.10 JFK Airport
225 48.5 11.51 Boerum Hill
250 52.0 15.26 JFK Airport
taxis["tip_pct"] = (taxis["tip"] / taxis["fare"] * 100).round(1)
taxis["tip_known"] = True
taxis.loc[taxis["payment"] != "credit card", "tip_known"] = False
taxis["tip_known"].value_counts()
tip_known
True     4577
False    1856
Name: count, dtype: int64
known = taxis[taxis["tip_known"]]
print(known["tip_pct"].median())
print(taxis["tip_pct"].median())
25.5
21.1

Quick check: among card rides, who tips a bigger percentage: green-cab or yellow-cab riders?

is_green = known["color"] == "green"
print(known.loc[is_green, "tip_pct"].median())
print(known.loc[~is_green, "tip_pct"].median())
10.3
26.3

Your turn#

Add a column long_trip that is True for trips over 10 miles, using loc. Then count it.

taxis["long_trip"] = False
taxis.loc[taxis["distance"] > 10, "long_trip"] = True
print(taxis["long_trip"].sum())
394

Mini-project: are airport runs worth it?#

Question: how do fares and tips compare for trips to or from an airport?

Plan: build an airport mask, compare fares, compare card tips, conclude.

pick = taxis["pickup_zone"].str.contains("Airport", na=False)
drop = taxis["dropoff_zone"].str.contains("Airport", na=False)
airport = pick | drop
print(airport.sum(), airport.mean().round(3))
407 0.063
cols = ["distance", "fare", "total"]
print(taxis.loc[airport, cols].mean().round(2))
print(taxis.loc[~airport, cols].mean().round(2))
distance    12.40
fare        37.97
total       50.42
dtype: float64
distance     2.39
fare        11.41
total       16.36
dtype: float64
print(taxis.loc[airport, "pickup_borough"].value_counts())
long = taxis["distance"] > 10
print(airport[long].mean().round(2))
pickup_borough
Queens       309
Manhattan     93
Brooklyn       5
Name: count, dtype: int64
0.61
card = taxis["tip_known"]
air_tip = taxis.loc[card & airport, ["tip", "tip_pct"]].median()
city_tip = taxis.loc[card & ~airport, ["tip", "tip_pct"]].median()
print(pd.DataFrame({"airport": air_tip, "city": city_tip}))
         airport   city
tip         8.31   2.15
tip_pct    23.50  25.80
import matplotlib.pyplot as plt
avg = pd.Series({"Airport": taxis.loc[airport, "total"].mean(),
                 "City": taxis.loc[~airport, "total"].mean()})
ax = avg.plot(kind="bar", rot=0, figsize=(6, 4))
ax.set_title("Average total paid per taxi ride, March 2019")
ax.set_xlabel("Trip type")
ax.set_ylabel("Average total (dollars)")
plt.show()
No description has been provided for this image
ratio = avg["Airport"] / avg["City"]
print(f"Airport rides: {airport.sum()}, worth {ratio:.1f}x a city "
      "ride on average")
Airport rides: 407, worth 3.1x a city ride on average

Recap#

  • A comparison makes a True/False mask; df[mask] keeps the True rows
  • Combine masks with &, | and ~, with brackets round each test
  • isin, between and str.contains replace long chains of tests
  • NaN never equals anything: use isna() and notna()
  • df.loc[mask, cols] selects, and df.loc[mask, col] = x updates

Challenges#

  1. Find the share of card rides with no tip, in each pickup borough.
  2. List zero-distance rides that still charged over 20 dollars.
  3. Count green-cab rides whose dropoff zone contains 'Park'.

What's next#

Lesson 10: Filtering Rows and Columns using Query and isin Write the same filters as readable text with query().

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.