Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | 1x 1x 1x 1x 1x 1x 1x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 4231x 4231x 1311x 4231x 39x 4x 4x 4x 4x 4x 4x 39x 6x 6x 6x 6x 6x 6x 6x 6x 6x 39x 4x 4x 4x 4x 4x 4x 4x 39x 2917x 2917x 2917x 2917x 641x 641x 641x 641x 641x 641x 641x 2904x 77863x 77863x 77863x 77863x 77863x 463x 463x 463x 77863x 835x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 34x 607x 2917x 39x 1311x 1311x 460x 460x 460x 1311x 467x 467x 467x 467x 467x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 461x 461x 461x 845x 1311x 39x 467x 467x 467x 467x 467x 118027x 118027x 118027x 118027x 118027x 118027x 467x 467x 39x 40x 40x 30x 30x 30x | /**
* DriftDetector — concept drift detection for streaming data.
*
* Two algorithms:
* - ADWIN (Adaptive Windowing): detects mean shifts by comparing
* sub-windows within an adaptive window.
* - KSWIN (Kolmogorov-Smirnov Windowing): detects distribution
* changes using KS test on sliding windows.
*
* When drift is detected, the anomaly detection threshold can be
* adjusted to reduce false positives during distribution transitions.
*
* @module detect/drift
*/
import type { DriftInfo } from '../types.js'
/** Result from drift detection update. */
export interface DriftUpdateResult {
/** Whether drift was detected on this update. */
detected: boolean
/** Drift information if detected. */
info?: DriftInfo
}
/** Configuration for DriftDetector. */
interface DriftConfig {
/** Minimum window size before drift detection is active. */
minWindowSize: number
/** ADWIN delta (confidence bound). */
delta: number
/** KSWIN alpha (significance level). */
alpha: number
/** KSWIN window size for reference distribution. */
windowSize: number
}
const DEFAULT_DRIFT_CONFIG: DriftConfig = {
minWindowSize: 100,
delta: 0.002,
alpha: 0.005,
windowSize: 100,
}
/**
* DriftDetector — detects concept drift in streaming numerical data.
*
* @example
* ```ts
* const drift = new DriftDetector('adwin')
* for (const value of stream) {
* const result = drift.update(value)
* if (result.detected) console.log('Drift at', result.info)
* }
* ```
*/
export class DriftDetector {
private type: 'adwin' | 'kswin'
private config: DriftConfig
// ADWIN state
private window: number[] = []
private total = 0
// KSWIN state
private refWindow: number[] = []
private testWindow: number[] = []
private driftCount = 0
// Shared state
private preDriftMean = 0
private postDriftMean = 0
private lastDetectionAt = 0
constructor(type: 'adwin' | 'kswin' = 'adwin', config?: Partial<DriftConfig>) {
this.type = type
this.config = { ...DEFAULT_DRIFT_CONFIG, ...config }
}
/**
* Update the drift detector with a new value.
* Returns whether drift was detected.
*/
update(value: number): DriftUpdateResult {
if (!Number.isFinite(value)) return { detected: false }
if (this.type === 'adwin') return this.updateAdwin(value)
return this.updateKswin(value)
}
/** Reset detector state. */
reset(): void {
this.window = []
this.total = 0
this.refWindow = []
this.testWindow = []
this.driftCount = 0
}
/** Export state for serialization. */
getState(): Record<string, unknown> {
return {
type: this.type,
config: this.config,
preDriftMean: this.preDriftMean,
postDriftMean: this.postDriftMean,
lastDetectionAt: this.lastDetectionAt,
driftCount: this.driftCount,
}
}
/** Restore from serialized state. */
static fromState(state: Record<string, unknown>): DriftDetector {
const d = new DriftDetector(state.type as 'adwin' | 'kswin', state.config as Partial<DriftConfig>)
d.preDriftMean = state.preDriftMean as number
d.postDriftMean = state.postDriftMean as number
d.lastDetectionAt = state.lastDetectionAt as number
d.driftCount = state.driftCount as number
return d
}
// ── ADWIN Implementation ──
private updateAdwin(value: number): DriftUpdateResult {
this.window.push(value)
this.total += value
const n = this.window.length
if (n < this.config.minWindowSize) return { detected: false }
// Check for optimal cut point
const epsilon = Math.sqrt(
(1 / (2 * Math.min(n, this.window.length))) *
Math.log(2 / this.config.delta)
)
let bestCut = -1
let bestDiff = 0
let leftTotal = 0
for (let i = 0; i < n - 1; i++) {
leftTotal += this.window[i]!
const leftMean = leftTotal / (i + 1)
const rightMean = (this.total - leftTotal) / (n - i - 1)
const diff = Math.abs(leftMean - rightMean)
if (diff > epsilon && diff > bestDiff) {
bestDiff = diff
bestCut = i
}
}
if (bestCut >= 0) {
const oldMean = this.total / n
const newTotal = this.total - this.window.slice(0, bestCut + 1).reduce((a, b) => a + b, 0)
const newMean = newTotal / (n - bestCut - 1)
this.preDriftMean = oldMean
this.postDriftMean = newMean
this.lastDetectionAt = Date.now()
this.driftCount++
// Drop old window portion
this.window = this.window.slice(bestCut + 1)
this.total = newTotal
return {
detected: true,
info: {
detector: 'adwin',
detectedAt: this.lastDetectionAt,
preDriftMean: oldMean,
postDriftMean: newMean,
magnitude: Math.abs(newMean - oldMean) / (Math.sqrt(variance(this.window)) || 1),
},
}
}
return { detected: false }
}
// ── KSWIN Implementation ──
private updateKswin(value: number): DriftUpdateResult {
this.testWindow.push(value)
if (this.refWindow.length < this.config.windowSize) {
this.refWindow.push(value)
return { detected: false }
}
if (this.testWindow.length >= this.config.windowSize) {
const ksStat = this.computeKS(this.refWindow, this.testWindow)
const threshold = Math.sqrt(
-0.5 * Math.log(this.config.alpha / 2)
) / Math.sqrt(this.config.windowSize)
if (ksStat > threshold) {
const oldMean = this.refWindow.reduce((a, b) => a + b, 0) / this.refWindow.length
const newMean = this.testWindow.reduce((a, b) => a + b, 0) / this.testWindow.length
this.preDriftMean = oldMean
this.postDriftMean = newMean
this.lastDetectionAt = Date.now()
this.driftCount++
// Shift: test window becomes new reference
this.refWindow = [...this.testWindow]
this.testWindow = []
return {
detected: true,
info: {
detector: 'kswin',
detectedAt: this.lastDetectionAt,
preDriftMean: oldMean,
postDriftMean: newMean,
magnitude: Math.abs(newMean - oldMean) / (Math.sqrt(variance(this.refWindow)) || 1),
},
}
}
// Slide windows
this.refWindow.shift()
this.refWindow.push(this.testWindow.shift()!)
}
return { detected: false }
}
private computeKS(a: number[], b: number[]): number {
const sorted = [...a, ...b].sort((x, y) => x - y)
const n = a.length
const m = b.length
let maxDiff = 0
for (let i = 0; i < sorted.length; i++) {
// Simplified: count how many from each set are ≤ current value
let countA = 0, countB = 0
for (const v of a) if (v <= sorted[i]!) countA++
for (const v of b) if (v <= sorted[i]!) countB++
const diff = Math.abs(countA / n - countB / m)
if (diff > maxDiff) maxDiff = diff
}
return maxDiff
}
}
/** Compute sample variance. */
function variance(values: number[]): number {
if (values.length < 2) return 0
const mean = values.reduce((a, b) => a + b, 0) / values.length
return values.reduce((s, v) => s + (v - mean) ** 2, 0) / (values.length - 1)
}
|