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.
@@ -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,5 @@
1
+ # Development tooling for wknn_1 (fallback for `uv sync --group dev`).
2
+ # Install on top of requirements.txt:
3
+ # pip install -r requirements.txt -r requirements-dev.txt
4
+ ruff>=0.6
5
+ pytest>=7.0
@@ -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."""