parzen_window 0.0.2__py3-none-any.whl

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,7 @@
1
+ from .core import ParzenWindowClassifier
2
+
3
+ __author__ = "Andrey Ferubko"
4
+ __version__ = "0.0.2"
5
+ __email__ = "ferubko1999@yandex.ru"
6
+
7
+ __all__ = ["ParzenWindowClassifier"]
@@ -0,0 +1,83 @@
1
+ """Dataset compaction via convex-neighborhood analysis.
2
+
3
+ A training point contributes almost nothing to the decision boundary when
4
+ it is (a) surrounded entirely by same-class neighbors -- i.e. it sits deep
5
+ inside a convex, single-class neighborhood -- and (b) far from the nearest
6
+ point of any other class. Such points are safe to drop to free up memory,
7
+ since the kernel weight they contribute is dominated by points closer to
8
+ the actual boundary.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+ from sklearn.neighbors import NearestNeighbors
15
+
16
+
17
+ def compute_removal_scores(X: np.ndarray, y: np.ndarray, n_neighbors: int) -> np.ndarray:
18
+ """Score each point by how safe it is to remove (higher = safer).
19
+
20
+ A point scores 0 (never removed) unless all of its ``n_neighbors``
21
+ nearest neighbors share its label; otherwise its score is the distance
22
+ to the nearest point of a *different* class -- a proxy for how far it
23
+ sits from the decision boundary.
24
+ """
25
+ n_samples = len(X)
26
+ k = min(n_neighbors, n_samples - 1)
27
+ if k < 1:
28
+ return np.zeros(n_samples)
29
+
30
+ neighbors = NearestNeighbors(n_neighbors=k + 1).fit(X)
31
+ _, indices = neighbors.kneighbors(X)
32
+ indices = indices[:, 1:] # drop the point itself (distance 0)
33
+ homogeneous = (y[indices] == y[:, None]).all(axis=1)
34
+
35
+ margin = np.zeros(n_samples)
36
+ for label in np.unique(y):
37
+ own, other = y == label, y != label
38
+ if not other.any() or not own.any():
39
+ continue
40
+ boundary = NearestNeighbors(n_neighbors=1).fit(X[other])
41
+ boundary_distance, _ = boundary.kneighbors(X[own])
42
+ margin[own] = boundary_distance[:, 0]
43
+
44
+ return np.where(homogeneous, margin, 0.0)
45
+
46
+
47
+ def select_points_to_remove(
48
+ X: np.ndarray,
49
+ y: np.ndarray,
50
+ reduction_fraction: float,
51
+ n_neighbors: int = 5,
52
+ min_per_class: int = 2,
53
+ ) -> np.ndarray:
54
+ """Pick indices that can be dropped without touching the decision boundary.
55
+
56
+ Points are removed greedily, safest (highest score) first, until either
57
+ ``reduction_fraction`` of the dataset has been removed, points with a
58
+ zero score are reached (mixed neighborhood or near the boundary), or a
59
+ class would drop below ``min_per_class`` remaining points.
60
+ """
61
+ if not 0.0 < reduction_fraction < 1.0:
62
+ raise ValueError("reduction_fraction must be in (0, 1)")
63
+
64
+ n_samples = len(X)
65
+ target_removals = int(n_samples * reduction_fraction)
66
+ if target_removals <= 0:
67
+ return np.array([], dtype=int)
68
+
69
+ scores = compute_removal_scores(X, y, n_neighbors)
70
+ order = np.argsort(scores)[::-1]
71
+ remaining_per_class = {label: int(np.sum(y == label)) for label in np.unique(y)}
72
+
73
+ to_remove = []
74
+ for idx in order:
75
+ if len(to_remove) >= target_removals or scores[idx] <= 0:
76
+ break
77
+ label = y[idx]
78
+ if remaining_per_class[label] - 1 < min_per_class:
79
+ continue
80
+ remaining_per_class[label] -= 1
81
+ to_remove.append(idx)
82
+
83
+ return np.array(sorted(to_remove), dtype=int)
parzen_window/core.py ADDED
@@ -0,0 +1,219 @@
1
+ """Parzen window classifier with incremental updates, compaction and persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from scipy.spatial.distance import cdist
7
+ from sklearn.base import BaseEstimator, ClassifierMixin
8
+ from sklearn.neighbors import NearestNeighbors
9
+ from sklearn.utils.validation import check_array, check_is_fitted
10
+
11
+ from . import io as _io
12
+ from .compaction import select_points_to_remove
13
+ from .kernels import resolve_kernel
14
+ from .scheduler import CompactionScheduler
15
+
16
+ _NOT_FITTED_MSG = "This ParzenWindowClassifier instance is not fitted yet. Call fit() first."
17
+
18
+
19
+ class ParzenWindowClassifier(BaseEstimator, ClassifierMixin):
20
+ """Non-parametric classifier based on the Parzen window method.
21
+
22
+ Parameters
23
+ ----------
24
+ h : float, default=0.5
25
+ Kernel bandwidth. Used as-is when ``adaptive_bandwidth=False``; used
26
+ as a scale factor on the per-point bandwidth when it is ``True``.
27
+ kernel : {"gaussian", "epanechnikov", "quartic", "triangular", "rectangular"}
28
+ Kernel function shape.
29
+ adaptive_bandwidth : bool, default=False
30
+ When ``True``, each training point gets its own bandwidth equal to
31
+ ``h`` times its distance to its ``bandwidth_neighbors``-th nearest
32
+ neighbor, instead of a single global ``h``. This lets the kernel
33
+ widen in sparse regions and narrow in dense ones (a variable-kernel
34
+ / "balloon" estimator), which the fixed-bandwidth Parzen window
35
+ cannot do.
36
+ bandwidth_neighbors : int, default=5
37
+ Number of neighbors used to derive the adaptive bandwidth. Ignored
38
+ when ``adaptive_bandwidth=False``.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ h: float = 0.5,
44
+ kernel: str = "gaussian",
45
+ adaptive_bandwidth: bool = False,
46
+ bandwidth_neighbors: int = 5,
47
+ ):
48
+ self.h = h
49
+ self.kernel = kernel
50
+ self.adaptive_bandwidth = adaptive_bandwidth
51
+ self.bandwidth_neighbors = bandwidth_neighbors
52
+ self._scheduler = CompactionScheduler()
53
+ self._bandwidth_cache = None
54
+
55
+ # -- persistence-related plumbing --------------------------------
56
+
57
+ def __getstate__(self):
58
+ state = self.__dict__.copy()
59
+ state.pop("_scheduler", None)
60
+ return state
61
+
62
+ def __setstate__(self, state):
63
+ self.__dict__.update(state)
64
+ self._scheduler = CompactionScheduler()
65
+
66
+ @property
67
+ def is_compacting(self) -> bool:
68
+ return self._scheduler.is_compacting
69
+
70
+ # -- fitting --------------------------------------------------------
71
+
72
+ def fit(self, X, y):
73
+ """(Re)fit the model from scratch, discarding any previous data."""
74
+ X = check_array(X)
75
+ y = np.asarray(y)
76
+ return self._scheduler.call(self._fit_impl, X, y)
77
+
78
+ def _fit_impl(self, X, y):
79
+ self.X_train_ = X
80
+ self.y_train_ = y
81
+ self.classes_ = np.unique(y)
82
+ self._bandwidth_cache = None
83
+ return self
84
+
85
+ def partial_fit(self, X, y):
86
+ """Incrementally fit the model on a new batch, keeping prior data.
87
+
88
+ While a :meth:`compact` call is in progress on another thread, this
89
+ call does not fail -- it is queued and applied automatically right
90
+ after compaction finishes.
91
+ """
92
+ check_is_fitted(self, "X_train_", msg=_NOT_FITTED_MSG)
93
+ X = check_array(X)
94
+ y = np.asarray(y)
95
+ return self._scheduler.call(self._partial_fit_impl, X, y)
96
+
97
+ def _partial_fit_impl(self, X, y):
98
+ self.X_train_ = np.vstack([self.X_train_, X])
99
+ self.y_train_ = np.concatenate([self.y_train_, y])
100
+ self.classes_ = np.unique(self.y_train_)
101
+ self._bandwidth_cache = None
102
+ return self
103
+
104
+ # -- inference --------------------------------------------------------
105
+
106
+ def predict(self, X):
107
+ """Predict class labels. Queued automatically if compaction is running."""
108
+ check_is_fitted(self, "X_train_", msg=_NOT_FITTED_MSG)
109
+ X = check_array(X)
110
+ return self._scheduler.call(self._predict_impl, X)
111
+
112
+ def _predict_impl(self, X):
113
+ scores = self._class_scores(X)
114
+ return self.classes_[np.argmax(scores, axis=1)]
115
+
116
+ def predict_proba(self, X):
117
+ """Predict per-class weights normalized to sum to 1 for each sample."""
118
+ check_is_fitted(self, "X_train_", msg=_NOT_FITTED_MSG)
119
+ X = check_array(X)
120
+ return self._scheduler.call(self._predict_proba_impl, X)
121
+
122
+ def _predict_proba_impl(self, X):
123
+ scores = self._class_scores(X)
124
+ totals = scores.sum(axis=1, keepdims=True)
125
+ totals[totals == 0] = 1.0 # query point has ~zero kernel weight everywhere
126
+ return scores / totals
127
+
128
+ def _class_scores(self, X):
129
+ kernel_fn = resolve_kernel(self.kernel)
130
+ distances = cdist(X, self.X_train_)
131
+ kernel_values = kernel_fn(distances / self._bandwidth())
132
+ scores = np.zeros((len(X), len(self.classes_)))
133
+ for i, label in enumerate(self.classes_):
134
+ scores[:, i] = kernel_values[:, self.y_train_ == label].sum(axis=1)
135
+ return scores
136
+
137
+ def _bandwidth(self):
138
+ if not self.adaptive_bandwidth:
139
+ return self.h
140
+ if self._bandwidth_cache is None:
141
+ self._bandwidth_cache = self._compute_adaptive_bandwidth()
142
+ return self._bandwidth_cache
143
+
144
+ def _compute_adaptive_bandwidth(self):
145
+ n_samples = len(self.X_train_)
146
+ k = min(self.bandwidth_neighbors, n_samples - 1)
147
+ if k < 1:
148
+ return np.full(n_samples, self.h)
149
+ neighbors = NearestNeighbors(n_neighbors=k + 1).fit(self.X_train_)
150
+ distances, _ = neighbors.kneighbors(self.X_train_)
151
+ bandwidth = self.h * distances[:, -1]
152
+ bandwidth[bandwidth == 0] = self.h
153
+ return bandwidth
154
+
155
+ # -- memory management --------------------------------------------------------
156
+
157
+ def compact(
158
+ self, reduction_fraction: float, n_neighbors: int = 5, min_per_class: int = 2
159
+ ) -> int:
160
+ """Drop training points that lie deep inside their own class's territory.
161
+
162
+ Uses convex-neighborhood analysis (see :mod:`parzen_window.compaction`)
163
+ to find points that are both surrounded entirely by same-class
164
+ neighbors and far from the decision boundary, then removes up to
165
+ ``reduction_fraction`` of the dataset. While this runs, other threads'
166
+ ``predict``/``partial_fit`` calls are queued and executed automatically
167
+ as soon as compaction completes, instead of failing.
168
+
169
+ Returns the number of points actually removed.
170
+ """
171
+ check_is_fitted(self, "X_train_", msg=_NOT_FITTED_MSG)
172
+ return self._scheduler.run_exclusively(
173
+ self._compact_impl, reduction_fraction, n_neighbors, min_per_class
174
+ )
175
+
176
+ def _compact_impl(self, reduction_fraction, n_neighbors, min_per_class):
177
+ to_remove = select_points_to_remove(
178
+ self.X_train_, self.y_train_, reduction_fraction, n_neighbors, min_per_class
179
+ )
180
+ if len(to_remove) == 0:
181
+ return 0
182
+ mask = np.ones(len(self.X_train_), dtype=bool)
183
+ mask[to_remove] = False
184
+ self.X_train_ = self.X_train_[mask]
185
+ self.y_train_ = self.y_train_[mask]
186
+ self._bandwidth_cache = None
187
+ return len(to_remove)
188
+
189
+ # -- persistence --------------------------------------------------------
190
+
191
+ def save(self, path) -> None:
192
+ """Save model weights to a ``.npz`` file for backup or transfer."""
193
+ check_is_fitted(self, "X_train_", msg=_NOT_FITTED_MSG)
194
+ self._scheduler.call(
195
+ _io.save_npz,
196
+ path,
197
+ h=self.h,
198
+ kernel=self.kernel,
199
+ adaptive_bandwidth=self.adaptive_bandwidth,
200
+ bandwidth_neighbors=self.bandwidth_neighbors,
201
+ X_train=self.X_train_,
202
+ y_train=self.y_train_,
203
+ classes=self.classes_,
204
+ )
205
+
206
+ @classmethod
207
+ def load(cls, path) -> ParzenWindowClassifier:
208
+ """Load a model previously saved with :meth:`save`."""
209
+ data = _io.load_npz(path)
210
+ model = cls(
211
+ h=data["h"],
212
+ kernel=data["kernel"],
213
+ adaptive_bandwidth=data["adaptive_bandwidth"],
214
+ bandwidth_neighbors=data["bandwidth_neighbors"],
215
+ )
216
+ model.X_train_ = data["X_train"]
217
+ model.y_train_ = data["y_train"]
218
+ model.classes_ = data["classes"]
219
+ return model
parzen_window/io.py ADDED
@@ -0,0 +1,33 @@
1
+ """Persisting model weights to/from ``.npz`` files for backups and transfer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+
7
+
8
+ def save_npz(
9
+ path, *, h, kernel, adaptive_bandwidth, bandwidth_neighbors, X_train, y_train, classes
10
+ ) -> None:
11
+ np.savez(
12
+ path,
13
+ h=np.asarray(h),
14
+ kernel=np.asarray(kernel),
15
+ adaptive_bandwidth=np.asarray(adaptive_bandwidth),
16
+ bandwidth_neighbors=np.asarray(bandwidth_neighbors),
17
+ X_train=X_train,
18
+ y_train=y_train,
19
+ classes=classes,
20
+ )
21
+
22
+
23
+ def load_npz(path) -> dict:
24
+ with np.load(path, allow_pickle=False) as data:
25
+ return {
26
+ "h": float(data["h"]),
27
+ "kernel": str(data["kernel"]),
28
+ "adaptive_bandwidth": bool(data["adaptive_bandwidth"]),
29
+ "bandwidth_neighbors": int(data["bandwidth_neighbors"]),
30
+ "X_train": data["X_train"],
31
+ "y_train": data["y_train"],
32
+ "classes": data["classes"],
33
+ }
@@ -0,0 +1,45 @@
1
+ """Kernel functions used by :class:`parzen_window.core.ParzenWindowClassifier`.
2
+
3
+ Every kernel accepts a numpy array of normalized distances ``u = distance / h``
4
+ and returns the corresponding weight, vectorized over arrays of any shape.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+
11
+
12
+ def gaussian(u: np.ndarray) -> np.ndarray:
13
+ return np.exp(-0.5 * u**2) / np.sqrt(2 * np.pi)
14
+
15
+
16
+ def epanechnikov(u: np.ndarray) -> np.ndarray:
17
+ return np.where(np.abs(u) <= 1, 0.75 * (1 - u**2), 0.0)
18
+
19
+
20
+ def quartic(u: np.ndarray) -> np.ndarray:
21
+ return np.where(np.abs(u) <= 1, (15.0 / 16.0) * (1 - u**2) ** 2, 0.0)
22
+
23
+
24
+ def triangular(u: np.ndarray) -> np.ndarray:
25
+ return np.where(np.abs(u) <= 1, 1 - np.abs(u), 0.0)
26
+
27
+
28
+ def rectangular(u: np.ndarray) -> np.ndarray:
29
+ return np.where(np.abs(u) <= 1, 0.5, 0.0)
30
+
31
+
32
+ KERNELS = {
33
+ "gaussian": gaussian,
34
+ "epanechnikov": epanechnikov,
35
+ "quartic": quartic,
36
+ "triangular": triangular,
37
+ "rectangular": rectangular,
38
+ }
39
+
40
+
41
+ def resolve_kernel(name: str):
42
+ try:
43
+ return KERNELS[name]
44
+ except KeyError as exc:
45
+ raise ValueError(f"Unknown kernel {name!r}. Supported kernels: {sorted(KERNELS)}") from exc
@@ -0,0 +1,82 @@
1
+ """Thread-safety around dataset compaction.
2
+
3
+ While :meth:`~parzen_window.core.ParzenWindowClassifier.compact` runs, the
4
+ training set is being rewritten in place, so calls to ``predict`` or
5
+ ``partial_fit`` made from other threads cannot run concurrently with it.
6
+ Rather than failing, such calls are queued and executed automatically, in
7
+ arrival order, right after compaction finishes -- acting as a small
8
+ scheduler for deferred work.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ from collections import deque
15
+ from collections.abc import Callable
16
+
17
+
18
+ class _PendingCall:
19
+ __slots__ = ("_func", "_args", "_kwargs", "_done", "_result", "_error")
20
+
21
+ def __init__(self, func: Callable, args: tuple, kwargs: dict):
22
+ self._func = func
23
+ self._args = args
24
+ self._kwargs = kwargs
25
+ self._done = threading.Event()
26
+ self._result = None
27
+ self._error = None
28
+
29
+ def run(self) -> None:
30
+ try:
31
+ self._result = self._func(*self._args, **self._kwargs)
32
+ except Exception as error: # re-raised in the waiting thread
33
+ self._error = error
34
+ finally:
35
+ self._done.set()
36
+
37
+ def wait(self):
38
+ self._done.wait()
39
+ if self._error is not None:
40
+ raise self._error
41
+ return self._result
42
+
43
+
44
+ class CompactionScheduler:
45
+ """Serializes access to model data and defers calls during compaction."""
46
+
47
+ def __init__(self) -> None:
48
+ self._lock = threading.RLock()
49
+ self._compacting = threading.Event()
50
+ self._queue: deque[_PendingCall] = deque()
51
+ self._queue_lock = threading.Lock()
52
+
53
+ @property
54
+ def is_compacting(self) -> bool:
55
+ return self._compacting.is_set()
56
+
57
+ def call(self, func: Callable, *args, **kwargs):
58
+ """Run ``func`` now, or queue it while compaction is in progress."""
59
+ if self._compacting.is_set():
60
+ pending = _PendingCall(func, args, kwargs)
61
+ with self._queue_lock:
62
+ self._queue.append(pending)
63
+ return pending.wait()
64
+ with self._lock:
65
+ return func(*args, **kwargs)
66
+
67
+ def run_exclusively(self, func: Callable, *args, **kwargs):
68
+ """Run ``func`` (compaction) with exclusive access to the data."""
69
+ with self._lock:
70
+ self._compacting.set()
71
+ try:
72
+ return func(*args, **kwargs)
73
+ finally:
74
+ self._compacting.clear()
75
+ self._drain_queue()
76
+
77
+ def _drain_queue(self) -> None:
78
+ with self._queue_lock:
79
+ pending_calls = list(self._queue)
80
+ self._queue.clear()
81
+ for pending in pending_calls:
82
+ pending.run()
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.5
2
+ Name: parzen_window
3
+ Version: 0.0.2
4
+ Summary: Parzen window classification with incremental learning, memory-efficient compaction and adaptive bandwidth.
5
+ Project-URL: Homepage, https://github.com/89605502155/parzen_window
6
+ Project-URL: Download, https://github.com/89605502155/parzen_window/archive/main.zip
7
+ Author-email: Andrey Ferubko <ferubko1999@yandex.ru>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: numpy
23
+ Requires-Dist: scikit-learn
24
+ Requires-Dist: scipy
25
+ Description-Content-Type: text/markdown
26
+
27
+ # parzen_window
28
+
29
+ [Русский](#русский) | [English](#english)
30
+
31
+ ---
32
+
33
+ ## English
34
+
35
+ `parzen_window` is a Python library implementing the Parzen window method for
36
+ classification, built on top of `numpy` and `scikit-learn` (`BaseEstimator`,
37
+ `ClassifierMixin`).
38
+
39
+ ### Features
40
+
41
+ - Five kernel shapes: `gaussian`, `epanechnikov`, `quartic`, `triangular`, `rectangular`.
42
+ - **Adaptive bandwidth** — each training point can get its own kernel width,
43
+ derived from the distance to its `k`-th nearest neighbor, instead of a
44
+ single fixed `h` for the whole dataset (a variable-kernel / "balloon"
45
+ estimator). This is the library's scientific contribution over plain
46
+ fixed-bandwidth Parzen window implementations: it widens the kernel in
47
+ sparse regions and narrows it in dense ones automatically.
48
+ - **Incremental learning** — `partial_fit()` extends the model with a new
49
+ batch of data without discarding what it already learned.
50
+ - **Dataset compaction** — `compact()` reduces memory usage by removing
51
+ training points that are deep inside their own class's territory (found
52
+ via convex-neighborhood analysis: a point is dropped only if all its
53
+ nearest neighbors share its label *and* it is far from the nearest point
54
+ of a different class). This shrinks the stored dataset with minimal
55
+ impact on the decision boundary.
56
+ - **Thread-safe compaction with a call scheduler** — while `compact()` is
57
+ running, the training data is locked for writes. `predict()` and
58
+ `partial_fit()` calls from other threads do not fail during this time:
59
+ they are queued and executed automatically as soon as compaction
60
+ finishes.
61
+ - **`.npz` persistence** — `save()`/`load()` for backups and moving a model
62
+ between machines.
63
+ - Fully compatible with `pickle`/`joblib`.
64
+
65
+ ### Installation
66
+
67
+ ```bash
68
+ pip install parzen_window
69
+ ```
70
+
71
+ ### Quick start
72
+
73
+ ```python
74
+ from parzen_window import ParzenWindowClassifier
75
+ from sklearn.datasets import load_iris
76
+ from sklearn.model_selection import train_test_split
77
+
78
+ X, y = load_iris(return_X_y=True)
79
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
80
+
81
+ model = ParzenWindowClassifier(h=1.0, kernel="quartic")
82
+ model.fit(X_train, y_train)
83
+ predictions = model.predict(X_test)
84
+ probabilities = model.predict_proba(X_test)
85
+ ```
86
+
87
+ Incremental learning:
88
+
89
+ ```python
90
+ model = ParzenWindowClassifier(h=0.5, kernel="gaussian")
91
+ model.fit(X_batch_1, y_batch_1)
92
+ model.partial_fit(X_batch_2, y_batch_2) # extends, does not discard X_batch_1
93
+ ```
94
+
95
+ Adaptive bandwidth:
96
+
97
+ ```python
98
+ model = ParzenWindowClassifier(
99
+ h=1.0, kernel="gaussian", adaptive_bandwidth=True, bandwidth_neighbors=5
100
+ )
101
+ model.fit(X_train, y_train)
102
+ ```
103
+
104
+ Compacting a large dataset to cut memory usage by ~25%:
105
+
106
+ ```python
107
+ removed = model.compact(0.25) # returns the number of points actually removed
108
+ ```
109
+
110
+ Backups:
111
+
112
+ ```python
113
+ model.save("model.npz")
114
+ restored = ParzenWindowClassifier.load("model.npz")
115
+ ```
116
+
117
+ ### Development
118
+
119
+ The project uses [uv](https://docs.astral.sh/uv/) for dependency management.
120
+
121
+ ```bash
122
+ uv sync --dev # install dependencies
123
+ uv run ruff check . # lint
124
+ uv run ruff format . # format
125
+ uv build # build sdist + wheel
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Русский
131
+
132
+ `parzen_window` — библиотека на Python, реализующая метод Парзеновского окна
133
+ для классификации, построенная поверх `numpy` и `scikit-learn`
134
+ (`BaseEstimator`, `ClassifierMixin`).
135
+
136
+ ### Возможности
137
+
138
+ - Пять видов ядер: `gaussian`, `epanechnikov`, `quartic`, `triangular`, `rectangular`.
139
+ - **Адаптивная ширина окна** — каждая точка обучающей выборки может получить
140
+ собственную ширину ядра, вычисленную по расстоянию до её `k`-го ближайшего
141
+ соседа, вместо единого фиксированного `h` на весь датасет (вариант
142
+ variable-kernel / "balloon"-оценки). Это научная новизна библиотеки по
143
+ сравнению с обычными реализациями Парзеновского окна с фиксированной
144
+ шириной: окно автоматически расширяется в разреженных областях и
145
+ сужается в плотных.
146
+ - **Дообучение** — `partial_fit()` расширяет модель новым батчем данных, не
147
+ теряя уже накопленное.
148
+ - **Разрядка датасета** — `compact()` уменьшает потребление памяти, удаляя
149
+ точки обучающей выборки, которые лежат глубоко на территории своего
150
+ класса (метод анализа выпуклых окрестностей: точка удаляется, только
151
+ если все её ближайшие соседи имеют ту же метку класса *и* она далека от
152
+ ближайшей точки другого класса). Это сокращает хранимый датасет с
153
+ минимальным влиянием на границу между классами.
154
+ - **Потокобезопасная разрядка с планировщиком вызовов** — пока выполняется
155
+ `compact()`, обучающие данные заблокированы для записи. Вызовы
156
+ `predict()` и `partial_fit()` из других потоков в это время не падают:
157
+ они встают в очередь и выполняются автоматически сразу после завершения
158
+ разрядки.
159
+ - **Сохранение в `.npz`** — `save()`/`load()` для бэкапов и переноса модели
160
+ между устройствами.
161
+ - Полная совместимость с `pickle`/`joblib`.
162
+
163
+ ### Установка
164
+
165
+ ```bash
166
+ pip install parzen_window
167
+ ```
168
+
169
+ ### Быстрый старт
170
+
171
+ ```python
172
+ from parzen_window import ParzenWindowClassifier
173
+ from sklearn.datasets import load_iris
174
+ from sklearn.model_selection import train_test_split
175
+
176
+ X, y = load_iris(return_X_y=True)
177
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
178
+
179
+ model = ParzenWindowClassifier(h=1.0, kernel="quartic")
180
+ model.fit(X_train, y_train)
181
+ predictions = model.predict(X_test)
182
+ probabilities = model.predict_proba(X_test)
183
+ ```
184
+
185
+ Дообучение:
186
+
187
+ ```python
188
+ model = ParzenWindowClassifier(h=0.5, kernel="gaussian")
189
+ model.fit(X_batch_1, y_batch_1)
190
+ model.partial_fit(X_batch_2, y_batch_2) # расширяет, не теряя X_batch_1
191
+ ```
192
+
193
+ Адаптивная ширина окна:
194
+
195
+ ```python
196
+ model = ParzenWindowClassifier(
197
+ h=1.0, kernel="gaussian", adaptive_bandwidth=True, bandwidth_neighbors=5
198
+ )
199
+ model.fit(X_train, y_train)
200
+ ```
201
+
202
+ Разрядка большого датасета, чтобы сократить потребление памяти примерно на 25%:
203
+
204
+ ```python
205
+ removed = model.compact(0.25) # возвращает число реально удалённых точек
206
+ ```
207
+
208
+ Бэкапы:
209
+
210
+ ```python
211
+ model.save("model.npz")
212
+ restored = ParzenWindowClassifier.load("model.npz")
213
+ ```
214
+
215
+ ### Разработка
216
+
217
+ Проект использует [uv](https://docs.astral.sh/uv/) для управления зависимостями.
218
+
219
+ ```bash
220
+ uv sync --dev # установить зависимости
221
+ uv run ruff check . # линтер
222
+ uv run ruff format . # форматирование
223
+ uv build # сборка sdist + wheel
224
+ ```
@@ -0,0 +1,10 @@
1
+ parzen_window/__init__.py,sha256=OspRBpbnhkD3XEzMp2BKf5MkmPx6ftrDr2krqo4CAWk,175
2
+ parzen_window/compaction.py,sha256=YPjxSig0rFZpsLr-vsND9QCisXLS4UVmoijiHt3pU3o,3130
3
+ parzen_window/core.py,sha256=yX3etp6I_eh0oDg9C_4qHeLcZ-fwlnP48eX0AVcUsXw,8636
4
+ parzen_window/io.py,sha256=VI4BisLjByXaHPvae39pmljwXqe9hU-v-TlFdTLIpJs,1020
5
+ parzen_window/kernels.py,sha256=M9GPPY93h-taYfkp05MAjWUdGJ8HSzj122bZYdg3Sko,1223
6
+ parzen_window/scheduler.py,sha256=5sRrw2mh320cRrQx9AUBwvC4HYBTyurcvyhvzit-Bx8,2764
7
+ parzen_window-0.0.2.dist-info/METADATA,sha256=_NRimgct2knqm5MnRE4OaqOO4pFn5cDF9mKELCmymOE,8822
8
+ parzen_window-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ parzen_window-0.0.2.dist-info/licenses/LICENSE,sha256=VnRHTC1ELnJKvHM4h2fDqX_VVD-O4p3FJNFmOK6MUJo,1092
10
+ parzen_window-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Andery Ferubko
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.