kumulant

stat.decay

Time-weighted moments. Each observation enters the accumulator with a weight that shrinks toward zero with age, so older observations contribute less. Two parallel sub-families: timestamp-based decay (the Decaying* stats) and step-based EWMA (the Ewma* stats).

The two flavours

Timestamp-based: Decaying*

DecayingSumStat, DecayingMeanStat, DecayingVarianceStat are the time-weighted counterparts of com.eignex.kumulant.stat.summary.SumStat / com.eignex.kumulant.stat.summary.MeanStat / com.eignex.kumulant.stat.summary.VarianceStat. The decay schedule is configured via DecayWeighting:

  • HalfLife(durationMillis): exponential decay specified by the half-life of an observation's weight.

  • TimeConstant(tau): exponential decay specified by the time constant (e-fold) of the weighting function.

  • Custom(weighting): caller-supplied decay function from (elapsedNanos) to a multiplicative weight.

Because weight is a function of the timestamp delta, these handle irregular update intervals correctly. A stream that fires once a second and then once an hour applies decay over the actual elapsed time, not over the update count.

Step-based: Ewma*

EwmaMeanStat and EwmaVarianceStat are the classical exponentially-weighted moving variants with a step-based decay: alpha * new + (1 - alpha) * old. Smoothing factor lives in DecayWeighting.Alpha.

These are cheaper and more familiar but assume roughly fixed intervals between observations. Mixed-interval streams should reach for the timestamp-based variants instead.

Picking by need

Result shapes

DecayingMeanStat / EwmaMeanStat expose WeightedMeanResult, the same shape as the non-decayed MeanStat. DecayingVarianceStat / EwmaVarianceStat expose WeightedVarianceResult. This is intentional: downstream traits like com.eignex.kumulant.core.HasCenterScale and com.eignex.kumulant.core.HasSampleVariance work identically on decayed and undecayed results, so a band / standardize / scoring step doesn't care which provenance the moment came from.

Merge

Decayed stats with the same schedule merge cleanly: the weight formulation makes the Chan-style parallel merge work the same way it does for the undecayed Welford family. Stats with different schedules (different half-lives, different time constants) cannot be merged meaningfully.

Nothing checks that, and nothing can: merge takes a Result, and none of the decay results carry their schedule on the wire, so a mismatch is undetectable at the merge site. Keeping the schedule out of the result is deliberate, since it is deployment configuration rather than accumulated state, so matching schedules is the caller's responsibility. Merging accumulators with different half-lives or alphas produces numbers, and they mean nothing. The hashed sketches took the other choice and do carry a HasherRef, because there a mismatch silently breaks a guarantee the type itself makes.

Concurrency

All variants are Welford-coupled (mean + variance + total weight cells that have to stay consistent). Locked under com.eignex.kumulant.core.Concurrency.Strict / com.eignex.kumulant.core.Concurrency.HighWrite. Under com.eignex.kumulant.core.Concurrency.Relaxed the cells race; the drift is the same small ULP-level magnitude as the undecayed Welford family.

Types

Link copied to clipboard
@Serializable
@SerialName(value = "DecayingMeanResult")
data class DecayingMeanResult(val mean: Double, val totalWeights: Double, val timestampNanos: Long) : HasObservationCount

Snapshot of an exponentially time-decayed weighted mean at timestampNanos.

Link copied to clipboard
class DecayingMeanStat(val weighting: DecayWeighting.HalfLife, val concurrency: Concurrency = Concurrency.None) : SeriesStat<DecayingMeanResult>

Exponentially decaying weighted mean: Sum(v_i*w_i*decay) / Sum(w_i*decay).

Link copied to clipboard
@Serializable
@SerialName(value = "DecayingSumResult")
data class DecayingSumResult(val sum: Double, val timestampNanos: Long) : Result

Snapshot of an exponentially time-decayed sum at timestampNanos.

Link copied to clipboard
class DecayingSumStat(val weighting: DecayWeighting.HalfLife, val concurrency: Concurrency = Concurrency.None) : SeriesStat<DecayingSumResult>

Exponentially decaying sum driven by wall-clock elapsed time.

Link copied to clipboard
@Serializable
@SerialName(value = "DecayingVarianceResult")
data class DecayingVarianceResult(val mean: Double, val variance: Double, val totalWeights: Double, val timestampNanos: Long) : HasObservationCount

Snapshot of an exponentially time-decayed weighted variance at timestampNanos.

Link copied to clipboard
class DecayingVarianceStat(val weighting: DecayWeighting.HalfLife, val concurrency: Concurrency = Concurrency.None) : SeriesStat<DecayingVarianceResult>

Exponentially decaying weighted variance over the recent time window.

Link copied to clipboard
sealed interface DecayWeighting

Shared decay strategy for exponentially weighted stats.

Link copied to clipboard
class EwmaMeanStat(val weighting: DecayWeighting.Alpha, val concurrency: Concurrency = Concurrency.None) : SeriesStat<WeightedMeanResult>

Exponentially weighted moving average driven by cumulative observation weight.

Link copied to clipboard
class EwmaVarianceStat(val weighting: DecayWeighting.Alpha, val concurrency: Concurrency = Concurrency.None) : SeriesStat<WeightedVarianceResult>

Exponentially weighted moving variance driven by cumulative observation weight.

Functions

Link copied to clipboard

Shorthand for DecayWeighting.Alpha - usable as a shared weighting across stats.

Link copied to clipboard

Shorthand for DecayWeighting.HalfLife - usable as a shared weighting across stats.