Mathew K Analytics

Lesson 10 · Pandas Full Course

Pandas query() and isin(): Write Filters That Read Like English | Pandas Course #10

In this lesson you'll be able to: Write row filters as readable sentences with query Use Python variables inside a query with @ Match many values at once…

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 10: Filtering Rows and Columns using Query and isin#

In this lesson you'll be able to:

  • Write row filters as readable sentences with query
  • Use Python variables inside a query with @
  • Match many values at once with isin, and flip it with ~
  • Pick columns by name, pattern or type with filter
  • Combine row and column filters to answer a real question Dataset: restaurant tips (data/pandas/tips.csv)

Warm-up: boolean masks from lesson 9#

Lesson 9 filtered rows with masks, & and |. Same tool, tips data.

import pandas as pd
df = pd.read_csv("data/pandas/tips.csv")
print(df.shape)
df.head()
(244, 7)
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4
mask = (df["time"] == "Dinner") & (df["tip"] > 5)
print(mask.sum())
df[mask].head(3)
15
total_bill tip sex smoker day time size
23 39.42 7.58 Male No Sat Dinner 4
44 30.40 5.60 Male No Sun Dinner 4
47 32.40 6.00 Male No Sun Dinner 4

Write filters as sentences with query#

Problem: masks get cluttered with brackets, df and &.

big_tips = df.query("tip > 5")
print(len(big_tips))
big_tips.head(3)
18
total_bill tip sex smoker day time size
23 39.42 7.58 Male No Sat Dinner 4
44 30.40 5.60 Male No Sun Dinner 4
47 32.40 6.00 Male No Sun Dinner 4
dinner_big = df.query("time == 'Dinner' and tip > 5")
print(len(dinner_big))
print(dinner_big.equals(df[mask]))
15
True
df.query("size >= 5 or total_bill > 45")[["total_bill", "size"]]
total_bill size
59 48.27 4
125 29.80 6
141 34.30 6
142 41.19 5
143 27.05 6
155 29.85 5
156 48.17 6
170 50.81 3
182 45.35 3
185 20.69 5
187 30.46 5
212 48.33 4
216 28.15 5
df.query("15 <= total_bill <= 20").shape
(67, 7)

Common mistake: one equals sign inside query

df.query("size = 4")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 1
----> 1 df.query("size = 4")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5195, in DataFrame.query(self, expr, parser, engine, local_dict, global_dict, resolvers, level, inplace)
   5192     msg = f"expr must be a string to be evaluated, {type(expr)} given"
   5193     raise ValueError(msg)
-> 5195 res = self.eval(
   5196     expr,
   5197     level=level + 1,
   5198     parser=parser,
   5199     target=None,
   5200     engine=engine,
   5201     local_dict=local_dict,
   5202     global_dict=global_dict,
   5203     resolvers=resolvers or (),
   5204 )
   5206 try:
   5207     result = self.loc[res]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5375, in DataFrame.eval(self, expr, inplace, **kwargs)
   5372     kwargs["target"] = self
   5373 kwargs["resolvers"] = tuple(kwargs.get("resolvers", ())) + resolvers
-> 5375 return _eval(expr, inplace=inplace, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\eval.py:370, in eval(expr, parser, engine, local_dict, global_dict, resolvers, level, target, inplace)
    361 # get our (possibly passed-in) scope
    362 env = ensure_scope(
    363     level + 1,
    364     global_dict=global_dict,
   (...)
    367     target=target,
    368 )
--> 370 parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
    372 if engine == "numexpr" and (
    373     (
    374         is_extension_array_dtype(parsed_expr.terms.return_type)
   (...)
    383     )
    384 ):
    385     warnings.warn(
    386         "Engine has switched to 'python' because numexpr does not support "
    387         "extension array dtypes. Please set your engine to python manually.",
    388         RuntimeWarning,
    389         stacklevel=find_stack_level(),
    390     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:819, in Expr.__init__(self, expr, engine, parser, env, level)
    817 self.parser = parser
    818 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
--> 819 self.terms = self.parse()

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:838, in Expr.parse(self)
    834 def parse(self):
    835     """
    836     Parse an expression.
    837     """
--> 838     return self._visitor.visit(self.expr)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:426, in BaseExprVisitor.visit_Module(self, node, **kwargs)
    424     raise SyntaxError("only a single expression is allowed")
    425 expr = node.body[0]
--> 426 return self.visit(expr, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:628, in BaseExprVisitor.visit_Assign(self, node, **kwargs)
    626     raise SyntaxError("left hand side of an assignment must be a single name")
    627 if self.env.target is None:
--> 628     raise ValueError("cannot assign without a target object")
    630 try:
    631     assigner = self.visit(node.targets[0], **kwargs)

ValueError: cannot assign without a target object
df.query("size == 4").shape
(37, 7)

Quick check: how many lunch bills came to more than $20?

lunch_20 = df.query("time == 'Lunch' and total_bill > 20")
print(len(lunch_20))
16

Your turn#

Find Saturday bills for parties of 3 or more that tipped under $3.

sat = df.query("day == 'Sat' and size >= 3 and tip < 3")
sat[["total_bill", "tip", "size"]]
total_bill tip size
25 17.81 2.34 4
31 18.35 2.50 4
33 20.69 2.45 4
36 16.31 2.00 3
38 18.69 2.31 3
40 16.04 2.24 3
64 17.59 2.64 3
102 44.30 2.50 3
210 30.06 2.00 3
230 24.01 2.00 4

Use Python variables inside query with @#

Problem: thresholds change, so don't hard-code them.

limit = 4.5
generous = df.query("tip > @limit and smoker == 'No'")
print(len(generous))
21
avg_tip = df["tip"].mean()
print(round(avg_tip, 2))
above = df.query("tip > @avg_tip")
print(len(above), len(df))
3.0
121 244

Common mistake: forgetting the @

df.query("tip > avg_tip")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\scope.py:232, in Scope.resolve(self, key, is_local)
    231 if self.has_resolvers:
--> 232     return self.resolvers[key]
    234 # if we're here that means that we have no locals and we also have
    235 # no resolvers

File ~\AppData\Local\Programs\Python\Python312\Lib\collections\__init__.py:1014, in ChainMap.__getitem__(self, key)
   1013         pass
-> 1014 return self.__missing__(key)

File ~\AppData\Local\Programs\Python\Python312\Lib\collections\__init__.py:1006, in ChainMap.__missing__(self, key)
   1005 def __missing__(self, key):
-> 1006     raise KeyError(key)

KeyError: 'avg_tip'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\scope.py:243, in Scope.resolve(self, key, is_local)
    239 try:
    240     # last ditch effort we look in temporaries
    241     # these are created when parsing indexing expressions
    242     # e.g., df[df > 0]
--> 243     return self.temps[key]
    244 except KeyError as err:

KeyError: 'avg_tip'

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

UndefinedVariableError                    Traceback (most recent call last)
Cell In[14], line 1
----> 1 df.query("tip > avg_tip")

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5195, in DataFrame.query(self, expr, parser, engine, local_dict, global_dict, resolvers, level, inplace)
   5192     msg = f"expr must be a string to be evaluated, {type(expr)} given"
   5193     raise ValueError(msg)
-> 5195 res = self.eval(
   5196     expr,
   5197     level=level + 1,
   5198     parser=parser,
   5199     target=None,
   5200     engine=engine,
   5201     local_dict=local_dict,
   5202     global_dict=global_dict,
   5203     resolvers=resolvers or (),
   5204 )
   5206 try:
   5207     result = self.loc[res]

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5375, in DataFrame.eval(self, expr, inplace, **kwargs)
   5372     kwargs["target"] = self
   5373 kwargs["resolvers"] = tuple(kwargs.get("resolvers", ())) + resolvers
-> 5375 return _eval(expr, inplace=inplace, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\eval.py:370, in eval(expr, parser, engine, local_dict, global_dict, resolvers, level, target, inplace)
    361 # get our (possibly passed-in) scope
    362 env = ensure_scope(
    363     level + 1,
    364     global_dict=global_dict,
   (...)
    367     target=target,
    368 )
--> 370 parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
    372 if engine == "numexpr" and (
    373     (
    374         is_extension_array_dtype(parsed_expr.terms.return_type)
   (...)
    383     )
    384 ):
    385     warnings.warn(
    386         "Engine has switched to 'python' because numexpr does not support "
    387         "extension array dtypes. Please set your engine to python manually.",
    388         RuntimeWarning,
    389         stacklevel=find_stack_level(),
    390     )

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:819, in Expr.__init__(self, expr, engine, parser, env, level)
    817 self.parser = parser
    818 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
--> 819 self.terms = self.parse()

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:838, in Expr.parse(self)
    834 def parse(self):
    835     """
    836     Parse an expression.
    837     """
--> 838     return self._visitor.visit(self.expr)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:426, in BaseExprVisitor.visit_Module(self, node, **kwargs)
    424     raise SyntaxError("only a single expression is allowed")
    425 expr = node.body[0]
--> 426 return self.visit(expr, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:429, in BaseExprVisitor.visit_Expr(self, node, **kwargs)
    428 def visit_Expr(self, node, **kwargs):
--> 429     return self.visit(node.value, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:729, in BaseExprVisitor.visit_Compare(self, node, **kwargs)
    727     op = self.translate_In(ops[0])
    728     binop = ast.BinOp(op=op, left=node.left, right=comps[0])
--> 729     return self.visit(binop)
    731 # recursive case: we have a chained comparison, a CMP b CMP c, etc.
    732 left = node.left

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:541, in BaseExprVisitor.visit_BinOp(self, node, **kwargs)
    540 def visit_BinOp(self, node, **kwargs):
--> 541     op, op_class, left, right = self._maybe_transform_eq_ne(node)
    542     left, right = self._maybe_downcast_constants(left, right)
    543     return self._maybe_evaluate_binop(op, op_class, left, right)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:462, in BaseExprVisitor._maybe_transform_eq_ne(self, node, left, right)
    460     left = self.visit(node.left, side="left")
    461 if right is None:
--> 462     right = self.visit(node.right, side="right")
    463 op, op_class, left, right = self._rewrite_membership_op(node, left, right)
    464 return op, op_class, left, right

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:420, in BaseExprVisitor.visit(self, node, **kwargs)
    418 method = f"visit_{type(node).__name__}"
    419 visitor = getattr(self, method)
--> 420 return visitor(node, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:551, in BaseExprVisitor.visit_Name(self, node, **kwargs)
    550 def visit_Name(self, node, **kwargs) -> Term:
--> 551     return self.term_type(node.id, self.env, **kwargs)

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\ops.py:91, in Term.__init__(self, name, env, side, encoding)
     89 tname = str(name)
     90 self.is_local = tname.startswith(LOCAL_TAG) or tname in DEFAULT_GLOBALS
---> 91 self._value = self._resolve_name()
     92 self.encoding = encoding

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\ops.py:115, in Term._resolve_name(self)
    110 if local_name in self.env.scope and isinstance(
    111     self.env.scope[local_name], type
    112 ):
    113     is_local = False
--> 115 res = self.env.resolve(local_name, is_local=is_local)
    116 self.update(res)
    118 if hasattr(res, "ndim") and isinstance(res.ndim, int) and res.ndim > 2:

File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\scope.py:245, in Scope.resolve(self, key, is_local)
    243     return self.temps[key]
    244 except KeyError as err:
--> 245     raise UndefinedVariableError(key, is_local) from err

UndefinedVariableError: name 'avg_tip' is not defined
df.query("tip > @avg_tip").shape
(121, 7)
rate = df.query("tip / total_bill > 0.25")
rate[["total_bill", "tip", "size"]]
total_bill tip size
51 10.29 2.60 2
67 3.07 1.00 1
93 16.32 4.30 2
109 14.31 4.00 2
149 7.51 2.00 2
172 7.25 5.15 2
178 9.60 4.00 2
183 23.17 6.50 4
221 13.42 3.48 2
232 11.61 3.39 2
per_head = df.query("tip > size * 1.5")
print(len(per_head))
per_head["size"].value_counts()
55
size
2    44
3     5
4     4
1     2
Name: count, dtype: int64

Your turn#

Store 0.10 in a variable, then find Sunday bills that tipped less than that share.

low = 0.10
stingy = df.query("day == 'Sun' and tip / total_bill < @low")
stingy[["total_bill", "tip", "size"]]
total_bill tip size
0 16.99 1.01 2
48 28.55 2.05 3
153 24.55 2.00 4
173 31.85 3.18 2
175 32.90 3.11 2
182 45.35 3.50 3
184 40.55 3.00 2
187 30.46 2.00 5
190 15.69 1.50 2

Match many values at once with isin#

Problem: "day is Sat or Sun or ..." gets long fast.

print(df["day"].unique())
<ArrowStringArray>
['Sun', 'Sat', 'Thur', 'Fri']
Length: 4, dtype: str
weekend = df["day"].isin(["Sat", "Sun"])
print(weekend.sum())
df[weekend]["day"].value_counts()
163
day
Sat    87
Sun    76
Name: count, dtype: int64
weekdays = df[~weekend]
weekdays["day"].value_counts()
day
Thur    62
Fri     19
Name: count, dtype: int64

Common mistake: comparing a column to a list with ==

df[df["day"] == ["Sat", "Sun"]]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[22], line 1
----> 1 df[df["day"] == ["Sat", "Sun"]]

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:42, in OpsMixin.__eq__(self, other)
     40 @unpack_zerodim_and_defer("__eq__")
     41 def __eq__(self, other):
---> 42     return self._cmp_method(other, operator.eq)

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:332, in comparison_op(left, right, op)
    327 if isinstance(rvalues, (np.ndarray, ABCExtensionArray)) and not rvalues_is_zerodim:
    328     # TODO: make this treatment consistent across ops and classes.
    329     #  We are not catching all listlikes here (e.g. frozenset, tuple)
    330     #  The ambiguous case is object-dtype.  See GH#27803
    331     if len(lvalues) != len(rvalues):
--> 332         raise ValueError(
    333             "Lengths must match to compare", lvalues.shape, rvalues.shape
    334         )
    336 if should_extension_dispatch(lvalues, rvalues) or (
    337     (isinstance(rvalues, (Timedelta, BaseOffset, Timestamp)) or right is NaT)
    338     and lvalues.dtype != object
    339 ):
    340     # Call the method on lvalues
    341     res_values = op(lvalues, rvalues)

ValueError: ('Lengths must match to compare', (244,), (2,))
df[df["day"].isin(["Sat", "Sun"])].shape
(163, 7)

Common mistake: passing a single string to isin

df[df["day"].isin("Sat")]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[24], line 1
----> 1 df[df["day"].isin("Sat")]

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`
df[df["day"].isin(["Sat"])].shape
(87, 7)
quiet = df["day"].value_counts().index[-2:]
print(list(quiet))
print(df["day"].isin(quiet).sum())
['Thur', 'Fri']
81
party = df["size"].isin([5, 6])
df[party][["total_bill", "tip", "size", "day"]]
total_bill tip size day
125 29.80 4.20 6 Thur
141 34.30 6.70 6 Thur
142 41.19 5.00 5 Thur
143 27.05 5.00 6 Thur
155 29.85 5.14 5 Sun
156 48.17 5.00 6 Sun
185 20.69 5.00 5 Sun
187 30.46 2.00 5 Sun
216 28.15 3.00 5 Sat

Quick check: what share of bills were weekend dinners?

wk_dinner = weekend & (df["time"] == "Dinner")
print(round(wk_dinner.mean(), 3))
0.668

Use in, not in and backticks inside query#

Problem: bring isin and odd column names into query strings.

df.query("day in ['Sat', 'Sun']").shape
(163, 7)
slow_days = ["Thur", "Fri"]
rest = df.query("day not in @slow_days and size > 2")
rest["day"].value_counts()
day
Sun    37
Sat    32
Name: count, dtype: int64
menu = df.rename(columns={"total_bill": "total bill"})
print(menu.columns.tolist())
['total bill', 'tip', 'sex', 'smoker', 'day', 'time', 'size']

Common mistake: a column name with a space inside query

menu.query("total bill > 45")
Traceback (most recent call last):

  File ~\AppData\Roaming\Python\Python312\site-packages\IPython\core\interactiveshell.py:3577 in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)

  Cell In[32], line 1
    menu.query("total bill > 45")

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5195 in query
    res = self.eval(

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:5375 in eval
    return _eval(expr, inplace=inplace, **kwargs)

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\eval.py:370 in eval
    parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:819 in __init__
    self.terms = self.parse()

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:838 in parse
    return self._visitor.visit(self.expr)

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:416 in visit
    raise e

  File ~\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\computation\expr.py:412 in visit
    node = ast.fix_missing_locations(ast.parse(clean))

  File ~\AppData\Local\Programs\Python\Python312\Lib\ast.py:52 in parse
    return compile(source, filename, mode, flags,

  File <unknown>:1
    total bill >45
          ^
SyntaxError: invalid syntax
menu.query("`total bill` > 45")
total bill tip sex smoker day time size
59 48.27 6.73 Male No Sat Dinner 4
156 48.17 5.00 Male No Sun Dinner 6
170 50.81 10.00 Male Yes Sat Dinner 3
182 45.35 3.50 Male Yes Sun Dinner 3
212 48.33 9.00 Male No Sat Dinner 4

Your turn#

With query: Fri or Sat bills, a party size other than 2, and a tip of at least $4.

busy = df.query("day in ['Fri', 'Sat'] and size != 2 "
                 "and tip >= 4")
busy[["day", "size", "tip", "total_bill"]]
day size tip total_bill
23 Sat 4 7.58 39.42
39 Sat 3 5.00 31.27
59 Sat 4 6.73 48.27
95 Fri 4 4.73 40.17
170 Sat 3 10.00 50.81
211 Sat 4 5.16 25.89
212 Sat 4 9.00 48.33
214 Sat 3 6.50 28.17
238 Sat 3 4.67 35.83
239 Sat 3 5.92 29.03

Filter columns by name, pattern or type#

Problem: keep just the columns you need, without typing them all.

df.filter(items=["day", "time", "tip"]).head(3)
day time tip
0 Sun Dinner 1.01
1 Sun Dinner 1.66
2 Sun Dinner 3.50
df.filter(like="ti").head(3)
tip time
0 1.01 Dinner
1 1.66 Dinner
2 3.50 Dinner
df.filter(regex="^t").head(3)
total_bill tip time
0 16.99 1.01 Dinner
1 10.34 1.66 Dinner
2 21.01 3.50 Dinner
nums = df.select_dtypes(include="number")
print(nums.columns.tolist())
['total_bill', 'tip', 'size']
lunch = df.query("time == 'Lunch'")
lunch[["total_bill", "tip", "sex"]].head()
total_bill tip sex
77 27.20 4.00 Male
78 22.76 3.00 Male
79 17.29 2.71 Male
80 19.44 3.00 Male
81 16.66 3.40 Male
fri_lunch = df["day"].isin(["Fri"]) & (df["time"] == "Lunch")
df.loc[fri_lunch, ["total_bill", "tip", "size"]]
total_bill tip size
220 12.16 2.20 2
221 13.42 3.48 2
222 8.58 1.92 1
223 15.98 3.00 3
224 13.42 1.58 2
225 16.27 2.50 2
226 10.09 2.00 2
top = df.query("tip >= 5").sort_values("total_bill",
                                        ascending=False)
top[["total_bill", "tip", "day"]].head()
total_bill tip day
170 50.81 10.00 Sat
212 48.33 9.00 Sat
59 48.27 6.73 Sat
156 48.17 5.00 Sun
197 43.11 5.00 Thur

Quick check: which columns have an underscore in their name?

df.filter(like="_").columns.tolist()
['total_bill']

Your turn#

Keep only the columns whose names end with the letter e.

df.filter(regex="e$").head(3)
time size
0 Dinner 2
1 Dinner 3
2 Dinner 3

Mini-project: do bigger parties tip a smaller share?#

Plan: add tip %, filter small and large parties, compare, check.

df["tip_pct"] = (df["tip"] / df["total_bill"] * 100).round(1)
small = df[df["size"].isin([1, 2])]
large = df.query("size >= 4")
print(len(small), len(large))
160 46
print(round(small["tip_pct"].mean(), 1))
print(round(large["tip_pct"].mean(), 1))
16.7
14.6
print(small["tip_pct"].median(), large["tip_pct"].median())
15.7 14.45
wk = "day in ['Sat', 'Sun'] and time == 'Dinner'"
s_wk = small.query(wk)["tip_pct"].median()
l_wk = large.query(wk)["tip_pct"].median()
print(s_wk, l_wk)
15.75 14.45
s_gen = len(small.query("tip_pct >= 20"))
l_gen = len(large.query("tip_pct >= 20"))
print(s_gen, round(s_gen / len(small), 3))
print(l_gen, round(l_gen / len(large), 3))
32 0.2
3 0.065
import matplotlib.pyplot as plt
meds = df.groupby("size")["tip_pct"].median()
ax = meds.plot(kind="bar", rot=0, figsize=(7, 4))
ax.set_title("Median tip by party size")
ax.set_xlabel("Party size")
ax.set_ylabel("Tip, % of bill")
plt.show()
No description has been provided for this image

Recap#

  • query writes row filters as readable strings: and, or, ranges
  • @ brings Python variables into a query
  • isin matches a list of values; ~ flips it
  • in, not in and backticks work inside query strings
  • filter and select_dtypes choose columns by name, pattern or type

Challenges#

  1. Find dinners where a female smoker paid over $30, with query.
  2. Keep Thur and Fri lunches with isin, then their bill and tip.
  3. Store the median bill in a variable and query bills above it.

What's next#

Lesson 11: Setting, Resetting, and Sorting Indexes Turn a column into row labels, and put them in order.

Found this useful?

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