Skip to main content

subspaceknn

CI PyPI Python License

Interpretable k-nearest-neighbour classification by ensembling kNN models fitted on low-dimensional feature subspaces.

SubspaceKNNClassifier fits one k-nearest-neighbour model per small subset of features, ranks those subspaces by cross-validated performance, and lets the best of them vote, each weighted by its score. Because every member of the ensemble lives in a space of one, two or three features, a prediction can be explained by showing the neighbourhoods that produced it, and each subspace can be drawn with its decision regions.

The method generalises the interpretable kNN (ikNN) idea of Brett Kennedy, described in his article Interpretable kNN (ikNN), from pairs of features to subspaces of any small size. This package is an independent implementation written from the description of the method. It shares no code, text or results with the original.

Installation

pip install subspaceknn            # core: numpy and scikit-learn
pip install "subspaceknn[plot]"    # adds matplotlib for plot_subspaces

The package supports Python 3.10 to 3.13 and scikit-learn 1.6 or newer.

Quick start

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

from subspaceknn import SubspaceKNNClassifier

iris = load_iris(as_frame=True)
X, y = iris.data, iris.target_names[iris.target]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0, stratify=y)

clf = SubspaceKNNClassifier(subspace_size=(1, 2), n_subspaces=4).fit(X_train, y_train)
print(clf.score(X_test, y_test))

for subspace, score, weight in zip(clf.subspaces_, clf.subspace_scores_, clf.subspace_weights_):
    print(list(X.columns[list(subspace)]), f"score={score:.3f}", f"weight={weight:.3f}")

The estimator follows the scikit-learn contract, so it works inside Pipeline, GridSearchCV and cross_val_score, accepts data frames, and exposes predict, predict_proba and score. Feature scales matter for nearest neighbours, so put a StandardScaler in front of it unless the features are already comparable.

Explaining a prediction

explanation = clf.explain(X_test.iloc[:1])[0]
print(explanation.prediction, explanation.agreement())
for vote in explanation.votes:
    print(vote.feature_names, vote.prediction, f"weight={vote.weight:.3f}")

explain returns one Explanation per sample. It holds the ensemble prediction and probabilities and one SubspaceVote per subspace with the features involved, that subspace's own prediction and probabilities, its cross-validated score and its voting weight. agreement() is the total weight of the subspaces that voted for the final prediction, and to_records() produces rows ready for pandas.DataFrame.from_records.

Drawing the subspaces

from subspaceknn.plotting import plot_subspaces

fig = plot_subspaces(clf, X_train, y_train, sample=X_test.iloc[0].to_numpy())
fig.savefig("subspaces.png")

One panel per subspace: a strip plot with decision intervals for one feature, a scatter plot with decision regions for two, a 3-D scatter for three. The highlighted sample is the point being explained.

How it works

For subspace sizes d in subspace_size, the estimator enumerates every d-subset of the features, fits a KNeighborsClassifier on each subset and scores it with stratified cross-validation on the training data (macro-F1 by default). The n_subspaces best subsets form the ensemble. For a new sample the class probabilities are the weighted average of the subspace models' probabilities,

p(c | x) = sum_s w_s * p_s(c | x),        w_s = score_s / sum_t score_t,

and the prediction is the class with the largest probability. voting="hard" replaces p_s by the one-hot prediction of each subspace and weighting="uniform" replaces w_s by equal weights.

The number of subsets grows combinatorially with the number of features, so max_candidates caps how many are cross-validated. Above the cap, features are screened by the cross-validated score of their one-dimensional model, and only the best-scoring features are combined, as many as keep the candidate count within the cap. Everything is deterministic: subsets are enumerated in lexicographic order and ties keep that order.

The full description, including how tiny training sets are handled, is in docs/method.md.

Parameters

Parameter Default Meaning
n_neighbors 5 Neighbours used by every subspace model.
subspace_size 2 Size of each subspace, or a sequence of sizes to enumerate together.
n_subspaces 5 Number of best subspaces that vote; None uses all candidates.
max_candidates 100 Cap on cross-validated subspaces; triggers feature screening above it.
voting "soft" "soft" averages probabilities, "hard" averages one-hot votes.
weighting "score" Weight by cross-validated score, or "uniform".
cv 5 Folds, or any scikit-learn splitter, used to score subspaces.
scoring "f1_macro" Any scikit-learn scorer; higher must be better.
knn_weights, metric "uniform", "minkowski" Passed to the subspace models.

Fitted attributes include subspaces_, subspace_scores_, subspace_weights_, estimators_, the full candidate_subspaces_ with candidate_scores_, the screened_features_, and feature_scores_, a coarse feature-relevance measure. See the class docstring for the complete list.

Does interpretability cost accuracy?

Five-fold stratified cross-validated macro-F1 on scikit-learn's toy datasets, features standardised, everything else at its defaults:

Dataset kNN Subspaces of size 2 Sizes 1, 2 and 3 (8 subspaces)
iris (4 features) 0.953 0.953 0.953
wine (13 features) 0.960 0.945 0.967
breast cancer (30 features) 0.962 0.946 0.943

The ensemble stays within a couple of points of plain kNN while every one of its votes is a picture. The test suite asserts this stays true. Details in docs/benchmark.md.

Limitations

  • Features must be numeric and are used as given; encode categorical features and scale everything first.
  • Candidate subspaces grow as the binomial coefficient of the feature count; rely on max_candidates or a sequence of small sizes for wide data.
  • The voting weights are cross-validated scores, not calibrated probabilities. Treat predict_proba as a ranking rather than a probability estimate.
  • Classification only.

Development

git clone https://github.com/DiogoRibeiro7/subspaceknn.git
cd subspaceknn
uv sync --all-extras
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytest

The test suite runs scikit-learn's estimator contract (check_estimator) against two configurations, plus behavioural, explanation, plotting and benchmark tests. See CONTRIBUTING.md.

License and attribution

MIT, see LICENSE. The method is due to Brett Kennedy's ikNN; this implementation, its generalisation to arbitrary subspace sizes, and everything in this repository were written independently.

Release files for subspaceknn 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for subspaceknn 0.1.0
File Size Uploaded
subspaceknn-0.1.0.tar.gz 190.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for subspaceknn 0.1.0
File Interpreter ABI Platform
subspaceknn-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size:206.3 kB

Release files / subspaceknn-0.1.0.tar.gz

Download URL subspaceknn-0.1.0.tar.gz
Size 190.7 kB
Tags Source
SHA-256 checksum
How to use checksums
e5048b15de999840fd811001a96af6fd97feada013612dd5951fea7b3a982f47
BLAKE2b-256 checksum
How to use checksums
fcd11cefc85884ad4cf1985352b48dfb59be5dbe19fff7139ae11842dddb1cae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / subspaceknn-0.1.0-py3-none-any.whl

Download URL subspaceknn-0.1.0-py3-none-any.whl
Size 15.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5748bcf3ee525514412210090c43b93aa904e75b5bcff5986251275ecffb7ba9
BLAKE2b-256 checksum
How to use checksums
dbf17484d0b3cda5484b7821101860606f59baaf93afdbf9181aab90709b96ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page