Scatter Plots
A scatter plot displays the relationship between two numeric variables by plotting one point per observation at coordinates (x, y). It is the fundamental tool of bivariate analysis and the visual companion to the correlation coefficient.
What a Scatter Plot Reveals
| Property | What to look for |
|---|---|
| Direction | Upward slope = positive; downward = negative |
| Form | Linear, curved (quadratic/exponential), or no pattern |
| Strength | Tightly packed around a line = strong; widely scattered = weak |
| Outliers | Isolated points far from the main cloud |
| Clusters | Distinct groups of points → possible subgroups |
| Heteroscedasticity | A fan/cone shape — variance changes across x (a regression assumption violation) |
The most important case is E. A perfect U-shaped (parabolic) relationship has a Pearson correlation of approximately zero, because r measures only linear association. The scatter plot shows the relationship immediately; the correlation coefficient hides it completely. This is precisely why you plot before you compute.
Which Variable Goes Where
| Axis | Variable |
|---|---|
| X-axis (horizontal) | Independent / explanatory / predictor variable |
| Y-axis (vertical) | Dependent / response / outcome variable |
Example: hours studied (x) vs marks scored (y) — study hours plausibly influence marks, not the reverse.
Python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(42)
hours = np.random.uniform(1, 12, 80)
marks = (5.5 * hours + 30 + np.random.normal(0, 6, 80)).clip(0, 100)
df = pd.DataFrame({"hours": hours, "marks": marks})
fig, ax = plt.subplots(figsize=(8, 5.5))
ax.scatter(df["hours"], df["marks"], color="#168B99", alpha=0.7,
s=60, edgecolor="white")
# Add the least-squares trend line
m, c = np.polyfit(df["hours"], df["marks"], 1)
x_line = np.linspace(df["hours"].min(), df["hours"].max(), 100)
ax.plot(x_line, m * x_line + c, color="#ef4444", linewidth=2,
label=f"y = {m:.2f}x + {c:.2f}")
r = df["hours"].corr(df["marks"])
ax.set_title(f"Study Hours vs Marks (r = {r:.3f})", fontsize=13, fontweight="bold")
ax.set_xlabel("Hours Studied per Week")
ax.set_ylabel("Marks Scored")
ax.legend()
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.show()
# The four canonical patterns, side by side
np.random.seed(1)
n = 120
x = np.random.uniform(0, 10, n)
patterns = {
"Strong Positive": 4 * x + np.random.normal(0, 2, n),
"Weak Positive": 2 * x + np.random.normal(0, 12, n),
"No Relationship": np.random.normal(20, 8, n),
"Non-linear (U)": (x - 5) ** 2 + np.random.normal(0, 2, n),
}
fig, axes = plt.subplots(1, 4, figsize=(18, 4))
for ax, (name, y) in zip(axes, patterns.items()):
ax.scatter(x, y, alpha=0.6, color="#168B99", s=35)
ax.set_title(f"{name}\nr = {np.corrcoef(x, y)[0, 1]:.3f}")
ax.set_xlabel("x")
plt.tight_layout()
plt.show()
# Note the U-shaped panel: r is near ZERO despite an obvious perfect relationship
# Adding more dimensions: colour (hue), size, and shape
tips = sns.load_dataset("tips")
fig, axes = plt.subplots(1, 3, figsize=(17, 4.5))
sns.scatterplot(data=tips, x="total_bill", y="tip", ax=axes[0], color="#168B99")
axes[0].set_title("Basic — 2 variables")
sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time",
style="smoker", ax=axes[1])
axes[1].set_title("4 variables via hue + style")
# BUBBLE CHART — size encodes a third numeric variable
sns.scatterplot(data=tips, x="total_bill", y="tip", size="size",
sizes=(20, 300), hue="day", alpha=0.6, ax=axes[2])
axes[2].set_title("Bubble chart — size = party size")
plt.tight_layout()
plt.show()
# Regression plot with a 95% confidence band
plt.figure(figsize=(8, 5))
sns.regplot(data=tips, x="total_bill", y="tip",
scatter_kws={"alpha": 0.5, "s": 45}, line_kws={"color": "#ef4444"})
plt.title("Tip vs Total Bill with Regression Line and 95% CI")
plt.show()
# Faceted: one scatter per category
sns.lmplot(data=tips, x="total_bill", y="tip", col="time", hue="smoker", height=4)
plt.show()
Handling Overplotting
With thousands of points, markers overlap and the plot becomes a solid blob. Fixes:
big = pd.DataFrame({
"x": np.random.normal(0, 1, 20000),
"y": np.random.normal(0, 1, 20000),
})
fig, axes = plt.subplots(1, 4, figsize=(19, 4))
axes[0].scatter(big["x"], big["y"], s=8)
axes[0].set_title("Overplotted — unreadable")
axes[1].scatter(big["x"], big["y"], s=8, alpha=0.03)
axes[1].set_title("Transparency (alpha)")
axes[2].hexbin(big["x"], big["y"], gridsize=40, cmap="viridis")
axes[2].set_title("Hexbin — binned density")
sample = big.sample(1500)
axes[3].scatter(sample["x"], sample["y"], s=12, alpha=0.5)
axes[3].set_title("Random subsample")
plt.tight_layout()
plt.show()
| Technique | How it helps |
|---|---|
| Transparency (alpha) | Dense regions appear darker |
| Smaller markers | Less overlap |
| Hexbin / 2-D histogram | Aggregates points into coloured bins |
| Contour / density plot | Shows density as level curves |
| Sampling | Plot a random subset |
| Jitter | Small random offsets for discrete/rounded values |
Scatter Plot Matrix (Pair Plot)
For k numeric variables, plot every pairwise scatter in a k×k grid, with distributions on the diagonal.
iris = sns.load_dataset("iris")
sns.pairplot(iris, hue="species", diag_kind="kde", height=2.2)
plt.suptitle("Scatter Plot Matrix — Iris Dataset", y=1.02)
plt.show()
# Instantly visible: petal_length and petal_width separate the three species cleanly.
# This single chart is why petal measurements dominate any Iris classifier (Unit 3).
Scatter Plots and Residual Analysis
A specialised scatter plot — residuals vs fitted values — is the standard diagnostic for regression (Unit 3):
from sklearn.linear_model import LinearRegression
X = tips[["total_bill"]]
y = tips["tip"]
model = LinearRegression().fit(X, y)
residuals = y - model.predict(X)
plt.figure(figsize=(8, 4.5))
plt.scatter(model.predict(X), residuals, alpha=0.6, color="#168B99")
plt.axhline(0, color="#ef4444", linestyle="--", linewidth=2)
plt.xlabel("Fitted values")
plt.ylabel("Residuals")
plt.title("Residual Plot — look for a random, patternless cloud")
plt.show()
How to read it:
| Residual pattern | Diagnosis |
|---|---|
| Random cloud around zero | Model assumptions satisfied ✓ |
| Curved pattern | Relationship is non-linear — add a polynomial term |
| Funnel/cone shape | Heteroscedasticity — try a log transform or weighted regression |
| Points far from zero | Outliers with high influence |
Chart Selection Recap
| Number of numeric variables | Chart |
|---|---|
| 1 | Histogram, box plot, density plot |
| 2 | Scatter plot |
| 3 | Bubble chart (size), 3-D scatter, scatter with hue |
| 4+ | Pair plot, correlation heatmap, PCA projection |
Scatter plots show apparent relationships. The final lesson of this unit provides the formal machinery for deciding whether an apparent pattern is real: hypothesis testing.