Skillversitas

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.