Sample Weights#
Not every measurement deserves the same say in a fit. Use sample_weight when
observations differ in precision — different instruments, replicate-derived variances, or
regions of the design space that carry little information about the quantity you care
about.
import numpy as np
from jaxsr import BasisLibrary, SymbolicRegressor
X = np.random.randn(100, 2)
y = 2.0 * X[:, 0] + 0.5 * X[:, 1] ** 2
variances = np.repeat([0.01, 1.0], 50) # first half measured 100x more precisely
library = (
BasisLibrary(n_features=2, feature_names=["a", "b"])
.add_constant()
.add_linear()
.add_polynomials(max_degree=2)
)
model = SymbolicRegressor(basis_library=library, max_terms=3)
model.fit(X, y, sample_weight=1.0 / variances)
print(model.expression_)
What a weight means#
Observation i is modelled as having variance sigma² / wᵢ, so the fit minimises
A point with twice the weight is treated as twice as precise, i.e. as carrying twice the information.
Only ratios matter. Weights are normalised to average 1 internally, so w, 2*w and
w/1000 give an identical model, MSE and information criteria. The units of your weights
are irrelevant — pass raw inverse variances without rescaling.
Weights apply everywhere, not just to the coefficients#
A weighted fit whose model selection is unweighted, or whose reported R² is unweighted, is worse than no weighting at all — it looks right and isn’t. In JAXSR the weights run through the whole pipeline:
Stage |
Effect of weights |
|---|---|
Term selection ( |
Every candidate subset is fitted and scored on the weighted objective |
|
Weighted MSE, \(\sum_i w_i r_i^2 / n\) |
AIC / BIC / AICc |
Computed from the weighted MSE |
|
Weighted R², about the weighted mean of |
Negligible-term pruning ( |
Contributions measured on the weighted design matrix |
Constraint refitting |
The least-squares term is weighted; the constraints themselves are not |
|
Weighted \(s^2 (\Phi^\top W \Phi)^{-1}\) |
|
Weighted leverages |
|
Residuals resampled in whitened space, each replicate refit by weighted least squares |
|
Each weight follows its row into the resample, groups included |
|
Weighted sums of squares, about the weighted mean |
|
Each weight follows its row into the folds, under every splitting strategy |
|
Weighted leverages, whitened LOO residuals |
Parametric bases ( |
Nonlinear parameters are optimised against the weighted objective |
Effective sample size#
The n used by AIC, BIC and AICc stays the nominal number of observations. Weighting
says how much you trust each measurement; it does not create or destroy measurements.
This is why duplicating rows is not a valid way to emulate weights. Duplication
inflates n, and the k·log(n) term in BIC moves with it, so the comparison between
models shifts for a reason unrelated to the data.
When most of the weight sits on a handful of rows, the nominal n overstates how much
information a model comparison actually rests on. Check the Kish effective sample size:
model.effective_sample_size_ # (Σw)² / Σw²
For 200 points where half carry weight ≈ 0, this returns ≈ 100. Use that number, not n,
as your intuition for how strong the evidence is.
Recipes#
Known measurement variances#
model.fit(X, y, sample_weight=1.0 / variances)
Replicate means#
If yᵢ is the mean of nᵢ replicates sharing a noise variance, its variance is
sigma²/nᵢ, so the weight is the replicate count:
model.fit(X_means, y_means, sample_weight=replicate_counts)
A smooth taper instead of a hard threshold#
Some rows carry little information about the quantity being fitted — for example when a derived response is a ratio whose denominator passes through zero. Dropping those rows below a cutoff discards partially-informative data and puts a step where a taper belongs. Down-weight them continuously instead:
# information about the slope scales with |y_x|; taper smoothly near zero
w = y_x**2 / (y_x**2 + eps**2)
model.fit(X, y_ratio, sample_weight=w)
Choose eps on the scale of the noise in y_x, not as a keep/drop boundary.
Excluding a row#
A weight of exactly 0 removes a row from the fit but keeps it in n. If you mean to
drop the observation, drop it from X and y instead — that also corrects n and every
criterion computed from it.
Interpreting weighted intervals#
For a weighted fit, sigma_ is the noise level of a unit-weight observation, and
predict_interval() gives the interval for a new observation of unit weight — one as
precise as an average training point. If the new observation has a known precision
w_new, scale the half-width by 1/sqrt(w_new).
model.fit(X, y, sample_weight=1.0 / variances)
y_pred, lo, hi = model.predict_interval(X_test) # for a unit-weight observation
Validation#
Bad weights are rejected up front rather than silently absorbed. Each of these raises
ValueError:
an array whose length differs from
len(y)negative entries
NaNorinfall zeros
Weights are orthogonal to the resampling level#
cross_validate and bootstrap_model_selection both let you choose what gets
resampled — rows, or whole groups. That choice and the weights are independent: a
weight describes how precise a row is, a group describes which rows are not independent
of each other. Pass both when both are true.
cross_validate(model, X, y, groups=run_id, sample_weight=1 / variances)
bootstrap_model_selection(model, X, y, groups=run_id, sample_weight=1 / variances)
A group whose rows all carry zero weight is scored NaN rather than 0.0, so it cannot
be mistaken for a group the model predicted perfectly. A fold with no weight on either
side raises instead — there is nothing there to fit or score.
bootstrap_model_selection(..., resample_fn=...) rejects sample_weight: each replicate
regenerates its own rows, so a stored weight has no row to belong to. Apply the weighting
inside your resample_fn.
What is not weighted#
conformal_predict_split()— its coverage guarantee comes from exchangeability of the user-supplied calibration residuals, not from the fit. With calibration points of unequal precision the interval remains valid but is uninformatively wide for the precise ones. Usemethod="jackknife+"for a weight-aware interval.SymbolicClassifier— weights are not implemented for classification.max_errorincompute_all_metrics()— a maximum has no weighted analogue; it is reported over the rows with non-zero weight.