core/analyzers/baseline.analyzer.ts

core/analyzers/baseline.analyzer.ts is a file in GovLab Patterns. 98 lines of code and 43 definitions.

import type { Marginals, Significance, Transition, Uniformity } from "#types/baseline.types";
import { increment, sumOf } from "#core/counters/base.counter";

const A1 = 0.254829592;
const A2 = -0.284496736;
const A3 = 1.421413741;
const A4 = -1.453152027;
const A5 = 1.061405429;
const P = 0.3275911;
const ERFC_MAX = 2;
const ALPHA = 0.05;
const NINE = 9;
const THREE = 3;
const THIRD = 1 / THREE;
const MIN_SERIES = 3;
const HALF = 0.5;
const TWO = 2;
const SQUARE = 2;

const INSIGNIFICANT: Significance = { pValue: 1, significant: false, statistic: 0 };

export const erf = function erf(x: number): number {
    const sign = x < 0 ? -1 : 1;
    const ax = Math.abs(x);
    const t = 1 / (1 + P * ax);
    const y = 1 - ((((A5 * t + A4) * t + A3) * t + A2) * t + A1) * t * Math.exp(-ax * ax);
    return sign * y;
};

export const erfc = function erfc(x: number): number {
    return Math.min(ERFC_MAX, Math.max(0, 1 - erf(x)));
};

export const normalSignificance = function normalSignificance(z: number): Significance {
    const pValue = erfc(Math.abs(z) / Math.SQRT2);
    return { pValue, significant: pValue < ALPHA, statistic: z };
};

export const chiSquareSf = function chiSquareSf(statistic: number, dof: number): number {
    if (dof <= 0 || statistic <= 0) {
        return 1;
    }
    const cubeRoot = (statistic / dof) ** THIRD;
    const mean = 1 - TWO / (NINE * dof);
    const stddev = Math.sqrt(TWO / (NINE * dof));
    return HALF * erfc((cubeRoot - mean) / stddev / Math.SQRT2);
};

export const autocorrelationSignificance = function autocorrelationSignificance(
    correlation: number,
    count: number,
): Significance {
    return count < MIN_SERIES ? INSIGNIFICANT : normalSignificance(correlation * Math.sqrt(count));
};

const marginalsOf = function marginalsOf(transitions: readonly Transition[]): Marginals {
    const row = new Map<string, number>();
    const col = new Map<string, number>();
    for (const { source, target, count } of transitions) {
        increment(row, source, count);
        increment(col, target, count);
    }
    return { col, row };
};

const chiFromTransitions = function chiFromTransitions(
    transitions: readonly Transition[],
    marginals: Marginals,
    total: number,
): number {
    let sum = 0;
    for (const { source, target, count } of transitions) {
        const expected = (marginals.row.get(source) ?? 0) * (marginals.col.get(target) ?? 0);
        if (expected > 0) {
            sum += (count * count * total) / expected;
        }
    }
    return sum - total;
};

export const transitionIndependence = function transitionIndependence(
    transitions: readonly Transition[],
): Significance {
    const total = sumOf(transitions.map((transition) => transition.count));
    if (total === 0) {
        return INSIGNIFICANT;
    }
    const marginals = marginalsOf(transitions);
    const dof = (marginals.row.size - 1) * (marginals.col.size - 1);
    if (dof <= 0) {
        return INSIGNIFICANT;
    }
    const chi = chiFromTransitions(transitions, marginals, total);
    const pValue = chiSquareSf(chi, dof);
    return { pValue, significant: pValue < ALPHA, statistic: chi };
};

export const uniformity = function uniformity(counts: ReadonlyMap<string, number>): Uniformity {
    const categories = counts.size;
    const total = sumOf(counts.values());
    if (categories <= 1 || total === 0) {
        return { chiSquare: 0, dof: 0, pValue: 1, uniform: true };
    }
    const expected = total / categories;
    const statistic = sumOf([...counts.values()].map((count) => (count - expected) ** SQUARE / expected));
    const dof = categories - 1;
    const pValue = chiSquareSf(statistic, dof);
    return { chiSquare: statistic, dof, pValue, uniform: pValue >= ALPHA };
};