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_"))

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.