Lesson 26 · Pandas Full Course
Pandas Data Transformation Challenge: Use Every Tool on One Dataset | Pandas Course #26
In this lesson you'll be able to: Inspect a new table before changing anything Build new columns with fast, vectorised maths Recode values with map,…
- CoursePandas Full Course
- Lesson26 of 65
- FormatJupyter notebook · 41 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.
📓 Full notebook
Download .ipynbPandas 26: Hands-on - Data Transformation Challenge#
In this lesson you'll be able to:
- Inspect a new table before changing anything
- Build new columns with fast, vectorised maths
- Recode values with map, replace, cut and apply
- Filter rows with masks, isin and query
- Chain every step into one readable pipeline Dataset: 244 restaurant bills and tips (data/pandas/tips.csv)
Warm-up: a quick pipeline#
Lesson 25 chained methods into pipelines. One more, on new data.
import pandas as pd
df = pd.read_csv("data/pandas/tips.csv")
print(df.shape)
df.head()
(df
.query("time == 'Lunch'")
.sort_values("tip", ascending=False)
.head(3))
Inspect before you transform#
Problem: know the types and quirks of a table before changing it.
df.info()
df.describe().round(2)
df.value_counts(["day", "time"])
Quick check: what does df.size return: party sizes, or something else?
print(df.size)
print(df["size"].head(3).tolist())
print(df.duplicated().sum())
df[df.duplicated(keep=False)]
Build new columns with vectorised maths#
Problem: a 5 dollar tip means little until you know the bill.
pct = df["tip"] / df["total_bill"] * 100
df["tip_pct"] = pct.round(1)
df[["total_bill", "tip", "tip_pct"]].head()
df["per_person"] = (df["total_bill"] / df["size"]).round(2)
df[["total_bill", "size", "per_person"]].head(3)
df["tip_pct"].describe().round(1)
Common mistake: using a whole column in a plain
if
if df["tip_pct"] > 20:
print("generous")
df["generous"] = df["tip_pct"] > 20
print(df["generous"].sum())
print(round(df["generous"].mean(), 3))
Your turn#
Add tip_each: the tip divided by party size. Show the top 3.
df["tip_each"] = (df["tip"] / df["size"]).round(2)
df.nlargest(3, "tip_each")[["tip", "size", "tip_each"]]
Recode values with map, replace, cut and apply#
Problem: short codes and raw numbers are hard to read in a report.
print(df["day"].unique().tolist())
names = {"Thur": "Thursday", "Fri": "Friday",
"Sat": "Saturday", "Sun": "Sunday"}
df["day_name"] = df["day"].map(names)
print(df["day_name"].value_counts())
Quick check: what does map do with a code that's missing from the dictionary?
only_wknd = {"Sat": "Saturday", "Sun": "Sunday"}
print(df["day"].map(only_wknd).isna().sum())
labels = {"Yes": "Smoker", "No": "Non-smoker"}
df["smoker"] = df["smoker"].replace(labels)
df["smoker"].value_counts()
bands = [0, 10, 15, 20, 100]
band_names = ["Low", "Fair", "Good", "Generous"]
df["tip_band"] = pd.cut(df["tip_pct"], bins=bands,
labels=band_names)
df["tip_band"].value_counts(sort=False)
def party_type(n):
return "Group" if n >= 4 else "Small"
df["party"] = df["size"].apply(party_type)
df["party"].value_counts()
Common mistake: calling the function inside
apply
df["size"].apply(party_type())
df["size"].apply(party_type).head(3)
Your turn#
Label bills over 30 dollars as Big, the rest Regular. Count them.
import numpy as np
big = df["total_bill"] > 30
df["bill_size"] = np.where(big, "Big", "Regular")
df["bill_size"].value_counts()
Filter rows with masks, isin and query#
Problem: pull out exactly the tables a question is about.
cols = ["total_bill", "tip", "tip_pct", "day", "time"]
generous = df[df["tip_pct"] > 20]
print(len(generous))
generous[cols].head()
Common mistake: joining two conditions with
and
df[df["tip_pct"] > 20 and df["time"] == "Dinner"]
mask = (df["tip_pct"] > 20) & (df["time"] == "Dinner")
print(mask.sum())
df.query("tip_pct > 20 and time == 'Dinner'").shape
weekend = df[df["day"].isin(["Sat", "Sun"])]
weekday = df[~df["day"].isin(["Sat", "Sun"])]
print(len(weekend), len(weekday))
Quick check: do weekend tables tip a bigger share than weekday ones?
print(round(weekend["tip_pct"].mean(), 2))
print(round(weekday["tip_pct"].mean(), 2))
Your turn#
How many Saturday tables of 3 or more tipped under 10 percent?
sat = df.query("day == 'Sat' and size >= 3")
low = sat.query("tip_pct < 10")
print(len(low), len(sat))
Chain the steps into one pipeline#
Problem: loose lines are hard to rerun. A chain reads top to bottom.
raw = pd.read_csv("data/pandas/tips.csv")
print(raw.columns.tolist())
Common mistake: using a new column in the same
assignwithout a lambda
raw.assign(
tip_pct=raw["tip"] / raw["total_bill"] * 100,
high=raw["tip_pct"] > 20,
)
tidy = raw.assign(
tip_pct=lambda d: d["tip"] / d["total_bill"] * 100,
high=lambda d: d["tip_pct"] > 20,
)
tidy[["tip", "tip_pct", "high"]].head(3).round(1)
report = (
raw.assign(tip_pct=lambda d: d["tip"] / d["total_bill"] * 100)
.query("size >= 4")
.sort_values("tip_pct", ascending=False)
.loc[:, ["day", "size", "total_bill", "tip_pct"]]
.round(1)
)
report.head()
def add_tip_cols(d):
d = d.assign(tip_pct=d["tip"] / d["total_bill"] * 100)
return d.assign(tip_each=d["tip"] / d["size"])
raw.pipe(add_tip_cols).nlargest(3, "tip_each").round(2)
Your turn#
From raw: dinner only, add per_person, show the top 3 spenders.
top = (
raw.query("time == 'Dinner'")
.assign(per_person=lambda d: d["total_bill"] / d["size"])
.nlargest(3, "per_person")
)
top[["day", "size", "total_bill", "per_person"]].round(2)
Mini-project: are smokers more generous tippers?#
Plan: compare averages, check the extremes, then conclude.
is_smoker = df["smoker"] == "Smoker"
smoke = df.loc[is_smoker, "tip_pct"]
other = df.loc[~is_smoker, "tip_pct"]
print(round(smoke.mean(), 2), round(other.mean(), 2))
print(smoke.median(), other.median())
show = ["total_bill", "tip", "tip_pct", "smoker"]
df.nlargest(3, "tip_pct")[show]
print(round(smoke[smoke < 50].mean(), 2))
print(round(smoke.std(), 2), round(other.std(), 2))
spend = df.groupby("smoker")["per_person"].median()
spend
import matplotlib.pyplot as plt
ax = df.boxplot(column="tip_pct", by="smoker", figsize=(7, 4))
plt.suptitle("")
ax.set_title("Tip percentage: smokers vs non-smokers")
ax.set_xlabel("Table")
ax.set_ylabel("Tip as % of bill")
plt.show()
gap = smoke.median() - other.median()
print(f"Median gap: {gap:.1f} points")
Recap#
- Inspect types, ranges and duplicates before transforming
- Column maths is vectorised; never put a column in a plain if
- map, replace, cut and apply turn codes into readable labels
- Combine masks with & and |, or use query
- assign with lambdas and pipe build a rerunnable pipeline
Challenges#
- Is the lunch tip percentage higher than dinner? Use masks.
- Band
per_personinto Low, Mid and High with cut. - Write one chain: Saturday only, tip_pct, top 5 by tip_pct.
What's next#
Lesson 27: Grouping Data with groupby() and Aggregations Summarise every group in one line, on the Titanic passenger list.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



