kumulant

stat.change

Drift detectors. Each member tracks a running statistic, applies a configurable threshold, and exposes an alarm flag on its result. The three differ in what they assume about the in-control state and how they decide a shift has occurred.

Picking a detector

StatResultReach for it when
CusumStatCusumResultThe in-control mean is known up front. Configure target, allowance, threshold; the two-sided cumulative sum alarm fires once it crosses threshold.
PageHinkleyStatPageHinkleyResultThe in-control mean must be learned online. Tracks cumulative deviation from the running mean against a tolerance and a threshold.
AdwinStatAdwinResultNeither a target nor a fixed window is known. Maintains an exponential-histogram window of recent observations and drops the older portion whenever a statistically-significant mean shift is detected against the Hoeffding bound.

How they compare

CusumStat is the strictest and the cheapest if you already know the in-control target. It tracks two cumulative deviations from the target and alarms when either crosses a configured threshold; the allowance absorbs in-control noise. Suitable for monitoring against a known SLO or a calibrated baseline.

PageHinkleyStat is the next step up: same one-sided cumulative-deviation idea but the reference is the running mean rather than a fixed target. Add a tolerance to absorb in-control fluctuation. Suitable for streams whose baseline drifts over time but where you still want to alarm on sudden shifts.

AdwinStat is the heaviest and the most flexible. Maintains a window of recent observations as an exponential histogram of buckets; on every update it checks whether the window can be split into a recent portion and an older portion that differ by more than the Hoeffding bound at confidence delta. When such a split exists, the older portion is dropped and the running statistic reflects only the post-shift regime. Suitable when the stream may have multiple regimes and you want the detector to track the current one without explicit reset.

Output

All three produce a result with the running running statistic plus an alarm: Boolean field. The alarm doesn't auto-reset; caller decides what to do (page, reset the stat, write to an event store). Repeated alarm reads return the same value until the underlying statistic falls back below threshold.

For drift detectors on classification accuracy specifically, feed the binary correctness signal 1[predicted == truth] through one of these stats. The same pattern works for any binary signal.

Merge

All three drift detectors merge only approximately; the state is an order-dependent recurrence with no exact parallel combine. CusumStat and PageHinkleyStat average their cumulative-deviation cells (and PageHinkleyStat weight-averages the running mean); AdwinStat carries over the change counter without reconstructing the windowed histogram. Treat merge as a roll-up convenience; for distributed drift detection, run a detector per stream rather than merging partials.

Concurrency

All three keep coupled state (running statistic + previous reference) that has to stay consistent across reads. Locked under com.eignex.kumulant.core.Concurrency.Strict / com.eignex.kumulant.core.Concurrency.HighWrite. Under com.eignex.kumulant.core.Concurrency.Relaxed the cells race and the alarm signal may flicker briefly under contention.

Types

Link copied to clipboard
@Serializable
@SerialName(value = "AdwinResult")
data class AdwinResult(val delta: Double, val windowLength: Long, val mean: Double, val variance: Double, val changesDetected: Long, val alarm: Boolean) : Result

Snapshot from an AdwinStat adaptive-windowing change detector.

Link copied to clipboard
class AdwinStat(val delta: Double = 0.002, val maxBucketsPerSize: Int = 5, val concurrency: Concurrency = Concurrency.None) : SeriesStat<AdwinResult>

ADWIN2 (Bifet & Gavaldà, 2007) adaptive-windowing change detector. Maintains an exponential-histogram window of recent observations whose buckets grow as 2^0, 2^1, 2^2, ...; on every update the detector enumerates all bucket boundaries and drops the older half whenever the mean difference exceeds the Bernstein-flavoured Hoeffding bound

Link copied to clipboard
@Serializable
@SerialName(value = "CusumResult")
data class CusumResult(val target: Double, val referenceValue: Double, val threshold: Double, val cusumPositive: Double, val cusumNegative: Double, val alarmUp: Boolean, val alarmDown: Boolean) : Result

Snapshot from a two-sided CusumStat change-point detector.

Link copied to clipboard
class CusumStat(val target: Double = 0.0, val referenceValue: Double = 0.5, val threshold: Double = 5.0, val concurrency: Concurrency = Concurrency.None) : SeriesStat<CusumResult>

Two-sided cumulative-sum (CUSUM) change-point detector. Tracks

Link copied to clipboard
@Serializable
@SerialName(value = "PageHinkleyResult")
data class PageHinkleyResult(val delta: Double, val threshold: Double, val count: Long, val mean: Double, val cumulativePositive: Double, val cumulativeNegative: Double, val minPositive: Double, val maxNegative: Double, val alarmUp: Boolean, val alarmDown: Boolean) : Result

Snapshot from a PageHinkleyStat change-point detector.

Link copied to clipboard
class PageHinkleyStat(val delta: Double = 0.005, val threshold: Double = 50.0, val concurrency: Concurrency = Concurrency.None) : SeriesStat<PageHinkleyResult>

Page-Hinkley change-point detector. Tracks the running mean alongside two one-sided cumulative-drift signals