Skip to content

KNN

Algorithm

K-nearest neighbors classifies a sample using nearby labeled training observations. Feature scale affects Euclidean distance, so ToyML offers optional standardization. Training stores the observations; prediction computes their distances to the query and returns the most frequent label among the k closest observations.

Inputs and limitations

  • Pass a non-empty list of equally sized finite numeric feature lists and one label per observation.
  • Use a positive integer k no greater than the training sample count. predict accepts one feature list.
  • Labels should be immutable, hashable values.
  • Select std_transform=False for already scaled data.

Minimal example

from toyml.classification import KNN

samples = [[0.0], [0.2], [4.0], [4.2]]
model = KNN(k=2, std_transform=False).fit(samples, ["near", "near", "far", "far"])
print(model.predict([0.1]))

Numerical stability

Compute a scaled Euclidean norm for validated finite coordinates.

With \(d_i = |x_i - y_i|\) and finite \(s = \max_i d_i > 0\):

\[ \sqrt{\sum_i d_i^2} = \sqrt{s^2 \sum_i (d_i / s)^2} = s \sqrt{\sum_i (d_i / s)^2}. \]

Scaling keeps the largest squared term at one; fsum reduces summation error. Zero distance is handled before division. An overflowing coordinate difference returns infinity rather than evaluating inf / inf.

Source code in src/toyml/_distance.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def euclidean_distance(x: list[float], y: list[float]) -> float:
    r"""Compute a scaled Euclidean norm for validated finite coordinates.

    With $d_i = |x_i - y_i|$ and finite $s = \max_i d_i > 0$:

    $$
    \sqrt{\sum_i d_i^2}
    = \sqrt{s^2 \sum_i (d_i / s)^2}
    = s \sqrt{\sum_i (d_i / s)^2}.
    $$

    Scaling keeps the largest squared term at one; fsum reduces summation error.
    Zero distance is handled before division. An overflowing coordinate difference
    returns infinity rather than evaluating inf / inf.
    """
    differences = [abs(xi - yi) for xi, yi in zip(x, y, strict=True)]
    scale = max(differences, default=0.0)
    if scale == 0.0:
        return 0.0
    if math.isinf(scale):
        return math.inf
    return scale * math.sqrt(math.fsum((difference / scale) ** 2 for difference in differences))

API

toyml.classification.knn.KNN dataclass

KNN(k: int, std_transform: bool = True)

K-Nearest Neighbors classification algorithm implementation.

This class implements the K-Nearest Neighbors algorithm for classification tasks. It supports optional standardization of the input data.

Examples:

>>> dataset = [[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]]
>>> labels = ["A", "A", "B", "B"]
>>> knn = KNN(k=3, std_transform=True).fit(dataset, labels)
>>> knn.predict([2.5, 3.5])
'A'
ATTRIBUTE DESCRIPTION
k

The number of nearest neighbors to consider for classification.

TYPE: int

std_transform

Whether to standardize the input data (default: True).

TYPE: bool

dataset_

The fitted dataset (standardized if std_transform is True).

TYPE: list[list[float]] | None

labels_

The labels corresponding to the fitted dataset.

TYPE: list[Any] | None

standardizer_

The Standardizer instance if std_transform is True.

TYPE: Standardizer | None

References
  1. Li Hang
  2. Tan
  3. Zhou Zhihua
  4. Murphy
  5. Harrington

fit

fit(dataset: list[list[float]], labels: list[Any]) -> KNN

Fit the KNN model to the given dataset and labels.

PARAMETER DESCRIPTION
dataset

The input dataset to fit the model to.

TYPE: list[list[float]]

labels

The labels corresponding to the input dataset.

TYPE: list[Any]

RETURNS DESCRIPTION
KNN

The fitted KNN instance.

RAISES DESCRIPTION
ValueError

If features, labels, or the neighbor count are invalid.

TypeError

If std_transform is not a boolean.

Source code in src/toyml/classification/knn.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def fit(self, dataset: list[list[float]], labels: list[Any]) -> KNN:
    """Fit the KNN model to the given dataset and labels.

    Args:
        dataset: The input dataset to fit the model to.
        labels: The labels corresponding to the input dataset.

    Returns:
        The fitted KNN instance.

    Raises:
        ValueError: If features, labels, or the neighbor count are invalid.
        TypeError: If std_transform is not a boolean.
    """
    _validate_dataset(dataset)
    self._validate_k(len(dataset))
    if not isinstance(self.std_transform, bool):
        msg = "std_transform must be a boolean"
        raise TypeError(msg)
    if len(labels) != len(dataset):
        msg = "The number of labels must match the number of samples"
        raise ValueError(msg)
    try:
        for label in labels:
            hash(label)
    except TypeError as exc:
        msg = "Each label must be hashable"
        raise ValueError(msg) from exc

    standardizer = None
    if self.std_transform:
        standardizer = Standardizer()
        fitted_data = standardizer.fit_transform(dataset)
    else:
        fitted_data = [row.copy() for row in dataset]
    # Commit a complete fit; validation or preprocessing failures leave the old model intact.
    self.dataset_ = fitted_data
    self.labels_ = labels.copy()
    self.standardizer_ = standardizer
    return self

predict

predict(x: list[float]) -> Any

Predict the label of the input data.

PARAMETER DESCRIPTION
x

The input data to predict.

TYPE: list[float]

RETURNS DESCRIPTION
Any

The predicted label.

RAISES DESCRIPTION
ValueError

If the model is unfitted or the query or neighbor count is invalid.

