Implement the iforest method of IsoKernel - #75
Merged
Merged
Conversation
IsoKernel documented `iforest` alongside `anne` and `inne`, and dispatched to
it, but IK_IForest was a docstring with `def __init__(self): pass` under it.
Following the documentation crashed:
>>> IsoKernel(method="iforest").fit(X)
TypeError: IK_IForest.__init__() takes 1 positional argument but 4 were given
It now partitions the space the way the name says: each estimator draws
max_samples points and grows an isolation tree over them, cutting on one
feature at a time at a random threshold, to the height a standard isolation
forest stops at. A point's feature is the leaf it lands in, so the cells are
axis-parallel boxes where `anne` has Voronoi cells and `inne` has hyperspheres.
The tree numbers its leaves by their position in its node array, which runs
past the number of leaves, so those ids cannot be used as feature columns
directly and are mapped onto a contiguous range. That range always fits the
block width the other two methods use: a tree grown on max_samples points has
at most that many leaves, because every leaf holds at least one of them.
Tests now cover all three methods rather than the two that worked, and three
new cases check what the kernel is supposed to guarantee: exactly one cell per
estimator, reproducibility from random_state, and that two neighbours alone in
a sparse region score as more similar than two neighbours inside a crowd, which
is the property the kernel exists for. Measured on that fixture, iforest gives
0.970 against 0.227, next to anne's 0.977 against 0.110.
IDKD, IDKC, IKAHC and ICID still accept only the methods their papers specify;
widening those is a separate decision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference was inherited from the stub, which had copied it from _ik_anne.py: the AAAI 2019 paper that introduces aNNE, not the tree-based construction implemented here. That one is Ting, Zhu and Zhou, KDD 2018. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IsoKernel offers three partitionings drawn from two papers but cited only the AAAI 2019 one, so anyone reaching for `iforest` had no way to find where it comes from. Both are listed now, and the `method` parameter says what shape of cell each produces, which is the only thing that actually differs between them. The attributions are written in plain text rather than as reST citation references: mkdocstrings renders these docstrings as Markdown, so a `[1]_` reaches the API page as the literal characters. _ik_inne.py keeps the AAAI 2019 reference, which is the right one for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every References section used reST citation syntax, `.. [1]`, but mkdocstrings renders these docstrings as Markdown. The markers reached the API pages as literal characters, so a reader saw ".. [1] Liu, F. T., ..." rather than a numbered reference. The same went for the inline `[1]_` refs in IDKD. They are numbered lists now, across all twenty files that carry references, and continuation lines are indented to sit under the citation text so they stay part of the list item. That indent had been 0, 3, 7 or 8 spaces depending on the file. Checked by building the docs: seventeen API pages render an ordered list where none did before, and no page leaks a marker. Attributions written into prose, in IDKD and in IsoKernel's method parameter, name the author and year instead, since a citation reference has nothing to point at in Markdown. The numbered list in PSKC's class description is left alone: it enumerates the steps of the clustering loop and was never a citation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
IsoKerneldocumentsiforestalongsideanneandinne, and dispatches to it — butIK_IForestwas a docstring withdef __init__(self): passunderneath. Following the documentation crashed:The implementation
Each estimator draws
max_samplespoints and grows an isolation tree over them — one feature at a time, random threshold, to the height a standard isolation forest stops at (ceil(log2(ψ))). A point's feature is the leaf it lands in, so the cells are axis-parallel boxes, whereannehas Voronoi cells andinnehas hyperspheres.Built from
ExtraTreeRegressordirectly rather thanIsolationForest, which would also fit an anomaly scorer (offset_) over all of X that this has no use for. The structure mirrors_ik_anne.py: per-estimator seeds, per-estimator subsample.One thing worth knowing
A tree numbers its leaves by their position in its node array, so the ids run past the number of leaves and can't be used as feature columns:
They're mapped onto a contiguous range. That range always fits the block width the other two methods use — a tree grown on ψ points has at most ψ leaves, because every leaf holds at least one of them — so
transformkeeps the existing(n_samples, n_estimators × max_samples)shape contract thattest_IsoKernel_transformalready asserts. Verified empirically across ψ from 4 to 256; leaf counts topped out at ψ every time.Tests
methodin the test module was["inne", "anne"]— iforest was excluded because it didn't work. It's now included, so the three existing tests cover it, plus three new cases asserting what the kernel is supposed to guarantee:..._is_one_hot_per_estimatorinneis excluded from the lower bound — a point outside every hypersphere legitimately falls in no cell..._is_reproduciblerandom_state→ identical embedding; different → different..._similarity_is_higher_in_sparse_regionsOn that last fixture:
anneinneiforestAlso added a case for the unknown-method
ValueError, which had no coverage.Scope
IDKD,IDKC,IKAHCandICIDeach whitelist only the methods their papers specify —IKAHCrejects anything outside["inne", "anne"]explicitly. Widening those changes published algorithm behaviour and is a separate decision, so they're untouched.Verification
ikpykit/kernel/tests/— 25 passed locally. The full suite is left to CI on this PR; local runs are slow here. All pre-commit hooks pass.🤖 Generated with Claude Code