Metadata-Version: 2.4
Name: wknn_1
Version: 0.2.0
Summary: scikit-learn compatible weighted k-Nearest-Neighbours regressor
Project-URL: Homepage, https://example.com/your-username/wknn_1
Project-URL: Repository, https://example.com/your-username/wknn_1
Author-email: Your Name <you@example.com>
License-Expression: MIT
License-File: LICENSE
Keywords: knn,machine-learning,regression,scikit-learn,weighted-knn
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: <3.15,>=3.8
Requires-Dist: numpy>=1.21
Requires-Dist: scikit-learn>=1.0
Description-Content-Type: text/markdown

# wknn_1

A scikit-learn compatible **weighted k-Nearest-Neighbours regressor** with
learnable, transferable weights, online fine-tuning, regularization, an adaptive
neighbourhood size, and training-set condensing.

*Документация доступна на двух языках. English first, [русская версия ниже](#русская-версия).*

---

- [English](#english)
  - [Why this library](#why-this-library)
  - [Installation](#installation)
  - [Quick start](#quick-start)
  - [Using it for classification](#using-it-for-classification)
  - [Constructor parameters](#constructor-parameters)
  - [Core methods](#core-methods)
  - [Learnable & transferable weights (v0.2)](#learnable--transferable-weights-v02)
  - [Online / batch fine-tuning (v0.2)](#online--batch-fine-tuning-v02)
  - [Regularization (v0.2)](#regularization-v02)
  - [Adaptive neighbourhood size (v0.2)](#adaptive-neighbourhood-size-v02)
  - [Condensing the training set (v0.2)](#condensing-the-training-set-v02)
  - [Weight functions](#weight-functions)
  - [Persistence](#persistence)
  - [Exceptions](#exceptions)
  - [scikit-learn integration](#scikit-learn-integration)
  - [Fitted attributes](#fitted-attributes)
  - [Development](#development)
- [Русская версия](#русская-версия)

---

# English

## Why this library

Classical kNN in scikit-learn already supports distance weighting, custom weight
callables and custom metrics. `wknn_1` adds the pieces a non-parametric kNN
cannot offer, because here the neighbour weights become a **learnable
parameter**:

- a **learnable, transferable** per-rank weight vector (train it, save it to
  `.npz`, reuse it on a related dataset);
- **online / batch fine-tuning** via `partial_fit` (scikit-learn's kNN has none);
- **regularization** of both the prediction and the learned weights;
- an **adaptive neighbourhood size** bounded by a max deviation parameter;
- **condensing** to keep an ever-growing memorised set bounded;
- a **per-dimension** weighting via `MixedWeightFunction` (a different formula
  per predictor column — sklearn's callable only sees scalar distances).

It stays a drop-in scikit-learn estimator: `Pipeline`, `clone`, `GridSearchCV`,
`get_params` / `set_params` and `score` all work.

## Installation

```bash
pip install wknn_1
# or, with uv:
uv add wknn_1
```

From a local build:

```bash
pip install dist/wknn_1-0.2.0-py3-none-any.whl
```

Requirements: Python ≥ 3.8, `numpy`, `scikit-learn`.

## Quick start

```python
import numpy as np
from wknn_1 import WKNNRegressor

X = np.random.rand(200, 3)
y = X @ np.array([1.0, 2.0, -1.0])          # a linear target, for illustration

model = WKNNRegressor(n_neighbors=7, weights="inverse").fit(X, y)
y_pred = model.predict(X[:5])
print(y_pred)
print("R^2:", model.score(X, y))            # RegressorMixin.score
```

**Inputs must be numpy arrays.** Passing a Python list raises `InvalidInputError`
(the only exception is a per-rank weight vector, which may be a list).

## Using it for classification

`wknn_1` is a *regressor*. To classify, regress on the numeric class labels and
round to the nearest label. A minimal adapter:

```python
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from wknn_1 import WKNNRegressor

class WKNNClassifier(BaseEstimator, ClassifierMixin):
    def __init__(self, n_neighbors=5, weights="inverse", **kw):
        self.n_neighbors, self.weights, self.kw = n_neighbors, weights, kw
    def fit(self, X, y):
        self.classes_ = np.unique(y)
        self._lo, self._hi = self.classes_.min(), self.classes_.max()
        self._r = WKNNRegressor(self.n_neighbors, weights=self.weights,
                                **self.kw).fit(np.asarray(X, float), y.astype(float))
        return self
    def predict(self, X):
        raw = self._r.predict(np.asarray(X, float))
        return np.clip(np.rint(raw), self._lo, self._hi).astype(int)
```

## Constructor parameters

```python
WKNNRegressor(
    n_neighbors=5, *, weights="linear", metric="minkowski", p=2,
    standardize=True, regularization=0.0, max_neighbor_deviation=0,
    learning_rate=0.5, momentum=0.9, random_state=None,
)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `n_neighbors` | int | `5` | Base number of neighbours *k* per prediction. |
| `weights` | see below | `"linear"` | How neighbours are weighted: a name, a `BaseWeightFunction`, a callable, or a per-rank vector. |
| `metric` | str | `"minkowski"` | Distance metric, forwarded to `NearestNeighbors`. |
| `p` | int | `2` | Minkowski order (`2` = Euclidean, `1` = Manhattan). |
| `standardize` | bool | `True` | Standardize predictors (zero mean, unit variance) before neighbour search. |
| `regularization` | float | `0.0` | λ ≥ 0. Shrinks predictions toward the global mean and learned weights toward uniform. |
| `max_neighbor_deviation` | int | `0` | *d* ≥ 0. Allows the per-query neighbourhood to vary in `[k−d, k+d]`. `0` = fixed *k*. |
| `learning_rate` | float | `0.5` | Nesterov GD step η for weight learning. |
| `momentum` | float | `0.9` | Nesterov GD momentum μ. |
| `random_state` | int / None | `None` | Seed for the internal split in `fit_weights`. |

## Core methods

### `fit(X, y) -> self`
Fit on numpy predictors `X` (2-D) and 1-D responses `y`. Validates input, checks
`n_neighbors ≤ n_samples`, standardizes, builds the neighbour index, and resolves
the weight spec (so a bad spec fails here, not at predict time).

```python
model = WKNNRegressor(5, weights="gaussian").fit(X, y)
```

### `predict(X) -> np.ndarray`
Predict 1-D targets for `X`. Applies regularization and adaptive-k if configured.

```python
preds = model.predict(X_new)
```

### `refit(X, y) -> self`
Retrain from scratch, discarding all previous state (equivalent to clearing the
model and calling `fit` again; reads clearly at call sites).

```python
model.refit(X2, y2)
```

### `score(X, y, sample_weight=None) -> float`
Coefficient of determination R² (from `RegressorMixin`).

```python
r2 = model.score(X_test, y_test)
```

## Learnable & transferable weights (v0.2)

The prediction is a differentiable function of a per-rank weight vector
**w** ∈ ℝ<sup>k</sup>, so **w** can be *learned* by Nesterov accelerated gradient
descent, saved, and reused on a related dataset.

### `fit_weights(X=None, y=None, *, epochs=100, validation_fraction=0.3, record=False)`
Learn the per-rank vector. With `X`/`y` omitted, the stored training set is split
internally (a temporary index on one part, the learning signal from the other) so
the solution does not collapse to 1-NN. Sets `weight_vector_` and switches
prediction to use it. Returns the loss `history` when `record=True`, else `self`.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.05).fit(X, y)
history = model.fit_weights(epochs=100, record=True)
print("loss:", history[0], "->", history[-1])
print("learned weights:", model.weight_vector_)
```

### `export_weights(path) -> path` / `import_weights(path) -> self`
Save / load **only** the learned weight vector as `.npz` — a small portable
artefact for transfer learning.

```python
model.export_weights("weights.npz")

fresh = WKNNRegressor(7, weights="inverse").fit(X_similar, y_similar)
fresh.import_weights("weights.npz")          # reuse the learned weights
```

### `transfer_weights(source) -> self`
Copy the learned vector from another fitted model.

```python
target.transfer_weights(model)
```

### `set_weight_vector(w) -> self`
Install a weight vector directly; its length must equal `n_neighbors`.

```python
model.set_weight_vector(np.array([0.4, 0.25, 0.15, 0.1, 0.1]))   # k == 5
```

## Online / batch fine-tuning (v0.2)

### `partial_fit(X, y, classes=None, *, epochs=1) -> self`
Incremental training, like a neural-network step. The first call behaves like
`fit`; later calls learn on the new batch as a held-out signal (before adding it),
then append the batch to memory and rebuild the index. `n_samples_` grows with
each batch.

**About the signature.** The three arguments after `X`, `y` are there for
different reasons, so none of them is accidental:

- **`epochs`** *(functional)* — how many gradient fine-tuning steps to take on
  the new batch. `epochs=1` is a single online step; larger values do heavier
  batch fine-tuning. This is a real knob you will use (e.g. `epochs=3` when a
  batch carries more information).
- **`classes=None`** *(compatibility only, ignored)* — scikit-learn's
  `partial_fit` convention passes the full label set on the first call so a
  *classifier* can size its output. `wknn_1` is a *regressor* and never reads
  this argument; it is accepted solely so the method matches the scikit-learn
  `partial_fit(X, y, classes=...)` signature and drops into tools that call it
  that way. You can always omit it.
- **`*`** — the bare star simply marks everything after it as keyword-only, so
  `epochs` must be written as `epochs=3`, never as a bare positional `3`. This
  prevents a call like `partial_fit(X, y, something, 3)` from silently binding
  the wrong value to `classes`.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.05)
model.partial_fit(X0, y0)                    # ~ fit
for Xb, yb in batches:
    model.partial_fit(Xb, yb, epochs=3)      # fine-tune + grow memory
print("memory size:", model.n_samples_)
```

## Regularization (v0.2)

Set `regularization=λ` (λ ≥ 0). It acts in two places:

- **prediction** — a scale-invariant shrinkage toward the global mean
  `μ_y`: `y_reg = (y_hat + λ·μ_y) / (1 + λ)`;
- **weight learning** — an L2 penalty pulling the learned weights toward the
  uniform vector, which reduces overfitting and improves transfer.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.3).fit(X, y)
```

## Adaptive neighbourhood size (v0.2)

Set `max_neighbor_deviation=d`. For each query the neighbourhood size is chosen in
`[k−d, k+d]` to minimise the variance of neighbour targets — the region shrinks
where a tight, consistent cluster exists and expands where neighbours disagree.
`d = 0` reproduces fixed-*k* behaviour exactly.

```python
model = WKNNRegressor(5, weights="inverse", max_neighbor_deviation=3).fit(X, y)
```

## Condensing the training set (v0.2)

### `condense(ratio) -> self`
Remove a fraction `ratio ∈ (0, 1)` of memorised points. Redundant interior points
in dense same-class regions are dropped first; boundary points (with other-class
neighbours) are protected; removal is stratified so no class is wiped out. Lets an
ever-growing stream stay bounded when paired with `partial_fit`.

> `condense` groups points by their `y_` value, so it is designed for **discrete
> / class-like labels** (the classification setting). With a continuous target,
> every point is its own group and nothing is removed.

```python
y_cls = (X[:, 0] > 0.5).astype(float)        # class-like labels
model = WKNNRegressor(7, weights="inverse").fit(X, y_cls)
model.condense(0.4)                          # drop ~40% of points
print("kept:", model.n_samples_)
```

## Weight functions

The `weights` parameter accepts four kinds of specification.

**1. A named standard function.** One of:

| Name | Formula (d = neighbour distance) | Notes |
|---|---|---|
| `uniform` | `w = 1` | Reduces to plain kNN averaging. |
| `linear` | `w = 1 − (d − lo)/(hi − lo)` | Nearest → 1, farthest → 0; no hyperparameters. |
| `inverse` / `distance` | `w = 1 / (|d| + eps)` | Classic inverse-distance weighting. |
| `exponential` | `w = exp(−alpha·|d|)` | Heavier tail; `alpha` controls width. |
| `gaussian` | `w = exp(−d² / (2·sigma²))` | RBF kernel; `sigma` controls width. |

```python
WKNNRegressor(5, weights="inverse").fit(X, y)
```

**2. A `StandardWeightFunction` instance** — the named functions with explicit
options:

```python
from wknn_1 import StandardWeightFunction
wf = StandardWeightFunction("gaussian", sigma=0.5, aggregate="mean",
                            center=False, scale=True, normalize=False)
WKNNRegressor(5, weights=wf).fit(X, y)
```

Constructor: `StandardWeightFunction(formula="linear", *, center=False,
scale=True, normalize=False, aggregate="mean", alpha=1.0, sigma=1.0, eps=1e-12)`.
`center` / `scale` / `normalize` are applied to the **distances** (along the
neighbour axis); `aggregate` ∈ `{"mean", "sum", "product"}` collapses the
per-feature weights into one weight per neighbour.

**3. A `MixedWeightFunction`** — a **different formula per predictor dimension**
(not possible with sklearn's callable, which only sees scalar distances):

```python
from wknn_1 import MixedWeightFunction, StandardWeightFunction
mixed = MixedWeightFunction([
    StandardWeightFunction("linear"),        # dimension 0
    StandardWeightFunction("gaussian", sigma=0.3),  # dimension 1
], aggregate="product")
WKNNRegressor(5, weights=mixed).fit(X[:, :2], y)     # X has 2 columns here
```

**4. A per-rank vector** (list or ndarray of length `n_neighbors`) — the *i*-th
nearest neighbour gets weight `w[i]`:

```python
WKNNRegressor(3, weights=[0.6, 0.3, 0.1]).fit(X, y)
```

**Other weight helpers.**

`FixedFeatureWeightFunction(feature_weights, *, eps=1e-12)` — a kernel driven by a
fixed per-feature importance vector `v`:
`w = 1 / (1 + Σ_j v_j·|Δ_j| + eps)`.

```python
from wknn_1 import FixedFeatureWeightFunction
wf = FixedFeatureWeightFunction(np.array([2.0, 0.5, 1.0]))    # 3 features
WKNNRegressor(5, weights=wf).fit(X, y)
```

`RankWeightFunction(rank_weights)` — the explicit per-rank form used internally by
option 4.

### Registry

```python
from wknn_1 import list_weight_functions, register_weight_function, get_weight_function

list_weight_functions()
# ['distance', 'exponential', 'gaussian', 'inverse', 'linear', 'uniform']

# register your own named factory:
from wknn_1 import StandardWeightFunction
register_weight_function("sharp_gauss",
                         lambda: StandardWeightFunction("gaussian", sigma=0.2),
                         overwrite=True)
WKNNRegressor(5, weights="sharp_gauss").fit(X, y)

# resolve a spec to a concrete function (mostly internal):
fn = get_weight_function("inverse", n_neighbors=5)
```

## Persistence

### `save_weights(path, fmt=None) -> Path` / `load_weights(path, fmt=None) -> self`
Save / load the **full fitted state** (standardized training matrix, targets,
scaler, params, and any learned `weight_vector_`). Format is chosen by `fmt` or by
the file extension.

| Format | Extension | Notes |
|---|---|---|
| npz | `.npz` | Recommended; numpy-native, portable. |
| pickle | `.pkl` / `.pickle` | Preserves arbitrary callable weight specs. |
| json | `.json` | Human-readable; callable specs stored as a placeholder. |

```python
model.save_weights("model.npz")
restored = WKNNRegressor().load_weights("model.npz")
np.allclose(model.predict(X[:5]), restored.predict(X[:5]))   # True
```

> A callable `weights` spec cannot be serialized to npz/json (only a placeholder
> is stored). Use pickle, or re-attach the callable via `set_params` after load.

### `delete(*, remove_files=True) -> None`
Tear down the model: remove on-disk files it created, drop learned weights, fitted
attributes and metadata. Hyperparameters are kept, so the object is ready for a
fresh `fit`.

```python
model.delete()               # also deletes files saved by save_weights
```

## Exceptions

All inherit from `WKNNError`.

| Exception | Raised when |
|---|---|
| `WKNNError` | Base class for every library error. |
| `InvalidInputError` | Non-numpy input, wrong shapes, bad `n_neighbors`, bad hyperparameters. |
| `NotFittedError` | `predict` (etc.) called before `fit` / `load_weights`. |
| `WeightFunctionError` | A weight spec is unknown or a callable returns a wrong shape. |
| `PersistenceError` | Save/load fails, or transfer/export without a learned vector. |

```python
from wknn_1 import NotFittedError
try:
    WKNNRegressor().predict(X)
except NotFittedError as e:
    print("fit first:", e)
```

## scikit-learn integration

```python
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV

pipe = make_pipeline(StandardScaler(), WKNNRegressor())
grid = GridSearchCV(pipe, {
    "wknnregressor__n_neighbors": [3, 5, 7],
    "wknnregressor__weights": ["uniform", "inverse", "gaussian"],
    "wknnregressor__regularization": [0.0, 0.3],
})
grid.fit(X, y)
print(grid.best_params_)
```

`clone`, `get_params` and `set_params` behave as expected because only the
estimator defines `__init__` and it merely stores its arguments.

## Fitted attributes

Available after `fit`:

| Attribute | Meaning |
|---|---|
| `n_features_in_` | Number of predictor columns seen during `fit`. |
| `n_samples_` | Current size of the memorised set (grows via `partial_fit`, shrinks via `condense`). |
| `Xt_` | Standardized training matrix. |
| `y_` | Stored 1-D targets. |
| `scaler_` | The fitted standardizer (or `None` if `standardize=False`). |
| `weight_vector_` | The learned per-rank vector (present after `fit_weights` / `import_weights` / `set_weight_vector`). |

## Development

```bash
# editable install with dev extras
pip install -e .
pip install pytest

# run the test suite (45 tests: 35 core + 10 v0.2 features)
pytest -q

# build wheel + sdist
python -m build            # or: uv build
```

---

# Русская версия

Библиотека **`wknn_1`** — совместимый со scikit-learn **регрессор на основе
взвешенного метода k ближайших соседей** с обучаемыми и переносимыми весами,
онлайн-дообучением, регуляризацией, адаптивным размером окрестности и
прореживанием обучающей выборки.

## Зачем нужна библиотека

Классический kNN в scikit-learn уже умеет взвешивать по расстоянию, принимать
пользовательскую весовую функцию и произвольную метрику. `wknn_1` добавляет то,
чего у непараметрического kNN нет в принципе, — потому что здесь веса соседей
становятся **обучаемым параметром**:

- **обучаемый и переносимый** вектор весов по рангам (обучить, сохранить в
  `.npz`, применить к похожему набору);
- **онлайн- и батч-дообучение** через `partial_fit` (у kNN из sklearn его нет);
- **регуляризация** предсказания и обучаемых весов;
- **адаптивное число соседей**, ограниченное параметром максимального отклонения;
- **прореживание** (`condense`), чтобы бесконечно растущая память оставалась
  ограниченной;
- **своя формула на каждое измерение** через `MixedWeightFunction` (callable в
  sklearn видит только скалярные расстояния и так не умеет).

При этом модель остаётся полноценным эстиматором scikit-learn: работают
`Pipeline`, `clone`, `GridSearchCV`, `get_params` / `set_params` и `score`.

## Установка

```bash
pip install wknn_1
# или через uv:
uv add wknn_1
```

Из локальной сборки:

```bash
pip install dist/wknn_1-0.2.0-py3-none-any.whl
```

Требования: Python ≥ 3.8, `numpy`, `scikit-learn`.

## Быстрый старт

```python
import numpy as np
from wknn_1 import WKNNRegressor

X = np.random.rand(200, 3)
y = X @ np.array([1.0, 2.0, -1.0])

model = WKNNRegressor(n_neighbors=7, weights="inverse").fit(X, y)
print(model.predict(X[:5]))
print("R^2:", model.score(X, y))
```

**Входные данные — только numpy-массивы.** Передача списка вызывает
`InvalidInputError` (исключение — вектор весов по рангам, он может быть списком).

## Применение для классификации

`wknn_1` — это *регрессор*. Для классификации выполняют регрессию по числовым
меткам классов и округляют к ближайшей метке. Минимальный адаптер:

```python
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from wknn_1 import WKNNRegressor

class WKNNClassifier(BaseEstimator, ClassifierMixin):
    def __init__(self, n_neighbors=5, weights="inverse", **kw):
        self.n_neighbors, self.weights, self.kw = n_neighbors, weights, kw
    def fit(self, X, y):
        self.classes_ = np.unique(y)
        self._lo, self._hi = self.classes_.min(), self.classes_.max()
        self._r = WKNNRegressor(self.n_neighbors, weights=self.weights,
                                **self.kw).fit(np.asarray(X, float), y.astype(float))
        return self
    def predict(self, X):
        raw = self._r.predict(np.asarray(X, float))
        return np.clip(np.rint(raw), self._lo, self._hi).astype(int)
```

## Параметры конструктора

```python
WKNNRegressor(
    n_neighbors=5, *, weights="linear", metric="minkowski", p=2,
    standardize=True, regularization=0.0, max_neighbor_deviation=0,
    learning_rate=0.5, momentum=0.9, random_state=None,
)
```

| Параметр | Тип | По умолчанию | Значение |
|---|---|---|---|
| `n_neighbors` | int | `5` | Базовое число соседей *k*. |
| `weights` | см. ниже | `"linear"` | Способ взвешивания: имя, `BaseWeightFunction`, callable или вектор по рангам. |
| `metric` | str | `"minkowski"` | Метрика расстояния (передаётся в `NearestNeighbors`). |
| `p` | int | `2` | Порядок Минковского (`2` — евклидова, `1` — манхэттенская). |
| `standardize` | bool | `True` | Стандартизация признаков перед поиском соседей. |
| `regularization` | float | `0.0` | λ ≥ 0. Сдвиг предсказания к глобальному среднему и весов к равномерным. |
| `max_neighbor_deviation` | int | `0` | *d* ≥ 0. Размер окрестности варьируется в `[k−d, k+d]`. `0` — фиксированное *k*. |
| `learning_rate` | float | `0.5` | Шаг η для обучения весов (NAG). |
| `momentum` | float | `0.9` | Момент μ (NAG). |
| `random_state` | int / None | `None` | Зерно для внутреннего сплита в `fit_weights`. |

## Основные методы

### `fit(X, y) -> self`
Обучение на numpy-предикторах `X` (2-D) и одномерном `y`. Проверяет вход,
условие `n_neighbors ≤ n_samples`, стандартизует, строит индекс соседей и
разрешает спецификацию весов (некорректная спецификация падает здесь, а не при
предсказании).

```python
model = WKNNRegressor(5, weights="gaussian").fit(X, y)
```

### `predict(X) -> np.ndarray`
Предсказание одномерных ответов для `X`. Применяет регуляризацию и адаптивное
число соседей, если они заданы.

```python
preds = model.predict(X_new)
```

### `refit(X, y) -> self`
Полное переобучение с нуля, отбрасывая всё прежнее состояние.

```python
model.refit(X2, y2)
```

### `score(X, y, sample_weight=None) -> float`
Коэффициент детерминации R² (из `RegressorMixin`).

```python
r2 = model.score(X_test, y_test)
```

## Обучаемые и переносимые веса (v0.2)

Предсказание — дифференцируемая функция вектора весов по рангам
**w** ∈ ℝ<sup>k</sup>, поэтому **w** можно **обучать** ускоренным градиентным
спуском Нестерова, сохранять и применять к похожему набору данных.

### `fit_weights(X=None, y=None, *, epochs=100, validation_fraction=0.3, record=False)`
Обучает вектор по рангам. Если `X`/`y` не заданы, обучающая выборка делится
внутренне (индекс соседей — по одной части, обучающий сигнал — по другой), чтобы
решение не вырождалось в 1-NN. Устанавливает `weight_vector_` и переключает
предсказание на него. При `record=True` возвращает историю потерь, иначе `self`.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.05).fit(X, y)
history = model.fit_weights(epochs=100, record=True)
print("потери:", history[0], "->", history[-1])
print("выученные веса:", model.weight_vector_)
```

### `export_weights(path) -> path` / `import_weights(path) -> self`
Сохранить / загрузить **только** выученный вектор весов в `.npz` — компактный
переносимый артефакт для трансферного обучения.

```python
model.export_weights("weights.npz")

fresh = WKNNRegressor(7, weights="inverse").fit(X_similar, y_similar)
fresh.import_weights("weights.npz")
```

### `transfer_weights(source) -> self`
Скопировать выученный вектор из другой обученной модели.

```python
target.transfer_weights(model)
```

### `set_weight_vector(w) -> self`
Установить вектор весов напрямую; его длина обязана равняться `n_neighbors`.

```python
model.set_weight_vector(np.array([0.4, 0.25, 0.15, 0.1, 0.1]))   # k == 5
```

## Онлайн- и батч-дообучение (v0.2)

### `partial_fit(X, y, classes=None, *, epochs=1) -> self`
Инкрементальное обучение, как шаг нейросети. Первый вызов эквивалентен `fit`;
последующие обучают веса на новом батче как на отложенном сигнале (до его
добавления), затем добавляют батч в память и перестраивают индекс. `n_samples_`
растёт с каждым батчем.

**О сигнатуре.** Три аргумента после `X`, `y` присутствуют по разным причинам,
и ни один из них не случаен:

- **`epochs`** *(рабочий параметр)* — сколько шагов градиентного дообучения
  сделать на новом батче. `epochs=1` — один онлайн-шаг; большие значения дают
  более основательное батч-дообучение. Это реально используемый параметр
  (например, `epochs=3`, когда батч несёт больше информации).
- **`classes=None`** *(только для совместимости, игнорируется)* — по соглашению
  scikit-learn метод `partial_fit` получает на первом вызове полный набор меток,
  чтобы *классификатор* мог задать размер выхода. `wknn_1` — это *регрессор*, и
  он никогда не читает этот аргумент; он принимается лишь для того, чтобы
  сигнатура совпадала со scikit-learn `partial_fit(X, y, classes=...)` и метод
  подходил инструментам, вызывающим его именно так. Его всегда можно не
  указывать.
- **`*`** — одиночная звёздочка помечает всё, что идёт после неё, как
  «только по имени», поэтому `epochs` пишется как `epochs=3`, а не как позиционный
  `3`. Это защищает от вызова вроде `partial_fit(X, y, нечто, 3)`, где значение по
  ошибке связалось бы с `classes`.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.05)
model.partial_fit(X0, y0)                    # ~ fit
for Xb, yb in batches:
    model.partial_fit(Xb, yb, epochs=3)      # дообучение + рост памяти
print("размер памяти:", model.n_samples_)
```

## Регуляризация (v0.2)

Параметр `regularization=λ` (λ ≥ 0) действует в двух местах:

- **предсказание** — масштабно-инвариантный сдвиг к глобальному среднему `μ_y`:
  `y_reg = (y_hat + λ·μ_y) / (1 + λ)`;
- **обучение весов** — штраф L2, притягивающий веса к равномерному вектору, что
  снижает переобучение и улучшает перенос.

```python
model = WKNNRegressor(7, weights="inverse", regularization=0.3).fit(X, y)
```

## Адаптивное число соседей (v0.2)

Параметр `max_neighbor_deviation=d`. Для каждого запроса размер окрестности
выбирается в `[k−d, k+d]` так, чтобы минимизировать дисперсию ответов соседей:
окрестность сжимается там, где есть плотное согласованное ядро, и расширяется
там, где соседи расходятся. `d = 0` в точности воспроизводит фиксированное *k*.

```python
model = WKNNRegressor(5, weights="inverse", max_neighbor_deviation=3).fit(X, y)
```

## Прореживание обучающей выборки (v0.2)

### `condense(ratio) -> self`
Удаляет долю `ratio ∈ (0, 1)` запомненных точек. Сначала отбрасываются
избыточные внутренние точки плотных однородных областей; граничные точки (с
соседями других классов) защищены; удаление стратифицировано, ни один класс не
исчезает. В связке с `partial_fit` удерживает память ограниченной.

> `condense` группирует точки по значению `y_`, поэтому предназначен для
> **дискретных меток** (задача классификации). При непрерывном отклике каждая
> точка образует свою группу и ничего не удаляется.

```python
y_cls = (X[:, 0] > 0.5).astype(float)        # метки-классы
model = WKNNRegressor(7, weights="inverse").fit(X, y_cls)
model.condense(0.4)                          # удалить ~40% точек
print("осталось:", model.n_samples_)
```

## Весовые функции

Параметр `weights` принимает четыре вида спецификации.

**1. Имя стандартной функции:**

| Имя | Формула (d — расстояние соседа) | Примечание |
|---|---|---|
| `uniform` | `w = 1` | Сводится к обычному усреднению kNN. |
| `linear` | `w = 1 − (d − lo)/(hi − lo)` | Ближайший → 1, дальний → 0; без гиперпараметров. |
| `inverse` / `distance` | `w = 1 / (|d| + eps)` | Классическое обратное расстояние. |
| `exponential` | `w = exp(−alpha·|d|)` | Более тяжёлый хвост; ширина — `alpha`. |
| `gaussian` | `w = exp(−d² / (2·sigma²))` | RBF-ядро; ширина — `sigma`. |

```python
WKNNRegressor(5, weights="inverse").fit(X, y)
```

**2. Экземпляр `StandardWeightFunction`** — те же функции с явными параметрами:

```python
from wknn_1 import StandardWeightFunction
wf = StandardWeightFunction("gaussian", sigma=0.5, aggregate="mean",
                            center=False, scale=True, normalize=False)
WKNNRegressor(5, weights=wf).fit(X, y)
```

Конструктор: `StandardWeightFunction(formula="linear", *, center=False,
scale=True, normalize=False, aggregate="mean", alpha=1.0, sigma=1.0, eps=1e-12)`.
Флаги `center` / `scale` / `normalize` применяются к **расстояниям** (вдоль оси
соседей); `aggregate` ∈ `{"mean", "sum", "product"}` сворачивает покоординатные
веса в один вес на соседа.

**3. `MixedWeightFunction`** — **своя формула на каждое измерение** (в sklearn
так нельзя, callable видит только скалярные расстояния):

```python
from wknn_1 import MixedWeightFunction, StandardWeightFunction
mixed = MixedWeightFunction([
    StandardWeightFunction("linear"),                 # измерение 0
    StandardWeightFunction("gaussian", sigma=0.3),    # измерение 1
], aggregate="product")
WKNNRegressor(5, weights=mixed).fit(X[:, :2], y)      # здесь у X два столбца
```

**4. Вектор по рангам** (список или ndarray длины `n_neighbors`) — *i*-й по
близости сосед получает вес `w[i]`:

```python
WKNNRegressor(3, weights=[0.6, 0.3, 0.1]).fit(X, y)
```

**Дополнительные функции весов.**

`FixedFeatureWeightFunction(feature_weights, *, eps=1e-12)` — ядро по фиксированному
вектору важностей признаков `v`: `w = 1 / (1 + Σ_j v_j·|Δ_j| + eps)`.

```python
from wknn_1 import FixedFeatureWeightFunction
wf = FixedFeatureWeightFunction(np.array([2.0, 0.5, 1.0]))    # 3 признака
WKNNRegressor(5, weights=wf).fit(X, y)
```

`RankWeightFunction(rank_weights)` — явная форма весов по рангам (её использует
вариант 4).

### Реестр функций

```python
from wknn_1 import list_weight_functions, register_weight_function, get_weight_function
from wknn_1 import StandardWeightFunction

list_weight_functions()
# ['distance', 'exponential', 'gaussian', 'inverse', 'linear', 'uniform']

register_weight_function("sharp_gauss",
                         lambda: StandardWeightFunction("gaussian", sigma=0.2),
                         overwrite=True)
WKNNRegressor(5, weights="sharp_gauss").fit(X, y)

fn = get_weight_function("inverse", n_neighbors=5)   # разрешить спецификацию
```

## Сохранение и загрузка

### `save_weights(path, fmt=None) -> Path` / `load_weights(path, fmt=None) -> self`
Сохранить / загрузить **полное обученное состояние** (стандартизованная матрица,
ответы, скейлер, параметры и, если есть, выученный `weight_vector_`). Формат
определяется `fmt` или расширением файла.

| Формат | Расширение | Примечание |
|---|---|---|
| npz | `.npz` | Рекомендуется; numpy-совместимо, переносимо. |
| pickle | `.pkl` / `.pickle` | Сохраняет произвольные callable-спецификации. |
| json | `.json` | Читаемый; callable сохраняется как заполнитель. |

```python
model.save_weights("model.npz")
restored = WKNNRegressor().load_weights("model.npz")
np.allclose(model.predict(X[:5]), restored.predict(X[:5]))   # True
```

> Спецификацию `weights` в виде callable нельзя сериализовать в npz/json
> (сохраняется лишь заполнитель). Используйте pickle или заново задайте функцию
> через `set_params` после загрузки.

### `delete(*, remove_files=True) -> None`
Полностью сбрасывает модель: удаляет созданные ею файлы, выученные веса,
обученные атрибуты и метаданные. Гиперпараметры сохраняются, объект готов к
новому `fit`.

```python
model.delete()
```

## Исключения

Все наследуются от `WKNNError`.

| Исключение | Когда возникает |
|---|---|
| `WKNNError` | Базовый класс всех ошибок библиотеки. |
| `InvalidInputError` | Не-numpy вход, неверные формы, некорректный `n_neighbors` или гиперпараметры. |
| `NotFittedError` | `predict` и т. п. вызваны до `fit` / `load_weights`. |
| `WeightFunctionError` | Неизвестная спецификация весов или callable вернул неверную форму. |
| `PersistenceError` | Ошибка сохранения/загрузки либо перенос/экспорт без выученного вектора. |

```python
from wknn_1 import NotFittedError
try:
    WKNNRegressor().predict(X)
except NotFittedError as e:
    print("сначала fit:", e)
```

## Интеграция со scikit-learn

```python
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV

pipe = make_pipeline(StandardScaler(), WKNNRegressor())
grid = GridSearchCV(pipe, {
    "wknnregressor__n_neighbors": [3, 5, 7],
    "wknnregressor__weights": ["uniform", "inverse", "gaussian"],
    "wknnregressor__regularization": [0.0, 0.3],
})
grid.fit(X, y)
print(grid.best_params_)
```

`clone`, `get_params` и `set_params` работают штатно, поскольку `__init__` только
сохраняет аргументы и ничего не преобразует.

## Атрибуты после обучения

| Атрибут | Значение |
|---|---|
| `n_features_in_` | Число столбцов-признаков, увиденных при `fit`. |
| `n_samples_` | Текущий размер памяти (растёт при `partial_fit`, падает при `condense`). |
| `Xt_` | Стандартизованная обучающая матрица. |
| `y_` | Сохранённые одномерные ответы. |
| `scaler_` | Обученный стандартизатор (или `None` при `standardize=False`). |
| `weight_vector_` | Выученный вектор по рангам (после `fit_weights` / `import_weights` / `set_weight_vector`). |

## Разработка

```bash
pip install -e .
pip install pytest

# запуск тестов (45: 35 базовых + 10 на возможности v0.2)
pytest -q

# сборка wheel + sdist
python -m build            # или: uv build
```

---

*License: MIT. Version 0.2.0.*
