Skip to content

Commit b1047f9

Browse files
committed
nw_predict etc., another v. bump
1 parent e863b8a commit b1047f9

7 files changed

Lines changed: 574 additions & 4 deletions

File tree

hbw/__init__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,33 @@
1010
"""
1111

1212
from ._kernels import _KERNELS
13-
from .kde import kde_bandwidth, kde_bandwidth_mv, lscv, lscv_grad, lscv_mv, lscv_score
13+
from .kde import (
14+
kde_bandwidth,
15+
kde_bandwidth_mv,
16+
kde_evaluate,
17+
kde_evaluate_mv,
18+
lscv,
19+
lscv_grad,
20+
lscv_mv,
21+
lscv_score,
22+
)
1423
from .nw import (
1524
loocv_mse,
1625
loocv_mse_grad,
1726
loocv_mse_mv,
1827
loocv_mse_score,
1928
nw_bandwidth,
2029
nw_bandwidth_mv,
30+
nw_predict,
31+
nw_predict_mv,
2132
)
2233

2334
__all__ = [
2435
"_KERNELS",
2536
"kde_bandwidth",
2637
"kde_bandwidth_mv",
38+
"kde_evaluate",
39+
"kde_evaluate_mv",
2740
"loocv_mse",
2841
"loocv_mse_grad",
2942
"loocv_mse_mv",
@@ -34,5 +47,7 @@
3447
"lscv_score",
3548
"nw_bandwidth",
3649
"nw_bandwidth_mv",
50+
"nw_predict",
51+
"nw_predict_mv",
3752
]
38-
__version__ = "0.5.0"
53+
__version__ = "0.6.0"

hbw/kde.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,132 @@ def _newton_armijo_mv_numba(
377377
return h
378378

379379

380+
def kde_evaluate(
381+
x_train: ArrayLike,
382+
x_eval: ArrayLike,
383+
h: float,
384+
kernel: str = "gauss",
385+
) -> NDArray[Any]:
386+
"""Evaluate kernel density estimate at given points.
387+
388+
Parameters
389+
----------
390+
x_train
391+
Training sample data (1D array-like).
392+
x_eval
393+
Points at which to evaluate the density (1D array-like).
394+
h
395+
Bandwidth (obtained from kde_bandwidth).
396+
kernel
397+
Kernel function: "gauss", "epan", "unif", "biweight", "triweight", or "cosine".
398+
399+
Returns
400+
-------
401+
NDArray
402+
Estimated density values at x_eval locations.
403+
404+
Examples
405+
--------
406+
>>> import numpy as np
407+
>>> x = np.random.randn(1000)
408+
>>> h = kde_bandwidth(x)
409+
>>> x_grid = np.linspace(-3, 3, 100)
410+
>>> density = kde_evaluate(x, x_grid, h)
411+
412+
Notes
413+
-----
414+
This function returns point estimates only. No confidence intervals or
415+
standard errors are provided. Key assumptions: smooth underlying density,
416+
IID observations, continuous density (no point masses).
417+
418+
For inference, consider bootstrap resampling or see statsmodels.nonparametric.
419+
"""
420+
x_tr = np.asarray(x_train, dtype=float).ravel()
421+
x_ev = np.asarray(x_eval, dtype=float).ravel()
422+
423+
if kernel not in _KERNELS:
424+
raise ValueError(f"kernel must be one of {list(_KERNELS.keys())}, got {kernel!r}")
425+
426+
K, _, _, _, _, _ = _KERNELS[kernel]
427+
n = len(x_tr)
428+
429+
u = (x_ev[:, None] - x_tr[None, :]) / h
430+
density = K(u).sum(axis=1) / (n * h)
431+
432+
return density
433+
434+
435+
def kde_evaluate_mv(
436+
data_train: ArrayLike,
437+
data_eval: ArrayLike,
438+
h: float,
439+
kernel: str = "gauss",
440+
) -> NDArray[Any]:
441+
"""Evaluate multivariate kernel density estimate at given points using product kernel.
442+
443+
Parameters
444+
----------
445+
data_train
446+
Training sample data, shape (n_train, d).
447+
data_eval
448+
Points at which to evaluate the density, shape (n_eval, d).
449+
h
450+
Bandwidth (scalar, applied to all dimensions; obtained from kde_bandwidth_mv).
451+
kernel
452+
Kernel function: "gauss", "epan", "unif", "biweight", "triweight", or "cosine".
453+
454+
Returns
455+
-------
456+
NDArray
457+
Estimated density values at data_eval locations.
458+
459+
Examples
460+
--------
461+
>>> import numpy as np
462+
>>> data = np.random.randn(500, 2)
463+
>>> h = kde_bandwidth_mv(data)
464+
>>> data_grid = np.column_stack([np.linspace(-3, 3, 50), np.linspace(-3, 3, 50)])
465+
>>> density = kde_evaluate_mv(data, data_grid, h)
466+
467+
Notes
468+
-----
469+
This function returns point estimates only. No confidence intervals or
470+
standard errors are provided. Key assumptions: smooth underlying density,
471+
IID observations, continuous density (no point masses). Uses product kernel
472+
with isotropic bandwidth; data should be standardized for best results.
473+
474+
For inference, consider bootstrap resampling or see statsmodels.nonparametric.
475+
"""
476+
data_tr = np.asarray(data_train, dtype=float)
477+
data_ev = np.asarray(data_eval, dtype=float)
478+
479+
if data_tr.ndim == 1:
480+
data_tr = data_tr.reshape(-1, 1)
481+
if data_ev.ndim == 1:
482+
data_ev = data_ev.reshape(-1, 1)
483+
484+
if data_tr.ndim != 2:
485+
raise ValueError(f"data_train must be 2D array, got shape {data_tr.shape}")
486+
if data_ev.ndim != 2:
487+
raise ValueError(f"data_eval must be 2D array, got shape {data_ev.shape}")
488+
if data_tr.shape[1] != data_ev.shape[1]:
489+
raise ValueError(f"data_train and data_eval must have same number of dimensions, got {data_tr.shape[1]} and {data_ev.shape[1]}")
490+
if kernel not in _KERNELS:
491+
raise ValueError(f"kernel must be one of {list(_KERNELS.keys())}, got {kernel!r}")
492+
493+
K, _, _, _, _, _ = _KERNELS[kernel]
494+
n = len(data_tr)
495+
d = data_tr.shape[1]
496+
497+
U = (data_ev[:, None, :] - data_tr[None, :, :]) / h
498+
K_vals = K(U)
499+
K_prod = np.prod(K_vals, axis=2)
500+
501+
density = K_prod.sum(axis=1) / (n * h**d)
502+
503+
return density
504+
505+
380506
def kde_bandwidth_mv(
381507
data: ArrayLike,
382508
kernel: str = "gauss",

hbw/nw.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,156 @@ def _newton_armijo_mv_nw_numba(
598598
return h
599599

600600

601+
def nw_predict(
602+
x_train: ArrayLike,
603+
y_train: ArrayLike,
604+
x_test: ArrayLike,
605+
h: float,
606+
kernel: str = "gauss",
607+
) -> NDArray[Any]:
608+
"""Nadaraya-Watson kernel regression predictions.
609+
610+
Parameters
611+
----------
612+
x_train
613+
Training predictor values (1D array-like).
614+
y_train
615+
Training response values (1D array-like).
616+
x_test
617+
Test predictor values where predictions are desired (1D array-like).
618+
h
619+
Bandwidth (obtained from nw_bandwidth).
620+
kernel
621+
Kernel function: "gauss", "epan", "unif", "biweight", "triweight", or "cosine".
622+
623+
Returns
624+
-------
625+
NDArray
626+
Predicted values at x_test locations.
627+
628+
Examples
629+
--------
630+
>>> import numpy as np
631+
>>> x = np.linspace(-2, 2, 200)
632+
>>> y = np.sin(x) + 0.1 * np.random.randn(len(x))
633+
>>> h = nw_bandwidth(x, y)
634+
>>> y_pred = nw_predict(x, y, x, h) # in-sample predictions
635+
636+
Notes
637+
-----
638+
This function returns point estimates only. No confidence intervals or
639+
standard errors are provided. Key assumptions: smooth regression function,
640+
IID observations, design density bounded away from zero in region of interest.
641+
642+
For inference, consider bootstrap resampling or see statsmodels.nonparametric.
643+
"""
644+
x_tr = np.asarray(x_train, dtype=float).ravel()
645+
y_tr = np.asarray(y_train, dtype=float).ravel()
646+
x_te = np.asarray(x_test, dtype=float).ravel()
647+
648+
if len(x_tr) != len(y_tr):
649+
raise ValueError(f"x_train and y_train must have same length, got {len(x_tr)} and {len(y_tr)}")
650+
if kernel not in _KERNELS:
651+
raise ValueError(f"kernel must be one of {list(_KERNELS.keys())}, got {kernel!r}")
652+
653+
K, _, _, _, _, _ = _KERNELS[kernel]
654+
655+
u = (x_te[:, None] - x_tr[None, :]) / h
656+
w = K(u)
657+
658+
w_sum = w.sum(axis=1)
659+
w_sum_safe = np.where(w_sum == 0, np.finfo(float).eps, w_sum)
660+
y_pred = (w @ y_tr) / w_sum_safe
661+
662+
zero_weight_mask = w_sum == 0
663+
if np.any(zero_weight_mask):
664+
y_pred[zero_weight_mask] = np.mean(y_tr)
665+
666+
return y_pred
667+
668+
669+
def nw_predict_mv(
670+
data_train: ArrayLike,
671+
y_train: ArrayLike,
672+
data_test: ArrayLike,
673+
h: float,
674+
kernel: str = "gauss",
675+
) -> NDArray[Any]:
676+
"""Multivariate Nadaraya-Watson kernel regression predictions using product kernel.
677+
678+
Parameters
679+
----------
680+
data_train
681+
Training predictor values, shape (n_train, d).
682+
y_train
683+
Training response values (1D array of length n_train).
684+
data_test
685+
Test predictor values where predictions are desired, shape (n_test, d).
686+
h
687+
Bandwidth (scalar, applied to all dimensions; obtained from nw_bandwidth_mv).
688+
kernel
689+
Kernel function: "gauss", "epan", "unif", "biweight", "triweight", or "cosine".
690+
691+
Returns
692+
-------
693+
NDArray
694+
Predicted values at data_test locations.
695+
696+
Examples
697+
--------
698+
>>> import numpy as np
699+
>>> data = np.random.randn(500, 2)
700+
>>> y = np.sin(data[:, 0]) + 0.5 * data[:, 1] + 0.3 * np.random.randn(500)
701+
>>> h = nw_bandwidth_mv(data, y)
702+
>>> y_pred = nw_predict_mv(data, y, data, h) # in-sample predictions
703+
704+
Notes
705+
-----
706+
This function returns point estimates only. No confidence intervals or
707+
standard errors are provided. Key assumptions: smooth regression function,
708+
IID observations, design density bounded away from zero in region of interest.
709+
Uses product kernel with isotropic bandwidth; data should be standardized
710+
for best results.
711+
712+
For inference, consider bootstrap resampling or see statsmodels.nonparametric.
713+
"""
714+
data_tr = np.asarray(data_train, dtype=float)
715+
y_tr = np.asarray(y_train, dtype=float).ravel()
716+
data_te = np.asarray(data_test, dtype=float)
717+
718+
if data_tr.ndim == 1:
719+
data_tr = data_tr.reshape(-1, 1)
720+
if data_te.ndim == 1:
721+
data_te = data_te.reshape(-1, 1)
722+
723+
if data_tr.ndim != 2:
724+
raise ValueError(f"data_train must be 2D array, got shape {data_tr.shape}")
725+
if data_te.ndim != 2:
726+
raise ValueError(f"data_test must be 2D array, got shape {data_te.shape}")
727+
if len(data_tr) != len(y_tr):
728+
raise ValueError(f"data_train and y_train must have same length, got {len(data_tr)} and {len(y_tr)}")
729+
if data_tr.shape[1] != data_te.shape[1]:
730+
raise ValueError(f"data_train and data_test must have same number of dimensions, got {data_tr.shape[1]} and {data_te.shape[1]}")
731+
if kernel not in _KERNELS:
732+
raise ValueError(f"kernel must be one of {list(_KERNELS.keys())}, got {kernel!r}")
733+
734+
K, _, _, _, _, _ = _KERNELS[kernel]
735+
736+
U = (data_te[:, None, :] - data_tr[None, :, :]) / h
737+
K_vals = K(U)
738+
w = np.prod(K_vals, axis=2)
739+
740+
w_sum = w.sum(axis=1)
741+
w_sum_safe = np.where(w_sum == 0, np.finfo(float).eps, w_sum)
742+
y_pred = (w @ y_tr) / w_sum_safe
743+
744+
zero_weight_mask = w_sum == 0
745+
if np.any(zero_weight_mask):
746+
y_pred[zero_weight_mask] = np.mean(y_tr)
747+
748+
return y_pred
749+
750+
601751
def nw_bandwidth_mv(
602752
data: ArrayLike,
603753
y: ArrayLike,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "hbw"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Fast kernel bandwidth selection via analytic Hessian Newton optimization"
99
readme = "README.md"
1010
license = "MIT"

0 commit comments

Comments
 (0)