Repository navigation
Conversation
ISplineTransformer integrated each M-spline with a fixed 200-point
trapezoid grid over the full knot range, so a basis function whose
support is narrower than one grid step came out identically zero or
ramped up over the wrong interval. This happens on skewed data under
the default quantile placement and with a larger output_dim, and it
also broke the ordering I_0 >= I_1 >= ... >= I_{K-1} of every row.
Each I-spline is now the antiderivative of its B-spline (itself a
B-spline of degree p + 1) divided by its analytic integral
(t_{k+p+1} - t_k) / (p + 1), which is exact for any knot spacing.
Values below / above the knot range stay 0 / 1. A basis function with
zero-width support (a repeated knot) becomes the step function at that
knot, its limit as the support shrinks.
Fixes #53
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 #53.
ISplineTransformerreturned inaccurate and sometimes identically zero basis columns whenever an M-spline's support was narrow compared to the knot range. This happens on skewed data under the default quantile placement and with a largeroutput_dim, also throughPreprocessor, and without a warning.Root cause
ISplineTransformer._ispline_basisintegrated each M-spline with the trapezoid rule on a fixed 200-point grid over the whole knot range, then interpolated the cumulative integral linearly withnp.interp. A basis function whose support spans only a few grid cells ramped up over whole cells instead of over its support. One whose support falls between two grid points integrated to 0, and since normalization only ranif max_integral > 1e-10, its column stayed all zeros. Such rows also break the ordering I_0 ≥ I_1 ≥ … ≥ I_{K-1} that every I-spline row satisfies.Fix
ISplineTransformer._ispline_columnsevaluates every I-spline column in closed form. The antiderivative of the B-spline basis,BSpline(knots, np.eye(n_coef), degree).antiderivative(), is itself a B-spline of degreep + 1. Each column is that antiderivative minus its value at the left boundary, divided by the analytic integral(t_{k+p+1} - t_k) / (p + 1).xis clipped to the knot range, so values below and above it are 0 and 1. At the right boundary the value is pinned to the full integral instead of being evaluated, so a repeated boundary knot cannot put the evaluation on a zero-width interval. A basis function with zero-width support (a repeated knot) becomes the step function at that knot, its limit as the support shrinks._ispline_basisdelegates to_ispline_columns, and_design_matrixbuilds the whole design in one call instead of one call per column.Behaviour changes
output_dim. On benign data (normal,output_dim=6) the old values were within about 2e-4 of the exact ones. The golden regression baselines pass unchanged.mainarises when an interior knot coincides with a boundary knot on tied data, is now the step function at that knot instead of an all-zero column.Documentation
[0, 1]") anddocs/representations/spline_expansions.md(I_k(x) = ∫_{τ_k}^{x} M_k(t) dt) already describe the fixed behaviour; the implementation now matches them exactly, so they are unchanged._ispline_columnshas a docstring describing the closed form, the boundary values and the zero-width case;_ispline_basisrefers to it.Tests
test_ispline_matches_exact_integral_on_tight_knots[lognormal(0, 2)-{6,10,30}, normal-{20,50}, lognormal(0, 1)-20, exponential-20]: the transform equals the normalized antiderivative of each B-spline basis function to1e-10, no column is all zero, and every row is ordered I_0 ≥ I_1 ≥ …. The cases cover the issue's reproduction and every row of its error table. Fails onmain(all 7 cases), passes here.test_ispline_reaches_zero_and_one_at_the_range_boundaries: on lognormal(0, 2) data withoutput_dim=10, every column is exactly 0 at the training minimum and exactly 1 at the training maximum. Fails onmain, passes here.Reproduction
Output of the script from #53 on this branch:
Checks
just test: 1446 passed, 59 skipped, 7 xfailed; coverage 93.54% (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/expansion/spline/i_spline.py. The PRs for MSplineTransformer: absolute clipping thresholds make the basis depend on feature scale #63 (fix(splines): drop absolute clipping thresholds from the M-spline basis #107), 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), 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), B/M/I spline and two-feature thin-plate spline transformers encode a missing value as an all-zero row #87 (fix(splines): expand a missing value to a NaN row in the B/M/I and thin-plate bases #122) and A 2-D y (multi-output target) crashes the B/M/I splines even when unsupervised, and target-aware methods with unrelated errors #88 (fix(splines): ignore y when unsupervised and reject a multi-output target #123) add tests totests/expansion/spline/test_spline_expansions.pyin other places, and each merges cleanly with this branch._design_matrix, so thenp.nan_to_numkept inISplineTransformer._design_matrixbecomes a no-op there. The PR for A partial spline policy mapping drops the "clip" default, and "extrapolate"/"warn" give all-zero basis rows #84 (fix(splines): keep family policy defaults and extrapolate out-of-range rows #121) documents that under"extrapolate"and"warn"an I-spline stays at 0 below and 1 above the range, which the clipping in_ispline_columnsprovides. With this branch and the branches for MSplineTransformer: absolute clipping thresholds make the basis depend on feature scale #63, A partial spline policy mapping drops the "clip" default, and "extrapolate"/"warn" give all-zero basis rows #84 and B/M/I spline and two-feature thin-plate spline transformers encode a missing value as an all-zero row #87 combined, the full test suite passes (1499 passed, 59 skipped, 7 xfailed).This change was previously part of #75, which is closed in favour of one pull request per issue.