Exploratory data analysis and charts
Explore a year of coffee shop sales end to end: check data quality, spot outliers, make histograms, boxplots, scatter plots, a correlation heatmap and time charts, then write down what you found before modelling.
Exploratory data analysis (EDA) is what you do before any model: look at the data, check it for problems and find the patterns worth modelling. Good EDA prevents most bad models.
You will explore one year of daily sales for a coffee shop: how many cups were sold, the temperature, whether it was a weekend and whether a promotion ran. You will check quality, make six kinds of chart with matplotlib and seaborn, and write down what you found. Prerequisites: the previous lesson on pandas, and a Colab or Kaggle notebook.
Step 1: Create the practice data
The data below is made up by a program, so we know what pattern it hides, and every learner gets identical numbers (the random seed is fixed). The shop's average sales rise with temperature and on weekends and promotion days. Two problems are added on purpose: 8 missing temperatures and one wrong entry of 900 cups. Run this cell once. It saves sales.csv in your notebook.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 365
dates = pd.date_range("2025-11-01", periods=n, freq="D")
temp = 24 + 6 * np.sin((dates.day_of_year.to_numpy() - 80) / 365 * 2 * np.pi) + rng.normal(0, 2, n)
weekend = (dates.day_of_week >= 5).astype(int)
promo = rng.binomial(1, 0.2, n)
cups = 90 + 6 * (temp - 24) + 25 * weekend + 18 * promo + rng.normal(0, 10, n)
sales = pd.DataFrame({
"date": dates,
"temp_c": temp.round(1),
"is_weekend": weekend,
"promo": promo,
"cups_sold": cups.round().astype(int),
})
# Make it messy, like real data: 8 missing temperatures and one wrong entry
sales.loc[rng.choice(n, 8, replace=False), "temp_c"] = np.nan
sales.loc[100, "cups_sold"] = 900
sales.to_csv("sales.csv", index=False)
print("saved sales.csv with", len(sales), "rows")This is practice data, not real Brew & Bean data. You will use it again in the regression lesson.
Step 2: First look
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sales = pd.read_csv("sales.csv", parse_dates=["date"])
print(sales.shape)
print(sales.head())
print(sales.isna().sum())
print(sales.describe().round(1))Expected: 365 rows and 5 columns, temp_c has 8 missing values, and describe() shows cups_sold with a mean near 103 but a maximum of 900.
Read the describe() table slowly. The mean of 103 and median (50%) of 99 are close, but the max is 900 while 75% of days are 125 or less. One day is far away from the rest. That is an outlier. It could be a true busy day or a typing mistake; you must find out which.
Step 3: Check data quality
A quality checklist
- 1Missing values.
temp_chas 8 missing. Decide how to handle them: here we fill with the median, because only 8 of 365 are missing. - 2Outliers. Use the IQR rule: values below Q1 minus 1.5 times IQR, or above Q3 plus 1.5 times IQR, are flagged. IQR is Q3 minus Q1.
- 3Impossible values. A coffee shop selling 900 cups when the busiest normal day is about 185 is suspicious. In real work you would ask the owner or check the billing log. Here we remove it.
- 4Duplicates and types. Run
sales.duplicated().sum()and checksales.dtypes. Both are fine in this dataset.
q1, q3 = sales["cups_sold"].quantile([0.25, 0.75])
iqr = q3 - q1
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print("normal range:", low, "to", high)
print(sales[(sales["cups_sold"] < low) | (sales["cups_sold"] > high)])
clean = sales[sales["cups_sold"] < 500].copy()
clean["temp_c"] = clean["temp_c"].fillna(clean["temp_c"].median())
print(len(clean), clean.isna().sum().sum())The rule flags only the 900 row (the date shown is 2026-02-09). After cleaning you keep 364 rows and 0 missing values. We kept the original sales untouched and made clean.
Outliers are not always errors
Do not delete outliers just because they look odd. Remove a value only when you have a reason to believe it is an error. A real outlier (a festival day, a bulk order) may be exactly what your business cares about.
Step 4: One variable at a time
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].hist(clean["cups_sold"], bins=20, color="#4C78A8", edgecolor="white")
axes[0].set_title("Cups sold per day")
axes[0].set_xlabel("Cups")
axes[0].set_ylabel("Number of days")
sns.boxplot(data=clean, x="is_weekend", y="cups_sold", ax=axes[1])
axes[1].set_title("Cups sold: weekday (0) vs weekend (1)")
plt.tight_layout()
plt.show()The histogram shows the shape: where most days fall and how spread out they are. The boxplot shows the median (line), the middle 50% of days (box) and unusual days (dots). Weekend sales sit higher.
Use a histogram for the shape of one number. Use a boxplot to compare a number across groups. Use a bar chart for counts or averages of categories.
Step 5: Relationships between variables
sns.scatterplot(data=clean, x="temp_c", y="cups_sold", hue="promo", alpha=0.7)
plt.title("Hotter days sell more; promo days sell more at the same temperature")
plt.xlabel("Temperature (deg C)")
plt.ylabel("Cups sold")
plt.show()
print(clean.groupby("is_weekend")["cups_sold"].mean().round(1))
print(clean.groupby("promo")["cups_sold"].mean().round(1))The points rise from bottom-left to top-right: a positive relationship with temperature. Colouring by promo shows promo days sit higher. Average cups: weekday about 93.6, weekend about 118.9; no promo about 96.8, promo about 116.7.
cols = ["temp_c", "is_weekend", "promo", "cups_sold"]
corr = clean[cols].corr().round(2)
print(corr)
sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1)
plt.title("Correlation between columns")
plt.show()The correlation of temp_c with cups_sold is about 0.85, a strong positive link. is_weekend is about 0.35 and promo about 0.25. Correlation runs from -1 to +1; 0 means no straight-line relationship.
A classic trap
Correlation is not causation. Ice-cream sales and drowning deaths both rise in summer, but ice cream does not drown anyone: heat drives both. Here we built the data, so we know temperature really drives sales. In real data, always ask what else could explain the link.
Step 6: Patterns over time
monthly = clean.set_index("date")["cups_sold"].resample("MS").mean()
monthly.plot(kind="line", marker="o", figsize=(9, 3.5))
plt.title("Average cups per day, by month")
plt.ylabel("Cups")
plt.xlabel("Month")
plt.show()
clean["weekday"] = clean["date"].dt.day_name()
order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
clean.groupby("weekday")["cups_sold"].mean().reindex(order).plot(kind="bar", figsize=(8, 3.5))
plt.title("Average cups by day of week")
plt.ylabel("Cups")
plt.show()resample("MS") groups dates by month start. The line follows the seasons (temperature) and the bars show Saturday and Sunday above weekdays.
Step 7: Write down what you found
EDA ends with words, not just charts. A short written summary is what you hand to a manager or use to plan your model.
# Findings (written in a notebook text cell)
# 1. 365 rows; 8 missing temperatures filled with median; one 900-cup entry removed (billing error).
# 2. Sales swing with temperature: correlation about 0.85.
# 3. Weekends average about 25 cups more than weekdays; promo days about 20 more.
# 4. Next step: build a regression model using temp_c, is_weekend and promo.Always record what you removed or changed and why, so someone can repeat or challenge your work.
Choosing the right chart
Chart chooser
| Question | Chart | Code |
|---|---|---|
| How is one number spread? | Histogram | plt.hist(x) or sns.histplot |
| Compare a number across groups | Boxplot | sns.boxplot(x=, y=) |
| Do two numbers move together? | Scatter plot | sns.scatterplot(x=, y=) |
| Compare category totals or averages | Bar chart | plot(kind="bar") |
| How does a number change over time? | Line chart | plot(kind="line") |
| Which columns relate to each other? | Heatmap of correlations | sns.heatmap(corr) |
Always give a chart a title, labelled axes and units. A chart that needs explanation in a meeting is a chart that is not finished.
I explored a dataset in pandas. Here is what I found:
[paste describe() output and your findings]
Act as a senior data scientist. Tell me: (1) what I may have missed in data quality, (2) which charts I should add, (3) any claims I made that could be wrong because of correlation vs causation, and (4) what I should check before building a model. Keep it to a short list.What to expect: Good feedback names specific checks (e.g. duplicates, units, time order). Verify each suggestion on your data before accepting it.
Try it: Explore it yourself
Answer with code and a chart each: (1) Is the effect of a promotion bigger on weekends than on weekdays? (hint: group by two columns) (2) How many days had a temperature above 30 and what was the average cups sold on those days? (3) Remove the outlier check: what happens to the correlation of temperature and cups if you keep the 900 row?
Key takeaways
- EDA comes before modelling: check shape, types, missing values, duplicates and outliers first.
- Use
describe(), histograms and boxplots for single columns; scatter plots, group averages and a correlation heatmap for relationships. - Remove or fix an outlier only with a reason, and record what you changed.
- Correlation shows that two things move together, not that one causes the other.
- End EDA with a written summary that states the next step.
Check yourself
Finishing the quiz marks the lesson as done.
1. In describe(), the mean is 103, the median 99 and the max 900. What does this suggest?
2. You want to compare how sales are spread on weekdays versus weekends. Which chart fits best?
3. Two columns have a correlation of 0.85. What can you safely conclude?
4. Why create clean as a copy instead of editing sales directly?
5. The IQR rule flags a value. What should you do next?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.