๐ฏ The project: "How many ice creams will we sell tomorrow?"ยถ
A small shop wants to staff and stock correctly. We'll go end-to-end through the lifecycle:
| Stage | What we do | Output |
|---|---|---|
| 1 ยท Frame | Turn the business need into a data question + metric | a clear goal |
| 2 ยท Collect | Gather the raw data | a raw dataset |
| 3 ยท Clean | Fix duplicates, missing values, outliers | a tidy dataset |
| 4 ยท Explore | Look for the pattern (EDA) | insight + correlation |
| 5 ยท Model | Learn the temperatureโsales relationship | a trained model |
| 6 ยท Evaluate & Deploy | Test it, then predict & decide | a usable forecast |
โ๏ธ Setup, imports & the book's plotting styleยถ
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(3)
NAVY="#0a1230"; INK="#1a2138"; INK_SOFT="#4a5578"
CYAN="#0891b2"; PURPLE="#7c3aed"; AMBER="#d97706"
GREEN="#059669"; PINK="#db2777"; BLUE="#2563eb"
GRID="#e6e9f2"
plt.rcParams.update({
"figure.facecolor":"white","axes.facecolor":"white","figure.dpi":110,"font.size":11,
"axes.edgecolor":GRID,"axes.linewidth":1.2,"axes.grid":True,"grid.color":GRID,"axes.axisbelow":True,
"axes.spines.top":False,"axes.spines.right":False,
"axes.titlesize":15,"axes.titleweight":"bold","axes.titlecolor":INK,
"axes.labelcolor":INK_SOFT,"axes.labelsize":11.5,"xtick.color":INK_SOFT,"ytick.color":INK_SOFT,"legend.frameon":False,
})
def titlecard(ax, title, subtitle=None):
ax.set_title(title, loc="left", pad=18)
if subtitle:
ax.text(0, 1.02, subtitle, transform=ax.transAxes, fontsize=10.5, color=INK_SOFT, va="bottom")
print("โ
Environment ready.")
โ Environment ready.
QUESTION = "Can we predict daily ice-cream sales from the day's temperature?"
TARGET = "sales" # what we want to predict (the label)
FEATURE = "temperature" # what we predict it from
METRIC = "RMSE (typical error in number of ice creams) โ lower is better"
print("QUESTION :", QUESTION)
print("PREDICT :", TARGET, " FROM:", FEATURE)
print("SUCCESS :", METRIC)
QUESTION : Can we predict daily ice-cream sales from the day's temperature? PREDICT : sales FROM: temperature SUCCESS : RMSE (typical error in number of ice creams) โ lower is better
A good question is specific and measurable. "Sell more" is a wish; "predict daily sales from temperature, measured by RMSE" is something data can actually answer.
# 14 days of records (temperature in ยฐC, ice creams sold). Real data is rarely clean:
raw = pd.DataFrame({
"temperature": [18, 20, 21, 23, 25, 26, 28, 30, 31, 33, 25, 27, 22, 24],
"sales": [22, 28, 30, 35, 40, 44, 52, 60, 5, 70, 40, None, 26, 34],
})
print(f"Collected {len(raw)} rows.")
raw
Collected 14 rows.
| temperature | sales | |
|---|---|---|
| 0 | 18 | 22.0 |
| 1 | 20 | 28.0 |
| 2 | 21 | 30.0 |
| 3 | 23 | 35.0 |
| 4 | 25 | 40.0 |
| 5 | 26 | 44.0 |
| 6 | 28 | 52.0 |
| 7 | 30 | 60.0 |
| 8 | 31 | 5.0 |
| 9 | 33 | 70.0 |
| 10 | 25 | 40.0 |
| 11 | 27 | NaN |
| 12 | 22 | 26.0 |
| 13 | 24 | 34.0 |
Notice three real-world problems hiding in here: a duplicate day, a missing sales value (NaN), and an outlier (a hot day with only 5 sales, almost certainly a typo).
data = raw.copy()
before = len(data)
# (a) DUPLICATES โ drop exact repeat rows
data = data.drop_duplicates().reset_index(drop=True)
print(f"Removed {before - len(data)} duplicate row(s).")
# (b) MISSING VALUES โ fill the gap with the average sales
fill_value = round(data["sales"].mean())
n_missing = int(data["sales"].isna().sum())
data["sales"] = data["sales"].fillna(fill_value)
print(f"Filled {n_missing} missing sales value(s) with the mean ({fill_value}).")
# (c) OUTLIERS โ a hot day with almost no sales is a data-entry error; drop it
outliers = data[(data["temperature"] > 28) & (data["sales"] < 15)]
data = data.drop(outliers.index).reset_index(drop=True)
print(f"Removed {len(outliers)} outlier row(s).")
print(f"\nClean dataset: {len(data)} rows ready for analysis.")
Removed 1 duplicate row(s). Filled 1 missing sales value(s) with the mean (37). Removed 1 outlier row(s). Clean dataset: 12 rows ready for analysis.
Each fix matters: duplicates double-count, missing values break math, and outliers distort the pattern. Garbage in โ garbage out.
print(data[["temperature","sales"]].describe().round(1))
corr = data["temperature"].corr(data["sales"])
print(f"\nCorrelation(temperature, sales) = {corr:.2f} -> strong, positive")
temperature sales count 12.0 12.0 mean 24.8 39.8 std 4.3 14.4 min 18.0 22.0 25% 21.8 29.5 50% 24.5 36.0 75% 27.2 46.0 max 33.0 70.0 Correlation(temperature, sales) = 0.96 -> strong, positive
fig, ax = plt.subplots(figsize=(9,4.8))
ax.scatter(data["temperature"], data["sales"], color=GREEN, s=70, alpha=0.8, edgecolor="white", linewidth=1)
titlecard(ax, "Sales rise with Temperature", f"Exploratory scatter ยท correlation = {corr:.2f}")
ax.set_xlabel("Temperature (ยฐC)"); ax.set_ylabel("Ice creams sold")
plt.tight_layout(); plt.show()
EDA confirms a clear, strong upward relationship, warm days sell more. That pattern is what the model will learn.
# Split into TRAIN (learn from) and TEST (judge on) โ never evaluate on data you trained on
idx = rng.permutation(len(data))
test_idx = idx[:4]
train_idx = idx[4:]
train, test = data.iloc[train_idx], data.iloc[test_idx]
print(f"Train on {len(train)} days, test on {len(test)} held-out days.")
# "Train" the model = fit the best straight line on the TRAIN data only
slope, intercept = np.polyfit(train["temperature"], train["sales"], 1)
print(f"Learned model: sales = {intercept:.1f} + {slope:.1f} * temperature")
Train on 8 days, test on 4 held-out days. Learned model: sales = -34.3 + 3.0 * temperature
fig, ax = plt.subplots(figsize=(9,4.8))
ax.scatter(train["temperature"], train["sales"], color=CYAN, s=70, alpha=0.85, edgecolor="white", label="Training days")
xs = np.linspace(16, 35, 100)
ax.plot(xs, slope*xs+intercept, color=PURPLE, lw=2.6, label="Learned model")
titlecard(ax, "The Model Learns the Trend", "A straight line fit to the training data")
ax.set_xlabel("Temperature (ยฐC)"); ax.set_ylabel("Ice creams sold"); ax.legend(loc="upper left")
plt.tight_layout(); plt.show()
# Evaluate on the TEST days the model never saw
pred_test = slope*test["temperature"] + intercept
rmse = np.sqrt(np.mean((test["sales"].values - pred_test.values)**2))
ss_res = np.sum((test["sales"].values - pred_test.values)**2)
ss_tot = np.sum((test["sales"].values - test["sales"].mean())**2)
r2 = 1 - ss_res/ss_tot
print(f"On unseen test days: RMSE = {rmse:.1f} ice creams, Rยฒ = {r2:.2f}")
# DEPLOY = wrap the model in a simple tool anyone can use
def predict_sales(temp_c):
return max(0, round(slope*temp_c + intercept))
tomorrow = 29
print(f"\n๐ฆ Forecast: tomorrow is {tomorrow}ยฐC -> stock for about {predict_sales(tomorrow)} ice creams.")
On unseen test days: RMSE = 4.1 ice creams, Rยฒ = 0.90 ๐ฆ Forecast: tomorrow is 29ยฐC -> stock for about 53 ice creams.
# Communicate: show predictions vs. reality on the test days
fig, ax = plt.subplots(figsize=(9,4.8))
ax.scatter(train["temperature"], train["sales"], color="#c7c9d9", s=45, alpha=0.7, edgecolor="white", label="train")
ax.scatter(test["temperature"], test["sales"], color=PINK, s=90, edgecolor="white", linewidth=1, zorder=5, label="actual (test)")
ax.plot(xs, slope*xs+intercept, color=PURPLE, lw=2.4, label="model")
ax.scatter([tomorrow],[predict_sales(tomorrow)], color=AMBER, s=200, marker="*", zorder=6, edgecolor="white", linewidth=1.2, label=f"forecast @ {tomorrow}ยฐC")
titlecard(ax, "Does the Forecast Hold Up?", f"Tested on unseen days ยท RMSE = {rmse:.1f}")
ax.set_xlabel("Temperature (ยฐC)"); ax.set_ylabel("Ice creams sold"); ax.legend(loc="upper left", fontsize=9)
plt.tight_layout(); plt.show()
And in production you'd monitor it: if a heatwave or a new flavor shifts behavior, the errors grow, a signal to loop back to Stage 2 with fresh data. The lifecycle is a circle, not a line.
- Frame โ a measurable question (predict sales from temperature, judged by RMSE).
- Collect โ a raw, messy 14-day dataset.
- Clean โ removed a duplicate, filled a missing value, dropped an outlier.
- Explore โ found a strong positive correlation (โ 0.98).
- Model โ fit a line on the training days only.
- Evaluate & Deploy โ scored it on unseen days, then shipped a
predict_sales()tool.