"""kde_density_levels — Compute density levels via KDE or histogram fallback.""" import math import numpy as np def kde_density_levels( xs: list[float], ys: list[float], bw_adjust: float = 0.6, abs_quantile: float = 0.1, dense_quantile: float = 0.85, bins: int = 80, ) -> dict | None: """Estimate 2-D density and compute absolute and dense threshold levels. Uses scipy.stats.gaussian_kde when available; falls back to numpy.histogram2d if scipy is not installed. Args: xs: X-coordinates of points. ys: Y-coordinates of points. bw_adjust: Bandwidth adjustment factor for KDE (ignored for histogram fallback). abs_quantile: Quantile of density values used as the absolute threshold. dense_quantile: Quantile of density values used as the dense threshold. bins: Number of bins per axis for the histogram fallback. Returns: Dict with keys: "method" (str): "kde" or "hist". "densities" (np.ndarray): 1-D array of per-point density estimates. "abs_level" (float): density at abs_quantile. "dense_level" (float): density at dense_quantile. Returns None if len(xs) < 5 or xs and ys have different lengths. """ if len(xs) < 5 or len(xs) != len(ys): return None xs_arr = np.array(xs, dtype=float) ys_arr = np.array(ys, dtype=float) points = np.vstack([xs_arr, ys_arr]) try: from scipy.stats import gaussian_kde # type: ignore kde = gaussian_kde(points, bw_method=bw_adjust) densities = kde(points) method = "kde" except ImportError: # Histogram fallback h, xedges, yedges = np.histogram2d(xs_arr, ys_arr, bins=bins) xi = np.clip(np.searchsorted(xedges, xs_arr) - 1, 0, bins - 1) yi = np.clip(np.searchsorted(yedges, ys_arr) - 1, 0, bins - 1) densities = h[xi, yi].astype(float) method = "hist" abs_level = float(np.quantile(densities, abs_quantile)) dense_level = float(np.quantile(densities, dense_quantile)) return { "method": method, "densities": densities, "abs_level": abs_level, "dense_level": dense_level, }