Repository navigation
Conversation
On discrete or heavily tied features the selector's quantile top-up and its quantile fallbacks collapsed onto the tied values and the range boundary. The top-up sliced the quantile candidates before de-duplicating them, did not restrict them to the open range and skipped the spacing filter, and the fallbacks returned quantile_knots() verbatim. The result was too few locations (output narrower than output_dim, and fold-dependent widths that crashed CrossFittedTransformer with a misleading "disable adaptive sizing" error), duplicate locations, and locations on x_min/x_max that gave PLE a constant column. The top-up and both fallbacks now keep every selector-found location and fill the shortfall with strictly interior, spaced quantile candidates first, then uniform ones; only a request too dense for the spacing adds the remaining uniform points without it, and a zero-range feature keeps its repeated value. LightGBM's +/-1e-35 split-at-zero sentinel is mapped to the midpoint of the values it separates, and PLE treats any positive bin width as a real bin instead of using an absolute 1e-10 tolerance that encoded narrow bins as a constant 0.5. Locations move on tied or discrete features only; continuous features with enough splits are unchanged. Fixes #57
This was referenced Oct 16, 2026
This branch has not been deployed
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.
Fixes #57.
On discrete or heavily tied features, the target-aware location selectors (
"cart"and"lightgbm"placement) returned too few, duplicate or range-boundary locations. Feature maps and PLE came out narrower thanoutput_dimwith constant or duplicate columns, andCrossFittedTransformerfailed with a misleading "Disable adaptive sizing" error.Root cause
A tree on a feature with few levels can only split between levels, so it finds fewer splits than requested, and the selector tops up with quantile locations.
BaseLocationSelector._supplementtook the firstmissingquantile candidates before de-duplicating them. It also did not restrict them to the open range(x_min, x_max)and did not apply the spacing filter. On tied data the quantiles repeat and land on the boundary values. The small-sample and no-candidate fallbacks inselectreturnedquantile_knots()unchanged, duplicates and boundary values included. LightGBM reports a split at zero as the sentinel threshold1e-35, which was kept as a separate location next to0.0. PLE then turned boundary and duplicate thresholds into zero-width bins, which it encodes as a constant 0.5.Fix
BaseLocationSelector._supplementkeeps every location the selector found and fills only the shortfall with distinct candidates strictly inside the range that keepmin_location_spacingfrom every kept location: evenly spread quantile candidates first, then uniform ones. Only when the request is too dense for the spacing are the remaining uniform points added without it. A zero-range feature keeps its repeated value, as before.BaseLocationSelector.select(too few samples, no candidates) now go through_supplementinstead of returningquantile_knots()directly.LightGBMLocationSelector._extract_split_points_with_gains/_traverse_treemap the±1e-35split-at-zero sentinel to the midpoint between the values it separates (_midpoint), which is where CART would put that split.PLETransformerencodes the position inside any bin of positive width. Before, it treated bins narrower than an absolute1e-10as zero-width and filled them with 0.5.Behaviour changes
output_dim=6has no constant column, and the cross-fitting call no longer raises.0.5) instead of at1e-35.min_countsplits on continuous data, the top-up now takes evenly spread, spaced quantile candidates instead of the lowest ones. When the tree finds enough splits, nothing changes, and the golden regression baselines pass unchanged.1e-10(features on a tiny scale) instead of a constant 0.5.Documentation
docs/representations/choosing_a_method.md(edge-case behaviour): new "Discrete or heavily tied column" entry. Target-aware PLE and feature maps still get the requested number of distinct interior bin edges or centers, so the output width depends neither on the data nor on the cross-fitting fold.pretab/core/selectors.pymodule,BaseLocationSelector, theReturnssection ofBaseLocationSelector.select(betweenmin_countandmax_countdistinct interior locations; a single-valued feature repeats its value), and_supplement.numerical_encoding.md: "output_dim is exact here"; the feature-map base docstring: "keeps exactlyoutput_dimcenters") already describe the fixed behaviour and are unchanged.Tests
test_selector_tops_up_tied_features_with_distinct_interior_locations[{2,3,5}-{CART,LightGBM}LocationSelector]: 8 distinct, strictly interior locations on features with 2, 3 and 5 levels, for both selectors. Fails onmain, passes here.test_supplement_on_tied_data_respects_spacing_and_count: the top-up on tied data returns the requested count, keeps the existing split, respects the spacing and stays inside the range. Fails onmain, passes here.test_small_sample_fallback_on_tied_data_is_distinct_and_interior: the small-sample fallback returns distinct interior locations. Fails onmain, passes here.test_lightgbm_zero_split_sentinel_maps_to_the_bin_midpoint:0.5is selected and no location is near1e-35. Fails onmain, passes here.test_cross_fitting_a_discrete_feature_keeps_a_fixed_width[0-4]:CrossFittedTransformeraround a target-aware RBF on a 6-level feature returns 10 columns. Fails onmain(raises), passes here.test_target_aware_centers_keep_output_dim_on_a_discrete_feature[cart, lightgbm]: 10 distinct centers and 10 output columns. Fails onmain, passes here.test_ple_keeps_output_dim_and_no_constant_columns_on_discrete_features[2-6]: width 7 and no constant column on features with 2 to 6 levels. Fails onmain, passes here.test_ple_encodes_the_position_inside_narrow_bins_on_a_tiny_range_feature: on a feature of range1e-11, each value is encoded as(x - lower) / (upper - lower)in its bin. Fails onmainand with the selector change alone, passes here.test_supplement_fills_dense_requests_beyond_the_spacing: 150 distinct points on[0, 1], more than the 1% spacing allows. Guard test: passes on both by design.test_constant_feature_fallback_keeps_the_requested_count: a zero-range feature repeats its valuemin_counttimes. Guard test: passes on both by design.Reproduction
Output of the script from #57 on this branch (
LeakageWarnings omitted):Checks
just test: 1462 passed, 59 skipped, 7 xfailed; coverage 93.73% (gate 90%).scripts/quickstart.pypasses.just docs: strict build succeeds.just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).Merge notes
main.pretab/core/selectors.py, but in other functions (CART candidate ranking; the LightGBM objective), and the code merges cleanly. All three append tests at the end oftests/core/test_location_selectors.py, so whichever lands later gets an append/append conflict there: keep both blocks. With all three branches combined this way, the full test suite passes (1478 passed, 59 skipped, 7 xfailed). The single-class fallback added by LightGBM placement with task="classification" always uses the binary objective on raw labels #61 goes through this PR's_supplementand still returns the[1/3, 2/3]its test expects.pretab/core/selectors.pyin two places. In the imports, this PR addsuniform_knotsto the.knotsimport and An invalid task such as "Regression" is silently treated as classification by the splines and selectors #93 addsfrom .parameters import validate_task. In the docstring ofBaseLocationSelector.select, this PR rewrites theReturnstext and An invalid task such as "Regression" is silently treated as classification by the splines and selectors #93 adds aRaisessection. Keep both sides in each.This change was previously part of #75, which is closed in favour of one pull request per issue.