Visual walkthrough — Matplotlib and Seaborn visualization
The parent note (Matplotlib and Seaborn visualization) casually says:
kde=True"fits a Gaussian kernel at each data point, sums them → smooth density estimate."
That one sentence hides the single most important idea in statistical plotting. This page builds that smooth curve from absolute zero — no symbol used before it is drawn. By the end you will be able to draw a Kernel Density Estimate by hand, and you will know exactly what Seaborn is doing when it shades that soft blue hill over your histogram.
We will use only these ingredients, each defined when it first appears:
- a handful of numbers (our data points),
- a little bump we place on each point (the kernel),
- a knob controlling how wide each bump is (the bandwidth),
- and simple addition.
Step 1 — Start with raw data points on a line
WHAT. We place our measurements as dots on a horizontal axis. Let there be measurements. We call them:
- ::: the count of how many numbers we collected.
- ::: the -th measurement (the subscript is just a name-tag: is "the third value"). It has a position on the line.
WHY. Before we can smooth anything, we must see the bare data. Everything after this step is decoration added on top of these dots.
PICTURE. Six amber dots sit on a white axis. Notice three are bunched near the middle — your eye already suspects "crowded here" but has nothing to measure it with.

Step 2 — The histogram: our first crude answer (and why it stutters)
WHAT. We chop the axis into equal-width boxes called bins and count how many dots fall in each. The count becomes the bar height.
WHY. This is the obvious first attempt at "measuring crowding": tall bar = many dots = crowded. The parent note uses ax.hist(...) for exactly this.
PICTURE. The same six dots, now with bars behind them. Look at the red arrows: a dot sitting just left of a bin edge lands in a different bar than its neighbour a hair to the right. The curve jumps in ugly steps.

Step 3 — Replace each dot with a smooth bump: the kernel
WHAT. Over each data point we draw one identical bump. The most common bump is the Gaussian (bell) shape:
Let us earn every symbol:
- ::: the name of our bump-shape function ("K" for kernel). Feed it a number , it returns a height.
- ::: how far you are from the centre of the bump, in bump-widths. At you are at the peak; at you are far out in the tail.
- ::: the bell itself. The minus sign makes height shrink as you move away; squaring makes it shrink symmetrically (left and right the same) and fast.
- ::: a fixed scaling number () chosen so the total area under one bump equals exactly 1. This is the whole reason we can call it a density.
WHY. One dot → one smooth, area-1 hill centred on that dot. Now "crowding" can be added up smoothly instead of counted in boxes.
PICTURE. A single amber dot with a cyan bell centred exactly on it. The dashed white line marks the peak sitting right over the dot; the tails fade to zero on both sides.

Step 4 — The bandwidth knob : how wide is each bump?
WHAT. We need to control how fat each bump is. We introduce a positive number called the bandwidth and place the scaled bump for point like this:
Term by term:
- ::: a position on the axis where we are asking "how tall is the bump here?" (this is the moving question-point, not a data point).
- ::: the fixed centre of this particular bump (a data point from Step 1).
- ::: raw distance from the centre to our question-point.
- ::: that distance measured in units of . Dividing by is what feeds the correct into : if is large, you must travel far before gets big, so the bump is wide.
- out front ::: squashes the height by the same factor it stretches the width, so the area stays exactly 1 no matter what is. (Widen the pile, lower the pile — same amount of sand.)
WHY. is the single most important dial in KDE. It trades detail against smoothness.
PICTURE. The same three data points drawn three times with narrow, medium, and wide . Watch the bumps go from three separate spikes → three gentle merging hills → one shapeless blob.

Step 5 — Add all the bumps and divide by : the KDE formula
WHAT. The full Kernel Density Estimate:
Reading it left to right:
- ::: our estimated density at position . The little hat means "estimate" — a guess of the true density built from a sample.
- ::: "add up the following for every data point, from to ." This is literally stacking the bumps.
- ::: the single scaled bump from Step 4, one per data point.
- out front ::: divides by the count. Each bump has area 1, so of them have area ; dividing by makes the total area under the whole curve equal 1 — the mathematical badge that says "this really is a probability density."
WHY. Addition is all we need because the bumps are already smooth and positive. The result inherits smoothness (sum of smooth things is smooth) and positivity (sum of positive things is positive). No boxes, no edges, no staircase.
PICTURE. Six faint cyan bumps (one per dot) and the bold amber curve that is their sum-divided-by-. The tall part of the curve sits exactly over the crowd of dots — the very crowding your eye guessed in Step 1, now measured.

Step 6 — Edge case: a boundary the KDE cannot see
WHAT. The KDE assigns a positive height even at , claiming data can exist where it physically cannot.
WHY it happens. The kernel is never exactly zero for finite (an exponential only approaches zero). It has no idea a boundary exists — we never told it.
PICTURE. Loss values hugging ; the amber KDE curve droops below the axis line into the greyed "impossible" region, marked with a red warning arrow.

Step 7 — Where this lives in Seaborn (the payoff)
WHAT. Everything above is what a single line of Seaborn runs for you:
import seaborn as sns
sns.histplot(data, kde=True, bins=30) # histogram (Step 2) + KDE (Step 5)
sns.kdeplot(data, bw_adjust=0.5) # KDE alone; bw_adjust scales h (Step 4)kde=True::: overlays the Step 5 curve on the Step 2 bars.bw_adjust::: multiplies Seaborn's automatically chosen .bw_adjust=0.5halves it (spikier),2.0doubles it (smoother) — the Step 4 knob in your hands.data::: a NumPy array (NumPy arrays and operations) or a DataFrame column (Pandas DataFrames).
WHY it matters for ML. Comparing loss distributions across optimizers (gradient descent), or checking whether predicted-probability histograms are calibrated (classification metrics) — all rely on reading these smooth curves correctly, including knowing when the boundary lie of Step 6 is fooling you.
The one-picture summary
One figure compresses the whole journey: dots (Step 1) → a bump per dot (Steps 3–4) → their sum, the KDE (Step 5). If you can redraw this from memory, you understand kernel density estimation.

Recall Feynman retelling — say it like a story
You dumped a handful of numbers on a line and asked "where's the crowd?" Counting them in boxes gave a jumpy staircase that changes if you nudge the boxes — no good. So you did something gentler: over each number you laid a little identical hill of sand, tallest at the number and tapering off to the sides. A knob called sets how fat each hill is — thin hills give a spiky noisy shape, fat hills smear everything into a blob, so you pick a middle width. Then you simply added all the hills up and divided by how many there were, so the total sand still adds to one. Where numbers were crowded, hills overlapped and the total rose into a peak; where they were sparse, it sank. That total is the smooth density curve. The one catch: the hills have tails that leak, so if your numbers can't go below zero, the curve still dribbles into the forbidden zone — and you fix that by reflecting, clipping, or working in log-space. Seaborn does all of this when you write kde=True.