Source code in src/toyml/classification/knn.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def predict(self, x: list[float]) -> Any:  # noqa: ANN401
    """Predict the label of the input data.

    Args:
        x: The input data to predict.

    Returns:
        The predicted label.

    Raises:
        ValueError: If the model is unfitted or the query or neighbor count is invalid.
    """
    if self.dataset_ is None or self.labels_ is None:
        msg = "The model is not fitted yet!"
        raise ValueError(msg)

    self._validate_k(len(self.dataset_))
    _validate_dataset([x], len(self.dataset_[0]))
    if self.std_transform:
        if self.standardizer_ is None:
            msg = "Cannot find the standardization!"
            raise ValueError(msg)
        x = self.standardizer_.transform([x])[0]
    distances = [euclidean_distance(x, point) for point in self.dataset_]
    # get k-nearest neighbors' label
    k_nearest_labels = [
        label for _, label in sorted(zip(distances, self.labels_, strict=True), key=lambda x: x[0])
    ][: self.k]
    label = Counter(k_nearest_labels).most_common(1)[0][0]
    return label

toyml.classification.knn.Standardizer dataclass

Standardizer()

A class for standardizing numerical datasets.

Provides methods to fit a standardization model to a dataset, transform datasets using the fitted model, and perform both operations in a single step.

fit

fit(dataset: list[list[float]]) -> Standardizer

Fit the standardization model to the given dataset.

PARAMETER DESCRIPTION
dataset

The input dataset to fit the model to.

TYPE: list[list[float]]

RETURNS DESCRIPTION
Standardizer

The fitted Standardizer instance.

RAISES DESCRIPTION
ValueError

If features are empty, inconsistent, non-finite, or cannot be scaled.

Source code in src/toyml/classification/knn.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def fit(self, dataset: list[list[float]]) -> Standardizer:
    """Fit the standardization model to the given dataset.

    Args:
        dataset: The input dataset to fit the model to.

    Returns:
        The fitted Standardizer instance.

    Raises:
        ValueError: If features are empty, inconsistent, non-finite, or cannot be scaled.
    """
    dimension = _validate_dataset(dataset)
    try:
        means = self._dataset_column_means(dataset)
        stds = self._dataset_column_stds(dataset)
    except OverflowError as exc:
        msg = "Standardization statistics exceed the floating-point range"
        raise ValueError(msg) from exc
    self._dimension = dimension
    self._means = means
    self._stds = stds
    return self

transform

transform(dataset: list[list[float]]) -> list[list[float]]

Transform the given dataset using the fitted standardization model.

PARAMETER DESCRIPTION
dataset

The input dataset to transform.

TYPE: list[list[float]]

RETURNS DESCRIPTION
list[list[float]]

New standardized rows; the input dataset is unchanged.

RAISES DESCRIPTION
ValueError

If unfitted or given invalid or unrepresentable feature values.

Source code in src/toyml/classification/knn.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def transform(self, dataset: list[list[float]]) -> list[list[float]]:
    """Transform the given dataset using the fitted standardization model.

    Args:
        dataset: The input dataset to transform.

    Returns:
        New standardized rows; the input dataset is unchanged.

    Raises:
        ValueError: If unfitted or given invalid or unrepresentable feature values.
    """
    if self._dimension is None:
        msg = "The model is not fitted yet!"
        raise ValueError(msg)
    return self.standardization(dataset)

fit_transform

fit_transform(dataset: list[list[float]]) -> list[list[float]]

Fit the standardization model to the dataset and transform it in one step.

PARAMETER DESCRIPTION
dataset

The input dataset to fit and transform.

TYPE: list[list[float]]

RETURNS DESCRIPTION
list[list[float]]

New standardized rows; the input dataset is unchanged.

Source code in src/toyml/classification/knn.py
200
201
202
203
204
205
206
207
208
209
210
def fit_transform(self, dataset: list[list[float]]) -> list[list[float]]:
    """Fit the standardization model to the dataset and transform it in one step.

    Args:
        dataset: The input dataset to fit and transform.

    Returns:
        New standardized rows; the input dataset is unchanged.
    """
    self.fit(dataset)
    return self.transform(dataset)

standardization

standardization(dataset: list[list[float]]) -> list[list[float]]

Standardize the given numerical dataset.

The standardization is performed by subtracting the mean and dividing by the standard deviation for each feature. When the standard deviation is 0, all the values in the column are the same, here we set std to 1 to make every value in the column become 0 and avoid division by zero.

PARAMETER DESCRIPTION
dataset

The input dataset to standardize.

TYPE: list[list[float]]

RETURNS DESCRIPTION
list[list[float]]

New standardized rows; the input dataset is unchanged.

RAISES DESCRIPTION
ValueError

If the model has not been fitted yet.

Source code in src/toyml/classification/knn.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def standardization(self, dataset: list[list[float]]) -> list[list[float]]:
    """Standardize the given numerical dataset.

    The standardization is performed by subtracting the mean and dividing
    by the standard deviation for each feature.
    When the standard deviation is 0, all the values in the column are the same,
    here we set std to 1 to make every value in the column become 0 and avoid division by zero.

    Args:
        dataset: The input dataset to standardize.

    Returns:
        New standardized rows; the input dataset is unchanged.

    Raises:
        ValueError: If the model has not been fitted yet.
    """
    if self._dimension is None:
        msg = "The model is not fitted yet!"
        raise ValueError(msg)
    _validate_dataset(dataset, self._dimension)
    # Small nonzero standard deviations carry real variation regardless of feature units.
    scales = [1.0 if std == 0.0 else std for std in self._stds]
    result = [
        [(value - mean) / scale for value, mean, scale in zip(row, self._means, scales, strict=True)]
        for row in dataset
    ]
    if not all(math.isfinite(value) for row in result for value in row):
        msg = "Standardized values exceed the floating-point range"
        raise ValueError(msg)
    return result