Lesson 25 · Pandas Full Course
Method Chaining in Pandas: Write Clean, Readable Data Pipelines | Pandas Course #25
In this lesson you'll be able to: Turn a pile of temporary variables into one readable chain Filter and sort mid-chain with query and loc with a lambda Add…
- CoursePandas Full Course
- Lesson25 of 65
- FormatJupyter notebook · 37 code cells
- Data1 dataset
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.
- titanic.csv preview58.9 KB
📓 Full notebook
Download .ipynbPandas 25: Chaining Operations and Method Pipelines#
In this lesson you'll be able to:
- Turn a pile of temporary variables into one readable chain
- Filter and sort mid-chain with query and loc with a lambda
- Add columns mid-chain with assign and lambda
- Group, reshape and plot at the end of a pipeline
- Plug in your own steps with pipe, and debug a chain Dataset: the Titanic passenger list (data/pandas/titanic.csv)
Warm-up: vectorised flags from lesson 24#
Lesson 24 built columns with vectorised maths. One quick recall.
import pandas as pd
import numpy as np
df = pd.read_csv("data/pandas/titanic.csv")
print(df.shape)
df[["Survived", "Pclass", "Sex", "Age", "Fare"]].head(3)
df["child"] = np.where(df["Age"] < 16, "child", "adult")
df.groupby("child")["Survived"].mean().round(2)
From temporary variables to one chain#
Problem: many steps leave a trail of half-finished variables.
step1 = df[df["Embarked"] == "S"]
step2 = step1[step1["Age"] >= 18]
step3 = step2.groupby("Pclass")["Age"].mean()
step4 = step3.sort_values(ascending=False)
step4.round(1)
(
df
.query("Embarked == 'S' and Age >= 18")
.groupby("Pclass")["Age"]
.mean()
.sort_values(ascending=False)
.round(1)
)
- Each line is one step, read top to bottom
- Every method returns a new object, so the next one can use it
- No temporary names, and the original df is untouched
Common mistake: a multi-line chain without the outer brackets
result = df
.query("Age > 60")
result = (df.query("Age > 60")
.sort_values("Age", ascending=False))
result[["Name", "Age", "Survived"]].head(3)
Your turn#
Write one chain: the median fare paid by women in first class.
fare = (df
.query("Sex == 'female' and Pclass == 1")
["Fare"]
.median())
print(round(fare, 2))
Filter and sort inside a chain#
Problem: pick rows mid-chain without breaking the flow.
min_fare = 100
(df
.query("Fare > @min_fare and Pclass == 1")
.sort_values("Fare", ascending=False)
[["Name", "Fare", "Survived"]]
.head(3))
(df
.loc[lambda d: d["Cabin"].notna()]
.groupby("Pclass")
.size())
(df
.query("Embarked in ['C', 'Q']")
.groupby("Embarked")["Survived"]
.mean()
.round(2))
Quick check: what share of third class passengers have a recorded cabin?
share = (df
.query("Pclass == 3")
["Cabin"]
.notna()
.mean())
print(round(share, 3))
(df
.sort_values(["Pclass", "Age"])
.groupby("Pclass")
.head(1)
.reset_index(drop=True)
[["Pclass", "Name", "Age", "Survived"]])
Add columns mid-chain with assign#
Problem: create new columns without leaving the chain.
titles = (df
.assign(Title=lambda d: d["Name"].str.extract(
r",\s*([^.]+)\.", expand=False))
["Title"]
.value_counts()
.head(5))
titles
(df
.assign(family=lambda d: d["SibSp"] + d["Parch"] + 1,
alone=lambda d: d["family"] == 1)
.groupby("alone")["Survived"]
.mean()
.round(2))
(df
.assign(Age=lambda d: d["Age"].fillna(d["Age"].median()))
["Age"]
.describe()
.round(1))
print(df["Age"].isna().sum())
Common mistake: using df instead of a lambda inside assign
(df.assign(family=lambda d: d["SibSp"] + d["Parch"] + 1)
.assign(alone=df["family"] == 1))
n_alone = (df.assign(family=lambda d: d["SibSp"] + d["Parch"] + 1)
.assign(alone=lambda d: d["family"] == 1)
["alone"].sum())
print(n_alone)
Your turn#
In one chain: add a fare per person column, then show the 3 highest.
(df
.assign(family=lambda d: d["SibSp"] + d["Parch"] + 1,
per_person=lambda d: d["Fare"] / d["family"])
.nlargest(3, "per_person")
[["Name", "Fare", "family", "per_person"]]
.round(2))
Group, reshape and plot at the end of a chain#
Problem: go from raw rows to a finished summary in one flow.
summary = (df
.dropna(subset=["Embarked"])
.groupby(["Embarked", "Pclass"])
.agg(rate=("Survived", "mean"),
people=("Survived", "size"))
.reset_index()
.sort_values("rate", ascending=False))
summary.head().round(2)
(df
.merge(summary, on=["Embarked", "Pclass"], how="left")
[["Embarked", "Pclass", "Survived", "rate"]]
.head(3)
.round(2))
(df
.dropna(subset=["Embarked"])
.pivot_table(index="Pclass", columns="Embarked",
values="Fare", aggfunc="median")
.round(2))
import matplotlib.pyplot as plt
ax = (df
.groupby(["Pclass", "Sex"])["Survived"]
.mean()
.unstack()
.plot(kind="bar", rot=0, figsize=(7, 4)))
ax.set_title("Titanic survival rate by class and sex")
ax.set_xlabel("Passenger class")
ax.set_ylabel("Share who survived")
plt.show()
Your turn#
One chain: median age by port, without missing ports, oldest first.
(df
.dropna(subset=["Embarked"])
.groupby("Embarked")["Age"]
.median()
.sort_values(ascending=False))
Custom steps with pipe, and debugging a chain#
Problem: reuse your own functions and see inside a long chain.
def add_age_band(d):
bins = [0, 12, 18, 35, 60, 100]
names = ["child", "teen", "young", "adult", "senior"]
band = pd.cut(d["Age"], bins=bins, labels=names)
return d.assign(age_band=band)
(df.pipe(add_age_band)["age_band"]
.value_counts(sort=False))
def survival_by(d, col):
return d.groupby(col, observed=True)["Survived"].mean()
(df
.pipe(add_age_band)
.pipe(survival_by, col="age_band")
.round(2))
def peek(d, label):
print(label, d.shape)
return d
rate = (df
.pipe(peek, "start")
.dropna(subset=["Age"])
.pipe(peek, "with age")
.query("Sex == 'female'")
.pipe(peek, "women")["Survived"].mean())
print(round(rate, 2))
(df
.pipe(lambda d: d[d["Fare"] > d["Fare"].median()])
["Pclass"]
.value_counts())
Common mistake: inplace=True in the middle of a chain
(df.copy()
.dropna(subset=["Embarked"], inplace=True)
.groupby("Embarked").size())
(df
.dropna(subset=["Embarked"])
.groupby("Embarked").size())
Quick check: which step removed the most rows?
print(df["Age"].isna().sum(), (df["Sex"] == "male").sum())
Mini-project: did a pricier ticket buy survival?#
Plan: band the fares, rate survival by band and sex, check class, conclude.
bands = ["Low", "Mid", "High", "Top"]
fares = (df
.assign(band=lambda d: pd.qcut(d["Fare"], 4, labels=bands))
.groupby(["band", "Sex"], observed=True)["Survived"]
.mean()
.unstack()
.round(2))
fares
(df
.assign(band=lambda d: pd.qcut(d["Fare"], 4, labels=bands))
["band"]
.value_counts(sort=False))
(df
.assign(band=lambda d: pd.qcut(d["Fare"], 4, labels=bands))
.groupby("band", observed=True)["Pclass"]
.value_counts()
.unstack(fill_value=0))
(df
.query("Pclass == 3")
.assign(band=lambda d: pd.qcut(d["Fare"], 2,
labels=["cheaper", "dearer"]))
.groupby(["band", "Sex"], observed=True)["Survived"]
.mean().unstack().round(2))
ax = fares.plot(kind="bar", rot=0, figsize=(7, 4))
ax.set_title("Survival rate by fare band and sex")
ax.set_xlabel("Fare band (quarters of passengers)")
ax.set_ylabel("Share who survived")
plt.show()
low, top = fares.loc["Low", "male"], fares.loc["Top", "male"]
print(f"Men: {low:.0%} in the Low band, {top:.0%} in the Top band")
Recap#
- Wrap a chain in brackets and put one step on each line
- query and loc with a lambda filter rows mid-chain
- assign adds columns; use a lambda to see columns made earlier
- groupby, agg, pivot_table and plot can end a pipeline
- pipe plugs in your own functions, including a peek for debugging
Challenges#
- In one chain, find the survival rate by title for the top 5 titles.
- Write a pipe function that fills missing ages with the class median.
- Chain query, assign and plot to chart average fare by family size.
What's next#
Lesson 26: Hands-on - Data Transformation Challenge Put map, apply, lambdas, vectorised maths and chains to work together.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



