This page is a self-testing ladder. Each problem states cleanly what you must do; the solution hides inside a collapsible callout so you can try first, then reveal. We climb five rungs:
Everything here builds on the parent topic. Where data comes from, we lean on NumPy arrays and Pandas DataFrames.
Before we start, the two words the whole chapter hinges on:
This is the object-oriented (OO) interface.
Why: we explicitly hold handles fig and ax, and we call methods on the ax object (ax.plot). The pyplot state-machine version would be plt.plot(x, y) with no ax — it draws on a hidden "current Axes". Rule of thumb: if you see ax.something(...), it's OO.
Recall Solution L1.2
(a) ==ax.plot — a line for a continuous quantity over ordered epochs.
(b) ax.hist — bins one variable to show its distribution.
(c) ax.bar — one bar per category (model name).
(d) ax.scatter== — one dot per (feature1, feature2) pair reveals correlation/clusters.
Recall Solution L1.3
It returns a Matplotlib Axes object. This matters because every Seaborn plot is just Matplotlib underneath, so you can keep customising: ax.set_xlabel("Value"), ax.set_title(...), etc. Seaborn is a layer, not a replacement.
import numpy as npimport matplotlib.pyplot as pltfig, ax = plt.subplots(figsize=(8, 6))x = np.linspace(0, 2*np.pi, 100) # 100 evenly spaced pointsy = np.sin(x)ax.plot(x, y, label='sin(x)', color='#3b82f6', linewidth=2)ax.set_xlabel('Radians')ax.set_ylabel('Amplitude')ax.set_title('Sine Wave')ax.legend(loc='upper right')ax.grid(True, alpha=0.3)plt.tight_layout()
Why np.linspace(0, 2*np.pi, 100)? It gives exactly 100 points including both endpoints, so the curve is smooth and closes one full period. plt.tight_layout() stops the axis labels from being clipped.
Why density=True? It rescales bar heights so the total area = 1, turning counts into a probability density. That's the only scale on which a histogram is comparable to a smooth PDF curve. axvline draws a full-height vertical line at one x-value — perfect for a reference statistic.
Recall Solution L2.3
Visible height = value − floor.
XGBoost drawn height =0.91−0.75=0.16
LogReg drawn height =0.82−0.75=0.07
Visual ratio=0.16/0.07≈2.29
True accuracy ratio=0.91/0.82≈1.11
So XGBoost's bar looks ~2.3× taller though it is only ~11% more accurate. This is why cropped y-axes must always be labelled honestly.
The rising validation loss while training loss keeps dropping is the signature of overfitting (see Overfitting vs Underfitting). The model memorises training noise, so it improves on train but generalises worse on validation.
Pick the epoch of minimum validation loss (around epoch 20 in the figure — the orange curve's lowest point), not the last epoch. This is "early stopping". Look at the red dashed line marking that minimum.
Recall Solution L3.2
What log does: it plots logL instead of L, so equal ratios become equal distances. On a linear axis, once L is near its floor 0.1, all late epochs squash into a flat line and you can't see whether the model is still improving. On a log axis, the tail (say from 0.3→0.15, a 2× drop) is as visible as the early plunge. This is exactly what gradient-descent diagnostics need — you care about relative progress late in training.
A heatmap makes this readable at a glance — bright diagonal = good, bright off-diagonal = where errors live. See Confusion Matrix & Classification Metrics.
train_acc at epoch 10:loss=2.5e−10/10+0.1=2.5e−1+0.1≈2.5(0.36788)+0.1=1.01970acc=1−2.51.01970≈1−0.40788=0.59212
So train_acc≈0.592.
plt.subplots(1, 2) returns a Figure plus a tuple of two Axes — we unpack them as ax1, ax2 and draw one story per panel.
Recall Solution L4.2
Use sns.violinplot(data=df, x='Optimizer', y='Final Loss').
Why a violin beats three histograms: it draws a mirrored KDE (smooth density) per group side-by-side on a shared y-axis, so shapes are directly comparable in one glance — you see median, spread, and multi-modality together. Three separate histograms force the eye to jump between panels with possibly different scales. This is core Exploratory Data Analysis practice: pass column names, let Seaborn do the stats.
transform=ax.transAxes means "interpret (0.2, 0.8) as fractions of the Axes box", where (0,0) is bottom-left and (1,1) is top-right — independent of data range.
x-fraction 0.2 of [0, 1000] ⇒x=0+0.2×(1000−0)=200
y-fraction 0.8 of [−5, 5] ⇒y=−5+0.8×(5−(−5))=−5+0.8×10=3
So it lands at data point (200, 3).
Without the transform, ax.text(0.2, 0.8, ...) uses data coordinates directly: the text sits at data point (0.2, 0.8) — practically glued to the left edge and near the bottom, essentially off-screen for this range. That's why transAxes exists: to place annotations by position on the box, not by data value.
Recall Solution L5.2
All 500 values are identical, so the data has zero range. Matplotlib still builds 30 bins across a tiny auto-padded interval around 7.0, but every value falls into one single bin → exactly 1 non-empty bar of count 500 (the other 29 bars are height 0).
With density=True, the one bar's height = total×bin widthcount=500×w500=w1, which blows up as the bin width w shrinks — a spike. Lesson: a histogram of a constant is a degenerate spike; check data.std()==0 before trusting distribution plots.
Recall Solution L5.3
A Gaussian kernel has tails that extend past 0 into negative x. Summing kernels centred near 0 leaks density into x<0, where no probability can exist — so the KDE both shows impossible negative-x mass andunderestimates the true density right at the boundary (mass "spilled out").
Fixes: pass sns.kdeplot(data, clip=(0, 1)) to cut the curve at the valid range, or use sns.histplot(data, kde=True) and trust the bars near the edge, or transform the variable (e.g. logit) before estimating. The general rule: KDE assumes unbounded support — warn yourself whenever the true variable is bounded.
Recall Solution L5.4
For plt.subplots(2, 2), axes is a 2×2 NumPy array of Axes. Bottom-right is row 1, column 1:
axes[1, 1].plot(x, y) # bottom-right
For plt.subplots(1, 3), axes collapses to a 1-D array of length 3, so the middle panel is:
axes[1].plot(x, y) # middle of three
Why the shape changes: with a single row (or single column) Matplotlib squeezes the extra dimension away, so you index with one number, not two. Mixing these up (axes[1,1] on a 1×3 grid) throws IndexError.