Skip to content

fix(splines): ignore y when unsupervised and reject a multi-output target - #123

Open
ChrisW09 wants to merge 1 commit into
mainfrom
fix/88-spline-multioutput-y
Open

ChrisW09 wants to merge 1 commit into
mainfrom
fix/88-spline-multioutput-y

Conversation

@ChrisW09

@ChrisW09 ChrisW09 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #88.

A 2-D (multi-output) y made BSplineTransformer, MSplineTransformer and ISplineTransformer raise an IndexError in fit, even with unsupervised or explicit knot placement where y is never used, so a standard multi-output pipeline such as make_pipeline(BSplineTransformer(), Ridge()).fit(X, Y) failed. Target-aware placement, which needs a single target, failed with unrelated errors instead of saying so.

Root cause

BaseSplineTransformer.fit built y_arr = np.asarray(y).ravel() and sliced it with each feature's NaN mask on every path, not only when a selector used it. A target of shape (n, k) became n * k values, which a mask of length n cannot index. BaseLocationSelector.select and PLETransformer.fit flattened y the same way, so on the target-aware paths of the other families a multi-output target failed with a NumPy broadcasting ValueError (combining the NaN masks of x and y) or with PLE's "X and y must have same length" check.

Fix

  • BaseSplineTransformer.fit builds y_arr only when the target-aware selector (SplinePlacementAdapter) places the knots, and slices it by rows like X instead of flattening it. The unsupervised and explicit-knot_locations paths ignore y.
  • New pretab.core.validation.single_target(y, estimator, *, fix=None): returns a 1-D target or a column vector flattened, as before, and raises IncompatibleParamsError naming the estimator and the shape of y when it has more than one output column. The message points to one target column or target_aware=False.
  • The error's fix hint suits the estimator: the selectors suggest target_aware=False, while PLETransformer, which is always target-aware and has no such option, suggests fitting on one target column or choosing a method with unsupervised placement.
  • BaseLocationSelector.select (the CART and LightGBM selectors behind target-aware B/M/I, natural-cubic and cubic-regression splines and the RBF/ReLU/sigmoid/tanh feature maps) and PLETransformer.fit call single_target instead of np.asarray(y).ravel().

Behaviour changes

  • B/M/I splines with target_aware=False or explicit knot_locations ignore y entirely, whatever its shape and even when it is not row-aligned with X; knots and output equal those of fit(X). Multi-output pipelines and the unsupervised Preprocessor methods "bspline", "mspline" and "ispline" now fit.
  • Every target-aware placement with a multi-output y (B/M/I, natural-cubic, cubic-regression, RBF/ReLU/sigmoid/tanh feature maps and PLE, with "cart" or "lightgbm"; this includes the Preprocessor default ple and the regression presets) raises IncompatibleParamsError: <CARTLocationSelector | LightGBMLocationSelector | PLETransformer> places locations against a single target, but y has shape (200, 2). followed by a Fix: line. Before, it raised an IndexError, a broadcasting ValueError or, for PLE, a PretabDataError.
  • Knots and outputs for a 1-D or column-vector target are unchanged; no golden baseline changed.
  • pretab.core.validation exports the new helper single_target.

Documentation

  • Docstrings (NumPy format): the target_aware parameter of BaseSplineTransformer (True needs a single-target y, a multi-output y raises; False and explicit knot_locations ignore y), the y parameter of BaseLocationSelector.select and PLETransformer.fit (shape (n_samples,) or (n_samples, 1), a multi-output y raises), and single_target with a Raises section.
  • docs/core_concepts/target_awareness.md: new paragraph. Target-aware placement fits one model on a single target, so a multi-output y raises IncompatibleParamsError for ple and for an optional method with target_aware=True; a column vector counts as a single target; with target_aware=False the optional methods ignore y and fit inside a multi-output pipeline.

