Guide
The two estimators in detail.
LaplaceFeatures
A river Transformer. Each numeric feature key gets its own
Laplace forecaster. On every sample, the value v of key
k is replaced by two features: mu_k, the
forecaster's one-step predictive mean for this tick, and
z_k, the probability integral transform of v
under that predictive pushed through the standard normal quantile. When
the forecaster is calibrated, z is close to N(0,1) and reads directly as
how surprising the value was. The PIT is clamped so |z| never exceeds
about 7.03.
LaplaceFeatures(emit="both", prefix=("mu_", "z_"))
emit:"both"(default),"mu"or"z", choosing which scalars to emit per stream.prefix: the feature-name prefixes for the mean and the surprise.
Non-numeric values pass through untouched, so route them around any
downstream StandardScaler exactly as you would with raw
features. Booleans count as non-numeric. A non-finite numeric value is
imputed by the forecast itself: the model receives the predictive mean
and z = 0, meaning expected value, no news, and the forecaster state is
not advanced. Keys may appear and disappear mid-stream; each key's
forecaster starts on first sight.
Timing and alignment
Bodies advance exactly once per sample, inside learn_one.
transform_one on its own is pure, so calling
predict_one repeatedly does not change state. One river
subtlety is handled for you: river's Pipeline.learn_one
updates unsupervised transformers before transforming for the downstream
steps, which would hand the model fresher features at learn time than it
predicted with. LaplaceFeatures caches the pre-update
feature dict during learn_one and serves it to the
immediately following transform_one for the same sample, so
the predict path and the learn path see identical features.
LaplaceTarget
A regressor wrapper in the style of river's
TargetStandardScaler. Transformers never see y, so the
target's own history features need a wrapper. At prediction time the
inner regressor receives, alongside x, the forecaster's prediction of
the y it is about to predict as mu_y, and the surprise of
the previous y as zy. The same pre-update pair is used at
learn time, then the forecaster consumes the new y. The target itself
stays raw. This wrapper never transforms y, it only adds features.
LaplaceTarget(regressor, keys=("mu_y", "zy"))
The ablation made this quantitative: on river's own datasets,
LaplaceTarget alone, with raw features untouched, beat river's
recommended pipeline on three of four datasets untouched and 10/10
under feature contamination, and tied the fourth. If you adopt one
thing from this package, adopt this wrapper; add
LaplaceFeatures when you distrust the features
themselves.
The full recipe
from river import linear_model, preprocessing
from ice_skaters import LaplaceFeatures, LaplaceTarget
model = LaplaceTarget(
regressor=preprocessing.TargetStandardScaler(
regressor=LaplaceFeatures()
| preprocessing.StandardScaler()
| linear_model.LinearRegression()))
The front-end composes with river's scalers rather than replacing them:
the mu features live on the original scale, so the
StandardScaler still earns its keep, and
TargetStandardScaler handles the target's scale for the
inner learner as usual.
Robustness and serialization
Observations are winsorized before a forecaster consumes them, at 1e60 absolute plus a window twelve orders of magnitude above the current predictive level. The window is magnitude-relative rather than sigma-relative on purpose: after a stretch of missing-data zeros a legitimate value sits billions of sigmas out and must pass. The gate is exact identity on any stream doubles can represent comfortably, and z saturates near 7 long before it engages, so nothing observable changes except that nothing crashes. Both estimators hold only plain state, no closures, so they pickle and deep-copy like any river estimator.
Cost
About 390 microseconds per numeric stream per sample, single-threaded, roughly 900x river's StandardScaler, or 2,500 samples per second per stream. That is the price of a full distributional forecaster per stream, and it buys the calibration. Right for polls, sensors, market bars and anything at human timescales; wrong inside a hot path at hundreds of thousands of ticks per second.
When not to use it
Three boundaries, measured rather than guessed. Distance-based learners such as KNN do not benefit: neighbour averaging is already spike-robust and the extra dimensions degrade the metric. Streams that interleave many entities under one key handicap a single forecaster per key; give each entity its own key or wait for per-entity bodies. And if your heavy tails are signal rather than noise, taming them costs accuracy: with a correctly specified model and genuinely heavy-tailed inputs, the extreme points are the most informative ones. Numbers for all three are on the papers page.