Skip to content

fix: make location spacing order-independent - #22

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/selector-spacing-order-independent
Open

fix: make location spacing order-independent#22
ChrisW09 wants to merge 1 commit into
mainfrom
fix/selector-spacing-order-independent

Conversation

@ChrisW09

Copy link
Copy Markdown
Collaborator

Fixes #10.

Problem

BaseLocationSelector._enforce_spacing tests each candidate with
point - spaced[-1] >= min_distance, which is only meaningful for an ascending list.
LightGBMLocationSelector._ordered_candidates returns candidates in gain-descending
order — the base-class docstring at selectors.py:121-126 states that is the contract — so
any candidate positioned below the previous keeper yielded a negative difference and was
silently dropped.

Two consequences, both silent:

  1. The candidate set collapsed to a small cluster. On x ~ U(0, 10), y = sin(x),
    184 candidates became 8, all inside [6.27, 9.82].
  2. Because the survivors emerged in ascending order, _trim_over_max's points[:max_count]
    then kept the numerically lowest thresholds rather than the highest-gain ones,
    discarding the gain ranking that is the entire reason to choose lightgbm over cart.

Fix

Compare each candidate against every location kept so far rather than only the last one,
making the filter independent of arrival order.

For already-ascending input the two tests are equivalent — spaced[-1] is the nearest kept
location — so the single-tree path is bit-for-bit unchanged. The gain-ordered path now keeps
its ordering, which also makes the existing points[:max_count] trim correct without
touching it.

Result

before after
candidates surviving spacing 8 65
select(min=3, max=8) [6.27 … 9.82] [2.84 … 9.14]

PLE thresholds through the Preprocessor (feature scaled to [-1, 1]):

before: lightgbm [0.26 0.29 0.32 0.82 0.87 0.9  0.94]   <- lower 60% = one bin
after:  lightgbm [-0.43 -0.4 0.18 0.2 0.26 0.29 0.32]
        cart     [-0.3  0.15 0.26 0.38 0.73 0.82 0.91]

The remaining clustering in the lightgbm row is now genuine gain ranking (the steepest
parts of sin), not an artifact of the filter.

Tests

Five added to tests/test_location_selectors.py:

  • test_enforce_spacing_is_order_independent — ascending and shuffled input give the same set
  • test_enforce_spacing_still_drops_close_neighbours — the spacing guarantee still holds
  • test_enforce_spacing_matches_legacy_result_on_ascending_input — pins the no-change
    guarantee for the single-tree path against a literal copy of the old loop
  • test_lightgbm_locations_cover_the_feature_range — regression test for the reported bug
  • test_lightgbm_keeps_the_highest_gain_locations — gain order survives the filter

Full suite: 485 passed, 9 xfailed. ruff check clean on both changed files; pyright
unchanged at its pre-existing 71 errors.

🤖 Generated with Claude Code

``BaseLocationSelector._enforce_spacing`` compared each candidate only
against the most recently kept one (``point - spaced[-1] >= min_distance``),
which is correct only for ascending input. ``LightGBMLocationSelector``
deliberately returns candidates in gain-descending order, as the base-class
docstring requires, so every candidate positioned below the previous keeper
produced a negative difference and was dropped.

On a 2000-point sin(x) fit over [0, 10] this collapsed 184 candidates to 8,
all inside [6.27, 9.82], and because the survivors came out ascending the
subsequent ``points[:max_count]`` trim then kept the numerically lowest
thresholds instead of the highest-gain ones -- discarding the gain ranking
that motivates the lightgbm strategy in the first place.

Compare against every kept location instead. For ascending input this is
equivalent to the previous test, so the single-tree path is unchanged; the
gain-ordered path now retains 65 candidates and its trim is meaningful again.

Closes #10

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

fix: lightgbm placement clusters basis units into one subrange

1 participant