EduFlow

Section 2 · pandas Foundations

Filtering and Sorting Rows

18 min3 min readPython exercise

Lesson at a glance

Filter rows with boolean masks, combine conditions with &, | and ~, use isin and between for common filters, and sort or rank rows with sort_values and nlargest.

  1. 1Boolean masks
  2. 2Combining conditions
  3. 3Two shortcuts: isin and between
  4. 4Sorting and top-n

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

The head of growth’s question has a time window: Q4 2025. Before you can explain that quarter, you need to isolate it, and then slice it further: which of those orders used a coupon, which came through the marketplace, which were Black Friday orders. Every one of those is a filter, and pandas filters the same way every time: build a column of True/False, then keep the True rows.

Boolean masks

A comparison on a column does not return one answer; it returns one answer per row:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
is_free = orders["shipping_fee"] == 0
print(is_free.head())
print(is_free.sum(), "orders with free shipping")
print(round(is_free.mean(), 3), "share of all orders")

That boolean Series is called a mask. Summing it counts the True values, and its mean is the share of rows that match: 43% of Cartwheel’s orders shipped free. You often learn what you need from the count alone, without filtering anything.

To keep the matching rows, put the mask inside square brackets, or inside .loc when you also want to choose columns:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
cancelled = orders[orders["status"] == "cancelled"]
print(len(cancelled))
print(orders.loc[orders["shipping_fee"] == 0, ["order_id", "channel", "coupon_code"]].head())

Combining conditions

Real questions have several conditions. pandas uses & for and, | for or, and ~ for not, applied row by row:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
in_q4 = (orders["order_date"] >= "2025-10-01") & (orders["order_date"] < "2026-01-01")
not_cancelled = orders["status"] != "cancelled"
q4 = orders[in_q4 & not_cancelled]
print(len(orders[in_q4]), "orders in Q4 2025,", len(q4), "not cancelled")
print(q4["channel"].value_counts())

Giving each mask a name, as here, keeps long filters readable and lets you print and count each condition separately when a result looks wrong. A datetime column can be compared with a date written as a string, "2025-10-01", and pandas converts it for you. The upper bound is “before 1 January”, not “on or before 31 December”, because orders on 31 December have a time later than midnight.

Two shortcuts: isin and between

Matching any of several values with | chains gets long. isin takes a list:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
problem = orders["status"].isin(["cancelled", "returned"])
print(problem.sum(), "cancelled or returned")

black_friday_week = orders["order_date"].between("2025-11-24", "2025-11-30 23:59:59")
print(black_friday_week.sum(), "orders in Black Friday week 2025")

bf = orders[orders["coupon_code"] == "BLACKFRIDAY25"]
print(bf["order_date"].dt.month.value_counts().head(3))

between includes both ends, which is why the upper bound carries a time. The last lines contain a real finding: of 124 orders with the BLACKFRIDAY25 coupon, 91 are in November, and the rest are scattered across the year. Was the code left active, shared on a coupon site, or is the data wrong? Filters do not answer that, but they surface the question, and good analysts take it to the people who run the promotion before building on it. (.dt.month extracts the month number; Lesson 4.4 covers the dt accessor properly.)

A filtered table is a new table

A filter returns a new DataFrame. q4 above does not remember that it came from orders, and you should treat it as its own table: if you later want to add a column to it, you add it to q4, and orders stays untouched. In pandas 2 code you will often see .copy() after a filter: it makes that explicit and silences the SettingWithCopyWarning pandas 2 prints when you add a column to a filtered table. With Copy-on-Write, which is the default from pandas 3.0 and which you can switch on in pandas 2 with pd.options.mode.copy_on_write = True, every filtered result already behaves as an independent copy. Lesson 2.5 shows why this matters when you start changing values.

Sorting and top-n

sort_values orders rows by one or more columns:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
latest = orders.sort_values("order_date", ascending=False)
print(latest[["order_id", "order_date", "status"]].head(3))

by_fee = orders.sort_values(["shipping_fee", "order_date"], ascending=[False, False])
print(by_fee[["order_id", "shipping_fee", "order_date"]].head(3))
print(orders.nlargest(3, "shipping_fee")[["order_id", "shipping_fee"]])

With a list of columns, the second column breaks ties in the first, and ascending takes a matching list. nlargest(n, column) and nsmallest are shortcuts for “sort and take the top n”; they are faster on big tables and say what you mean.

The three latest orders are still processing or shipped: they were placed in the last days of December and had not been delivered when the data was exported. That is another decision for your revenue definition, which you will make in Section 4.

In the exercise you isolate Q4 2025 and measure its coupon use. Next you add new columns, such as each line’s revenue, and learn the one correct way to change values in a DataFrame.

Key takeaways

  • A comparison on a column returns a boolean mask; df[mask] or df.loc[mask, cols] keeps the rows where it is True.
  • Combine masks with & (and), | (or) and ~ (not), and wrap every condition in parentheses.
  • mask.sum() counts matching rows and mask.mean() gives their share, without filtering anything.
  • isin([...]) replaces long chains of == with |, and between(a, b) includes both ends.
  • sort_values takes lists for multi-column sorts; nlargest(n, col) is the shortcut for a top-n list.

Further reading

Check your understanding

3 questions

Question 1 of 3

Which line keeps web orders that used a coupon?

Question 2 of 3

mask = orders["shipping_fee"] == 0. What does mask.mean() return?

Question 3 of 3

You want the 10 orders with the highest shipping fee, newest first among equal fees. Which call is right?

Practice

Python exercise

Isolate Q4 2025

Using orders (with order_date already parsed): set q4 to the orders placed from 1 October to 31 December 2025 that were not cancelled. Set coupon_share to the share of q4 orders that used a coupon, rounded to 3 decimals. Set marketplace_coupons to the number of orders in the whole table that came through the "marketplace" channel and used a coupon. Set latest_ids to a list of the order_ids of the 3 most recent orders in q4, newest first.

Data files: orders.csv

import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])

# 1. Orders placed in Q4 2025 (October to December) that were NOT cancelled
q4 = None

# 2. Share of those Q4 orders that used any coupon, rounded to 3 decimals
coupon_share = None

# 3. In the whole table: number of marketplace orders that used a coupon
marketplace_coupons = None

# 4. order_id of the 3 most recent orders in q4, newest first, as a list
latest_ids = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • q4 has 642 orders
  • q4 has no cancelled orders and nothing before October 2025
  • coupon_share is 0.231
  • 44 marketplace orders used a coupon
  • latest_ids is [2017, 2735, 1550]