Scatter Plots
A scatter plot shows the relationship between two variables, one point per observation, and it is the workhorse for correlation, clustering, and outlier detection. Matplotlib's scatter is more expressive than plot with markers: point size and color can each encode a third and fourth variable, with legends for both, in a single call.
Basic Scatter
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
x = rng.normal(size=300)
y = 1.5 * x + rng.normal(scale=0.5, size=300)
fig, ax = plt.subplots()
ax.scatter(x, y, alpha=0.6)
ax.set_xlabel("x")
ax.set_ylabel("y")
plt.show()
With 300+ points, alpha below 1 is almost always right: overlapping points darken into density, which is itself information. For very large datasets (100k+), ax.plot(x, y, "o") or the fastpath scatter is faster and looks identical.
Encoding Size and Color
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(1)
x = rng.normal(size=400)
y = rng.normal(size=400) + 0.5 * x
size_var = rng.gamma(2.0, 20, size=400) # e.g. revenue
color_var = rng.normal(size=400) # e.g. margin
fig, ax = plt.subplots()
sc = ax.scatter(x, y,
s=size_var / 4, # point area ~ size variable
c=color_var,
cmap="RdYlBu",
alpha=0.7,
edgecolors="none")
fig.colorbar(sc, ax=ax, label="margin") # color legend
plt.show()
Two data dimensions now live in the marks themselves: area in s, color in c plus a colormap. fig.colorbar attaches the color scale and its legend. Use size sparingly - it is harder to read than color - and prefer a diverging colormap (RdYlBu) for a variable centered at zero and a sequential one (viridis) for magnitude.
Categorical Groups
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(2)
groups = {}
for name, mu in [("A", -1.5), ("B", 0.0), ("C", 1.5)]:
groups[name] = rng.normal(loc=mu, scale=0.6, size=150)
fig, ax = plt.subplots()
for i, (name, xs) in enumerate(groups.items()):
ys = rng.normal(loc=xs, scale=0.4)
ax.scatter(xs, ys, alpha=0.6, label=name,
color=plt.rcParams["axes.prop_cycle"].by_key()["color"][i])
ax.legend()
plt.show()
One call per category keeps the legend entries correct and the colors stable from the palette. This is the standard "three populations on one plane" view.
Practical Tips
- Set equal aspect only when the axes share units and geometry matters:
ax.set_aspect("equal"). - Log scale for multiplicative relationships:
ax.set_xscale("log"). - Outliers: clip or move them to the axis edge with an arrow instead of letting one extreme value squash the rest - or plot the bulk and report the tail in text.
- Trend overlay: fit a line (or lowess if you have statsmodels) and draw it behind the points with
zorder=1.
Summary
Use ax.scatter with alpha for any cloud of points, encode a third variable in size and a fourth in color with a colorbar, and loop per category for group labels. Keep aspect and scales honest, tame outliers deliberately, and overlay a fitted trend so the relationship is readable at a glance.