subspaceknn 0.1.0__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.
- subspaceknn-0.1.0/.editorconfig +15 -0
- subspaceknn-0.1.0/.gitattributes +7 -0
- subspaceknn-0.1.0/.gitignore +27 -0
- subspaceknn-0.1.0/.pre-commit-config.yaml +15 -0
- subspaceknn-0.1.0/.python-version +1 -0
- subspaceknn-0.1.0/CHANGELOG.md +21 -0
- subspaceknn-0.1.0/CODE_OF_CONDUCT.md +132 -0
- subspaceknn-0.1.0/CONTRIBUTING.md +65 -0
- subspaceknn-0.1.0/LICENSE +21 -0
- subspaceknn-0.1.0/PKG-INFO +161 -0
- subspaceknn-0.1.0/README.md +130 -0
- subspaceknn-0.1.0/SECURITY.md +25 -0
- subspaceknn-0.1.0/docs/RELEASING.md +47 -0
- subspaceknn-0.1.0/docs/benchmark.md +29 -0
- subspaceknn-0.1.0/docs/method.md +58 -0
- subspaceknn-0.1.0/examples/compare_with_knn.py +37 -0
- subspaceknn-0.1.0/examples/iris_explanations.py +51 -0
- subspaceknn-0.1.0/pyproject.toml +119 -0
- subspaceknn-0.1.0/src/subspaceknn/__init__.py +15 -0
- subspaceknn-0.1.0/src/subspaceknn/_classifier.py +468 -0
- subspaceknn-0.1.0/src/subspaceknn/_explanation.py +92 -0
- subspaceknn-0.1.0/src/subspaceknn/plotting.py +275 -0
- subspaceknn-0.1.0/src/subspaceknn/py.typed +0 -0
- subspaceknn-0.1.0/tests/test_benchmark.py +43 -0
- subspaceknn-0.1.0/tests/test_classifier.py +219 -0
- subspaceknn-0.1.0/tests/test_explanation.py +66 -0
- subspaceknn-0.1.0/tests/test_plotting.py +48 -0
- subspaceknn-0.1.0/tests/test_sklearn_compat.py +21 -0
- subspaceknn-0.1.0/uv.lock +2196 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 4
|
|
10
|
+
|
|
11
|
+
[*.{yml,yaml,toml}]
|
|
12
|
+
indent_size = 2
|
|
13
|
+
|
|
14
|
+
[*.md]
|
|
15
|
+
trim_trailing_whitespace = false
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
.python-version.local
|
|
9
|
+
|
|
10
|
+
# Tooling caches
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
.coverage.*
|
|
16
|
+
coverage.xml
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# Editors and operating systems
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
.DS_Store
|
|
24
|
+
Thumbs.db
|
|
25
|
+
|
|
26
|
+
# Example outputs
|
|
27
|
+
examples/output/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Optional local hooks; CI runs the same tools. Install with `uvx pre-commit install`.
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
4
|
+
rev: v0.16.8
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff-check
|
|
7
|
+
args: [--fix]
|
|
8
|
+
- id: ruff-format
|
|
9
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
10
|
+
rev: v5.0.0
|
|
11
|
+
hooks:
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
- id: trailing-whitespace
|
|
14
|
+
- id: check-toml
|
|
15
|
+
- id: check-yaml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `SubspaceKNNClassifier`: a scikit-learn compatible classifier that fits k-nearest-neighbour models on every small feature subspace, ranks them by cross-validated score, and combines the best by weighted soft or hard voting. Subspaces can have any size or a mix of sizes; wide data is handled by screening features to keep the candidate count within `max_candidates`.
|
|
15
|
+
- `explain`, returning an `Explanation` per sample with one `SubspaceVote` per subspace, and `feature_scores_` as a coarse relevance measure.
|
|
16
|
+
- `subspaceknn.plotting.plot_subspaces`, drawing one-, two- and three-dimensional subspaces with decision regions where possible (optional `plot` extra).
|
|
17
|
+
- Test suite covering scikit-learn's estimator contract for two configurations, behaviour, explanations, plotting, and an accuracy comparison against plain kNN on the iris, wine and breast-cancer datasets.
|
|
18
|
+
- Project infrastructure: uv-based workflow, ruff and mypy in strict mode, CI across Python 3.10 to 3.13 and three operating systems, tag-driven PyPI release with trusted publishing, Dependabot, issue and pull request templates, contributing guide, security policy and code of conduct.
|
|
19
|
+
|
|
20
|
+
[Unreleased]: https://github.com/DiogoRibeiro7/subspaceknn/compare/v0.1.0...HEAD
|
|
21
|
+
[0.1.0]: https://github.com/DiogoRibeiro7/subspaceknn/releases/tag/v0.1.0
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
10
|
+
identity and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the overall
|
|
26
|
+
community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
31
|
+
any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email address,
|
|
35
|
+
without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
## Scope
|
|
52
|
+
|
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
+
an individual is officially representing the community in public spaces.
|
|
55
|
+
Examples of representing our community include using an official email address,
|
|
56
|
+
posting via an official social media account, or acting as an appointed
|
|
57
|
+
representative at an online or offline event.
|
|
58
|
+
|
|
59
|
+
## Enforcement
|
|
60
|
+
|
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
+
reported to the project maintainer, Diogo Ribeiro, through the contact details
|
|
63
|
+
on the GitHub profile [@DiogoRibeiro7](https://github.com/DiogoRibeiro7).
|
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
+
|
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
+
reporter of any incident.
|
|
68
|
+
|
|
69
|
+
## Enforcement Guidelines
|
|
70
|
+
|
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
+
|
|
74
|
+
### 1. Correction
|
|
75
|
+
|
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
+
unprofessional or unwelcome in the community.
|
|
78
|
+
|
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
|
82
|
+
|
|
83
|
+
### 2. Warning
|
|
84
|
+
|
|
85
|
+
**Community Impact**: A violation through a single incident or series of
|
|
86
|
+
actions.
|
|
87
|
+
|
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
+
like social media. Violating these terms may lead to a temporary or permanent
|
|
93
|
+
ban.
|
|
94
|
+
|
|
95
|
+
### 3. Temporary Ban
|
|
96
|
+
|
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
|
98
|
+
sustained inappropriate behavior.
|
|
99
|
+
|
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
+
communication with the community for a specified period of time. No public or
|
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
+
Violating these terms may lead to a permanent ban.
|
|
105
|
+
|
|
106
|
+
### 4. Permanent Ban
|
|
107
|
+
|
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
+
|
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within the
|
|
113
|
+
community.
|
|
114
|
+
|
|
115
|
+
## Attribution
|
|
116
|
+
|
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
+
version 2.1, available at
|
|
119
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
120
|
+
|
|
121
|
+
Community Impact Guidelines were inspired by
|
|
122
|
+
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
123
|
+
|
|
124
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
125
|
+
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
|
126
|
+
[https://www.contributor-covenant.org/translations][translations].
|
|
127
|
+
|
|
128
|
+
[homepage]: https://www.contributor-covenant.org
|
|
129
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
130
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
131
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
|
132
|
+
[translations]: https://www.contributor-covenant.org/translations
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributing to subspaceknn
|
|
2
|
+
|
|
3
|
+
Thank you for considering a contribution. The project has one goal: an interpretable nearest-neighbour classifier that behaves exactly like a scikit-learn estimator and explains every prediction it makes. The guidelines below keep that intact as the code grows.
|
|
4
|
+
|
|
5
|
+
## Ground rules
|
|
6
|
+
|
|
7
|
+
- Be respectful. The project follows the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
8
|
+
- Open an issue before large work so the design can be agreed first. Small fixes can go straight to a pull request.
|
|
9
|
+
- Every behavioural claim in the code or the documentation must be backed by a test.
|
|
10
|
+
|
|
11
|
+
## Development setup
|
|
12
|
+
|
|
13
|
+
You need Python 3.10 or newer and [uv](https://docs.astral.sh/uv/). There are no other build dependencies.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
git clone https://github.com/DiogoRibeiro7/subspaceknn.git
|
|
17
|
+
cd subspaceknn
|
|
18
|
+
uv sync --all-extras
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optional local hooks that run the same checks as CI: `uvx pre-commit install`.
|
|
22
|
+
|
|
23
|
+
## Before you open a pull request
|
|
24
|
+
|
|
25
|
+
Run the same checks CI runs. All of them must pass:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
uv run ruff check .
|
|
29
|
+
uv run ruff format --check .
|
|
30
|
+
uv run mypy
|
|
31
|
+
uv run pytest
|
|
32
|
+
uv run python examples/iris_explanations.py
|
|
33
|
+
uv build && uvx twine check dist/*
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Ruff runs with every rule enabled except the few listed in `pyproject.toml`, and mypy runs in strict mode on the package. Prefer fixing a finding over silencing it; when silencing is the right call, scope the `noqa` or `type: ignore` to the line and say why.
|
|
37
|
+
|
|
38
|
+
## Coding standards
|
|
39
|
+
|
|
40
|
+
- **scikit-learn contract first.** `__init__` only stores parameters, validation happens in `fit`, fitted state lives in attributes with a trailing underscore, `predict` has no side effects, `predict_proba` and `predict` agree, and invalid input raises an informative `ValueError`. `tests/test_sklearn_compat.py` runs `check_estimator`; a change that breaks it is a bug.
|
|
41
|
+
- **Deterministic by construction.** Subspace enumeration, ranking and tie-breaking are documented in `docs/method.md`; keep them that way and update the note if they change.
|
|
42
|
+
- **Explanations are data.** Anything a user needs to understand a prediction belongs in `Explanation` and `SubspaceVote`, not in printed output or plot side effects.
|
|
43
|
+
- **Optional dependencies stay optional.** matplotlib is imported lazily; the core package depends only on numpy and scikit-learn.
|
|
44
|
+
- **Typed and documented.** Public functions have numpy-style docstrings and complete type annotations.
|
|
45
|
+
|
|
46
|
+
## Testing standards
|
|
47
|
+
|
|
48
|
+
- Unit tests for behaviour and validation live in `tests/`; the scikit-learn contract runs against every configuration that changes a code path.
|
|
49
|
+
- Numbers that appear in the documentation come from `tests/test_benchmark.py`. If you change the method, rerun it and update `docs/benchmark.md` and the README table together.
|
|
50
|
+
- Keep tests deterministic: fixed seeds, fixed splitters, no network.
|
|
51
|
+
|
|
52
|
+
## Documentation
|
|
53
|
+
|
|
54
|
+
- Public API changes need a docstring update and, when user-visible, an entry under *Unreleased* in `CHANGELOG.md`.
|
|
55
|
+
- Design changes need a corresponding change in `docs/method.md`.
|
|
56
|
+
|
|
57
|
+
## Branches, commits and pull requests
|
|
58
|
+
|
|
59
|
+
- Branch from `main` using a short prefix: `feat/`, `fix/`, `test/`, `docs/`, `chore/`.
|
|
60
|
+
- Write commit subjects in the imperative mood, at most 72 characters.
|
|
61
|
+
- Keep pull requests focused and fill in the template, including how the change was validated.
|
|
62
|
+
|
|
63
|
+
## Licensing of contributions
|
|
64
|
+
|
|
65
|
+
By submitting a contribution you agree that it is licensed under the project's MIT license, without any additional terms or conditions.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diogo Ribeiro
|
|
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.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: subspaceknn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interpretable k-nearest-neighbour classification by ensembling kNN models fitted on low-dimensional feature subspaces.
|
|
5
|
+
Project-URL: Homepage, https://github.com/DiogoRibeiro7/subspaceknn
|
|
6
|
+
Project-URL: Repository, https://github.com/DiogoRibeiro7/subspaceknn
|
|
7
|
+
Project-URL: Issues, https://github.com/DiogoRibeiro7/subspaceknn/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/DiogoRibeiro7/subspaceknn/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Diogo Ribeiro
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ensemble,explainability,interpretable-machine-learning,knn,nearest-neighbours,scikit-learn
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: scikit-learn>=1.6
|
|
28
|
+
Provides-Extra: plot
|
|
29
|
+
Requires-Dist: matplotlib>=3.8; extra == 'plot'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# subspaceknn
|
|
33
|
+
|
|
34
|
+
[](https://github.com/DiogoRibeiro7/subspaceknn/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/subspaceknn/)
|
|
36
|
+
[](https://github.com/DiogoRibeiro7/subspaceknn)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
Interpretable k-nearest-neighbour classification by ensembling kNN models fitted on low-dimensional feature subspaces.
|
|
40
|
+
|
|
41
|
+
`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.
|
|
42
|
+
|
|
43
|
+
The method generalises the *interpretable kNN* (ikNN) idea of [Brett Kennedy](https://github.com/Brett-Kennedy/ikNN), described in his article [Interpretable kNN (ikNN)](https://towardsdatascience.com/interpretable-knn-iknn-33d38402b8fc), 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.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install subspaceknn # core: numpy and scikit-learn
|
|
49
|
+
pip install "subspaceknn[plot]" # adds matplotlib for plot_subspaces
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The package supports Python 3.10 to 3.13 and scikit-learn 1.6 or newer.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from sklearn.datasets import load_iris
|
|
58
|
+
from sklearn.model_selection import train_test_split
|
|
59
|
+
|
|
60
|
+
from subspaceknn import SubspaceKNNClassifier
|
|
61
|
+
|
|
62
|
+
iris = load_iris(as_frame=True)
|
|
63
|
+
X, y = iris.data, iris.target_names[iris.target]
|
|
64
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0, stratify=y)
|
|
65
|
+
|
|
66
|
+
clf = SubspaceKNNClassifier(subspace_size=(1, 2), n_subspaces=4).fit(X_train, y_train)
|
|
67
|
+
print(clf.score(X_test, y_test))
|
|
68
|
+
|
|
69
|
+
for subspace, score, weight in zip(clf.subspaces_, clf.subspace_scores_, clf.subspace_weights_):
|
|
70
|
+
print(list(X.columns[list(subspace)]), f"score={score:.3f}", f"weight={weight:.3f}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
### Explaining a prediction
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
explanation = clf.explain(X_test.iloc[:1])[0]
|
|
79
|
+
print(explanation.prediction, explanation.agreement())
|
|
80
|
+
for vote in explanation.votes:
|
|
81
|
+
print(vote.feature_names, vote.prediction, f"weight={vote.weight:.3f}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`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`.
|
|
85
|
+
|
|
86
|
+
### Drawing the subspaces
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from subspaceknn.plotting import plot_subspaces
|
|
90
|
+
|
|
91
|
+
fig = plot_subspaces(clf, X_train, y_train, sample=X_test.iloc[0].to_numpy())
|
|
92
|
+
fig.savefig("subspaces.png")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
## How it works
|
|
98
|
+
|
|
99
|
+
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,
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
p(c | x) = sum_s w_s * p_s(c | x), w_s = score_s / sum_t score_t,
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
The full description, including how tiny training sets are handled, is in [docs/method.md](docs/method.md).
|
|
110
|
+
|
|
111
|
+
## Parameters
|
|
112
|
+
|
|
113
|
+
| Parameter | Default | Meaning |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `n_neighbors` | `5` | Neighbours used by every subspace model. |
|
|
116
|
+
| `subspace_size` | `2` | Size of each subspace, or a sequence of sizes to enumerate together. |
|
|
117
|
+
| `n_subspaces` | `5` | Number of best subspaces that vote; `None` uses all candidates. |
|
|
118
|
+
| `max_candidates` | `100` | Cap on cross-validated subspaces; triggers feature screening above it. |
|
|
119
|
+
| `voting` | `"soft"` | `"soft"` averages probabilities, `"hard"` averages one-hot votes. |
|
|
120
|
+
| `weighting` | `"score"` | Weight by cross-validated score, or `"uniform"`. |
|
|
121
|
+
| `cv` | `5` | Folds, or any scikit-learn splitter, used to score subspaces. |
|
|
122
|
+
| `scoring` | `"f1_macro"` | Any scikit-learn scorer; higher must be better. |
|
|
123
|
+
| `knn_weights`, `metric` | `"uniform"`, `"minkowski"` | Passed to the subspace models. |
|
|
124
|
+
|
|
125
|
+
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.
|
|
126
|
+
|
|
127
|
+
## Does interpretability cost accuracy?
|
|
128
|
+
|
|
129
|
+
Five-fold stratified cross-validated macro-F1 on scikit-learn's toy datasets, features standardised, everything else at its defaults:
|
|
130
|
+
|
|
131
|
+
| Dataset | kNN | Subspaces of size 2 | Sizes 1, 2 and 3 (8 subspaces) |
|
|
132
|
+
| --- | ---: | ---: | ---: |
|
|
133
|
+
| iris (4 features) | 0.953 | 0.953 | 0.953 |
|
|
134
|
+
| wine (13 features) | 0.960 | 0.945 | 0.967 |
|
|
135
|
+
| breast cancer (30 features) | 0.962 | 0.946 | 0.943 |
|
|
136
|
+
|
|
137
|
+
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](docs/benchmark.md).
|
|
138
|
+
|
|
139
|
+
## Limitations
|
|
140
|
+
|
|
141
|
+
- Features must be numeric and are used as given; encode categorical features and scale everything first.
|
|
142
|
+
- Candidate subspaces grow as the binomial coefficient of the feature count; rely on `max_candidates` or a sequence of small sizes for wide data.
|
|
143
|
+
- The voting weights are cross-validated scores, not calibrated probabilities. Treat `predict_proba` as a ranking rather than a probability estimate.
|
|
144
|
+
- Classification only.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
git clone https://github.com/DiogoRibeiro7/subspaceknn.git
|
|
150
|
+
cd subspaceknn
|
|
151
|
+
uv sync --all-extras
|
|
152
|
+
uv run ruff check . && uv run ruff format --check .
|
|
153
|
+
uv run mypy
|
|
154
|
+
uv run pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The test suite runs scikit-learn's estimator contract (`check_estimator`) against two configurations, plus behavioural, explanation, plotting and benchmark tests. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
158
|
+
|
|
159
|
+
## License and attribution
|
|
160
|
+
|
|
161
|
+
MIT, see [LICENSE](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.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# subspaceknn
|
|
2
|
+
|
|
3
|
+
[](https://github.com/DiogoRibeiro7/subspaceknn/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/subspaceknn/)
|
|
5
|
+
[](https://github.com/DiogoRibeiro7/subspaceknn)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Interpretable k-nearest-neighbour classification by ensembling kNN models fitted on low-dimensional feature subspaces.
|
|
9
|
+
|
|
10
|
+
`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.
|
|
11
|
+
|
|
12
|
+
The method generalises the *interpretable kNN* (ikNN) idea of [Brett Kennedy](https://github.com/Brett-Kennedy/ikNN), described in his article [Interpretable kNN (ikNN)](https://towardsdatascience.com/interpretable-knn-iknn-33d38402b8fc), 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.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pip install subspaceknn # core: numpy and scikit-learn
|
|
18
|
+
pip install "subspaceknn[plot]" # adds matplotlib for plot_subspaces
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package supports Python 3.10 to 3.13 and scikit-learn 1.6 or newer.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from sklearn.datasets import load_iris
|
|
27
|
+
from sklearn.model_selection import train_test_split
|
|
28
|
+
|
|
29
|
+
from subspaceknn import SubspaceKNNClassifier
|
|
30
|
+
|
|
31
|
+
iris = load_iris(as_frame=True)
|
|
32
|
+
X, y = iris.data, iris.target_names[iris.target]
|
|
33
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0, stratify=y)
|
|
34
|
+
|
|
35
|
+
clf = SubspaceKNNClassifier(subspace_size=(1, 2), n_subspaces=4).fit(X_train, y_train)
|
|
36
|
+
print(clf.score(X_test, y_test))
|
|
37
|
+
|
|
38
|
+
for subspace, score, weight in zip(clf.subspaces_, clf.subspace_scores_, clf.subspace_weights_):
|
|
39
|
+
print(list(X.columns[list(subspace)]), f"score={score:.3f}", f"weight={weight:.3f}")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
### Explaining a prediction
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
explanation = clf.explain(X_test.iloc[:1])[0]
|
|
48
|
+
print(explanation.prediction, explanation.agreement())
|
|
49
|
+
for vote in explanation.votes:
|
|
50
|
+
print(vote.feature_names, vote.prediction, f"weight={vote.weight:.3f}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`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`.
|
|
54
|
+
|
|
55
|
+
### Drawing the subspaces
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from subspaceknn.plotting import plot_subspaces
|
|
59
|
+
|
|
60
|
+
fig = plot_subspaces(clf, X_train, y_train, sample=X_test.iloc[0].to_numpy())
|
|
61
|
+
fig.savefig("subspaces.png")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
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,
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
p(c | x) = sum_s w_s * p_s(c | x), w_s = score_s / sum_t score_t,
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
The full description, including how tiny training sets are handled, is in [docs/method.md](docs/method.md).
|
|
79
|
+
|
|
80
|
+
## Parameters
|
|
81
|
+
|
|
82
|
+
| Parameter | Default | Meaning |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `n_neighbors` | `5` | Neighbours used by every subspace model. |
|
|
85
|
+
| `subspace_size` | `2` | Size of each subspace, or a sequence of sizes to enumerate together. |
|
|
86
|
+
| `n_subspaces` | `5` | Number of best subspaces that vote; `None` uses all candidates. |
|
|
87
|
+
| `max_candidates` | `100` | Cap on cross-validated subspaces; triggers feature screening above it. |
|
|
88
|
+
| `voting` | `"soft"` | `"soft"` averages probabilities, `"hard"` averages one-hot votes. |
|
|
89
|
+
| `weighting` | `"score"` | Weight by cross-validated score, or `"uniform"`. |
|
|
90
|
+
| `cv` | `5` | Folds, or any scikit-learn splitter, used to score subspaces. |
|
|
91
|
+
| `scoring` | `"f1_macro"` | Any scikit-learn scorer; higher must be better. |
|
|
92
|
+
| `knn_weights`, `metric` | `"uniform"`, `"minkowski"` | Passed to the subspace models. |
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
## Does interpretability cost accuracy?
|
|
97
|
+
|
|
98
|
+
Five-fold stratified cross-validated macro-F1 on scikit-learn's toy datasets, features standardised, everything else at its defaults:
|
|
99
|
+
|
|
100
|
+
| Dataset | kNN | Subspaces of size 2 | Sizes 1, 2 and 3 (8 subspaces) |
|
|
101
|
+
| --- | ---: | ---: | ---: |
|
|
102
|
+
| iris (4 features) | 0.953 | 0.953 | 0.953 |
|
|
103
|
+
| wine (13 features) | 0.960 | 0.945 | 0.967 |
|
|
104
|
+
| breast cancer (30 features) | 0.962 | 0.946 | 0.943 |
|
|
105
|
+
|
|
106
|
+
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](docs/benchmark.md).
|
|
107
|
+
|
|
108
|
+
## Limitations
|
|
109
|
+
|
|
110
|
+
- Features must be numeric and are used as given; encode categorical features and scale everything first.
|
|
111
|
+
- Candidate subspaces grow as the binomial coefficient of the feature count; rely on `max_candidates` or a sequence of small sizes for wide data.
|
|
112
|
+
- The voting weights are cross-validated scores, not calibrated probabilities. Treat `predict_proba` as a ranking rather than a probability estimate.
|
|
113
|
+
- Classification only.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
git clone https://github.com/DiogoRibeiro7/subspaceknn.git
|
|
119
|
+
cd subspaceknn
|
|
120
|
+
uv sync --all-extras
|
|
121
|
+
uv run ruff check . && uv run ruff format --check .
|
|
122
|
+
uv run mypy
|
|
123
|
+
uv run pytest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The test suite runs scikit-learn's estimator contract (`check_estimator`) against two configurations, plus behavioural, explanation, plotting and benchmark tests. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
127
|
+
|
|
128
|
+
## License and attribution
|
|
129
|
+
|
|
130
|
+
MIT, see [LICENSE](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.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the `main` branch and to the latest release on PyPI. Older releases do not receive backports.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue for security problems.
|
|
10
|
+
|
|
11
|
+
Use GitHub private vulnerability reporting instead:
|
|
12
|
+
<https://github.com/DiogoRibeiro7/subspaceknn/security/advisories/new>
|
|
13
|
+
|
|
14
|
+
You can expect an acknowledgement within seven days. Once the report is confirmed, a fix and a coordinated disclosure date are agreed with the reporter before any advisory is published.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
`subspaceknn` is a pure-Python library depending on numpy and scikit-learn. Its public API validates every input and raises typed errors rather than failing unpredictably.
|
|
19
|
+
|
|
20
|
+
The following are treated as security-relevant and welcome through the private channel:
|
|
21
|
+
|
|
22
|
+
- unbounded resource use reachable through the public API with attacker-controlled input beyond what `max_candidates` documents;
|
|
23
|
+
- vulnerabilities in the dependency tree that affect this package.
|
|
24
|
+
|
|
25
|
+
Accuracy problems, surprising predictions or explanation semantics are correctness bugs, not vulnerabilities. Please report them as ordinary issues with a minimal reproduction.
|