Files
peardata/server/services/zscore.js
T
Raven Scott 462cce4b5b
CI / test (push) Successful in 58s
Release rolling / release (push) Successful in 7m10s
Updates
2026-07-18 19:16:58 -04:00

83 lines
2.3 KiB
JavaScript

/**
* Rolling z-score helpers for anomaly retrain / hybrid detection.
*/
/**
* @param {number[]} values
* @returns {{ mean: number, stdev: number, n: number }}
*/
export function stats(values) {
const xs = values.filter((v) => v != null && Number.isFinite(v))
const n = xs.length
if (!n) return { mean: 0, stdev: 0, n: 0 }
const mean = xs.reduce((a, b) => a + b, 0) / n
if (n < 2) return { mean, stdev: 0, n }
let sumSq = 0
for (const v of xs) sumSq += (v - mean) ** 2
const stdev = Math.sqrt(sumSq / (n - 1))
return { mean, stdev, n }
}
/**
* @param {number} value
* @param {number} mean
* @param {number} stdev
*/
export function zScore(value, mean, stdev) {
if (!Number.isFinite(value) || !Number.isFinite(mean)) return 0
if (!stdev || stdev < 1e-9) return value === mean ? 0 : value > mean ? 3 : -3
return (value - mean) / stdev
}
/**
* Derive warn/crit absolute thresholds from baseline + z multipliers.
* For `>` comparator: warn = mean + warnZ*stdev, crit = mean + critZ*stdev
* For `<` comparator: warn = mean - warnZ*stdev, crit = mean - critZ*stdev
*
* @param {{ mean: number, stdev: number }} s
* @param {string} comparator
* @param {{ warnZ?: number, critZ?: number, minStdev?: number }} [opts]
*/
export function thresholdsFromStats(s, comparator, opts = {}) {
const warnZ = opts.warnZ ?? 2
const critZ = opts.critZ ?? 3
const minStdev = opts.minStdev ?? 0.01
const stdev = Math.max(s.stdev, minStdev)
const op = comparator || '>'
if (op === '<' || op === '<=') {
return {
warn: s.mean - warnZ * stdev,
crit: s.mean - critZ * stdev,
mean: s.mean,
stdev,
}
}
return {
warn: s.mean + warnZ * stdev,
crit: s.mean + critZ * stdev,
mean: s.mean,
stdev,
}
}
/**
* Extract numeric dimension series from MetricStore points.
* @param {import('./store.js').MetricStore} store
* @param {string} chart
* @param {string} dimension
* @param {number} [maxPoints]
* @returns {number[]}
*/
export function seriesFromStore(store, chart, dimension, maxPoints = 300) {
const entry = store.series?.get?.(chart)
const points = entry?.points || []
const slice = points.slice(-maxPoints)
/** @type {number[]} */
const out = []
for (const p of slice) {
const v = p.values?.[dimension]
if (v != null && Number.isFinite(v)) out.push(v)
}
return out
}