Skip to content

fix: location supplement drops tree-selected high-end locations #18

Description

@ChrisW09

Summary

When a target-aware selector finds fewer locations than min_count, _supplement tops the
set up with quantile candidates, unions the two, and then truncates with
sorted(all_locations)[:target_count]. Truncating a sorted union always removes the
largest values — which are frequently the data-driven locations the selector just
worked to find. The result is a location set biased toward the low end of the feature range.

Reproduction

import numpy as np
from pretab.core.selectors import CARTLocationSelector

sel = CARTLocationSelector()
x = np.linspace(0, 10, 500).reshape(-1, 1)
print("existing [9.5, 9.7] topped up to 5:", np.round(sel._supplement([9.5, 9.7], x, 5), 3))
existing [9.5, 9.7] topped up to 5: [1.667 3.333 5.    6.667 8.333]

Both of the real, tree-selected locations were discarded and replaced by pure quantiles. The
supplement step was supposed to add to them.

Visible through the public API as thresholds bunching at the bottom of the range. Here the
target depends on the feature only above x > 9, so CART finds few splits and the
supplement path triggers:

import numpy as np, pandas as pd
from pretab import Preprocessor

rng = np.random.default_rng(3)
xs = rng.uniform(0, 10, 500)
ys = np.where(xs > 9, 5.0, 0.0) + 0.1 * rng.normal(size=500)
pre = Preprocessor(numerical_method="ple", output_dim=7).fit(pd.DataFrame({"x": xs}), ys)
th = pre.column_transformer_.named_transformers_["num_x"].named_steps["ple"].thresholds_[0]
print("PLE thresholds (feature scaled to [-1, 1]):", np.round(th, 3))
PLE thresholds (feature scaled to [-1, 1]): [-0.911 -0.83  -0.759 -0.712  0.439  0.796]

Four of six thresholds sit inside the bottom 10% of the scaled range.

Expected

Supplementing preserves the locations the selector found and fills the remaining budget with
spread-out candidates — the added locations should complement the existing ones, not
displace them.

Actual

The union is truncated from the top, so high-valued selector locations are dropped in favour
of low quantiles, and the resulting set clusters at the low end.

Root cause

pretab/core/selectors.py:150-157:

def _supplement(self, existing, x, target_count):
    if target_count - len(existing) <= 0:
        return existing
    quantile_candidates = quantile_knots(x, target_count)
    all_locations = set(existing) | set(quantile_candidates.tolist())
    return sorted(all_locations)[:target_count]     # <- drops the largest values

quantile_knots(x, target_count) already returns target_count candidates, so the union has
target_count + len(existing) entries and the slice always discards exactly the
len(existing) largest — statistically, the selector's own locations whenever they sit above
the median.

Suggested fix

Keep existing unconditionally and only draw as many supplements as are actually missing:

def _supplement(self, existing, x, target_count):
    missing = target_count - len(existing)
    if missing <= 0:
        return existing
    candidates = [c for c in quantile_knots(x, target_count).tolist() if c not in set(existing)]
    return sorted(set(existing) | set(candidates[:missing]))

If the union can still overshoot, down-sample by even spacing with
pretab.core.knots.select_knots rather than slicing a sorted list — the same primitive
trim_to_count already uses, which preserves coverage of the range instead of truncating one
end.

pretab/transformers/splines/mixins.py:119-130 (_clamp_interior_knots) has the same
shape of logic but already handles it correctly via select_knots, so that is a good
reference for the fix.

Regression test suggestion: assert that every element of existing is present in the output
of _supplement.

Impact

Any target-aware placement where the selector returns fewer than min_count locations —
common for weak or highly localized features. Reaches ple, the feature maps, and the
target-aware splines through both CARTLocationSelector and LightGBMLocationSelector.
Silent quality degradation, no error.

Environment

  • pretab 0.1.0 (main @ 51c3043)
  • Python 3.11.15, numpy 2.4.6, pandas 2.3.3, scikit-learn 1.9.0, scipy 1.17.1
  • macOS (darwin 25.5.0)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions