Skills101

Python for Data Science & Machine Learning · Lesson 7 of 12 · Updated ·

Statistics for Data Science — Averages, Spread, Distributions, Correlation and A/B Tests

You don't need a statistics degree for data work — but you do need these ideas to avoid embarrassing (and expensive) wrong conclusions.

1. Mean vs median: the salary trap

import numpy as np

packages = np.array([3.5, 3.6, 3.6, 4.0, 4.2, 4.5, 5.0, 6.0, 7.5, 45.0])   # LPA, one star offer
print("mean  ", packages.mean().round(2))
print("median", np.median(packages))
print("mean without the 45 LPA offer", packages[:-1].mean().round(2))
mean   8.69
median 4.35
mean without the 45 LPA offer 4.66

One 45 LPA offer pulls the mean up a lot; the median barely moves. When a coaching ad says "average package 8.69 LPA", ask for the median (here 4.35) — most students got far less. Use the median for skewed data (salaries, house prices, delivery times).

2. Spread: standard deviation and percentiles

import numpy as np

batch_a = np.array([68, 70, 71, 72, 69, 70, 71])
batch_b = np.array([40, 95, 60, 88, 52, 79, 76])
print("means:", batch_a.mean().round(1), batch_b.mean().round(1))
print("std:  ", batch_a.std().round(1), batch_b.std().round(1))
print("90th percentile of B:", np.percentile(batch_b, 90).round(1))
means: 70.1 70.0
std:   1.2 18.5
90th percentile of B: 90.8

Same average, completely different batches: standard deviation measures how spread out values are. A percentile says "90% of values are below this" — that's how exam ranks like "98 percentile" work.

3. The normal distribution

Many measurements (heights, exam marks, measurement errors) form a bell curve. For a normal distribution about 68% of values lie within 1 standard deviation of the mean, 95% within 2, and 99.7% within 3:

import numpy as np

rng = np.random.default_rng(0)
marks = rng.normal(loc=60, scale=10, size=100_000)
for k in (1, 2, 3):
    inside = np.mean(np.abs(marks - 60) <= k * 10) * 100
    print(f"within {k} sd: {inside:.1f}%")
within 1 sd: 68.4%
within 2 sd: 95.5%
within 3 sd: 99.7%
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(20, 100, 400)
pdf = np.exp(-((x - 60) ** 2) / (2 * 10 ** 2)) / (10 * np.sqrt(2 * np.pi))

fig, ax = plt.subplots(figsize=(7, 3.4))
ax.plot(x, pdf, color="#0f1626")
for k, colour in ((3, "#dbeafe"), (2, "#93c5fd"), (1, "#2563eb")):
    band = (x >= 60 - k * 10) & (x <= 60 + k * 10)
    ax.fill_between(x[band], pdf[band], color=colour)
ax.set_title("Marks ~ normal(mean 60, sd 10): 68% within 50–70, 95% within 40–80")
ax.set_xlabel("Marks")
ax.set_yticks([])
ax.spines[["top", "right", "left"]].set_visible(False)
plt.savefig("normal-68-95.svg")

Bell curve of marks with mean 60 and standard deviation 10: the dark band from 50 to 70 holds 68 percent, the middle band from 40 to 80 holds 95 percent, the light band from 30 to 90 holds 99.7 percent

The simulation confirms the 68–95–99.7 rule. A z-score (x − mean) / sd tells how unusual a value is: z = 2.5 is rare.

4. Correlation is not causation

import numpy as np

rng = np.random.default_rng(2)
study_hours = rng.uniform(0, 6, 200)
marks = 40 + 8 * study_hours + rng.normal(0, 8, 200)
phone_hours = 8 - study_hours + rng.normal(0, 1, 200)
print("study vs marks:", np.corrcoef(study_hours, marks)[0, 1].round(2))
print("phone vs marks:", np.corrcoef(phone_hours, marks)[0, 1].round(2))
study vs marks: 0.85
phone vs marks: -0.74

Correlation runs from −1 (perfect opposite) through 0 (none) to +1 (perfect together). Here phone time is strongly negatively correlated with marks — but in our simulation phone time doesn't affect marks at all; both are driven by study hours (a confounder). Only a controlled experiment can show cause.

5. A/B testing: did the change work?

Case: an ed-tech app tests a new "Start free trial" button. Version A (old) and B (new) are each shown to 2,000 random visitors.

import numpy as np

rng = np.random.default_rng(11)
a = rng.random(2000) < 0.050          # old button: true rate 5.0%
b = rng.random(2000) < 0.065          # new button: true rate 6.5%
print(f"A: {a.mean():.2%}  B: {b.mean():.2%}")

# Permutation test: if the button made no difference, how often would random shuffling
# produce a gap at least this big?
observed = b.mean() - a.mean()
both = np.concatenate([a, b])
gaps = []
for _ in range(5000):
    rng.shuffle(both)
    gaps.append(both[2000:].mean() - both[:2000].mean())
p_value = np.mean(np.array(gaps) >= observed)
print(f"observed gap {observed:.2%}, p-value {p_value:.3f}")
A: 5.10%  B: 6.00%
observed gap 0.90%, p-value 0.116

The p-value is the chance of seeing a gap this large if the button changed nothing. Below 0.05 we usually call the result statistically significant.

Look closely at this run: we built B to be better (6.5% vs 5.0%), yet with 2,000 visitors each the observed gap is only 0.9 points and the p-value is about 0.12 — not significant. The data can't yet rule out luck. That's the real lesson of A/B testing: small true improvements need large samples. Decide the sample size before the test (from the smallest gain worth detecting), never stop early because the result "looks good", and also ask whether the gain is big enough to matter for the business.

Common mistakes

  • Reporting the mean for skewed data.
  • Saying "X causes Y" from a correlation.
  • Small samples: 30 users can't settle an A/B test.
  • Running many tests and reporting only the one with p < 0.05.

Quick recap

  • Median for skewed data; standard deviation and percentiles for spread.
  • Normal distribution: 68–95–99.7 rule; z-scores measure unusualness.
  • Correlation (−1 to +1) ≠ causation; watch for confounders.
  • A/B tests: random split, fixed sample size, p-value < 0.05 plus a meaningful effect.

Practice what you learned

📝 Quiz: Statistics5 questions · instant answers & explanations · +10 XP · counts towards your free certificateTake the quiz →