/** * Centered rolling average over sorted array. * Returns array of { index, value, avg } where avg may be null at edges. */ export function rollingAverage(sorted, window = 7) { const half = Math.floor(window / 2) return sorted.map((item, i) => { const start = Math.max(0, i - half) const end = Math.min(sorted.length - 1, i + half) const slice = sorted.slice(start, end + 1) const avg = slice.reduce((s, x) => s + x.value, 0) / slice.length return { ...item, avg } }) } /** * Pearson correlation coefficient for two arrays of equal length. */ export function pearsonR(x, y) { const n = x.length if (n < 2) return 0 const mx = x.reduce((a, b) => a + b, 0) / n const my = y.reduce((a, b) => a + b, 0) / n const num = x.reduce((s, xi, i) => s + (xi - mx) * (y[i] - my), 0) const den = Math.sqrt( x.reduce((s, xi) => s + (xi - mx) ** 2, 0) * y.reduce((s, yi) => s + (yi - my) ** 2, 0) ) return den === 0 ? 0 : num / den } /** * Simple linear regression. Returns { slope, intercept }. */ export function linearRegression(x, y) { const n = x.length if (n < 2) return { slope: 0, intercept: 0 } const mx = x.reduce((a, b) => a + b, 0) / n const my = y.reduce((a, b) => a + b, 0) / n const slope = x.reduce((s, xi, i) => s + (xi - mx) * (y[i] - my), 0) / x.reduce((s, xi) => s + (xi - mx) ** 2, 0) const intercept = my - slope * mx return { slope, intercept } } /** * Human-readable correlation description. */ export function describeCorrelation(r) { const abs = Math.abs(r) if (abs >= 0.7) return 'strong' if (abs >= 0.4) return 'moderate' if (abs >= 0.2) return 'weak' return 'no meaningful' }