Tests

  • test_unsupervised_bmi_spline_ignores_a_multi_output_target[{B,M,I}SplineTransformer x default | uniform | target-aware with explicit knots]: fitting with a 2-D Y (and a NaN in X) gives the same knots and output as fitting without y. Fails on main, passes here.
  • test_unsupervised_bmi_spline_never_reads_y[{B,M,I}SplineTransformer x default | uniform]: a y of the wrong length is ignored. Fails on main, passes here.
  • test_bmi_spline_fits_in_a_multi_output_pipeline[{B,M,I}SplineTransformer]: make_pipeline(cls(), Ridge()) with a 2-D Y predicts shape (200, 2). Fails on main, passes here.
  • test_target_aware_bmi_spline_accepts_a_column_vector_target[{B,M,I}SplineTransformer]: a column-vector y gives the same knots as the flat y on the new row-sliced selector path. Guard test: passes on both by design.
  • test_target_aware_placement_rejects_a_multi_output_target[{BSpline, MSpline, ISpline, NaturalCubic, CubicReg, RBF, ReLU, Sigmoid, Tanh, PLE} x {cart, lightgbm}] (edge-case contract): a 2-D Y raises IncompatibleParamsError naming the shape, and a column vector gives the same output as the flat target. All 20 fail on main, pass here; the lightgbm cases skip without LightGBM.
  • test_unsupervised_numerical_method_accepts_a_multi_output_target[bspline, mspline, ispline, pspline, naturalspline, rbf] (Preprocessor, target_aware=False): the output with a 2-D Y equals the output without y. The bspline, mspline and ispline cases fail on main, pass here; the pspline, naturalspline and rbf cases are controls that pass on both by design.
  • test_target_aware_numerical_method_rejects_a_multi_output_target[bspline, naturalspline, rbf, ple] (Preprocessor): a 2-D Y raises IncompatibleParamsError, a column vector fits. Fails on main, passes here.
  • test_preprocessor_defaults_reject_a_multi_output_target[default-ple, regression-preset-bspline]: Preprocessor() and Preprocessor(preset="standard", task="regression") raise IncompatibleParamsError. Fails on main, passes here.
  • test_multi_output_error_suggests_a_fix_the_estimator_supports: the PLE message does not mention target_aware, the B-spline message does. Fails on main, passes here.

Reproduction

Output of the script from #88 on this branch:

SplineTransformer -> (200, 2)
NaturalCubicSplineTransformer -> (200, 2)
BSplineTransformer -> (200, 2)
Preprocessor('pspline') -> (200, 7)
Preprocessor('bspline') -> (200, 7)
Preprocessor('ple'), target-aware -> IncompatibleParamsError: PLETransformer places locations against a single target, but y has shape (200, 2).
Fix: fit on one target column, or use unsupervised placement (target_aware=False) for a multi-output target.
Preprocessor('naturalspline'), target-aware -> IncompatibleParamsError: CARTLocationSelector places locations against a single target, but y has shape (200, 2).
Fix: fit on one target column, or use unsupervised placement (target_aware=False) for a multi-output target.

Checks

  • just test: 1492 passed, 59 skipped, 7 xfailed; coverage 93.77% (gate 90%).
  • Minimum dependencies (CI job: Python 3.10, numpy 1.24.4, pandas 2.0.3, scipy 1.10.1, scikit-learn 1.6.0): 1469 passed, 82 skipped, 7 xfailed; scripts/quickstart.py passes.
  • 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).
  • Import direction between subpackages is unchanged.

Merge notes

This change was previously part of #97, which is closed in favour of one pull request per issue.

…rget

The B-, M- and I-spline transformers flattened y and masked it with each
feature's missing-value mask even when no target-aware selector used it,
so a multi-output target of n rows (2n values after ravel) raised an
IndexError in fit. make_pipeline(BSplineTransformer(), Ridge()).fit(X, Y)
with a 2-D Y failed, as did explicit knot_locations and the Preprocessor
with numerical_method="bspline" / "mspline" / "ispline" and
target_aware=False, while scikit-learn's SplineTransformer and the other
spline families and feature maps ignore y there.

Target-aware placement fits one decision tree or boosting model on one
target, but no error said so: the B/M/I splines (including the regression
presets) hit the same IndexError before the selector ran, the shared
location selector flattened y and failed with a NumPy broadcasting error
(natural-cubic, cubic-regression, feature maps), and PLE, the
Preprocessor's default method, reported "X and y must have same length".

y is now read only when the target-aware selector places the knots; the
unsupervised and explicit knot_locations paths ignore it. On the selector
path y is sliced by rows like X instead of being flattened. A shared
single_target() check in the location selector and PLE flattens a column
vector as before and raises an IncompatibleParamsError for more than one
output column, pointing to target_aware=False.

Fixes #88

This branch has not been deployed

No deployments
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.

A 2-D y (multi-output target) crashes the B/M/I splines even when unsupervised, and target-aware methods with unrelated errors

1 participant