wknn_1 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.
- wknn_1-0.1.0/.gitignore +28 -0
- wknn_1-0.1.0/.python-version +1 -0
- wknn_1-0.1.0/LICENSE +21 -0
- wknn_1-0.1.0/PKG-INFO +157 -0
- wknn_1-0.1.0/README.md +134 -0
- wknn_1-0.1.0/pyproject.toml +66 -0
- wknn_1-0.1.0/requirements-dev.txt +5 -0
- wknn_1-0.1.0/requirements.txt +13 -0
- wknn_1-0.1.0/src/wknn_1/__init__.py +46 -0
- wknn_1-0.1.0/src/wknn_1/_types.py +34 -0
- wknn_1-0.1.0/src/wknn_1/estimator.py +138 -0
- wknn_1-0.1.0/src/wknn_1/exceptions.py +31 -0
- wknn_1-0.1.0/src/wknn_1/mixins/__init__.py +15 -0
- wknn_1-0.1.0/src/wknn_1/mixins/persistence_mixin.py +125 -0
- wknn_1-0.1.0/src/wknn_1/mixins/prediction_mixin.py +57 -0
- wknn_1-0.1.0/src/wknn_1/mixins/preprocessing_mixin.py +29 -0
- wknn_1-0.1.0/src/wknn_1/mixins/validation_mixin.py +18 -0
- wknn_1-0.1.0/src/wknn_1/persistence.py +129 -0
- wknn_1-0.1.0/src/wknn_1/preprocessing.py +99 -0
- wknn_1-0.1.0/src/wknn_1/py.typed +0 -0
- wknn_1-0.1.0/src/wknn_1/validation.py +99 -0
- wknn_1-0.1.0/src/wknn_1/weights/__init__.py +30 -0
- wknn_1-0.1.0/src/wknn_1/weights/base.py +58 -0
- wknn_1-0.1.0/src/wknn_1/weights/mixed.py +155 -0
- wknn_1-0.1.0/src/wknn_1/weights/registry.py +97 -0
- wknn_1-0.1.0/src/wknn_1/weights/standard.py +120 -0
- wknn_1-0.1.0/tests/__init__.py +0 -0
- wknn_1-0.1.0/tests/test_estimator.py +107 -0
- wknn_1-0.1.0/tests/test_persistence.py +61 -0
- wknn_1-0.1.0/tests/test_validation.py +57 -0
- wknn_1-0.1.0/tests/test_weights.py +101 -0
- wknn_1-0.1.0/uv.lock +992 -0
wknn_1-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# uv / venv
|
|
10
|
+
.venv/
|
|
11
|
+
uv.lock.bak
|
|
12
|
+
|
|
13
|
+
# Test / cache
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# Saved models produced by examples/tests
|
|
20
|
+
*.npz
|
|
21
|
+
*.pkl
|
|
22
|
+
*.pickle
|
|
23
|
+
model.json
|
|
24
|
+
|
|
25
|
+
# OS / editor
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
wknn_1-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrey Ferubko, Oleg Kazakov
|
|
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.
|
wknn_1-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wknn_1
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: scikit-learn compatible weighted k-Nearest-Neighbours regressor
|
|
5
|
+
Project-URL: Homepage, https://example.com/your-username/wknn_1
|
|
6
|
+
Project-URL: Repository, https://example.com/your-username/wknn_1
|
|
7
|
+
Author-email: Your Name <you@example.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: knn,machine-learning,regression,scikit-learn,weighted-knn
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: <3.15,>=3.8
|
|
20
|
+
Requires-Dist: numpy>=1.21
|
|
21
|
+
Requires-Dist: scikit-learn>=1.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# wknn_1
|
|
25
|
+
|
|
26
|
+
A scikit-learn compatible **weighted k-Nearest-Neighbours regressor**.
|
|
27
|
+
|
|
28
|
+
> This README documents how to run, develop and deploy the library, with usage
|
|
29
|
+
> examples. A richer version with links to the underlying scientific papers
|
|
30
|
+
> will follow.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Drop-in scikit-learn estimator (`BaseEstimator` + `RegressorMixin`): works
|
|
35
|
+
with `Pipeline`, `clone`, `GridSearchCV`, `get_params`/`set_params`, `score`.
|
|
36
|
+
- Flexible `weights` parameter:
|
|
37
|
+
- **named** standard functions: `linear` (default), `inverse`/`distance`,
|
|
38
|
+
`exponential`, `gaussian`, `uniform`;
|
|
39
|
+
- a **custom callable**;
|
|
40
|
+
- a **`MixedWeightFunction`** with a *different formula per predictor
|
|
41
|
+
dimension* (e.g. linear decrease on dim 0, exponential growth on dim 1);
|
|
42
|
+
- a **per-rank weight vector** (list or ndarray, coerced to numpy).
|
|
43
|
+
- Standard weight functions standardize their inputs internally (centering /
|
|
44
|
+
scaling / normalization), so they are robust to non-standardized data.
|
|
45
|
+
- Predictors and responses must be `numpy.ndarray`; anything else raises an
|
|
46
|
+
informative `InvalidInputError`.
|
|
47
|
+
- Save / load trained weights in **npz**, **pickle** and **json** to move a
|
|
48
|
+
trained model between machines.
|
|
49
|
+
- `refit` for retraining and `delete` for tearing the model down (state +
|
|
50
|
+
on-disk files + metadata) — handy after training on the wrong data.
|
|
51
|
+
- Fully typed (`py.typed` ships with the package). Python 3.8 – 3.14.
|
|
52
|
+
|
|
53
|
+
## Install & run (uv)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# from the project root, in the uv-managed virtual environment
|
|
57
|
+
uv sync # create .venv and install deps + dev tools
|
|
58
|
+
uv run python -c "import wknn_1; print(wknn_1.__version__)"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import numpy as np
|
|
65
|
+
from wknn_1 import WKNNRegressor
|
|
66
|
+
|
|
67
|
+
X = np.random.default_rng(0).normal(size=(200, 3))
|
|
68
|
+
y = X @ np.array([2.0, -1.5, 0.5])
|
|
69
|
+
|
|
70
|
+
model = WKNNRegressor(n_neighbors=8, weights="linear").fit(X[:160], y[:160])
|
|
71
|
+
preds = model.predict(X[160:])
|
|
72
|
+
print(model.score(X[160:], y[160:]))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Per-dimension (mixed) weighting
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import numpy as np
|
|
79
|
+
from wknn_1 import WKNNRegressor, MixedWeightFunction
|
|
80
|
+
|
|
81
|
+
mixed = MixedWeightFunction(
|
|
82
|
+
[lambda d: np.clip(1.0 - d, 0.0, None), # linear decrease on dim 0
|
|
83
|
+
lambda d: np.exp(d)], # exponential growth on dim 1
|
|
84
|
+
aggregate="product",
|
|
85
|
+
)
|
|
86
|
+
model = WKNNRegressor(n_neighbors=6, weights=mixed).fit(X2d, y)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Per-rank weight vector
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# the i-th nearest neighbour always gets weight vec[i]
|
|
93
|
+
model = WKNNRegressor(n_neighbors=4, weights=[1.0, 0.5, 0.25, 0.1]).fit(X, y)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Custom callable
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
def gaussian_kernel(deltas): # deltas: (..., k, n_features)
|
|
100
|
+
dist = np.sqrt((deltas ** 2).sum(axis=-1))
|
|
101
|
+
return np.exp(-(dist ** 2)) # -> (..., k)
|
|
102
|
+
|
|
103
|
+
model = WKNNRegressor(weights=gaussian_kernel).fit(X, y)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Save, transfer, load, retrain, delete
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
model.save_weights("model.npz") # or .pkl / .json
|
|
110
|
+
restored = WKNNRegressor().load_weights("model.npz")
|
|
111
|
+
|
|
112
|
+
model.refit(X_new, y_new) # retrain from scratch
|
|
113
|
+
model.delete() # wipe state + files + metadata
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> Custom callable weight functions cannot be stored in `npz`/`json`; use
|
|
117
|
+
> `pickle`, or re-supply the callable via `set_params(weights=...)` after load.
|
|
118
|
+
|
|
119
|
+
### Extending the standard functions
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from wknn_1 import register_weight_function, StandardWeightFunction
|
|
123
|
+
|
|
124
|
+
register_weight_function(
|
|
125
|
+
"epanechnikov",
|
|
126
|
+
lambda: StandardWeightFunction("linear", normalize=True),
|
|
127
|
+
)
|
|
128
|
+
WKNNRegressor(weights="epanechnikov")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv run ruff check . # lint
|
|
135
|
+
uv run ruff format . # format
|
|
136
|
+
uv run pytest # tests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Deploy
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv build # builds sdist + wheel into dist/
|
|
143
|
+
# uv publish # later: upload to PyPI after testing
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Project layout
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
src/wknn_1/
|
|
150
|
+
estimator.py WKNNRegressor (composes the mixins below)
|
|
151
|
+
validation.py numpy-only input checks
|
|
152
|
+
preprocessing.py standardization helpers
|
|
153
|
+
persistence.py npz / pickle / json save & load
|
|
154
|
+
weights/ base, standard, mixed/fixed/rank, registry
|
|
155
|
+
mixins/ validation / preprocessing / prediction / persistence
|
|
156
|
+
tests/ pytest suite
|
|
157
|
+
```
|
wknn_1-0.1.0/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# wknn_1
|
|
2
|
+
|
|
3
|
+
A scikit-learn compatible **weighted k-Nearest-Neighbours regressor**.
|
|
4
|
+
|
|
5
|
+
> This README documents how to run, develop and deploy the library, with usage
|
|
6
|
+
> examples. A richer version with links to the underlying scientific papers
|
|
7
|
+
> will follow.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Drop-in scikit-learn estimator (`BaseEstimator` + `RegressorMixin`): works
|
|
12
|
+
with `Pipeline`, `clone`, `GridSearchCV`, `get_params`/`set_params`, `score`.
|
|
13
|
+
- Flexible `weights` parameter:
|
|
14
|
+
- **named** standard functions: `linear` (default), `inverse`/`distance`,
|
|
15
|
+
`exponential`, `gaussian`, `uniform`;
|
|
16
|
+
- a **custom callable**;
|
|
17
|
+
- a **`MixedWeightFunction`** with a *different formula per predictor
|
|
18
|
+
dimension* (e.g. linear decrease on dim 0, exponential growth on dim 1);
|
|
19
|
+
- a **per-rank weight vector** (list or ndarray, coerced to numpy).
|
|
20
|
+
- Standard weight functions standardize their inputs internally (centering /
|
|
21
|
+
scaling / normalization), so they are robust to non-standardized data.
|
|
22
|
+
- Predictors and responses must be `numpy.ndarray`; anything else raises an
|
|
23
|
+
informative `InvalidInputError`.
|
|
24
|
+
- Save / load trained weights in **npz**, **pickle** and **json** to move a
|
|
25
|
+
trained model between machines.
|
|
26
|
+
- `refit` for retraining and `delete` for tearing the model down (state +
|
|
27
|
+
on-disk files + metadata) — handy after training on the wrong data.
|
|
28
|
+
- Fully typed (`py.typed` ships with the package). Python 3.8 – 3.14.
|
|
29
|
+
|
|
30
|
+
## Install & run (uv)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# from the project root, in the uv-managed virtual environment
|
|
34
|
+
uv sync # create .venv and install deps + dev tools
|
|
35
|
+
uv run python -c "import wknn_1; print(wknn_1.__version__)"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import numpy as np
|
|
42
|
+
from wknn_1 import WKNNRegressor
|
|
43
|
+
|
|
44
|
+
X = np.random.default_rng(0).normal(size=(200, 3))
|
|
45
|
+
y = X @ np.array([2.0, -1.5, 0.5])
|
|
46
|
+
|
|
47
|
+
model = WKNNRegressor(n_neighbors=8, weights="linear").fit(X[:160], y[:160])
|
|
48
|
+
preds = model.predict(X[160:])
|
|
49
|
+
print(model.score(X[160:], y[160:]))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Per-dimension (mixed) weighting
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import numpy as np
|
|
56
|
+
from wknn_1 import WKNNRegressor, MixedWeightFunction
|
|
57
|
+
|
|
58
|
+
mixed = MixedWeightFunction(
|
|
59
|
+
[lambda d: np.clip(1.0 - d, 0.0, None), # linear decrease on dim 0
|
|
60
|
+
lambda d: np.exp(d)], # exponential growth on dim 1
|
|
61
|
+
aggregate="product",
|
|
62
|
+
)
|
|
63
|
+
model = WKNNRegressor(n_neighbors=6, weights=mixed).fit(X2d, y)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Per-rank weight vector
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# the i-th nearest neighbour always gets weight vec[i]
|
|
70
|
+
model = WKNNRegressor(n_neighbors=4, weights=[1.0, 0.5, 0.25, 0.1]).fit(X, y)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom callable
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
def gaussian_kernel(deltas): # deltas: (..., k, n_features)
|
|
77
|
+
dist = np.sqrt((deltas ** 2).sum(axis=-1))
|
|
78
|
+
return np.exp(-(dist ** 2)) # -> (..., k)
|
|
79
|
+
|
|
80
|
+
model = WKNNRegressor(weights=gaussian_kernel).fit(X, y)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Save, transfer, load, retrain, delete
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
model.save_weights("model.npz") # or .pkl / .json
|
|
87
|
+
restored = WKNNRegressor().load_weights("model.npz")
|
|
88
|
+
|
|
89
|
+
model.refit(X_new, y_new) # retrain from scratch
|
|
90
|
+
model.delete() # wipe state + files + metadata
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> Custom callable weight functions cannot be stored in `npz`/`json`; use
|
|
94
|
+
> `pickle`, or re-supply the callable via `set_params(weights=...)` after load.
|
|
95
|
+
|
|
96
|
+
### Extending the standard functions
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from wknn_1 import register_weight_function, StandardWeightFunction
|
|
100
|
+
|
|
101
|
+
register_weight_function(
|
|
102
|
+
"epanechnikov",
|
|
103
|
+
lambda: StandardWeightFunction("linear", normalize=True),
|
|
104
|
+
)
|
|
105
|
+
WKNNRegressor(weights="epanechnikov")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
uv run ruff check . # lint
|
|
112
|
+
uv run ruff format . # format
|
|
113
|
+
uv run pytest # tests
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Deploy
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv build # builds sdist + wheel into dist/
|
|
120
|
+
# uv publish # later: upload to PyPI after testing
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Project layout
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
src/wknn_1/
|
|
127
|
+
estimator.py WKNNRegressor (composes the mixins below)
|
|
128
|
+
validation.py numpy-only input checks
|
|
129
|
+
preprocessing.py standardization helpers
|
|
130
|
+
persistence.py npz / pickle / json save & load
|
|
131
|
+
weights/ base, standard, mixed/fixed/rank, registry
|
|
132
|
+
mixins/ validation / preprocessing / prediction / persistence
|
|
133
|
+
tests/ pytest suite
|
|
134
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wknn_1"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "scikit-learn compatible weighted k-Nearest-Neighbours regressor"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8,<3.15"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
|
9
|
+
keywords = ["knn", "weighted-knn", "regression", "scikit-learn", "machine-learning"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3.8",
|
|
12
|
+
"Programming Language :: Python :: 3.9",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=1.21",
|
|
22
|
+
"scikit-learn>=1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://example.com/your-username/wknn_1"
|
|
27
|
+
Repository = "https://example.com/your-username/wknn_1"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"ruff>=0.6",
|
|
32
|
+
"pytest>=7.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/wknn_1"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py38"
|
|
45
|
+
src = ["src", "tests"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = [
|
|
49
|
+
"E", # pycodestyle errors
|
|
50
|
+
"F", # pyflakes
|
|
51
|
+
"I", # isort
|
|
52
|
+
"UP", # pyupgrade
|
|
53
|
+
"B", # flake8-bugbear
|
|
54
|
+
"C4", # comprehensions
|
|
55
|
+
"SIM", # simplify
|
|
56
|
+
]
|
|
57
|
+
ignore = [
|
|
58
|
+
"UP006", "UP007", "UP035", # keep typing.X aliases for 3.8 compatibility
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"tests/*" = ["B018"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
addopts = "-q"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
iniconfig==2.3.0
|
|
2
|
+
joblib==1.5.3
|
|
3
|
+
narwhals==2.22.0
|
|
4
|
+
numpy==2.4.6
|
|
5
|
+
packaging==26.2
|
|
6
|
+
pluggy==1.6.0
|
|
7
|
+
pygments==2.20.0
|
|
8
|
+
pytest==9.0.3
|
|
9
|
+
ruff==0.15.16
|
|
10
|
+
scikit-learn==1.9.0
|
|
11
|
+
scipy==1.17.1
|
|
12
|
+
threadpoolctl==3.6.0
|
|
13
|
+
-e file:///home/anmr/%D0%A0%D0%B0%D0%B1%D0%BE%D1%87%D0%B8%D0%B9%20%D1%81%D1%82%D0%BE%D0%BB/%D0%BC%D0%BE%D0%B8%20%D0%BB%D0%B0%D0%BF%D0%BE%D0%BD%D1%8C%D0%BA%D0%B8/wknn_1
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""wknn_1: a scikit-learn compatible weighted k-Nearest-Neighbours regressor.
|
|
2
|
+
|
|
3
|
+
The public surface is intentionally small. The estimator lives in
|
|
4
|
+
:mod:`wknn_1.estimator`; the weighting machinery lives in :mod:`wknn_1.weights`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .estimator import WKNNRegressor
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
InvalidInputError,
|
|
12
|
+
NotFittedError,
|
|
13
|
+
PersistenceError,
|
|
14
|
+
WeightFunctionError,
|
|
15
|
+
WKNNError,
|
|
16
|
+
)
|
|
17
|
+
from .weights import (
|
|
18
|
+
BaseWeightFunction,
|
|
19
|
+
FixedFeatureWeightFunction,
|
|
20
|
+
MixedWeightFunction,
|
|
21
|
+
RankWeightFunction,
|
|
22
|
+
StandardWeightFunction,
|
|
23
|
+
get_weight_function,
|
|
24
|
+
list_weight_functions,
|
|
25
|
+
register_weight_function,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"WKNNRegressor",
|
|
32
|
+
"BaseWeightFunction",
|
|
33
|
+
"StandardWeightFunction",
|
|
34
|
+
"MixedWeightFunction",
|
|
35
|
+
"FixedFeatureWeightFunction",
|
|
36
|
+
"RankWeightFunction",
|
|
37
|
+
"get_weight_function",
|
|
38
|
+
"register_weight_function",
|
|
39
|
+
"list_weight_functions",
|
|
40
|
+
"WKNNError",
|
|
41
|
+
"InvalidInputError",
|
|
42
|
+
"NotFittedError",
|
|
43
|
+
"WeightFunctionError",
|
|
44
|
+
"PersistenceError",
|
|
45
|
+
"__version__",
|
|
46
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Type aliases shared across the package.
|
|
2
|
+
|
|
3
|
+
We rely on ``from __future__ import annotations`` everywhere so that modern
|
|
4
|
+
typing syntax stays valid as plain strings on Python 3.8. The runtime aliases
|
|
5
|
+
defined here only use constructs available since numpy 1.21.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Callable, Sequence, Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
#: A floating point numpy array (the canonical internal representation).
|
|
16
|
+
FloatArray = npt.NDArray[np.floating]
|
|
17
|
+
|
|
18
|
+
#: A callable that maps per-dimension distances of one feature column,
|
|
19
|
+
#: shape ``(..., k)``, to per-dimension weights of the same shape.
|
|
20
|
+
DimWeightCallable = Callable[[np.ndarray], np.ndarray]
|
|
21
|
+
|
|
22
|
+
#: A callable that maps a full deltas tensor, shape ``(..., k, n_features)``,
|
|
23
|
+
#: to neighbour weights of shape ``(..., k)``.
|
|
24
|
+
WeightCallable = Callable[[np.ndarray], np.ndarray]
|
|
25
|
+
|
|
26
|
+
#: Anything that can be coerced into a 1-D numpy vector.
|
|
27
|
+
VectorLike = Union[np.ndarray, Sequence[float]]
|
|
28
|
+
|
|
29
|
+
#: Everything the ``weights`` estimator parameter is allowed to be: a name,
|
|
30
|
+
#: a weight-function instance / callable, or a per-rank vector.
|
|
31
|
+
WeightSpec = Union[str, WeightCallable, VectorLike]
|
|
32
|
+
|
|
33
|
+
#: Supported persistence formats.
|
|
34
|
+
SaveFormat = str # one of {"npz", "pickle", "json"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The public estimator: :class:`WKNNRegressor`.
|
|
2
|
+
|
|
3
|
+
It is assembled from small, single-responsibility mixins plus scikit-learn's
|
|
4
|
+
:class:`~sklearn.base.BaseEstimator` and
|
|
5
|
+
:class:`~sklearn.base.RegressorMixin`. Only this class defines ``__init__``, so
|
|
6
|
+
``get_params`` / ``set_params`` / ``clone`` behave exactly as scikit-learn
|
|
7
|
+
expects.
|
|
8
|
+
|
|
9
|
+
Method resolution order (left to right): our mixins first, then the
|
|
10
|
+
scikit-learn mixins, with ``BaseEstimator`` last.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from sklearn.base import BaseEstimator, RegressorMixin
|
|
17
|
+
|
|
18
|
+
from ._types import FloatArray, WeightSpec
|
|
19
|
+
from .exceptions import InvalidInputError, NotFittedError
|
|
20
|
+
from .mixins import (
|
|
21
|
+
InputValidationMixin,
|
|
22
|
+
PersistenceMixin,
|
|
23
|
+
PredictionMixin,
|
|
24
|
+
PreprocessingMixin,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WKNNRegressor(
|
|
29
|
+
InputValidationMixin,
|
|
30
|
+
PreprocessingMixin,
|
|
31
|
+
PredictionMixin,
|
|
32
|
+
PersistenceMixin,
|
|
33
|
+
RegressorMixin,
|
|
34
|
+
BaseEstimator,
|
|
35
|
+
):
|
|
36
|
+
"""Weighted k-Nearest-Neighbours regressor (1-D targets).
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
n_neighbors:
|
|
41
|
+
Number of neighbours used for each prediction.
|
|
42
|
+
weights:
|
|
43
|
+
How neighbours are weighted. One of:
|
|
44
|
+
|
|
45
|
+
* a name, e.g. ``"linear"``, ``"inverse"``, ``"gaussian"``
|
|
46
|
+
(see :func:`wknn_1.list_weight_functions`);
|
|
47
|
+
* a :class:`~wknn_1.weights.BaseWeightFunction` instance, including
|
|
48
|
+
:class:`~wknn_1.weights.MixedWeightFunction` for per-dimension
|
|
49
|
+
formulas;
|
|
50
|
+
* any callable mapping deltas ``(..., k, n_features)`` to weights
|
|
51
|
+
``(..., k)``;
|
|
52
|
+
* a length-``n_neighbors`` vector (list or ndarray) of per-rank
|
|
53
|
+
weights.
|
|
54
|
+
metric, p:
|
|
55
|
+
Passed straight to :class:`sklearn.neighbors.NearestNeighbors`.
|
|
56
|
+
standardize:
|
|
57
|
+
Standardize predictors (zero mean, unit variance) before neighbour
|
|
58
|
+
search. Strongly recommended; defaults to ``True``.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
n_neighbors: int = 5,
|
|
64
|
+
*,
|
|
65
|
+
weights: WeightSpec = "linear",
|
|
66
|
+
metric: str = "minkowski",
|
|
67
|
+
p: int = 2,
|
|
68
|
+
standardize: bool = True,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.n_neighbors = n_neighbors
|
|
71
|
+
self.weights = weights
|
|
72
|
+
self.metric = metric
|
|
73
|
+
self.p = p
|
|
74
|
+
self.standardize = standardize
|
|
75
|
+
|
|
76
|
+
# -- fitting ----------------------------------------------------------
|
|
77
|
+
def fit(self, X: np.ndarray, y: np.ndarray) -> "WKNNRegressor":
|
|
78
|
+
"""Fit the model on numpy predictors *X* and 1-D responses *y*."""
|
|
79
|
+
X_arr, y_arr = self._validate_fit_input(X, y)
|
|
80
|
+
self._check_n_neighbors(X_arr.shape[0])
|
|
81
|
+
self.n_features_in_ = X_arr.shape[1]
|
|
82
|
+
self.Xt_ = self._fit_scaler(X_arr)
|
|
83
|
+
self.y_ = y_arr
|
|
84
|
+
self._build_index(self.Xt_)
|
|
85
|
+
# Resolve once so an invalid spec fails at fit time, not predict time.
|
|
86
|
+
self._resolve_weights()
|
|
87
|
+
self._deleted = False
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def refit(self, X: np.ndarray, y: np.ndarray) -> "WKNNRegressor":
|
|
91
|
+
"""Retrain from scratch on fresh data, discarding previous state.
|
|
92
|
+
|
|
93
|
+
Equivalent to clearing the model and calling :meth:`fit` again; kept as
|
|
94
|
+
an explicit method so retraining reads clearly at call sites.
|
|
95
|
+
"""
|
|
96
|
+
self._reset_fitted()
|
|
97
|
+
return self.fit(X, y)
|
|
98
|
+
|
|
99
|
+
# -- prediction -------------------------------------------------------
|
|
100
|
+
def predict(self, X: np.ndarray) -> FloatArray:
|
|
101
|
+
"""Predict 1-D targets for numpy predictors *X*."""
|
|
102
|
+
self._check_is_fitted()
|
|
103
|
+
X_arr = self._validate_predict_input(X)
|
|
104
|
+
self._check_feature_count(X_arr.shape[1])
|
|
105
|
+
Xt = self._apply_scaler(X_arr)
|
|
106
|
+
return self._weighted_predict(Xt)
|
|
107
|
+
|
|
108
|
+
# -- guards -----------------------------------------------------------
|
|
109
|
+
def _check_is_fitted(self) -> None:
|
|
110
|
+
if not self._is_fitted():
|
|
111
|
+
raise NotFittedError(
|
|
112
|
+
"This WKNNRegressor is not fitted yet. Call fit (or load_weights) first."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _check_n_neighbors(self, n_samples: int) -> None:
|
|
116
|
+
if not isinstance(self.n_neighbors, int) or self.n_neighbors < 1:
|
|
117
|
+
raise InvalidInputError("n_neighbors must be a positive integer.")
|
|
118
|
+
if self.n_neighbors > n_samples:
|
|
119
|
+
raise InvalidInputError(
|
|
120
|
+
f"n_neighbors={self.n_neighbors} exceeds the number of training "
|
|
121
|
+
f"samples ({n_samples})."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _check_feature_count(self, n_features: int) -> None:
|
|
125
|
+
if n_features != self.n_features_in_:
|
|
126
|
+
raise InvalidInputError(
|
|
127
|
+
f"X has {n_features} features, but the model was fitted with {self.n_features_in_}."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _reset_fitted(self) -> None:
|
|
131
|
+
for attr in ("Xt_", "y_", "scaler_", "n_features_in_", "_nn_"):
|
|
132
|
+
if hasattr(self, attr):
|
|
133
|
+
delattr(self, attr)
|
|
134
|
+
self._deleted = False
|
|
135
|
+
|
|
136
|
+
# -- scikit-learn tags ------------------------------------------------
|
|
137
|
+
def _more_tags(self) -> dict: # pragma: no cover - metadata only
|
|
138
|
+
return {"requires_y": True, "X_types": ["2darray"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Exception types raised across the library.
|
|
2
|
+
|
|
3
|
+
Everything derives from :class:`WKNNError` so callers can catch the whole
|
|
4
|
+
family with a single ``except``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WKNNError(Exception):
|
|
11
|
+
"""Base class for every error raised by :mod:`wknn_1`."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidInputError(WKNNError, TypeError):
|
|
15
|
+
"""Raised when predictors/responses are not the expected numpy objects.
|
|
16
|
+
|
|
17
|
+
Subclasses :class:`TypeError` as well so existing ``except TypeError``
|
|
18
|
+
handlers keep working.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFittedError(WKNNError):
|
|
23
|
+
"""Raised when prediction/persistence is attempted before ``fit``."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WeightFunctionError(WKNNError):
|
|
27
|
+
"""Raised when a weight specification is invalid or returns a bad shape."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PersistenceError(WKNNError):
|
|
31
|
+
"""Raised when saving/loading model weights fails."""
|