Repository navigation
Conversation
BaseSplineTransformer documented that output_dim is ignored when knot_locations is given, but it still checked the knot count against output_dim - degree - 1 (raising for any other count), trimmed the knots to the adaptive window, and clipped out-of-range knots onto the boundary, which silently replaced them by degenerate boundary knots. Explicit knots now fix the width (len(knot_locations) + degree + 1 basis functions per feature) and bypass output_dim and the adaptive bounds. Repeated knots are merged, knots on or outside a feature's fitted range are dropped with a DataWarning, and knot_locations must be a 1-D sequence of finite numbers (InvalidParamError otherwise). Fixes #81
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 #81.
BSplineTransformer,MSplineTransformerandISplineTransformerdocument thatknot_locationsoverridesoutput_dim, but they rejected any knot count other thanoutput_dim - degree - 1, trimmed or padded the knots in adaptive mode, and silently clipped knots on or outside the data range onto the boundary, which gave zero-width basis functions (all-zero columns). Users who need a fixed knot grid could not use these transformers for it.Root cause
BaseSplineTransformer._column_knotsran explicit knots through theoutput_dimmachinery of automatic placement. It checkedlen(knot_locations)againstoutput_dim - degree - 1(raisingIncompatibleParamsErrorwhenadaptive=False), then passed the knots to_adjust_internal_knots, which clips them to[x_min, x_max], de-duplicates them, tops them up with quantile / uniform knots below the minimum count of theoutput_dimwindow and down-samples them withselect_knotsabove its maximum.Fix
BaseSplineTransformer._column_knotshandles explicit knots first, before theoutput_dimwindow is resolved, and returns the boundary knots around_explicit_knots(x_min, x_max). The selector and automatic paths are unchanged (still_adjust_internal_knots, then the trailingnp.clip/np.unique).BaseSplineTransformer._explicit_knotsmerges repeated knots, keeps those strictly inside the feature's fitted range(x_min, x_max)and drops the others with aDataWarningthat lists them. A feature getslen(kept) + degree + 1basis functions.BaseSplineTransformer.fitskips theoutput_dimrange checks whenknot_locationsis given and calls the new_validate_knot_locations, which raisesInvalidParamErrorunlessknot_locationsis a 1-D sequence of finite numbers.IncompatibleParamsErrorimport is removed frompretab/expansion/spline/base.py.Behaviour changes
IncompatibleParamsError"knot_locations length must match output_dim - degree - 1 when adaptive=False" is no longer raised.output_dim,min_output_dimandmax_output_dimare ignored, including their range checks (anoutput_dimbelowdegree + 1or above 50 no longer raises), and adaptive mode neither trims nor pads the knots.DataWarninginstead of being clipped onto the boundary. With several features, a feature whose range excludes some knots therefore gets fewer columns than the others;knot_locations=[-5, 15]on[0, 10]gives no interior knot anddegree + 1columns instead of two boundary knots and two all-zero columns.knot_locations=[5, 5]on[0, 10]gives the interior knot[5.](5 columns) instead of[3.33, 6.67](6 columns).knot_locationsthat is not 1-D, non-numeric or non-finite raisesInvalidParamErroratfit.Documentation
BaseSplineTransformerdocstring,knot_locationsparameter (shared by the B-, M- and I-spline transformers): overridesoutput_dimand the adaptive bounds, a feature getslen(knot_locations) + degree + 1basis functions, repeated knots are merged, and knots on or outside a feature's fitted range are dropped for that feature with aDataWarning.docs/representations/spline_expansions.md, "Constructor highlights": the same contract in one sentence._explicit_knotsand_validate_knot_locations.Tests
test_knot_locations_set_the_width_regardless_of_output_dim[{1,4,12} knots-{B,M,I}SplineTransformer]:knots_holds exactly the given interior knots and the output haslen(knots) + 4columns with the defaultoutput_dim. Fails onmain(raises), passes here.test_knot_locations_are_kept_in_adaptive_mode[not-trimmed]:[2, 4, 6, 8]withmin_output_dim=5,max_output_dim=6is kept as given. Fails onmain([2, 8]), passes here.test_knot_locations_are_kept_in_adaptive_mode[not-padded]:[5]withmin_output_dim=7,max_output_dim=8is kept as given. Fails onmain([2.5, 5, 7.5]), passes here.test_knot_locations_outside_the_range_are_dropped_with_a_warning:[-5, 3, 3, 15]warns about[-5.0, 15.0]and leaves the single interior knot[3.0]. Fails onmain, passes here.test_knot_locations_on_the_boundary_are_dropped_instead_of_giving_a_dead_column:[0, 5]warns about[0.0], leaves[5.0]and no column is all-zero on the training data. Fails onmain, passes here.test_invalid_knot_locations_raise_a_typed_error[2-D, non-numeric, NaN]:InvalidParamErrornamingknot_locations. Fails onmain(IncompatibleParamsErrorfrom the length check), passes here.Reproduction
Output of the script from #81 on this branch, extended with the M- and I-spline cases and the
[5, 5]and[0, 5]cases from the issue text, with the warnings printed:Checks
just test: 1454 passed, 59 skipped, 7 xfailed; coverage 93.82% (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.BaseSplineTransformer._column_knots; whichever of the PRs for Spline knots on tied data land on the range boundary or repeat, producing dead columns and all-zero rows #56 (fix(splines): keep knots unique and strictly interior on tied data #101), Target-aware splines trim selector knots by position, dropping the dominant split, and cap adaptive width at 15 #69 (fix(splines): search the resolved knot window in target-aware placement #109) and B/M/I spline knot_locations are sized by output_dim and clipped to the data range instead of used as given #81 (fix(splines): honour explicit knot_locations regardless of output_dim #118) lands later needs a manual merge of_column_knots. The resolution is the same in any order: keep this PR's early return for explicit knots (withx_min,x_maxand the boundary vectors defined at the top of the method) and take the other PRs' changes to the selector and automatic paths.knot_locationsbranch (the length check and_adjust_internal_knotscall) and Target-aware splines trim selector knots by position, dropping the dominant split, and cap adaptive width at 15 #69 changes the selector call. Drop the old branch and the secondx_min/x_maxassignment, and keep Target-aware splines trim selector knots by position, dropping the dominant split, and cap adaptive width at 15 #69's selector branch (min_knots = min(min_knots, max_knots)and themin_knots/max_knotsarguments toget_knot_locations) asif selector is not None:. Both PRs also append tests at the end oftests/expansion/spline/test_spline_expansions.py: keep both blocks (Target-aware splines trim selector knots by position, dropping the dominant split, and cap adaptive width at 15 #69's import additions merge cleanly).pretab/expansion/spline/base.py. In theknot_locationsdocstring, keep this PR's paragraph: Spline knots on tied data land on the range boundary or repeat, producing dead columns and all-zero rows #56's text says that dropped knots are replaced by quantile / uniform knots, which no longer applies to explicit knots. At the end of_column_knots, this PR keeps the trailingnp.clip/np.uniqueand defines the boundary vectors at the top of the method, while Spline knots on tied data land on the range boundary or repeat, producing dead columns and all-zero rows #56 removes thenp.clip/np.uniqueand keeps the boundary vectors at the end. Take Spline knots on tied data land on the range boundary or repeat, producing dead columns and all-zero rows #56's removal and this PR's boundary vectors at the top: the conflict block resolves to nothing, and only thereturn np.concatenate(...)follows. The PR for Unsupervised quantile placement repeats RBF/ReLU/sigmoid/tanh centers on tied data, duplicating columns #82 (fix(placement): return distinct quantile locations on tied data #119) is stacked on Spline knots on tied data land on the range boundary or repeat, producing dead columns and all-zero rows #56's branch and carries the same conflict.tests/expansion/spline/test_spline_expansions.py(append/append conflict: keep both blocks). Its test that combinestarget_aware=Truewithknot_locations=[-0.5, 0.5]passes with or without this change.pretab/expansion/spline/base.py(Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 alsodocs/representations/spline_expansions.md), and those for ISplineTransformer: 200-point trapezoid integration produces wrong and all-zero basis columns #53 and MSplineTransformer: absolute clipping thresholds make the basis depend on feature scale #63 add tests to the same test file; all of them merge cleanly with this branch.This change was previously part of #86, which is closed in favour of one pull request per issue.