debiased-inference 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- debiased_inference-0.1.0/.gitignore +16 -0
- debiased_inference-0.1.0/LICENSE +21 -0
- debiased_inference-0.1.0/PKG-INFO +72 -0
- debiased_inference-0.1.0/README.md +23 -0
- debiased_inference-0.1.0/pyproject.toml +51 -0
- debiased_inference-0.1.0/src/debiased_inference/__init__.py +36 -0
- debiased_inference-0.1.0/src/debiased_inference/_results.py +90 -0
- debiased_inference-0.1.0/src/debiased_inference/_validation.py +81 -0
- debiased_inference-0.1.0/src/debiased_inference/bandwidth.py +144 -0
- debiased_inference-0.1.0/src/debiased_inference/kde.py +163 -0
- debiased_inference-0.1.0/src/debiased_inference/py.typed +1 -0
- debiased_inference-0.1.0/src/debiased_inference/regression.py +167 -0
- debiased_inference-0.1.0/src/debiased_inference/sets.py +453 -0
- debiased_inference-0.1.0/tests/test_kde.py +103 -0
- debiased_inference-0.1.0/tests/test_regression.py +73 -0
- debiased_inference-0.1.0/tests/test_sets.py +175 -0
- debiased_inference-0.1.0/uv.lock +569 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.Rhistory
|
|
3
|
+
.RData
|
|
4
|
+
.Rproj.user/
|
|
5
|
+
*.Rcheck/
|
|
6
|
+
/*.tar.gz
|
|
7
|
+
python/.pytest_cache/
|
|
8
|
+
python/.coverage
|
|
9
|
+
python/.mypy_cache/
|
|
10
|
+
python/.ruff_cache/
|
|
11
|
+
python/.venv/
|
|
12
|
+
python/build/
|
|
13
|
+
python/dist/
|
|
14
|
+
python/src/*.egg-info/
|
|
15
|
+
__pycache__/
|
|
16
|
+
*.py[cod]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gang Cheng and Yen-Chi Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: debiased-inference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bootstrap inference with debiased nonparametric estimators
|
|
5
|
+
Project-URL: Homepage, https://github.com/mathcg/debiased-inference
|
|
6
|
+
Project-URL: Repository, https://github.com/mathcg/debiased-inference.git
|
|
7
|
+
Project-URL: Issues, https://github.com/mathcg/debiased-inference/issues
|
|
8
|
+
Project-URL: Paper, https://doi.org/10.1214/19-EJS1575
|
|
9
|
+
Author: Gang Cheng, Yen-Chi Chen
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Gang Cheng and Yen-Chi Chen
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: bootstrap,confidence-bands,kernel-density,nonparametric-regression
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: numpy>=1.23
|
|
43
|
+
Provides-Extra: dev
|
|
44
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
Requires-Dist: pytest-cov>=4; extra == 'test'
|
|
47
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# debiased-inference (Python)
|
|
51
|
+
|
|
52
|
+
Python implementation of the procedures in Cheng and Chen,
|
|
53
|
+
[“Nonparametric Inference via Bootstrapping the Debiased
|
|
54
|
+
Estimator”](https://projecteuclid.org/journalArticle/Download?urlId=10.1214%2F19-EJS1575).
|
|
55
|
+
|
|
56
|
+
If you use these methods or this software, please cite the paper (Electronic
|
|
57
|
+
Journal of Statistics 13(1), 2019; doi:10.1214/19-EJS1575).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import numpy as np
|
|
61
|
+
from debiased_inference import kde_confidence_band
|
|
62
|
+
|
|
63
|
+
rng = np.random.default_rng(2026)
|
|
64
|
+
sample = rng.normal(size=300)
|
|
65
|
+
band = kde_confidence_band(sample, n_boot=499, random_state=2026)
|
|
66
|
+
print(band)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The package also provides debiased local-linear regression, density level-set
|
|
70
|
+
and inverse-regression confidence sets, normal-reference and cross-validated
|
|
71
|
+
bandwidth selectors, and a studentized density band. The project repository
|
|
72
|
+
contains the full statistical specification and cross-language API guide.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# debiased-inference (Python)
|
|
2
|
+
|
|
3
|
+
Python implementation of the procedures in Cheng and Chen,
|
|
4
|
+
[“Nonparametric Inference via Bootstrapping the Debiased
|
|
5
|
+
Estimator”](https://projecteuclid.org/journalArticle/Download?urlId=10.1214%2F19-EJS1575).
|
|
6
|
+
|
|
7
|
+
If you use these methods or this software, please cite the paper (Electronic
|
|
8
|
+
Journal of Statistics 13(1), 2019; doi:10.1214/19-EJS1575).
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import numpy as np
|
|
12
|
+
from debiased_inference import kde_confidence_band
|
|
13
|
+
|
|
14
|
+
rng = np.random.default_rng(2026)
|
|
15
|
+
sample = rng.normal(size=300)
|
|
16
|
+
band = kde_confidence_band(sample, n_boot=499, random_state=2026)
|
|
17
|
+
print(band)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The package also provides debiased local-linear regression, density level-set
|
|
21
|
+
and inverse-regression confidence sets, normal-reference and cross-validated
|
|
22
|
+
bandwidth selectors, and a studentized density band. The project repository
|
|
23
|
+
contains the full statistical specification and cross-language API guide.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "debiased-inference"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bootstrap inference with debiased nonparametric estimators"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Gang Cheng"},
|
|
14
|
+
{name = "Yen-Chi Chen"},
|
|
15
|
+
]
|
|
16
|
+
keywords = ["bootstrap", "confidence-bands", "kernel-density", "nonparametric-regression"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["numpy>=1.23"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7", "pytest-cov>=4"]
|
|
31
|
+
dev = ["ruff>=0.6"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/mathcg/debiased-inference"
|
|
35
|
+
Repository = "https://github.com/mathcg/debiased-inference.git"
|
|
36
|
+
Issues = "https://github.com/mathcg/debiased-inference/issues"
|
|
37
|
+
Paper = "https://doi.org/10.1214/19-EJS1575"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/debiased_inference"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build]
|
|
43
|
+
exclude = ["/.coverage"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
addopts = "-ra --strict-markers"
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py310"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Bootstrap inference with debiased nonparametric estimators."""
|
|
2
|
+
|
|
3
|
+
from ._results import ConfidenceBandResult, EstimateResult, SetEstimateResult
|
|
4
|
+
from .bandwidth import density_bandwidth, regression_bandwidth
|
|
5
|
+
from .kde import debiased_kde, kde_confidence_band
|
|
6
|
+
from .regression import debiased_local_linear, regression_confidence_band
|
|
7
|
+
from .sets import (
|
|
8
|
+
density_level_set,
|
|
9
|
+
density_level_set_confidence,
|
|
10
|
+
hausdorff_distance,
|
|
11
|
+
inverse_regression,
|
|
12
|
+
inverse_regression_confidence,
|
|
13
|
+
invert_confidence_band,
|
|
14
|
+
level_set,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ConfidenceBandResult",
|
|
19
|
+
"EstimateResult",
|
|
20
|
+
"SetEstimateResult",
|
|
21
|
+
"debiased_kde",
|
|
22
|
+
"debiased_local_linear",
|
|
23
|
+
"density_bandwidth",
|
|
24
|
+
"density_level_set",
|
|
25
|
+
"density_level_set_confidence",
|
|
26
|
+
"hausdorff_distance",
|
|
27
|
+
"inverse_regression",
|
|
28
|
+
"inverse_regression_confidence",
|
|
29
|
+
"invert_confidence_band",
|
|
30
|
+
"kde_confidence_band",
|
|
31
|
+
"level_set",
|
|
32
|
+
"regression_bandwidth",
|
|
33
|
+
"regression_confidence_band",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Immutable result containers for the public API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
FloatArray = NDArray[np.float64]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class EstimateResult:
|
|
15
|
+
"""A nonparametric estimate evaluated on a finite grid."""
|
|
16
|
+
|
|
17
|
+
points: FloatArray
|
|
18
|
+
estimate: FloatArray
|
|
19
|
+
bandwidth: float
|
|
20
|
+
tau: float
|
|
21
|
+
method: str
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return (
|
|
25
|
+
f"EstimateResult(method={self.method!r}, points={len(self.points)}, "
|
|
26
|
+
f"bandwidth={self.bandwidth:.6g}, tau={self.tau:.6g})"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ConfidenceBandResult:
|
|
32
|
+
"""A simultaneous bootstrap confidence band on a finite grid."""
|
|
33
|
+
|
|
34
|
+
points: FloatArray
|
|
35
|
+
estimate: FloatArray
|
|
36
|
+
lower: FloatArray
|
|
37
|
+
upper: FloatArray
|
|
38
|
+
critical_value: float
|
|
39
|
+
bandwidth: float
|
|
40
|
+
tau: float
|
|
41
|
+
confidence: float
|
|
42
|
+
n_boot: int
|
|
43
|
+
bootstrap_statistics: FloatArray
|
|
44
|
+
method: str
|
|
45
|
+
studentized: bool = False
|
|
46
|
+
standard_error: FloatArray | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def width(self) -> FloatArray:
|
|
50
|
+
"""Pointwise width of the simultaneous band."""
|
|
51
|
+
return self.upper - self.lower
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return (
|
|
55
|
+
f"ConfidenceBandResult(method={self.method!r}, points={len(self.points)}, "
|
|
56
|
+
f"confidence={self.confidence:.3g}, n_boot={self.n_boot}, "
|
|
57
|
+
f"bandwidth={self.bandwidth:.6g})"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class SetEstimateResult:
|
|
63
|
+
"""A grid-based estimate or confidence region for a level set.
|
|
64
|
+
|
|
65
|
+
``roots`` is a vector for a one-dimensional level set and an ``(k, 2)``
|
|
66
|
+
contour point cloud for a two-dimensional level set.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
points: FloatArray
|
|
70
|
+
mask: NDArray[np.bool_]
|
|
71
|
+
roots: FloatArray
|
|
72
|
+
level: float
|
|
73
|
+
method: str
|
|
74
|
+
radius: float | None = None
|
|
75
|
+
confidence: float | None = None
|
|
76
|
+
n_boot: int | None = None
|
|
77
|
+
bootstrap_statistics: FloatArray | None = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def geometry(self) -> FloatArray:
|
|
81
|
+
"""Return the interpolated level-set geometry."""
|
|
82
|
+
return self.roots
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
size = len(self.roots)
|
|
86
|
+
detail = f", radius={self.radius:.6g}" if self.radius is not None else ""
|
|
87
|
+
return (
|
|
88
|
+
f"SetEstimateResult(method={self.method!r}, level={self.level:.6g}, "
|
|
89
|
+
f"geometry_points={size}{detail})"
|
|
90
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Shared input validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike, NDArray
|
|
7
|
+
|
|
8
|
+
FloatArray = NDArray[np.float64]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def as_samples(x: ArrayLike, *, name: str = "x") -> FloatArray:
|
|
12
|
+
"""Return observations as an ``(n, d)`` float array."""
|
|
13
|
+
values = np.asarray(x, dtype=float)
|
|
14
|
+
if values.ndim == 1:
|
|
15
|
+
values = values[:, None]
|
|
16
|
+
if values.ndim != 2 or values.shape[0] < 2 or values.shape[1] < 1:
|
|
17
|
+
raise ValueError(f"{name} must contain at least two observations")
|
|
18
|
+
if not np.all(np.isfinite(values)):
|
|
19
|
+
raise ValueError(f"{name} must contain only finite values")
|
|
20
|
+
if np.all(np.ptp(values, axis=0) == 0):
|
|
21
|
+
raise ValueError(f"{name} must contain at least two distinct observations")
|
|
22
|
+
return values
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def as_vector(x: ArrayLike, *, name: str) -> FloatArray:
|
|
26
|
+
"""Return a finite, non-empty one-dimensional float array."""
|
|
27
|
+
values = np.asarray(x, dtype=float)
|
|
28
|
+
if values.ndim != 1 or values.size == 0:
|
|
29
|
+
raise ValueError(f"{name} must be a non-empty one-dimensional array")
|
|
30
|
+
if not np.all(np.isfinite(values)):
|
|
31
|
+
raise ValueError(f"{name} must contain only finite values")
|
|
32
|
+
return values
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def as_evaluation_points(
|
|
36
|
+
points: ArrayLike | None, samples: FloatArray, *, grid_size: int = 200
|
|
37
|
+
) -> FloatArray:
|
|
38
|
+
"""Validate or generate evaluation points with the samples' dimension."""
|
|
39
|
+
dimension = samples.shape[1]
|
|
40
|
+
if points is None:
|
|
41
|
+
if dimension != 1:
|
|
42
|
+
raise ValueError("evaluation points are required for multivariate data")
|
|
43
|
+
if not isinstance(grid_size, (int, np.integer)) or grid_size < 2:
|
|
44
|
+
raise ValueError("grid_size must be an integer of at least 2")
|
|
45
|
+
spread = float(np.std(samples[:, 0], ddof=1))
|
|
46
|
+
pad = 0.05 * max(float(np.ptp(samples[:, 0])), spread)
|
|
47
|
+
if pad == 0:
|
|
48
|
+
pad = 1.0
|
|
49
|
+
return np.linspace(samples[:, 0].min() - pad, samples[:, 0].max() + pad, grid_size)[:, None]
|
|
50
|
+
|
|
51
|
+
result = np.asarray(points, dtype=float)
|
|
52
|
+
if dimension == 1 and result.ndim == 1:
|
|
53
|
+
result = result[:, None]
|
|
54
|
+
if result.ndim != 2 or result.shape[1] != dimension or result.shape[0] == 0:
|
|
55
|
+
raise ValueError(f"evaluation points must have shape (m, {dimension})")
|
|
56
|
+
if not np.all(np.isfinite(result)):
|
|
57
|
+
raise ValueError("evaluation points must contain only finite values")
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def positive_scalar(value: float, *, name: str) -> float:
|
|
62
|
+
"""Validate a finite positive scalar."""
|
|
63
|
+
result = float(value)
|
|
64
|
+
if not np.isfinite(result) or result <= 0:
|
|
65
|
+
raise ValueError(f"{name} must be a finite positive scalar")
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def bootstrap_parameters(confidence: float, n_boot: int) -> tuple[float, int]:
|
|
70
|
+
"""Validate confidence level and bootstrap count."""
|
|
71
|
+
confidence = float(confidence)
|
|
72
|
+
if not 0 < confidence < 1:
|
|
73
|
+
raise ValueError("confidence must be strictly between 0 and 1")
|
|
74
|
+
if not isinstance(n_boot, (int, np.integer)) or n_boot < 1:
|
|
75
|
+
raise ValueError("n_boot must be a positive integer")
|
|
76
|
+
return confidence, int(n_boot)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def public_points(points: FloatArray) -> FloatArray:
|
|
80
|
+
"""Represent one-dimensional grids as vectors in public results."""
|
|
81
|
+
return points[:, 0].copy() if points.shape[1] == 1 else points.copy()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Bandwidth selectors for ordinary KDE and local-linear regression."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike
|
|
7
|
+
|
|
8
|
+
from ._validation import as_samples, as_vector, positive_scalar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def density_bandwidth(
|
|
12
|
+
x: ArrayLike,
|
|
13
|
+
*,
|
|
14
|
+
method: str = "normal_reference",
|
|
15
|
+
candidates: ArrayLike | None = None,
|
|
16
|
+
block_size: int = 512,
|
|
17
|
+
) -> float:
|
|
18
|
+
"""Select an isotropic bandwidth for the ordinary KDE.
|
|
19
|
+
|
|
20
|
+
``method="normal_reference"`` uses Silverman's robust rule
|
|
21
|
+
``0.9 min(sd, IQR / 1.34) n^(-1/5)``. In higher dimensions the scalar
|
|
22
|
+
scale is the geometric mean of positive marginal robust scales and the
|
|
23
|
+
normal-reference dimension adjustment is used.
|
|
24
|
+
|
|
25
|
+
``method="cv"`` minimizes the least-squares cross-validation criterion
|
|
26
|
+
over ``candidates``. If candidates are omitted, a geometric grid centered
|
|
27
|
+
on the normal-reference choice is used. Pairwise calculations are blocked
|
|
28
|
+
to use ``O(n * block_size)`` rather than ``O(n^2)`` memory.
|
|
29
|
+
"""
|
|
30
|
+
samples = as_samples(x)
|
|
31
|
+
if method not in {"normal_reference", "cv"}:
|
|
32
|
+
raise ValueError("method must be 'normal_reference' or 'cv'")
|
|
33
|
+
n, dimension = samples.shape
|
|
34
|
+
standard_deviation = np.std(samples, axis=0, ddof=1)
|
|
35
|
+
quartiles = np.percentile(samples, [25, 75], axis=0)
|
|
36
|
+
robust = (quartiles[1] - quartiles[0]) / 1.34
|
|
37
|
+
scales = np.minimum(standard_deviation, robust)
|
|
38
|
+
scales = np.where(scales > 0, scales, standard_deviation)
|
|
39
|
+
positive = scales[scales > 0]
|
|
40
|
+
if positive.size == 0:
|
|
41
|
+
raise ValueError("cannot select bandwidth from zero-scale data")
|
|
42
|
+
scale = float(np.exp(np.mean(np.log(positive))))
|
|
43
|
+
if dimension == 1:
|
|
44
|
+
factor = 0.9 * n ** (-1.0 / 5.0)
|
|
45
|
+
else:
|
|
46
|
+
factor = (4.0 / (dimension + 2.0)) ** (1.0 / (dimension + 4.0))
|
|
47
|
+
factor *= n ** (-1.0 / (dimension + 4.0))
|
|
48
|
+
reference = positive_scalar(scale * factor, name="selected bandwidth")
|
|
49
|
+
if method == "normal_reference":
|
|
50
|
+
if candidates is not None:
|
|
51
|
+
raise ValueError("candidates are only used when method='cv'")
|
|
52
|
+
return reference
|
|
53
|
+
|
|
54
|
+
if candidates is None:
|
|
55
|
+
candidate_values = reference * np.geomspace(0.35, 2.5, 31)
|
|
56
|
+
else:
|
|
57
|
+
candidate_values = as_vector(candidates, name="candidates")
|
|
58
|
+
if np.any(candidate_values <= 0):
|
|
59
|
+
raise ValueError("candidates must be positive")
|
|
60
|
+
if not isinstance(block_size, (int, np.integer)) or block_size < 1:
|
|
61
|
+
raise ValueError("block_size must be a positive integer")
|
|
62
|
+
scores = np.empty(candidate_values.size, dtype=float)
|
|
63
|
+
for index, candidate in enumerate(candidate_values):
|
|
64
|
+
h = float(candidate)
|
|
65
|
+
integrated_normalizer = (4.0 * np.pi * h**2) ** (-0.5 * dimension)
|
|
66
|
+
ordinary_normalizer = (2.0 * np.pi * h**2) ** (-0.5 * dimension)
|
|
67
|
+
integrated_sum = 0.0
|
|
68
|
+
ordinary_sum = 0.0
|
|
69
|
+
for start in range(0, n, int(block_size)):
|
|
70
|
+
stop = min(start + int(block_size), n)
|
|
71
|
+
differences = samples[start:stop, None, :] - samples[None, :, :]
|
|
72
|
+
distances_squared = np.sum(differences**2, axis=-1)
|
|
73
|
+
integrated_sum += integrated_normalizer * float(
|
|
74
|
+
np.sum(np.exp(-distances_squared / (4.0 * h**2)))
|
|
75
|
+
)
|
|
76
|
+
ordinary_sum += ordinary_normalizer * float(
|
|
77
|
+
np.sum(np.exp(-distances_squared / (2.0 * h**2)))
|
|
78
|
+
)
|
|
79
|
+
ordinary_off_diagonal = ordinary_sum - n * ordinary_normalizer
|
|
80
|
+
scores[index] = (
|
|
81
|
+
integrated_sum / n**2
|
|
82
|
+
- 2.0 * ordinary_off_diagonal / (n * (n - 1))
|
|
83
|
+
)
|
|
84
|
+
return float(candidate_values[int(np.argmin(scores))])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def regression_bandwidth(
|
|
88
|
+
x: ArrayLike,
|
|
89
|
+
y: ArrayLike,
|
|
90
|
+
*,
|
|
91
|
+
candidates: ArrayLike | None = None,
|
|
92
|
+
n_folds: int = 5,
|
|
93
|
+
random_state: int | None = 0,
|
|
94
|
+
) -> float:
|
|
95
|
+
"""Select a local-linear bandwidth by deterministic K-fold CV.
|
|
96
|
+
|
|
97
|
+
Candidate loss is mean squared prediction error from the ordinary
|
|
98
|
+
local-linear smoother. Failed boundary fits are excluded; a candidate is
|
|
99
|
+
eligible only if every held-out point can be predicted.
|
|
100
|
+
"""
|
|
101
|
+
from .regression import _local_polynomial
|
|
102
|
+
|
|
103
|
+
x_values = as_vector(x, name="x")
|
|
104
|
+
y_values = as_vector(y, name="y")
|
|
105
|
+
if x_values.size != y_values.size:
|
|
106
|
+
raise ValueError("x and y must have the same length")
|
|
107
|
+
if np.ptp(x_values) == 0:
|
|
108
|
+
raise ValueError("x must contain at least two distinct values")
|
|
109
|
+
n = x_values.size
|
|
110
|
+
if not isinstance(n_folds, (int, np.integer)) or not 2 <= n_folds <= n:
|
|
111
|
+
raise ValueError("n_folds must be an integer between 2 and len(x)")
|
|
112
|
+
|
|
113
|
+
if candidates is None:
|
|
114
|
+
scale = min(float(np.std(x_values, ddof=1)), float(np.ptp(x_values)) / 4.0)
|
|
115
|
+
base = max(scale * n ** (-1.0 / 5.0), np.finfo(float).eps)
|
|
116
|
+
candidate_values = base * np.geomspace(0.35, 2.5, 21)
|
|
117
|
+
else:
|
|
118
|
+
candidate_values = as_vector(candidates, name="candidates")
|
|
119
|
+
if np.any(candidate_values <= 0):
|
|
120
|
+
raise ValueError("candidates must be positive")
|
|
121
|
+
|
|
122
|
+
generator = np.random.default_rng(random_state)
|
|
123
|
+
order = generator.permutation(n)
|
|
124
|
+
folds = np.array_split(order, n_folds)
|
|
125
|
+
losses = np.full(candidate_values.size, np.inf)
|
|
126
|
+
for candidate_index, candidate in enumerate(candidate_values):
|
|
127
|
+
squared_errors: list[np.ndarray] = []
|
|
128
|
+
valid = True
|
|
129
|
+
for held_out in folds:
|
|
130
|
+
keep = np.ones(n, dtype=bool)
|
|
131
|
+
keep[held_out] = False
|
|
132
|
+
predicted = _local_polynomial(
|
|
133
|
+
x_values[keep], y_values[keep], x_values[held_out],
|
|
134
|
+
float(candidate), degree=1, derivative=0,
|
|
135
|
+
)
|
|
136
|
+
if np.any(~np.isfinite(predicted)):
|
|
137
|
+
valid = False
|
|
138
|
+
break
|
|
139
|
+
squared_errors.append((y_values[held_out] - predicted) ** 2)
|
|
140
|
+
if valid:
|
|
141
|
+
losses[candidate_index] = float(np.mean(np.concatenate(squared_errors)))
|
|
142
|
+
if not np.any(np.isfinite(losses)):
|
|
143
|
+
raise RuntimeError("all candidate bandwidths produced singular local fits")
|
|
144
|
+
return float(candidate_values[int(np.argmin(losses))])
|