Exercises — Week 4 — Charts That Change a Decision¶
Helen's board meeting is Monday. She asked for one screen, not a notebook. Rebuild the deck's plan-churn bar honestly, and give her the adoption and region cuts she'll ask about when someone pushes back.
What you are building¶
Three charts: adoption by plan, churn by region, and an honest plan-churn bar whose title is a claim and whose y-axis starts at 0.
Predict before you run¶
- Which plan adopts the most features?
- If the y-axis starts at 14% instead of 0, what lie does the screenshot tell?
- Should region churn be computed on events or on users?
Before you start¶
- Use the
Aggbackend (matplotlib.use("Agg")) in a terminal so matplotlib writes PNGs instead of blocking on a window.
Each task has three hints, closed by default. Open only as far as you need.
Task¶
Work in starter.py. Run from the repo root:
1. Adoption curve. For users with a signup_date, plot average features_adopted by plan_type as a bar. Annotate the winner.
Hint 1 — a nudge
A chart is an API response. If Helen has to squint to find the winner, the response is incomplete — put the number on the bar.
Hint 2 — the approach
load_customer_360() already has features_adopted and signup_date. groupby("plan_type")["features_adopted"].mean(), ax.bar, then ax.annotate the idxmax() with its value.
Hint 3 — most of the code
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
from lib.course_data import find_data_dir, load_customer_360
DATA = find_data_dir()
df = load_customer_360(DATA)
adopt = df[df["signup_date"].notna()].groupby("plan_type")["features_adopted"].mean().sort_values()
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.bar(adopt.index, adopt.values, color="#6366f1")
top = adopt.idxmax()
ax.annotate(f"{adopt[top]:.2f}", (top, adopt[top]), ha="center", va="bottom")
ax.set_title("Average features adopted by plan") # rewrite as a claim
fig.savefig("adoption_by_plan.png", dpi=120)
2. Region bars. Most-common region per user from events, then churn rate by region. Horizontal bars, sorted.
Hint 1 — a nudge
Should a region with chatty users count more than a region with quiet ones? Decide what one "vote" is before you group.
Hint 2 — the approach
Same collapse as Week 2: drop null regions, one mode per user_id, left-join onto the 360. Only then groupby("region")["is_churned"].mean(), sort_values(), ax.barh.
Hint 3 — most of the code
events = pd.read_csv(DATA / "user_events.csv", usecols=["user_id", "region"]).dropna()
region = events.groupby("user_id")["region"].agg(lambda s: s.mode().iat[0]).rename("region")
by_user = df.merge(region, on="user_id", how="left")
churn = by_user.groupby(by_user["region"].fillna("unknown"))["is_churned"].mean().sort_values()
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.barh(churn.index, churn.values, color="#0f766e")
ax.set_xlabel("churn rate (users)")
fig.savefig("churn_by_region.png", dpi=120)
3. Honest title. Rebuild the plan-churn bar so the title is a claim ("Free churn is ~2× paid") and the y-axis starts at 0.
Hint 1 — a nudge
Screenshot the chart and cover the axis labels. What would a board member conclude from the bar heights alone?
Hint 2 — the approach
Compute churn by plan, ax.bar, then ax.set_ylim(0, ...) explicitly — matplotlib autoscaling is not a promise. The title is a sentence someone could disagree with.
Hint 3 — most of the code
plan_churn = df.groupby("plan_type")["is_churned"].mean().sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.bar(plan_churn.index, plan_churn.values, color="#b91c1c")
ax.set_ylim(0, plan_churn.max() * 1.15)
ax.set_ylabel("churn rate")
ax.set_title("TODO: one sentence the numbers support")
fig.savefig("plan_churn_honest.png", dpi=120)
Success criteria¶
- Winner annotated on the adoption bars.
- Region chart is user-grain.
- Honest chart: title is a sentence, ylim starts at 0.
After you run¶
A chart is an API response. If the PM can misquote the title, rewrite the title. This is the screen Helen actually shows the board.