statistics.kde brings kernel density estimation to Python’s standard library. It is useful when a mean, median, or histogram is not enough to describe how observations are distributed. Instead of splitting values into rigid bins, KDE builds a smooth function that highlights concentrations, gaps, and possible multiple modes.
This guide explains what kernel density estimation means, how to call statistics.kde, how bandwidth changes the result, how kernels differ, and how to use the returned function responsibly in exploratory analysis and production code.
What kernel density estimation does
A KDE places a small curve around every observation. Those curves are combined and normalized into a continuous density estimate. The result resembles a smoothed histogram, but its shape is controlled mainly by bandwidth rather than bin edges.
Suppose you record API latency. A histogram might use bins from 0–50, 50–100, and 100–150 milliseconds. Moving those boundaries can change the picture. KDE avoids fixed bins and instead asks how strongly each observation should influence nearby values.
Basic statistics.kde example
from statistics import kde
sample = [1.2, 1.5, 1.7, 2.0, 2.1, 2.2, 3.8, 4.0, 4.2]
density = kde(sample, h=0.35)
for x in [1.0, 1.5, 2.0, 2.5, 3.0, 4.0]:
print(x, density(x))
The function returns another callable. Calling it with a number gives the estimated density at that location. The result is not the probability of that exact number. Probability is represented by area over an interval, while density is a local intensity and can be greater than one depending on scale.
Reading the curve
Evaluate the callable over an ordered grid to obtain points for a chart. High regions indicate where observations are concentrated. Low regions suggest sparse areas or gaps. In the sample above, the curve should reveal one cluster around 1–2.2 and another around 4.
This matters because a single average can be misleading. The average may fall near 2.5 even though few observations occur there. KDE preserves more of the sample’s shape.
Bandwidth is the key choice
The h argument controls smoothing. A bandwidth that is too small produces many narrow peaks and often follows noise. A bandwidth that is too large can merge separate groups and hide meaningful structure.
values = [10, 10.5, 11, 11.2, 14, 14.3, 15]
narrow = kde(values, h=0.2)
balanced = kde(values, h=0.8)
wide = kde(values, h=2.0)
There is no universal bandwidth. Scale, sample size, and the question being asked all matter. During exploration, compare several values. For automated decisions, validate the selected bandwidth against held-out data and domain knowledge.
Choosing a kernel
The optional kernel argument changes the shape placed around each observation. Documented choices include normal, logistic, sigmoid, rectangular, triangular, parabolic, and related forms. The normal kernel is a sensible starting point. Compact-support kernels can be useful when distant observations should contribute nothing.
normal = kde(sample, h=0.35, kernel="normal")
triangular = kde(sample, h=0.35, kernel="triangular")
Bandwidth usually has a stronger visual effect than switching between reasonable kernels, but the kernel still belongs in analysis metadata for reproducibility.
Building chart data
start = min(sample) - 1
end = max(sample) + 1
steps = 200
xs = [start + i * (end - start) / (steps - 1) for i in range(steps)]
ys = [density(x) for x in xs]
The standard-library function does not draw a chart. The generated arrays can be used with Matplotlib, a web chart, a spreadsheet, or an API response. For lazy sequence processing, see Python itertools.pairwise. For cumulative transformations, read Python itertools.accumulate.
Clean data first
KDE cannot repair bad input. Confirm units, remove invalid values, and investigate outliers before fitting the function. Mixing seconds with milliseconds creates an impressive but meaningless curve. Decide whether extreme points are measurement errors, valid rare events, or members of another population.
import math
from statistics import kde
raw = [1.2, None, 1.5, float("nan"), 2.0, 2.1]
clean = [x for x in raw if isinstance(x, (int, float)) and math.isfinite(x)]
if len(clean) < 2:
raise ValueError("Not enough observations")
f = kde(clean, h=0.3)
For explicit model APIs, review Python dataclasses.KW_ONLY. For lightweight result objects, see Python SimpleNamespace.
KDE versus a histogram
Histograms are excellent for direct counts. KDE is excellent for a continuous view and for comparing shapes. Many reports should include both. A histogram shows how many observations are in each range, while KDE highlights the estimated structure.
Do not let a smooth curve hide a small sample. Always report sample size, bandwidth, kernel, and ideally the original observations or a supporting histogram.
Small samples and boundary bias
With very few values, every observation strongly affects the curve. KDE can still be calculated, but confidence should be limited. It describes the sample and does not prove that the wider population has the same peaks.
Boundaries are another concern. Durations and weights cannot be negative, yet symmetric kernels may assign some density below zero. That does not create negative observations; it shows that standard KDE may require transformation or boundary correction for constrained domains.
Anomaly scoring
A low-density score can identify unusual values. Fit the estimator on historical observations, evaluate a new value, and compare the score with a calibrated threshold.
history = [100, 102, 98, 101, 99, 103, 97, 100]
f = kde(history, h=2.0)
new_value = 118
score = f(new_value)
if score < 0.001:
print("Unusual value; review it")
Low density is not automatically an error. It can be a legitimate rare event. Calibrate thresholds using known examples, record the score, preserve the training sample version, and log the bandwidth.
Testing the estimator
The callable is deterministic for a fixed sample and configuration, which makes property-based tests straightforward. Verify non-negative results, higher density near clusters, and expected changes when bandwidth changes.
def test_density_is_higher_near_sample():
from statistics import kde
f = kde([0.0, 0.1, 0.2, 0.3], h=0.2)
assert f(0.15) > f(3.0)
assert f(0.15) >= 0
Avoid strict equality for floating-point values unless necessary. Prefer tolerances and behavioral relationships.
Performance and architecture
Evaluating many grid points against a large sample can be expensive because each evaluation considers the observations. Cache repeated grids when appropriate, limit chart resolution, and measure before optimizing. Keep data cleaning, estimator construction, and presentation in separate functions.
For safe resource composition in analysis pipelines, Python contextlib.ExitStack provides useful patterns. It is especially helpful when input files, temporary directories, or optional resources are opened dynamically.
When external libraries are better
statistics.kde is a strong choice for teaching, scripts, prototypes, and projects that prefer the standard library. Scientific systems may require multidimensional KDE, observation weights, automatic bandwidth selection, advanced integration, or optimized vector operations. SciPy, NumPy, pandas, statsmodels, and scikit-learn remain important for those cases.
The official Python statistics documentation defines the current API and supported kernels. For broader statistical interpretation, the NIST Engineering Statistics Handbook is a reliable reference.
Production checklist
Record the Python version because this API is available only in modern releases. Validate finite numeric input. Keep units consistent. Compare bandwidths before choosing one. Store kernel and bandwidth with results. Report sample size. Do not call point density a probability. Recalibrate anomaly thresholds when the underlying population changes.
Monitoring is also important. A density model fitted months ago may no longer represent current traffic, prices, or user behavior. Track drift and rebuild the estimator when the distribution changes.
Practical helper
from statistics import kde
def build_density(values, bandwidth, kernel="normal"):
clean = [float(value) for value in values]
if len(clean) < 2:
raise ValueError("At least two observations are required")
if bandwidth <= 0:
raise ValueError("bandwidth must be positive")
return kde(clean, h=bandwidth, kernel=kernel)
This wrapper centralizes validation and makes the chosen configuration explicit. A larger application can extend it with finite-value checks, unit metadata, logging, and sample versioning.
Conclusion
statistics.kde makes distribution analysis available without an external dependency. It can reveal clusters, compare shapes, support exploratory charts, and provide simple anomaly scores. Its value depends on disciplined input validation and a justified bandwidth.
Start by evaluating a small sample on a grid and comparing several bandwidths. Then add tests, documentation, monitoring, and clear communication of limitations. A KDE should not merely look smooth; it should answer a defined question with reproducible settings.







