Skip to content

Implement the iforest method of IsoKernel - #75

Merged
xhan97 merged 4 commits into
mainfrom
fix/ik-iforest
Aug 16, 2026
Merged

Implement the iforest method of IsoKernel#75
xhan97 merged 4 commits into
mainfrom
fix/ik-iforest

Conversation

@xhan97

@xhan97 xhan97 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

The bug

IsoKernel documents iforest alongside anne and inne, and dispatches to it — but IK_IForest was a docstring with def __init__(self): pass underneath. Following the documentation crashed:

>>> IsoKernel(method="iforest").fit(X)
TypeError: IK_IForest.__init__() takes 1 positional argument but 4 were given

The implementation

Each estimator draws max_samples points 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, where anne has Voronoi cells and inne has hyperspheres.

Built from ExtraTreeRegressor directly rather than IsolationForest, 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:

tree 0: node_count=15  distinct leaves=8   max leaf id=14
tree 2: node_count=19  distinct leaves=10  max leaf id=18

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 transform keeps the existing (n_samples, n_estimators × max_samples) shape contract that test_IsoKernel_transform already asserts. Verified empirically across ψ from 4 to 256; leaf counts topped out at ψ every time.

Tests

method in 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:

Test Checks
..._is_one_hot_per_estimator Exactly one cell per estimator. inne is excluded from the lower bound — a point outside every hypersphere legitimately falls in no cell
..._is_reproducible Same random_state → identical embedding; different → different
..._similarity_is_higher_in_sparse_regions The property the kernel exists for: two neighbours alone in a sparse region score as more similar than two neighbours inside a crowd

On that last fixture:

method sparse pair dense pair
anne 0.977 0.110
inne 0.153 0.003
iforest 0.970 0.227

Also added a case for the unknown-method ValueError, which had no coverage.

Scope

IDKD, IDKC, IKAHC and ICID each whitelist only the methods their papers specify — IKAHC rejects 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

xhan97 and others added 4 commits August 16, 2026 22:28
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>
@xhan97
xhan97 merged commit 03ab01e into main Aug 16, 2026
19 of 32 checks passed
@xhan97
xhan97 deleted the fix/ik-iforest branch August 16, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant