Source code for gufo.stats.kde
"""Kernel Density Estimation — standalone mark and histogram overlay."""
from dataclasses import dataclass
from typing import Optional
import numpy as np
try:
from scipy.stats import gaussian_kde
except ImportError:
gaussian_kde = None
from . import _require_scipy
[docs]
@dataclass
class KDE:
"""Configuration for a KDE curve.
Used as an overlay on .histogram() via histogram(kde=gufo.kde(...)).
For a standalone density plot, use Chart.kdeplot() instead.
Users create instances via gufo.kde(), never directly.
"""
bw_method: Optional[object] = None
color: Optional[str] = None
linestyle: str = "-"
linewidth: float = 2.0
alpha: float = 1.0
label: Optional[str] = None
fill: bool = False
n_points: int = 200
[docs]
def render(self, x, axes, *, scale_to_hist=False, hist_patches=None,
**extra_kwargs):
"""Compute and draw the KDE curve.
Parameters
----------
x : numpy array
The data to estimate density for.
axes : matplotlib Axes
Target axes to draw on.
scale_to_hist : bool
If True, scale the KDE to match histogram bar heights.
hist_patches : list or None
Patches from axes.hist(), used for bin width when scaling.
\**extra_kwargs
Additional keyword arguments passed through to matplotlib.
"""
_require_scipy("KDE")
x_clean = x[np.isfinite(x)]
if len(x_clean) < 2:
return
kernel = gaussian_kde(x_clean, bw_method=self.bw_method)
x_grid = np.linspace(x_clean.min(), x_clean.max(), self.n_points)
density = kernel(x_grid)
if scale_to_hist and hist_patches is not None:
bin_width = hist_patches[0].get_width() if hist_patches else 1.0
density = density * len(x_clean) * bin_width
kwargs = {
"linestyle": self.linestyle,
"linewidth": self.linewidth,
"alpha": self.alpha,
**extra_kwargs,
}
if self.color is not None:
kwargs["color"] = self.color
if self.label is not None:
kwargs["label"] = self.label
else:
kwargs["label"] = "KDE"
if self.fill:
axes.fill_between(x_grid, density, **kwargs)
else:
axes.plot(x_grid, density, **kwargs)