Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pretab/transformers/splines/base_spline.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class BaseSplineTransformer(BasePreTabTransformer):
>>> from pretab.transformers import BSplineTransformer
>>> X = np.linspace(0, 1, 50).reshape(-1, 1)
>>> BSplineTransformer(output_dim=8).fit_transform(X).shape
(50, 9)
(50, 8)
"""

def __init__(
Expand Down
14 changes: 11 additions & 3 deletions pretab/transformers/splines/bspline.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,30 @@ class BSplineTransformer(BaseSplineTransformer):
is expanded column by column and the results are stacked horizontally.

See :class:`~pretab.transformers.splines.base_spline.BaseSplineTransformer`
for the full parameter description. ``include_bias`` defaults to True here.
for the full parameter description.

.. note::
``include_bias`` defaults to ``False``. A B-spline basis over a clamped
knot vector is a partition of unity -- every row sums to 1 -- so an
extra intercept column is an exact linear combination of the basis and
makes the design matrix singular. Set it to ``True`` only if a
downstream model needs the explicit column and tolerates the
collinearity.

Examples
--------
>>> import numpy as np
>>> from pretab.transformers import BSplineTransformer
>>> X = np.linspace(0, 1, 50).reshape(-1, 1)
>>> BSplineTransformer(output_dim=8).fit_transform(X).shape
(50, 9)
(50, 8)
"""

def __init__(
self,
output_dim=UNSET,
degree: int = 3,
include_bias: bool = True,
include_bias: bool = False,
knot_locations: np.ndarray | None = None,
target_aware: bool = False,
placement_strategy: str = "quantile",
Expand Down
3 changes: 1 addition & 2 deletions tests/test_adaptive_output_dim.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@
"tprs": OUTPUT_DIM,
"mspline": OUTPUT_DIM,
"ispline": OUTPUT_DIM,
# B-spline defaults to include_bias=True -> output_dim + 1
"bspline": OUTPUT_DIM + 1,
"bspline": OUTPUT_DIM,
}

# Families that honor the adaptive window when driven through the Preprocessor.
Expand Down
43 changes: 43 additions & 0 deletions tests/test_spline_expansions.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,46 @@ def test_spline_penalty_matrix_symmetric(data):
P = transformer.get_penalty_matrix()
assert P.shape[0] == P.shape[1]
assert np.allclose(P, P.T, atol=1e-9)


# --------------------------------------------------------------------------- #
# A partition-of-unity basis must not carry a redundant intercept column.
#
# ``BSplineTransformer`` used to default to ``include_bias=True``. A B-spline
# basis over a clamped knot vector sums to 1 on every row, so the prepended
# column of ones is an exact linear combination of the rest and the design
# matrix is singular (condition number ~1e15).
# --------------------------------------------------------------------------- #
def test_bspline_default_design_is_full_rank():
X = np.linspace(0, 1, 200).reshape(-1, 1)

design = BSplineTransformer(output_dim=8).fit_transform(X)

assert design.shape[1] == 8
assert np.linalg.matrix_rank(design) == design.shape[1]
assert np.linalg.cond(design) < 1e6


def test_bspline_basis_is_a_partition_of_unity():
# This is *why* the bias column is redundant; pin it so the reasoning holds.
X = np.linspace(0, 1, 200).reshape(-1, 1)

design = BSplineTransformer(output_dim=8).fit_transform(X)

np.testing.assert_allclose(design.sum(axis=1), 1.0)


def test_bspline_include_bias_still_available_opt_in():
X = np.linspace(0, 1, 200).reshape(-1, 1)

design = BSplineTransformer(output_dim=8, include_bias=True).fit_transform(X)

assert design.shape[1] == 9
np.testing.assert_allclose(design[:, 0], 1.0)


@pytest.mark.parametrize("cls", [BSplineTransformer, MSplineTransformer, ISplineTransformer])
def test_bmi_splines_default_to_exactly_output_dim(cls):
X = np.linspace(0, 1, 200).reshape(-1, 1)

assert cls(output_dim=7).fit_transform(X).shape[1] == 7
Loading