Scikit-learn integration
When to use the estimator
Use BezierRegressor when a Bézier surrogate belongs in a scikit-learn
workflow: cloning estimators, cross-validation, a parameter grid, or a larger
pipeline. It delegates fitting to the same numerical core as fit and
exposes the complete numerical result. It does not introduce a different
meaning of degree, penalty or sample weighting.
Install pytorch-bsf[sklearn]. The estimator handles one simplex or product
domain with NumPy-compatible arrays. Use the Python Points and Samples
API for assembled mixed-cell observations, because a feature matrix alone
does not carry the required per-row cell IDs.
Prepare barycentric features
A segment input uses two columns [1-x, x], where 0 <= x <= 1. These
are dependent barycentric coordinates, not two arbitrary tabular features.
Do not pass a single x column expecting an ordinary polynomial regressor.
By default one input column denotes a constant domain and its numeric values
are ignored; the column is only a scikit-learn-compatible dummy feature.
import numpy as np
from sklearn.model_selection import GridSearchCV, KFold
from torch_bsf import QuadraticPenalty
from torch_bsf.sklearn import BezierRegressor
x = np.linspace(0, 1, 30)
X = np.column_stack([1 - x, x])
y = np.sin(np.pi * x) + .2 * x
estimator = BezierRegressor(degree=3)
estimator.fit(X, y)
prediction = estimator.predict(X)
assert prediction.shape == y.shape
assert estimator.result_.success
assert estimator.diagnostics_.compute_dtype == "float64"
dimensions=None infers one simplex from the column count. A triangle
uses three columns. A square represented as a product uses four columns,
[1-u, u, 1-v, v], and requires dimensions=(1, 1). A prism requires
dimensions=(2, 1) and five columns. Each factor block must independently
sum to one; the entire product row generally does not.
Choose degree and penalty through validation
search = GridSearchCV(
BezierRegressor(),
{"degree": [1, 2, 3, 5],
"penalty": [QuadraticPenalty(), QuadraticPenalty(ridge=1e-4)]},
scoring="neg_mean_squared_error",
cv=KFold(3, shuffle=True, random_state=7),
)
search.fit(X, y)
best = search.best_estimator_
assert best.result_.success
assert best.predict(X).shape == (30,)
Scikit-learn maximizes its score, so negative MSE closer to zero is better.
GridSearchCV refits its chosen estimator by default, unlike the core
select_degree operation, which requires an explicit full-data refit.
Inspect cv_results_ rather than reporting only the winning degree.
Increasing degree adds controls and can overfit noise. Increasing ridge
shrinks controls toward the output origin; increasing smoothness draws
adjacent controls together. Pass complete typed QuadraticPenalty objects
in the grid, as above; the penalty is an immutable numerical configuration,
not a nested scikit-learn estimator. See Regularization and numerical evidence for units and
Cross-validation and degree search for an actual before/after degree comparison.
Fig. 8 The numerical core’s degree comparison illustrates the same capacity trade-off used by the estimator. The figure is generated by the core three-fold search; it is not the output of the separate GridSearchCV example above, whose scoring and refit policy should be recorded separately.
Weights, outputs and evidence
estimator.fit(X, y, sample_weight=w) supplies row weights. Larger relative
weights emphasize particular observations. Uniformly scaling all weights
leaves the normalized regression problem unchanged. A one-dimensional y
returns a one-dimensional prediction; multioutput y returns a two-dimensional
array with the same output count. Predictions are NumPy arrays from the CPU
float64 model, even if the original input arrays were float32.
result_ holds metrics, diagnostics, model and termination status;
model_ is the shared BezierModel; diagnostics_ exposes solver
evidence directly. The estimator requires numerical success during fitting,
so a failed solve raises through require_success. A successful fit can
still be unidentifiable or statistically poor; inspect data rank and
held-out error. A high default R-squared score is not an application-specific
error guarantee, especially for low-variance outputs.
Avoid using a generic feature standardizer on barycentric columns inside a pipeline: it generally breaks their domain constraints. If ordinary scalar features need conversion into barycentric blocks, perform that explicit conversion first. Output transforms must be inverted when reporting original units and fitted independently within each validation fold. See Output scaling for that distinction.
For product degrees use a tuple matching dimensions. Fixed controls and
typed solvers use the same constructor arguments as the core API. If a search
changes degree, fixed global row numbers cannot be reused blindly: resolve
constraints for each space in a manual core validation loop. Save
best.model_ with io.save_model when only the common numerical model is
needed; store preprocessing and feature schemas alongside it in the application.
Fit independent input blocks as a product
u, v = np.meshgrid(np.linspace(0, 1, 8), np.linspace(0, 1, 7), indexing="ij")
u, v = u.ravel(), v.ravel()
product_X = np.column_stack([1 - u, u, 1 - v, v])
product_y = np.square(u) + v
product = BezierRegressor(dimensions=(1, 1), degree=(2, 1))
product.fit(product_X, product_y)
test_u = np.array([.13, .47, .81])
test_v = np.array([.22, .56, .73])
test_X = np.column_stack([1 - test_u, test_u, 1 - test_v, test_v])
assert np.allclose(product.predict(test_X), np.square(test_u) + test_v)
The domain is the square pictured in the
simploid overview. Its two factors have
different degrees because the target is quadratic in u and linear in
v. Without explicit dimensions, four columns would imply one
three-dimensional simplex, whose sum-to-one constraint these rows violate.
Random row folds assess interpolation among the sampled combinations. If
the intended task is prediction at entirely unseen operating conditions,
hold out complete v slices or use an appropriate grouped split instead.
Choose a validation design that matches the use of the fitted model rather
than relying on a default splitter to define that use implicitly.