Module · Framing
Exploratory data analysis — look before you fit
Lesson 2 of 8 · 12 min
Every course says "explore your data first". Almost none say what you are looking for. So people produce a wall of histograms, learn nothing, and fit the model they were always going to fit.
There are four questions worth asking before a regression, and each has a consequence you can act on. That is the whole lesson: four questions, four findings, and what each one changes about the line we fit next.
| Question | What a bad answer costs you |
|---|---|
| What does the target look like? | You predict the mean of something that has no typical case |
| Does the obvious predictor move with it? | You spend the lesson fitting a line to a cloud |
| Where is the money going today? | You produce a recommendation nobody can compare to the status quo |
| What else is worth a look? | You go in with one channel when three were sitting there |
What does the target look like?
- What a bad answer costs you
- You predict the mean of something that has no typical case
Does the obvious predictor move with it?
- What a bad answer costs you
- You spend the lesson fitting a line to a cloud
Where is the money going today?
- What a bad answer costs you
- You produce a recommendation nobody can compare to the status quo
What else is worth a look?
- What a bad answer costs you
- You go in with one channel when three were sitting there
Sales are a single hump, so the mean is a fair thing to predict
First, always, and most often skipped. Everything a regression does is aimed at the target, so its shape sets what is possible.
This one is good news, and it is worth knowing why. The distribution is a single hump with only a mild lean. The skew is 0.41 — so the mean (14.0) and the median (12.9) sit close together. A regression predicts the mean, and here the mean is a fair description of a typical market.
That will not always be true, and you should notice when it is not. Revenue per customer, deal size, order value and time-to-close are all famously right-skewed. A handful of enormous values drag the mean above almost every actual case. A model aimed at the mean inherits that bias. When you meet one, the usual answer is to model the log of the target instead. Not needed here — and the reason we know that is this chart, not a rule of thumb.
The range still matters: 1.6 to 27.0 thousand units. Markets differ by more than a factor of ten, so any claim that a model is accurate has to be relative to that spread. Is it any good? is entirely about not being fooled here.
TV moves with sales at 0.78, and the cloud bends
TV takes most of the budget, so it is the first thing anyone reaches for. Before trusting it, plot it — a correlation is one number, and several very different shapes produce the same one.
It slopes up, and firmly: correlation 0.78 — the strongest single relationship in the file. A line through this is worth fitting, which is what The line does.
Two things to notice now and act on later. The cloud bends. Sales climb quickly across the low budgets and flatten out at the top. That is what diminishing returns looks like in a scatter plot. And it fans out: among the lowest third of TV budgets, sales vary by 2.68k units; among the highest third, 4.52k. Small budgets land close together, big budgets land anywhere. A straight line is still a good first answer. It will systematically miss at the extremes, and the residual plot in Is it any good? is where that becomes visible instead of hypothetical.
The bend has a name and a lesson of its own. Media does not keep paying at the same rate forever — the tenth $1,000 does less than the first. That is saturation, it is modelled with a curve rather than a line, and it belongs to Regression II. Notice it here; you do not have to fix it yet.
TV already takes 73% of the budget and newspaper takes 15%
Not a modelling question — a briefing question, and the one that keeps this work honest. Everything this path produces is a proposed change to an existing split, so know the split.
TV averages 147.0 per market against 23.3 for radio and 30.6 for newspaper — so TV carries 73% of the media budget and newspaper 15%.
Read those two lines together and a question appears before any model exists: newspaper is taking more money than radio. Keep it in mind for the next chart.
Newspaper correlates 0.23 with sales and still takes 15% of the money
Finally, a shortlist: how strongly does each channel's budget move with sales? This is a way to decide what to try. It is never a way to decide what works.
TV leads at 0.78, radio follows at 0.58, and newspaper trails at 0.23. Newspaper is the weakest of the three — and it is still positive, still visible, and still taking 15% of the budget. On this evidence a reasonable person would keep buying it.
And they would be wrong — but not for a reason this chart can show. Newspaper and radio budgets move together (correlation 0.35): markets that spend on one tend to spend on the other. So some of newspaper's 0.23 may be radio instead. It shows up in newspaper's column because the two travel together, and a single-variable correlation cannot separate them. But the next lesson but one can, and what happens to newspaper there is the most useful thing in this path.
These are budgets somebody chose, not an experiment
Every column in this file except sales is a budget — a number somebody chose. That is not the same as a measurement, and it changes what a model can honestly claim.
Budgets are not assigned at random. A market may have got a large TV budget for several reasons. It may have got one because it was already a strong market. Or because a competitor launched there. Or because a regional manager pushed for it. None of that is in the file. So a coefficient here describes markets that were spent on this way, not what would happen if you moved the money. That gap is the commonest overclaim in marketing analytics.
It does not make the model useless. It makes the honest verb associated with rather than caused until you have an experiment — a holdout region, a staggered launch, a geo test. Say the weaker sentence and you keep your credibility for the one place it matters.
Five findings to carry into the next lesson
- The target is well behaved, so the mean is a fair thing to predict.
- TV is the strongest single predictor, and it is where the line goes first.
- The TV cloud bends — a straight line will miss at the extremes, and Is it any good? will show where.
- Newspaper looks plausible and travels with radio. Suspend judgement until More than one thing matters.
- These are budgets, not experiments. Associated with, not caused by.
Four questions, four findings, and each one limits what the next lesson may claim.
Run it yourself
Every number and chart above came out of this notebook. Open it, swap tv_spend for radio_spend in question 2, and see what the same four questions say about a different channel.
content/notebooks/a1-regression/look-before-you-fit.ipynb
Loads the media-mix file, answers the four questions in order, and writes out the figures this lesson prints. Runs unchanged in Colab.
Show the code6 cells
try:
from _figkit import save_fig, record
except ImportError: # Colab — no repo, no problem
def save_fig(name, plot, **kw):
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()
def record(key, value):
print(f'{key} = {value}'); return valuefrom pathlib import Path
import pandas as pd
# Local checkout first, the published copy otherwise — so this cell works
# unchanged in Colab, where there is no repo. Searched upwards rather than
# written as ../../../ so it does not depend on where jupyter was started.
REL = 'public/datasets/a1-regression/advertising-media-mix.csv'
here = Path.cwd()
LOCAL = next((p / REL for p in [here, *here.parents] if (p / REL).exists()), None)
CSV = LOCAL or 'https://raw.githubusercontent.com/Dr-Shashank-S-Sharma/expedify-ai-courses/main/datasets/a1-regression/advertising-media-mix.csv'
df = pd.read_csv(CSV)
CHANNELS = ['tv_spend', 'radio_spend', 'newspaper_spend']
df.head()sales = df['sales']
k = lambda v: f'{v:,.1f}'
record('n_markets', len(df))
record('n_missing', int(df.isna().sum().sum()))
record('median_sales', k(sales.median()))
record('mean_sales', k(sales.mean()))
record('min_sales', k(sales.min()))
record('max_sales', k(sales.max()))
record('sales_skew', round(float(sales.skew()), 2))
def plot(ax):
ax.hist(sales, bins=18, edgecolor='none')
ax.axvline(sales.mean(), linestyle='--', linewidth=1.4, color='#8a8a94')
ax.annotate('mean', xy=(sales.mean(), 0), xytext=(4, 4),
textcoords='offset points', fontsize=9, color='#8a8a94')
ax.set_xlabel('Sales (thousands of units)')
ax.set_ylabel('Markets')
save_fig('sales-distribution', plot)record('corr_tv', round(float(df['tv_spend'].corr(sales)), 2))
record('corr_radio', round(float(df['radio_spend'].corr(sales)), 2))
record('corr_newspaper', round(float(df['newspaper_spend'].corr(sales)), 2))
# Does the cloud fan out, and which way? Claimed in prose often enough that it
# is worth measuring: sales spread within the lowest and highest third of TV
# budgets. (It grows with budget — the opposite of what a first draft of this
# lesson asserted.)
thirds = pd.qcut(df['tv_spend'], 3, labels=['low', 'mid', 'high'])
spread = sales.groupby(thirds, observed=True).std()
record('spread_low_tv', round(float(spread['low']), 2))
record('spread_high_tv', round(float(spread['high']), 2))
def plot(ax):
ax.scatter(df['tv_spend'], sales, s=22, alpha=0.75)
ax.set_xlabel('TV budget ($ thousands)')
ax.set_ylabel('Sales (thousands of units)')
save_fig('sales-vs-tv', plot)means = df[CHANNELS].mean()
record('mean_tv_spend', k(means['tv_spend']))
record('mean_radio_spend', k(means['radio_spend']))
record('mean_newspaper_spend', k(means['newspaper_spend']))
record('share_newspaper_budget', f"{100 * means['newspaper_spend'] / means.sum():.0f}%")
record('share_tv_budget', f"{100 * means['tv_spend'] / means.sum():.0f}%")
def plot(ax):
labels = ['TV', 'Radio', 'Newspaper']
ax.bar(labels, [means[c] for c in CHANNELS])
ax.set_ylabel('Average budget per market ($ thousands)')
save_fig('spend-by-channel', plot, figsize=(7, 3.6))corr = df[CHANNELS].corrwith(sales).sort_values()
# The load-bearing number in this notebook. Newspaper and radio budgets move
# together, which is why newspaper looks like it works — lesson 5 collects this.
record('corr_news_radio', round(float(df['newspaper_spend'].corr(df['radio_spend'])), 2))
def plot(ax):
labels = {'tv_spend': 'TV', 'radio_spend': 'Radio', 'newspaper_spend': 'Newspaper'}
ax.barh([labels[c] for c in corr.index], corr.values)
ax.set_xlabel('Correlation with sales')
ax.set_xlim(0, 1)
save_fig('what-correlates', plot, figsize=(7, 3.2))The same file the notebook reads. Budgets in $ thousands, sales in thousands of units.
Next: we fit the line. What least squares is minimising, why the intercept is called base sales, and what the slope on TV claims per $1,000.

