generate-test-data-for-wknn 0.0.1__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.
- generate_test_data_for_wknn-0.0.1/PKG-INFO +191 -0
- generate_test_data_for_wknn-0.0.1/README.md +163 -0
- generate_test_data_for_wknn-0.0.1/pyproject.toml +49 -0
- generate_test_data_for_wknn-0.0.1/src/generate_test_data_for_wknn/__init__.py +4 -0
- generate_test_data_for_wknn-0.0.1/src/generate_test_data_for_wknn/_core.py +230 -0
- generate_test_data_for_wknn-0.0.1/src/generate_test_data_for_wknn/py.typed +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: generate-test-data-for-wknn
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Test-data generator for validating the weighted k-NN (wkNN) method.
|
|
5
|
+
Keywords: knn,weighted-knn,wknn,machine-learning,test-data
|
|
6
|
+
Author: Ferubko Andrey, Kazakov Oleg
|
|
7
|
+
Author-email: Ferubko Andrey <andrey.ferubko@petsplace.ru>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Dist: numpy>=1.24
|
|
21
|
+
Maintainer: Ferubko Andrey, Kazakov Oleg
|
|
22
|
+
Maintainer-email: Ferubko Andrey <andrey.ferubko@petsplace.ru>
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Project-URL: Homepage, https://github.com/89605502155/generate-test-data-for-wknn
|
|
25
|
+
Project-URL: Repository, https://github.com/89605502155/generate-test-data-for-wknn
|
|
26
|
+
Project-URL: Issues, https://github.com/89605502155/generate-test-data-for-wknn/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# generate-test-data-for-wknn
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/generate-test-data-for-wknn/)
|
|
32
|
+
[](https://pypi.org/project/generate-test-data-for-wknn/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
**EN** — Synthetic test-data generator for validating the **weighted k-NN (wkNN)** method.
|
|
36
|
+
**RU** — Генератор синтетических тестовых данных для проверки метода **взвешенного k-NN (wkNN)**.
|
|
37
|
+
|
|
38
|
+
*Authors / Авторы:* Ferubko Andrey, Kazakov Oleg
|
|
39
|
+
*Organization / Организация:* Bryansk State Technological University of Engineering /
|
|
40
|
+
Брянский государственный инженерно-технологический университет.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation / Установка
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install generate-test-data-for-wknn
|
|
48
|
+
# or with uv:
|
|
49
|
+
uv add generate-test-data-for-wknn
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## English documentation
|
|
55
|
+
|
|
56
|
+
The library exposes a single public function, `generate`, which builds a
|
|
57
|
+
labelled dataset `(x, y)` for testing weighted k-NN.
|
|
58
|
+
|
|
59
|
+
Each class is given a random **anchor** point inside the cube
|
|
60
|
+
`[min_coord, max_abs_coord]^dim`. Every other random point is assigned to the
|
|
61
|
+
class whose **total weight** is largest. Weights are functions of the
|
|
62
|
+
per-coordinate absolute differences `x_i = |p_i - anchor_i|` and must
|
|
63
|
+
**decrease** as distance grows (otherwise a `ValueError` is raised).
|
|
64
|
+
|
|
65
|
+
### Weight functions and parameter layout
|
|
66
|
+
|
|
67
|
+
| `weight_type` | Formula | `params` length | Constraint |
|
|
68
|
+
|----------------|---------|-----------------|------------|
|
|
69
|
+
| `"constant"` | `w = 1` | ignored | always valid (reduces to plain kNN) |
|
|
70
|
+
| `"linear"` | `w = max(Σ aᵢ·xᵢ + f, 0)` | `dim + 1` → `[a₁,…,a_d, f]` | all `aᵢ ≤ 0` |
|
|
71
|
+
| `"inverse"` | `w = Σ aᵢ / xᵢ^bᵢ + f` | `2·dim + 1` → `[a₁,b₁,…,a_d,b_d, f]` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
72
|
+
| `"exponential"`| `w = Σ aᵢ·exp(−bᵢ·xᵢ) + f` | `2·dim + 1` → `[a₁,b₁,…,a_d,b_d, f]` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
73
|
+
|
|
74
|
+
> Example (from the spec): for `dim=2`, `linear` with `params=[-1, -8, 9]`
|
|
75
|
+
> corresponds to `w = max(-1·x₁ + -8·x₂ + 9, 0)`.
|
|
76
|
+
|
|
77
|
+
### Parameters
|
|
78
|
+
|
|
79
|
+
| Name | Default | Meaning |
|
|
80
|
+
|------|---------|---------|
|
|
81
|
+
| `n_classes` | `2` | number of classes (binary by default) |
|
|
82
|
+
| `dim` | `2` | dimensionality of each point |
|
|
83
|
+
| `n_samples` | `30` | number of objects in the dataset |
|
|
84
|
+
| `weight_type` | `"inverse"` | one of the four weighting schemes |
|
|
85
|
+
| `params` | `None` | coefficient array (see table) |
|
|
86
|
+
| `max_abs_coord` | `1e4` | upper coordinate bound (abs value cap) |
|
|
87
|
+
| `min_coord` | `-1e4` | lower coordinate bound (use `0` for e.g. time series) |
|
|
88
|
+
| `outlier_ratio` | `0.0` | fraction of **borderline** labels to flip, must be in `[0, 1]` |
|
|
89
|
+
| `random_state` | `None` | RNG seed |
|
|
90
|
+
|
|
91
|
+
**Returns:** `(x, y)` — `x` is `float64` of shape `(m, dim)`, `y` is `int64` of
|
|
92
|
+
shape `(m,)`, where `m ≤ n_samples` (tie points are removed).
|
|
93
|
+
|
|
94
|
+
### Behavioural guarantees
|
|
95
|
+
|
|
96
|
+
- **Stratification:** classes are balanced (counts differ by at most 1).
|
|
97
|
+
- **Ties dropped:** points where the top-2 class weights are equal are removed.
|
|
98
|
+
- **Outliers:** the requested fraction of the *most borderline* points is flipped
|
|
99
|
+
to the runner-up class, spread evenly across the cube (not from a single plane).
|
|
100
|
+
- **Monotonicity guard:** non-decreasing weight parameters raise `ValueError`.
|
|
101
|
+
|
|
102
|
+
### Example
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import numpy as np
|
|
106
|
+
from generate_test_data_for_wknn import generate
|
|
107
|
+
|
|
108
|
+
# Binary, inverse-distance weights, 2-D, reproducible
|
|
109
|
+
x, y = generate(
|
|
110
|
+
n_classes=2,
|
|
111
|
+
dim=2,
|
|
112
|
+
n_samples=50,
|
|
113
|
+
weight_type="inverse",
|
|
114
|
+
params=np.array([2.0, 1.0, 3.0, 1.5, 0.1]), # a1,b1,a2,b2,f
|
|
115
|
+
max_abs_coord=100.0,
|
|
116
|
+
min_coord=0.0, # non-negative coords (e.g. time series)
|
|
117
|
+
outlier_ratio=0.1,
|
|
118
|
+
random_state=42,
|
|
119
|
+
)
|
|
120
|
+
print(x.shape, y.shape, np.bincount(y))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Документация на русском
|
|
126
|
+
|
|
127
|
+
Библиотека предоставляет одну публичную функцию `generate`, которая строит
|
|
128
|
+
размеченный набор данных `(x, y)` для проверки взвешенного k-NN.
|
|
129
|
+
|
|
130
|
+
Для каждого класса создаётся случайная **опорная точка** внутри куба
|
|
131
|
+
`[min_coord, max_abs_coord]^dim`. Каждой другой случайной точке присваивается
|
|
132
|
+
класс с наибольшим **суммарным весом**. Веса зависят от модулей разностей
|
|
133
|
+
координат `x_i = |p_i − anchor_i|` и **должны убывать** с ростом расстояния
|
|
134
|
+
(иначе возбуждается `ValueError`).
|
|
135
|
+
|
|
136
|
+
### Весовые функции и формат параметров
|
|
137
|
+
|
|
138
|
+
| `weight_type` | Формула | Длина `params` | Ограничение |
|
|
139
|
+
|----------------|---------|----------------|-------------|
|
|
140
|
+
| `"constant"` | `w = 1` | не используется | всегда допустимо (обычный kNN) |
|
|
141
|
+
| `"linear"` | `w = max(Σ aᵢ·xᵢ + f, 0)` | `dim + 1` | все `aᵢ ≤ 0` |
|
|
142
|
+
| `"inverse"` | `w = Σ aᵢ / xᵢ^bᵢ + f` | `2·dim + 1` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
143
|
+
| `"exponential"`| `w = Σ aᵢ·exp(−bᵢ·xᵢ) + f` | `2·dim + 1` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
144
|
+
|
|
145
|
+
### Параметры
|
|
146
|
+
|
|
147
|
+
| Имя | По умолчанию | Смысл |
|
|
148
|
+
|-----|--------------|-------|
|
|
149
|
+
| `n_classes` | `2` | число классов (по умолчанию бинарная задача) |
|
|
150
|
+
| `dim` | `2` | размерность точки |
|
|
151
|
+
| `n_samples` | `30` | число объектов |
|
|
152
|
+
| `weight_type` | `"inverse"` | схема взвешивания |
|
|
153
|
+
| `params` | `None` | массив коэффициентов (см. таблицу) |
|
|
154
|
+
| `max_abs_coord` | `1e4` | верхняя граница координат по модулю |
|
|
155
|
+
| `min_coord` | `-1e4` | нижняя граница (для временных рядов задайте `0`) |
|
|
156
|
+
| `outlier_ratio` | `0.0` | доля переставляемых **пограничных** меток, из `[0, 1]` |
|
|
157
|
+
| `random_state` | `None` | зерно генератора |
|
|
158
|
+
|
|
159
|
+
**Возвращает:** `(x, y)` — массивы NumPy; `m ≤ n_samples` (точки с ничьей удаляются).
|
|
160
|
+
|
|
161
|
+
### Гарантии поведения
|
|
162
|
+
|
|
163
|
+
- **Стратификация:** классы сбалансированы (разница ≤ 1 объект).
|
|
164
|
+
- **Удаление ничьих:** точки с равными весами двух лучших классов удаляются.
|
|
165
|
+
- **Выбросы:** заданная доля наиболее **пограничных** точек переставляется в
|
|
166
|
+
класс-«второе место», равномерно по всему кубу (а не из одной плоскости).
|
|
167
|
+
- **Проверка монотонности:** невозрастающие/неубывающие параметры → `ValueError`.
|
|
168
|
+
|
|
169
|
+
### Пример
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import numpy as np
|
|
173
|
+
from generate_test_data_for_wknn import generate
|
|
174
|
+
|
|
175
|
+
x, y = generate(
|
|
176
|
+
n_classes=3,
|
|
177
|
+
dim=2,
|
|
178
|
+
n_samples=60,
|
|
179
|
+
weight_type="exponential",
|
|
180
|
+
params=np.array([5.0, 0.01, 4.0, 0.02, 0.0]), # a1,b1,a2,b2,f
|
|
181
|
+
outlier_ratio=0.15,
|
|
182
|
+
random_state=7,
|
|
183
|
+
)
|
|
184
|
+
print(x.shape, y.shape, np.bincount(y))
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## License / Лицензия
|
|
190
|
+
|
|
191
|
+
MIT © Ferubko Andrey, Kazakov Oleg — Bryansk State Technological University of Engineering.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# generate-test-data-for-wknn
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/generate-test-data-for-wknn/)
|
|
4
|
+
[](https://pypi.org/project/generate-test-data-for-wknn/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**EN** — Synthetic test-data generator for validating the **weighted k-NN (wkNN)** method.
|
|
8
|
+
**RU** — Генератор синтетических тестовых данных для проверки метода **взвешенного k-NN (wkNN)**.
|
|
9
|
+
|
|
10
|
+
*Authors / Авторы:* Ferubko Andrey, Kazakov Oleg
|
|
11
|
+
*Organization / Организация:* Bryansk State Technological University of Engineering /
|
|
12
|
+
Брянский государственный инженерно-технологический университет.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation / Установка
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install generate-test-data-for-wknn
|
|
20
|
+
# or with uv:
|
|
21
|
+
uv add generate-test-data-for-wknn
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## English documentation
|
|
27
|
+
|
|
28
|
+
The library exposes a single public function, `generate`, which builds a
|
|
29
|
+
labelled dataset `(x, y)` for testing weighted k-NN.
|
|
30
|
+
|
|
31
|
+
Each class is given a random **anchor** point inside the cube
|
|
32
|
+
`[min_coord, max_abs_coord]^dim`. Every other random point is assigned to the
|
|
33
|
+
class whose **total weight** is largest. Weights are functions of the
|
|
34
|
+
per-coordinate absolute differences `x_i = |p_i - anchor_i|` and must
|
|
35
|
+
**decrease** as distance grows (otherwise a `ValueError` is raised).
|
|
36
|
+
|
|
37
|
+
### Weight functions and parameter layout
|
|
38
|
+
|
|
39
|
+
| `weight_type` | Formula | `params` length | Constraint |
|
|
40
|
+
|----------------|---------|-----------------|------------|
|
|
41
|
+
| `"constant"` | `w = 1` | ignored | always valid (reduces to plain kNN) |
|
|
42
|
+
| `"linear"` | `w = max(Σ aᵢ·xᵢ + f, 0)` | `dim + 1` → `[a₁,…,a_d, f]` | all `aᵢ ≤ 0` |
|
|
43
|
+
| `"inverse"` | `w = Σ aᵢ / xᵢ^bᵢ + f` | `2·dim + 1` → `[a₁,b₁,…,a_d,b_d, f]` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
44
|
+
| `"exponential"`| `w = Σ aᵢ·exp(−bᵢ·xᵢ) + f` | `2·dim + 1` → `[a₁,b₁,…,a_d,b_d, f]` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
45
|
+
|
|
46
|
+
> Example (from the spec): for `dim=2`, `linear` with `params=[-1, -8, 9]`
|
|
47
|
+
> corresponds to `w = max(-1·x₁ + -8·x₂ + 9, 0)`.
|
|
48
|
+
|
|
49
|
+
### Parameters
|
|
50
|
+
|
|
51
|
+
| Name | Default | Meaning |
|
|
52
|
+
|------|---------|---------|
|
|
53
|
+
| `n_classes` | `2` | number of classes (binary by default) |
|
|
54
|
+
| `dim` | `2` | dimensionality of each point |
|
|
55
|
+
| `n_samples` | `30` | number of objects in the dataset |
|
|
56
|
+
| `weight_type` | `"inverse"` | one of the four weighting schemes |
|
|
57
|
+
| `params` | `None` | coefficient array (see table) |
|
|
58
|
+
| `max_abs_coord` | `1e4` | upper coordinate bound (abs value cap) |
|
|
59
|
+
| `min_coord` | `-1e4` | lower coordinate bound (use `0` for e.g. time series) |
|
|
60
|
+
| `outlier_ratio` | `0.0` | fraction of **borderline** labels to flip, must be in `[0, 1]` |
|
|
61
|
+
| `random_state` | `None` | RNG seed |
|
|
62
|
+
|
|
63
|
+
**Returns:** `(x, y)` — `x` is `float64` of shape `(m, dim)`, `y` is `int64` of
|
|
64
|
+
shape `(m,)`, where `m ≤ n_samples` (tie points are removed).
|
|
65
|
+
|
|
66
|
+
### Behavioural guarantees
|
|
67
|
+
|
|
68
|
+
- **Stratification:** classes are balanced (counts differ by at most 1).
|
|
69
|
+
- **Ties dropped:** points where the top-2 class weights are equal are removed.
|
|
70
|
+
- **Outliers:** the requested fraction of the *most borderline* points is flipped
|
|
71
|
+
to the runner-up class, spread evenly across the cube (not from a single plane).
|
|
72
|
+
- **Monotonicity guard:** non-decreasing weight parameters raise `ValueError`.
|
|
73
|
+
|
|
74
|
+
### Example
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import numpy as np
|
|
78
|
+
from generate_test_data_for_wknn import generate
|
|
79
|
+
|
|
80
|
+
# Binary, inverse-distance weights, 2-D, reproducible
|
|
81
|
+
x, y = generate(
|
|
82
|
+
n_classes=2,
|
|
83
|
+
dim=2,
|
|
84
|
+
n_samples=50,
|
|
85
|
+
weight_type="inverse",
|
|
86
|
+
params=np.array([2.0, 1.0, 3.0, 1.5, 0.1]), # a1,b1,a2,b2,f
|
|
87
|
+
max_abs_coord=100.0,
|
|
88
|
+
min_coord=0.0, # non-negative coords (e.g. time series)
|
|
89
|
+
outlier_ratio=0.1,
|
|
90
|
+
random_state=42,
|
|
91
|
+
)
|
|
92
|
+
print(x.shape, y.shape, np.bincount(y))
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Документация на русском
|
|
98
|
+
|
|
99
|
+
Библиотека предоставляет одну публичную функцию `generate`, которая строит
|
|
100
|
+
размеченный набор данных `(x, y)` для проверки взвешенного k-NN.
|
|
101
|
+
|
|
102
|
+
Для каждого класса создаётся случайная **опорная точка** внутри куба
|
|
103
|
+
`[min_coord, max_abs_coord]^dim`. Каждой другой случайной точке присваивается
|
|
104
|
+
класс с наибольшим **суммарным весом**. Веса зависят от модулей разностей
|
|
105
|
+
координат `x_i = |p_i − anchor_i|` и **должны убывать** с ростом расстояния
|
|
106
|
+
(иначе возбуждается `ValueError`).
|
|
107
|
+
|
|
108
|
+
### Весовые функции и формат параметров
|
|
109
|
+
|
|
110
|
+
| `weight_type` | Формула | Длина `params` | Ограничение |
|
|
111
|
+
|----------------|---------|----------------|-------------|
|
|
112
|
+
| `"constant"` | `w = 1` | не используется | всегда допустимо (обычный kNN) |
|
|
113
|
+
| `"linear"` | `w = max(Σ aᵢ·xᵢ + f, 0)` | `dim + 1` | все `aᵢ ≤ 0` |
|
|
114
|
+
| `"inverse"` | `w = Σ aᵢ / xᵢ^bᵢ + f` | `2·dim + 1` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
115
|
+
| `"exponential"`| `w = Σ aᵢ·exp(−bᵢ·xᵢ) + f` | `2·dim + 1` | `aᵢ ≥ 0`, `bᵢ > 0` |
|
|
116
|
+
|
|
117
|
+
### Параметры
|
|
118
|
+
|
|
119
|
+
| Имя | По умолчанию | Смысл |
|
|
120
|
+
|-----|--------------|-------|
|
|
121
|
+
| `n_classes` | `2` | число классов (по умолчанию бинарная задача) |
|
|
122
|
+
| `dim` | `2` | размерность точки |
|
|
123
|
+
| `n_samples` | `30` | число объектов |
|
|
124
|
+
| `weight_type` | `"inverse"` | схема взвешивания |
|
|
125
|
+
| `params` | `None` | массив коэффициентов (см. таблицу) |
|
|
126
|
+
| `max_abs_coord` | `1e4` | верхняя граница координат по модулю |
|
|
127
|
+
| `min_coord` | `-1e4` | нижняя граница (для временных рядов задайте `0`) |
|
|
128
|
+
| `outlier_ratio` | `0.0` | доля переставляемых **пограничных** меток, из `[0, 1]` |
|
|
129
|
+
| `random_state` | `None` | зерно генератора |
|
|
130
|
+
|
|
131
|
+
**Возвращает:** `(x, y)` — массивы NumPy; `m ≤ n_samples` (точки с ничьей удаляются).
|
|
132
|
+
|
|
133
|
+
### Гарантии поведения
|
|
134
|
+
|
|
135
|
+
- **Стратификация:** классы сбалансированы (разница ≤ 1 объект).
|
|
136
|
+
- **Удаление ничьих:** точки с равными весами двух лучших классов удаляются.
|
|
137
|
+
- **Выбросы:** заданная доля наиболее **пограничных** точек переставляется в
|
|
138
|
+
класс-«второе место», равномерно по всему кубу (а не из одной плоскости).
|
|
139
|
+
- **Проверка монотонности:** невозрастающие/неубывающие параметры → `ValueError`.
|
|
140
|
+
|
|
141
|
+
### Пример
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import numpy as np
|
|
145
|
+
from generate_test_data_for_wknn import generate
|
|
146
|
+
|
|
147
|
+
x, y = generate(
|
|
148
|
+
n_classes=3,
|
|
149
|
+
dim=2,
|
|
150
|
+
n_samples=60,
|
|
151
|
+
weight_type="exponential",
|
|
152
|
+
params=np.array([5.0, 0.01, 4.0, 0.02, 0.0]), # a1,b1,a2,b2,f
|
|
153
|
+
outlier_ratio=0.15,
|
|
154
|
+
random_state=7,
|
|
155
|
+
)
|
|
156
|
+
print(x.shape, y.shape, np.bincount(y))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## License / Лицензия
|
|
162
|
+
|
|
163
|
+
MIT © Ferubko Andrey, Kazakov Oleg — Bryansk State Technological University of Engineering.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "generate-test-data-for-wknn"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Test-data generator for validating the weighted k-NN (wkNN) method."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Ferubko Andrey", email = "andrey.ferubko@petsplace.ru" },
|
|
10
|
+
{ name = "Kazakov Oleg" },
|
|
11
|
+
]
|
|
12
|
+
maintainers = [
|
|
13
|
+
{ name = "Ferubko Andrey", email = "andrey.ferubko@petsplace.ru" },
|
|
14
|
+
{ name = "Kazakov Oleg" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["knn", "weighted-knn", "wknn", "machine-learning", "test-data"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"numpy>=1.24",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/89605502155/generate-test-data-for-wknn"
|
|
36
|
+
Repository = "https://github.com/89605502155/generate-test-data-for-wknn"
|
|
37
|
+
Issues = "https://github.com/89605502155/generate-test-data-for-wknn/issues"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.11.9,<0.12.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/generate_test_data_for_wknn"]
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=9.1.1",
|
|
49
|
+
]
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Core implementation of the weighted-kNN test-data generator.
|
|
2
|
+
|
|
3
|
+
Everything lives inside :class:`WknnDataGenerator` — a stateless utility class
|
|
4
|
+
(no ``__init__``): call :meth:`WknnDataGenerator.generate` directly on the class.
|
|
5
|
+
All heavy lifting is delegated to NumPy (vectorised).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import ClassVar, Literal
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
|
|
15
|
+
WeightType = Literal["constant", "linear", "inverse", "exponential"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WknnDataGenerator:
|
|
19
|
+
"""Stateless generator of synthetic datasets for testing weighted k-NN.
|
|
20
|
+
|
|
21
|
+
The class is never instantiated: use the classmethod directly, e.g.
|
|
22
|
+
``x, y = WknnDataGenerator.generate(...)``. The module-level ``generate``
|
|
23
|
+
alias forwards to this method for convenience.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ---- configuration constants ---------------------------------------
|
|
27
|
+
_LEN_DIM: ClassVar[frozenset[str]] = frozenset({"linear"})
|
|
28
|
+
_LEN_2DIM: ClassVar[frozenset[str]] = frozenset({"inverse", "exponential"})
|
|
29
|
+
_EPS: ClassVar[float] = 1e-9 # guards divisions / powers against zero
|
|
30
|
+
_MAX_ATTEMPTS: ClassVar[int] = 50
|
|
31
|
+
|
|
32
|
+
# ---- private helpers (static: no state needed) ----------------------
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _default_params(weight_type: WeightType, dim: int) -> NDArray[np.float64] | None:
|
|
35
|
+
"""Sensible, *decreasing* default coefficients so ``generate()`` works."""
|
|
36
|
+
if weight_type == "constant":
|
|
37
|
+
return None
|
|
38
|
+
if weight_type == "linear":
|
|
39
|
+
# w = max(sum(-1 * x_i) + 1, 0): decreasing (all slopes <= 0).
|
|
40
|
+
return np.concatenate([-np.ones(dim), [1.0]])
|
|
41
|
+
if weight_type in ("inverse", "exponential"):
|
|
42
|
+
# inverse: w = sum(1 / x_i^1) + 0 (a=1>=0, b=1>0)
|
|
43
|
+
# exponential: w = sum(1 * exp(-1*x_i)) (a=1>=0, b=1>0)
|
|
44
|
+
return np.concatenate([np.tile([1.0, 1.0], dim), [0.0]])
|
|
45
|
+
raise ValueError(f"unknown weight_type={weight_type!r}.")
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _validate_params(
|
|
49
|
+
cls,
|
|
50
|
+
weight_type: WeightType,
|
|
51
|
+
params: NDArray[np.float64] | None,
|
|
52
|
+
dim: int,
|
|
53
|
+
) -> tuple[NDArray[np.float64], float]:
|
|
54
|
+
"""Validate params, enforce a *decreasing* weight, return (coeffs, bias)."""
|
|
55
|
+
if weight_type == "constant":
|
|
56
|
+
return np.zeros(dim, dtype=float), 1.0
|
|
57
|
+
|
|
58
|
+
if params is None:
|
|
59
|
+
params = cls._default_params(weight_type, dim)
|
|
60
|
+
params = np.asarray(params, dtype=float).ravel()
|
|
61
|
+
|
|
62
|
+
if weight_type in cls._LEN_DIM:
|
|
63
|
+
if params.size != dim + 1:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"linear weights need dim+1={dim + 1} params, got {params.size}."
|
|
66
|
+
)
|
|
67
|
+
a, f = params[:dim], float(params[dim])
|
|
68
|
+
if np.any(a > 0):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"linear weight is non-decreasing: every coefficient a_i must be <= 0."
|
|
71
|
+
)
|
|
72
|
+
return a, f
|
|
73
|
+
|
|
74
|
+
if weight_type in cls._LEN_2DIM:
|
|
75
|
+
if params.size != 2 * dim + 1:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"{weight_type} weights need 2*dim+1={2 * dim + 1} params, "
|
|
78
|
+
f"got {params.size}."
|
|
79
|
+
)
|
|
80
|
+
ab, f = params[: 2 * dim].reshape(dim, 2), float(params[2 * dim])
|
|
81
|
+
a, b = ab[:, 0], ab[:, 1]
|
|
82
|
+
if np.any(a < 0) or np.any(b <= 0):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"{weight_type} weight is non-decreasing: need a_i >= 0 and b_i > 0."
|
|
85
|
+
)
|
|
86
|
+
return ab.ravel(), f
|
|
87
|
+
|
|
88
|
+
raise ValueError(f"unknown weight_type={weight_type!r}.")
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def _weights(
|
|
92
|
+
cls,
|
|
93
|
+
dx: NDArray[np.float64],
|
|
94
|
+
weight_type: WeightType,
|
|
95
|
+
coeffs: NDArray[np.float64],
|
|
96
|
+
bias: float,
|
|
97
|
+
) -> NDArray[np.float64]:
|
|
98
|
+
"""Vectorised weight for *normalised* |coordinate differences| (..., dim)."""
|
|
99
|
+
if weight_type == "constant":
|
|
100
|
+
return np.ones(dx.shape[:-1], dtype=float)
|
|
101
|
+
if weight_type == "linear":
|
|
102
|
+
return np.maximum(dx @ coeffs + bias, 0.0)
|
|
103
|
+
|
|
104
|
+
ab = coeffs.reshape(-1, 2)
|
|
105
|
+
a, b = ab[:, 0], ab[:, 1]
|
|
106
|
+
dxs = np.maximum(dx, cls._EPS)
|
|
107
|
+
if weight_type == "inverse":
|
|
108
|
+
return np.sum(a / np.power(dxs, b), axis=-1) + bias
|
|
109
|
+
return np.sum(a * np.exp(-b * dx), axis=-1) + bias # exponential
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def _assign(
|
|
113
|
+
cls,
|
|
114
|
+
x: NDArray[np.float64],
|
|
115
|
+
anchors: NDArray[np.float64],
|
|
116
|
+
scale: float,
|
|
117
|
+
weight_type: WeightType,
|
|
118
|
+
coeffs: NDArray[np.float64],
|
|
119
|
+
bias: float,
|
|
120
|
+
) -> tuple[NDArray[np.int64], NDArray[np.float64]]:
|
|
121
|
+
"""Return (labels, per-class score matrix); winner is ``argmax`` of score.
|
|
122
|
+
|
|
123
|
+
For the *constant* weight (plain kNN) all weights are equal, so the
|
|
124
|
+
class is decided by the *nearest anchor*: the score is ``-distance``,
|
|
125
|
+
which yields a proper, non-degenerate decision and meaningful margins.
|
|
126
|
+
"""
|
|
127
|
+
diff = x[:, None, :] - anchors[None, :, :] # (n, n_classes, dim)
|
|
128
|
+
if weight_type == "constant":
|
|
129
|
+
dist = np.linalg.norm(diff, axis=-1)
|
|
130
|
+
return np.argmax(-dist, axis=1).astype(np.int64), -dist
|
|
131
|
+
dx = np.abs(diff) / scale
|
|
132
|
+
w = cls._weights(dx, weight_type, coeffs, bias)
|
|
133
|
+
return np.argmax(w, axis=1).astype(np.int64), w
|
|
134
|
+
|
|
135
|
+
# ---- public API -----------------------------------------------------
|
|
136
|
+
@classmethod
|
|
137
|
+
def generate(
|
|
138
|
+
cls,
|
|
139
|
+
n_classes: int = 2,
|
|
140
|
+
dim: int = 2,
|
|
141
|
+
n_samples: int = 30,
|
|
142
|
+
weight_type: WeightType = "inverse",
|
|
143
|
+
params: NDArray[np.float64] | None = None,
|
|
144
|
+
max_abs_coord: float = 1e4,
|
|
145
|
+
min_coord: float = -1e4,
|
|
146
|
+
outlier_ratio: float = 0.0,
|
|
147
|
+
*,
|
|
148
|
+
random_state: int | None = None,
|
|
149
|
+
) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
|
|
150
|
+
"""Generate a synthetic dataset for testing weighted k-NN classification.
|
|
151
|
+
|
|
152
|
+
See README for the full parameter/weight-function reference. Returns
|
|
153
|
+
``(x, y)`` where ``x`` is float64 ``(m, dim)`` and ``y`` is int64
|
|
154
|
+
``(m,)``, with ``m <= n_samples`` (tie points are dropped).
|
|
155
|
+
"""
|
|
156
|
+
# ---- validation --------------------------------------------------
|
|
157
|
+
if n_classes < 2:
|
|
158
|
+
raise ValueError("n_classes must be >= 2.")
|
|
159
|
+
if dim < 1:
|
|
160
|
+
raise ValueError("dim must be >= 1.")
|
|
161
|
+
if n_samples < n_classes:
|
|
162
|
+
raise ValueError("n_samples must be >= n_classes.")
|
|
163
|
+
if not (min_coord < max_abs_coord):
|
|
164
|
+
raise ValueError("min_coord must be < max_abs_coord.")
|
|
165
|
+
if not (0.0 <= outlier_ratio <= 1.0):
|
|
166
|
+
raise ValueError("outlier_ratio must be within [0, 1].")
|
|
167
|
+
|
|
168
|
+
coeffs, bias = cls._validate_params(weight_type, params, dim)
|
|
169
|
+
rng = np.random.default_rng(random_state)
|
|
170
|
+
lo, hi = float(min_coord), float(max_abs_coord)
|
|
171
|
+
# Normalise distances so weights vary meaningfully regardless of cube size.
|
|
172
|
+
scale = max((hi - lo) / 4.0, cls._EPS)
|
|
173
|
+
|
|
174
|
+
per_class = n_samples // n_classes
|
|
175
|
+
remainder = n_samples - per_class * n_classes
|
|
176
|
+
takes = [per_class + (1 if c < remainder else 0) for c in range(n_classes)]
|
|
177
|
+
|
|
178
|
+
# ---- retry with fresh geometry until every class can be filled ---
|
|
179
|
+
last_counts: NDArray[np.int64] | None = None
|
|
180
|
+
for _attempt in range(cls._MAX_ATTEMPTS):
|
|
181
|
+
anchors = rng.uniform(lo, hi, size=(n_classes, dim))
|
|
182
|
+
pool = max(n_samples * 40, n_classes * 200)
|
|
183
|
+
cand = rng.uniform(lo, hi, size=(pool, dim))
|
|
184
|
+
labels, w = cls._assign(cand, anchors, scale, weight_type, coeffs, bias)
|
|
185
|
+
|
|
186
|
+
# Drop ties (top-2 class scores exactly equal).
|
|
187
|
+
top2 = np.sort(w, axis=1)[:, -2:]
|
|
188
|
+
keep = top2[:, 1] > top2[:, 0]
|
|
189
|
+
cand, labels, w = cand[keep], labels[keep], w[keep]
|
|
190
|
+
|
|
191
|
+
counts = np.bincount(labels, minlength=n_classes)
|
|
192
|
+
last_counts = counts
|
|
193
|
+
if np.all(counts >= np.asarray(takes)):
|
|
194
|
+
break
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"could not build a balanced, non-degenerate sample after "
|
|
198
|
+
f"{cls._MAX_ATTEMPTS} attempts (class counts {last_counts.tolist()}, "
|
|
199
|
+
f"needed {takes}). The chosen weight/params make one class dominate "
|
|
200
|
+
"the whole cube; try a different weight_type, params, or random_state."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# ---- stratified, balanced selection ------------------------------
|
|
204
|
+
chosen: list[int] = []
|
|
205
|
+
for c in range(n_classes):
|
|
206
|
+
idx_c = np.flatnonzero(labels == c)
|
|
207
|
+
chosen.extend(rng.choice(idx_c, size=takes[c], replace=False).tolist())
|
|
208
|
+
chosen = np.asarray(chosen, dtype=np.int64)
|
|
209
|
+
rng.shuffle(chosen)
|
|
210
|
+
x, y, w = cand[chosen], labels[chosen], w[chosen]
|
|
211
|
+
|
|
212
|
+
# ---- inject outliers on the borderline, spread across the cube ---
|
|
213
|
+
n_out = int(round(outlier_ratio * x.shape[0]))
|
|
214
|
+
if n_out > 0:
|
|
215
|
+
sw = np.sort(w, axis=1)
|
|
216
|
+
margin = sw[:, -1] - sw[:, -2] # smaller => more borderline
|
|
217
|
+
rank = np.empty(x.shape[0], dtype=np.int64)
|
|
218
|
+
rank[np.argsort(margin)] = np.arange(x.shape[0])
|
|
219
|
+
picked: list[int] = []
|
|
220
|
+
for bucket in np.array_split(np.argsort(x[:, 0]), n_out):
|
|
221
|
+
if bucket.size:
|
|
222
|
+
picked.append(int(bucket[np.argmin(rank[bucket])]))
|
|
223
|
+
for i in picked[:n_out]:
|
|
224
|
+
y[i] = int(np.argsort(w[i])[-2]) # flip to runner-up class
|
|
225
|
+
|
|
226
|
+
return x, y
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Backwards-compatible functional alias: `generate(...)` == class method.
|
|
230
|
+
generate = WknnDataGenerator.generate
|
|
File without changes
|