You have met the parent note : Figure, Axes, ax.plot, histograms, log scales, coordinate transforms. This page does something different — it lists every kind of situation a plotting task can throw at you, then walks a worked example for each. The goal: when you sit at a real dataset, you never hit a case you have not already solved on paper.
Before we start, one promise of notation. A bin is one of the equal-width buckets a histogram slices the number line into. A KDE (kernel density estimate) is a smooth curve laid over those bars — we will build it from zero when we reach it. Everything else we define the moment it appears.
Think of plotting problems as a grid. Each row is a kind of question the data asks ; each column is a twist that breaks a naive answer . If you can solve every cell, you can solve any real plot.
Cell
Case class
The twist you must survive
A
One continuous variable
Choosing bin count — too few hides structure, too many shows noise
B
Two variables, correlation
The sign of the slope (positive / negative / zero)
C
Values spanning huge range
Linear axis hides the tail → when to switch to log
D
Degenerate input
All values identical, or a single point, or empty array
E
Categorical comparison
Y-axis start point distorts small differences
F
Coordinate placement
Data coords vs axes coords for a caption
G
Grouped distributions
Comparing spread, not just centre (violin/box)
H
Real-world word problem
Translate a sentence into the right plot type
I
Exam-style twist
A plot that lies — spot and fix the distortion
The examples below are labelled with the cell(s) they cover. Together they touch every row.
Worked example Example 1 — Cell A: how many bins?
You have 1000 exam scores drawn from a normal distribution, mean 100 , standard deviation 15 . You must pick a bin count for ax.hist. A common rule is the square-root rule : number of bins ≈ n , where n is the sample size.
Forecast: guess how many bins 1000 suggests before reading on.
Compute 1000 .
Why this step? The rule turns "how many buckets?" into a single arithmetic question — it balances resolution (many bins) against stability (each bin has enough counts).
1000 = 31.62 … ⇒ round to 32 bins .
Estimate counts per bin. The data mostly lies within ± 3 σ , i.e. a range of 6 σ = 90 score-units. So bin width ≈ 90/32 ≈ 2.8 units.
Why this step? A sanity check: with 1000 points over 32 bins, the average bin holds ≈ 31 counts — enough to look smooth, not spiky.
Verify: 32 × 31.25 = 1000 . The bin count and average occupancy multiply back to the sample size. ✅ Figure below shows 8 bins (too coarse) vs 32 bins (just right).
Common mistake Too few bins (like 8 here) merges the two shoulders of a bell curve into a flat plateau — you lose the shape you were trying to see. Too many (200+) turns it into a comb of noise.
Worked example Example 2 — Cell B (positive slope): scatter + regression sign
Feature 1 is x ; Feature 2 is y = 2 x + noise . Seaborn's sns.regplot fits a straight line y = m x + c through the cloud. You are told the fitted slope came out to m = 1.98 . Is the relationship positive or negative, and by how much does y rise per unit of x ?
Forecast: positive or negative? By how much?
Read the sign of m . Here m = + 1.98 > 0 , so as x increases, y increases — a positive correlation.
Why this step? The slope's sign is the direction of the relationship. Everything else (the noise, the intercept) is decoration.
Read the magnitude . m = 1.98 means each + 1 in x lifts y by about 1.98 .
Why this step? Magnitude tells you strength of effect , which matters for feature importance in EDA .
Verify: The true generating rule was y = 2 x . The fit recovered 1.98 ≈ 2 — off by 0.02 because of the random noise. Sign and magnitude both match. ✅
Worked example Example 3 — Cell B (negative slope)
Now y = − 0.5 x + noise . The fit returns m = − 0.47 . What does the scatter look like, and what is the sign?
Forecast: which way does the line tilt?
Sign of m is − 0.47 < 0 → negative correlation: the line falls left-to-right.
Why this step? Same reasoning as Example 2 — never trust your eye on a noisy cloud; trust the fitted sign.
Magnitude 0.47 : each + 1 in x drops y by about half a unit.
Verify: True rule y = − 0.5 x ; recovered − 0.47 . Correct sign, magnitude within 0.03 . ✅
Worked example Example 4 — Cell B (zero slope, the degenerate correlation)
y is pure noise, independent of x . The fit returns m = 0.01 . What plot appears, and what do you conclude?
Forecast: what slope, and is there a relationship?
m = 0.01 ≈ 0 : the regression line is nearly flat.
Why this step? A flat line is the null result — "x tells you nothing about y ." You must recognise this so you don't chase a phantom feature.
The scatter is a shapeless blob with no tilt.
Verify: True correlation is 0 ; a slope of 0.01 is statistically indistinguishable from zero for a modest sample. ✅ Contrast this with Example 2's tilted cloud in the figure.
Worked example Example 5 — Cell C: linear vs log axis for training loss
Training loss over 50 epochs follows loss ( t ) = 2.5 e − t /10 + 0.1 , where t is the epoch. Here e is the number 2.718 … and e − t /10 is exponential decay — it halves-and-halves-again toward zero. Compute the loss at epochs 1 , 10 , and 50 , and decide whether a log y-axis helps.
Forecast: how much does the loss shrink from epoch 1 to 50? Guess the ratio.
Epoch 1: loss = 2.5 e − 0.1 + 0.1 = 2.5 ( 0.9048 ) + 0.1 = 2.362 .
Why this step? We need concrete numbers to see whether the range is large — that is exactly what decides linear vs log.
Epoch 10: loss = 2.5 e − 1 + 0.1 = 2.5 ( 0.3679 ) + 0.1 = 1.020 .
Epoch 50: loss = 2.5 e − 5 + 0.1 = 2.5 ( 0.006738 ) + 0.1 = 0.1168 .
Why this step? Now compare epoch 1 to epoch 50: 2.362 vs 0.1168 . The early values dwarf the late ones on a linear axis, so the interesting late-training behaviour gets squashed into a flat line near the bottom.
Decision: use ax.set_yscale('log'). A log axis makes equal ratios look like equal steps , so the tail (epochs 30–50) becomes readable — critical when diagnosing gradient descent convergence.
Verify: ratio of first to last loss = 2.362/0.1168 = 20.2 × . A 20-fold range is exactly the regime where log scale earns its keep. ✅
Worked example Example 6 — Cell D: degenerate inputs
Three broken cases hit ax.hist(data, bins=30). Predict what each produces.
Forecast: which of these crashes, and which draws something useless?
All values identical , e.g. data = [5, 5, 5, 5, 5]. Matplotlib places all 30 bins over a zero-width range; every count lands in one bin.
Why this step? Zero variance means there is no distribution to show — the histogram collapses to a single spike. This is your cue that the column is a constant and carries no information.
Single point , data = [7.0]. One count, one occupied bin, height = 1 .
Why this step? A histogram of one point cannot estimate a distribution — you would need more samples.
Empty array , data = []. No error, but every bin has height 0 — a blank plot.
Why this step? Silent emptiness is dangerous; always print len(data) before plotting so a filtering bug doesn't masquerade as "no data."
Verify: counts sum to the sample size in each case — 5 , 1 , and 0 respectively. The sum-of-heights equals n always holds, even in degenerate cases. ✅
Worked example Example 7 — Cell E + Cell I: the bar chart that lies
Four models have accuracies 0.82 , 0.85 , 0.89 , 0.91 . You draw a bar chart. Someone sets ax.set_ylim([0.75, 0.95]). Does XGBoost (0.91 ) look like it beats LogReg (0.82 ) by the true amount?
Forecast: by what factor does the drawn bar height exaggerate the real gap?
True gap: 0.91 − 0.82 = 0.09 accuracy points.
Why this step? The real difference is what we actually care about for model selection .
Drawn heights with axis starting at 0.75 : LogReg bar height = 0.82 − 0.75 = 0.07 ; XGBoost bar height = 0.91 − 0.75 = 0.16 .
Why this step? The eye compares pixel heights , not values. Cutting the axis inflates the visual ratio.
Visual ratio = 0.16/0.07 = 2.29 , but the honest ratio (axis from 0) = 0.91/0.82 = 1.11 .
Why this step? The chart makes an 11% edge look like a 129% landslide.
Verify: honest ratio 0.91/0.82 = 1.110 ; truncated-axis ratio 0.16/0.07 = 2.286 . The distortion factor is 2.286/1.110 = 2.06 × . Fix: either start the y-axis at 0 , or annotate exact values on the bars so no one is fooled. ✅
Worked example Example 8 — Cell F: placing a caption that survives zoom
You want the text "best epoch" at a fixed spot — 20% from the left, 80% from the bottom of the plot box — no matter what data range is shown. Which coordinate system, and what numbers?
Forecast: if you write ax.text(0.2, 0.8, ...) without a transform, where does it land when the x-axis runs 0–50?
Without transform: text goes to data coordinates ( 0.2 , 0.8 ) — near the far-left edge of a 0–50 axis, essentially off-screen.
Why this step? Default ax.text speaks data language; the number 0.2 means "x = 0.2 epochs," not "20% across."
With transform=ax.transAxes: the pair ( 0.2 , 0.8 ) now means axes coordinates — a normalized box where ( 0 , 0 ) is bottom-left and ( 1 , 1 ) is top-right. So ( 0.2 , 0.8 ) is always 20% right, 80% up, regardless of data range.
Why this step? transAxes decouples annotation placement from the data — the caption stays put even if you zoom or rescale.
Verify: axes-fraction 0.2 ⇒ pixel position 0.2 × ( axes width ) from the left; independent of the 0–50 data range. Placement is scale-invariant by construction. ✅
Worked example Example 9 — Cell G + Cell H: real-world word problem, grouped spread
"We ran three optimizers — Adam, SGD, RMSprop — 50 times each. Adam finishes at loss ∼ 0.12 with tiny variance; SGD at ∼ 0.15 but wildly spread. Which plot shows both centre AND spread, and which optimizer would you ship?"
Forecast: would a plain bar chart of the means answer this? Why not?
A bar of means shows 0.12 vs 0.15 — Adam wins on average. But it hides the spread : a bar has no notion of variance.
Why this step? In ML, a method that is sometimes great and sometimes terrible is risky. You must see the spread, not just the mean.
Use a violin plot (sns.violinplot). A violin is a KDE mirrored on both sides — its width at any height shows how many runs landed there. Fat middle = consistent; long thin tails = unstable.
Why this step? The violin encodes the full distribution per group, so centre and spread are visible side by side.
Read the answer: Adam's violin is a tight fat blob near 0.12 ; SGD's is a long stretched shape. Ship Adam — lower mean and lower variance.
Verify: Adam mean 0.12 < SGD mean 0.15 , and Adam's std (≈ 0.02 ) < SGD's std (≈ 0.05 ). Adam dominates on both centre and spread. ✅
Worked example Example 10 — Cell A revisited: building a KDE by hand
A histogram of { 0 , 1 , 3 } looks blocky. sns.histplot(kde=True) lays a smooth curve on top. Build the KDE value at x = 1 from zero, using a Gaussian kernel with bandwidth h = 1 (the width of each little bump).
The kernel is K ( u ) = 2 π 1 e − u 2 /2 , and the density estimate is
f ^ ( x ) = n h 1 ∑ i = 1 n K ( h x − x i ) .
Here n = 3 data points, x i are the data, and we evaluate at x = 1 .
Forecast: the point x = 1 sits right on a data point and next to another — will its density be high or low?
Distances ( x − x i ) / h for x = 1 : to 0 → 1 , to 1 → 0 , to 3 → − 2 .
Why this step? The KDE places one Gaussian bump at each data point; we measure how far x = 1 is from each bump's centre.
Kernel values: K ( 1 ) = 0.24197 , K ( 0 ) = 0.39894 , K ( − 2 ) = 0.05399 .
Why this step? A closer data point contributes a bigger bump-height — that is why dense regions get high density.
Sum and normalise: f ^ ( 1 ) = 3 ⋅ 1 1 ( 0.24197 + 0.39894 + 0.05399 ) = 3 0.69490 = 0.23163 .
Why this step? Dividing by nh makes the total area under f ^ equal to 1 — a proper density.
Verify: f ^ ( 1 ) = 0.23163 . The point near two data points has meaningfully positive density, dominated by the on-point bump K ( 0 ) — exactly what "smooth histogram" should do. ✅
Recall Quick self-test
A histogram of 400 points — square-root rule bin count? ::: 400 = 20 bins.
Regression slope is − 0.03 — positive, negative, or no relationship? ::: Effectively no relationship (slope ≈ 0 ).
Loss goes from 2.0 to 0.02 over training — linear or log y-axis? ::: Log — the 100 × range hides the tail on linear.
A bar chart with y-axis starting at 0.75 instead of 0 — what's the danger? ::: It exaggerates small differences; the eye reads inflated pixel heights.
ax.text(0.5, 0.5, s, transform=ax.transAxes) — where does the text go? ::: Dead centre of the Axes box, regardless of data range.
"Sign, Scale, Spread, Start."
Before trusting any plot: check the Sign of the slope, the Scale of the axis (linear/log), the Spread not just the mean, and the Start of the axis (does it lie?).
See also: 1.4.01-NumPy-arrays-and-operations for the arrays feeding these plots, 1.4.02-Pandas-DataFrames-and-data-manipulation for column-based Seaborn input, and 2.3.04-Confusion-matrix-and-classification-metrics for the heatmap cousin of these examples.