Skip to content

fix(splines): stop adding a collinear bias column to the B-spline basis - #39

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/bspline-no-default-bias
Open

fix(splines): stop adding a collinear bias column to the B-spline basis#39
ChrisW09 wants to merge 1 commit into
mainfrom
fix/bspline-no-default-bias

Conversation

@ChrisW09

Copy link
Copy Markdown
Collaborator

Fixes #33.

Problem

BSplineTransformer defaulted to include_bias=True, and it was the only spline family that
did. A B-spline basis over a clamped knot vector is a partition of unity — every row sums
to exactly 1 — so the prepended intercept column is an exact linear combination of the rest:

include_bias=True   (200, 9)  rank 8  cond 6.30e+15     <- singular
include_bias=False  (200, 8)  rank 8  cond 1.79e+01

Preprocessor(numerical_method="bspline") inherited that default, so this was the
out-of-the-box path, not an opt-in. Downstream, OLS has an unidentifiable intercept and a
regularized fit spends a coefficient on a direction that carries no information.

Fix

Default include_bias to False, matching MSplineTransformer, ISplineTransformer and
BaseSplineTransformer. include_bias=True stays available for callers who want the column
and accept the collinearity, and the docstring now explains why it is off by default.

One cause, three symptoms

The same change fixes two things filed alongside it in the issue:

before after
matrix_rank / columns 8 / 9 8 / 8
output_dims_ at output_dim=7 8 (every other method: 7) 7
output_dims_ at adaptive, max_output_dim=9 10 9

The width mismatch mattered beyond aesthetics: include_bias is not a Preprocessor
parameter, so there was no way to opt out, and max_output_dim — which users set to bound
width — was silently exceeded.

Tests

Four added to tests/test_spline_expansions.py:

  • test_bspline_default_design_is_full_rank — rank and cond < 1e6; fails on the old default
  • test_bspline_basis_is_a_partition_of_unity — pins why the bias column is redundant, so
    the reasoning is checked rather than just asserted in a comment
  • test_bspline_include_bias_still_available_opt_in
  • test_bmi_splines_default_to_exactly_output_dim, parametrised over B/M/I

Changed existing expectations

test_fixed_width_matches_expected[bspline] pinned OUTPUT_DIM + 1 with the comment
"B-spline defaults to include_bias=True -> output_dim + 1" — i.e. it encoded the asymmetry
being fixed. Updated to OUTPUT_DIM. Two docstring examples showing (50, 9) are now
(50, 8).

This changes a documented default. I think it is right — the old default produces a
singular matrix, which is a defect rather than a preference — but it is a behaviour change,
so flagging it explicitly. If you would rather keep the default and only warn when
include_bias=True is requested on a partition-of-unity basis, say so and I will swap it.

Related, not changed here: MSplineTransformer(include_bias=True) is also rank-deficient
(M-splines are rescaled B-splines, so their span also contains the constants — rank 8/9).
That one is opt-in, so I left it alone; a warning would be a reasonable follow-up.
ISplineTransformer(include_bias=True) is full rank and unaffected.

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

🤖 Generated with Claude Code

``BSplineTransformer`` defaulted to ``include_bias=True`` and was the only
spline family that did. A B-spline basis over a clamped knot vector is a
partition of unity -- every row sums to exactly 1 -- so the prepended intercept
column is an exact linear combination of the remaining columns and the design
matrix is singular:

    include_bias=True   (200, 9)  rank 8  cond 6.30e+15
    include_bias=False  (200, 8)  rank 8  cond 1.79e+01

``Preprocessor(numerical_method="bspline")`` inherited that default, so every
user of the method got a rank-deficient design: an unidentifiable intercept for
OLS, and a wasted direction for a regularized fit.

Default ``include_bias`` to False, matching MSpline, ISpline and the shared
base class. ``include_bias=True`` remains available for callers that want the
explicit column.

The same change resolves two further symptoms of the same cause. bspline was
the only method whose per-feature width differed from ``output_dim`` (8 for
output_dim=7, where every other family gave 7), and the only one that could
breach the adaptive window (10 against max_output_dim=9). Both now match.

``test_fixed_width_matches_expected[bspline]`` encoded the old ``output_dim + 1``
width and is updated; the two docstring examples showing ``(50, 9)`` are too.

Closes #33

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(splines): bspline include_bias=True makes the design rank-deficient

1 participant