core/converters/finding.vector.converter.ts

core/converters/finding.vector.converter.ts is a file in GovLab Patterns. 95 lines of code and 10 definitions.

import { NULL_MODEL_LABELS, VECTOR_STRINGS } from "#configuration/strings/representation.strings";
import { autocorrelationSignificance, normalSignificance } from "#core/analyzers/baseline.analyzer";
import { explanationFinding, makeFinding, reasonOf } from "#core/factories/finding.factory";
import type { Finding } from "#types/finding.types";
import { ROUNDING } from "#configuration/constants/math.constants";
import type { VectorSummary } from "#types/representation.types";
import { coordinate } from "#core/factories/axis.factory";
import { fixedTo } from "#core/normalizers/math.normalizer";
import { withSupport } from "#core/converters/finding.converter";

const distributionFinding = function distributionFinding(field: string, summary: VectorSummary): Finding {
    return explanationFinding({
        analysis: "statistical",
        explained: VECTOR_STRINGS.distributionExplained(
            summary.minimum.toFixed(ROUNDING.standard),
            summary.maximum.toFixed(ROUNDING.standard),
        ),
        field,
        name: "distribution",
        observation: {
            count: summary.count,
            max: fixedTo(summary.maximum, ROUNDING.fine),
            mean: fixedTo(summary.mean, ROUNDING.fine),
            min: fixedTo(summary.minimum, ROUNDING.fine),
            stddev: fixedTo(summary.stddev, ROUNDING.fine),
        },
        observed: VECTOR_STRINGS.distributionObserved(
            summary.mean.toFixed(ROUNDING.standard),
            summary.stddev.toFixed(ROUNDING.standard),
        ),
        ontology: "scale",
        representation: "number",
    });
};

const autocorrelationFinding = function autocorrelationFinding(field: string, summary: VectorSummary): Finding {
    const significance = autocorrelationSignificance(summary.autocorrelation, summary.count);
    const verdict = significance.significant ? VECTOR_STRINGS.autocorBeyond : VECTOR_STRINGS.autocorWithin;
    const explained = VECTOR_STRINGS.autocorExplained(verdict, significance.pValue.toFixed(ROUNDING.fine));
    return makeFinding({
        coordinate: coordinate({
            analysis: "sequential",
            ontology: "relation",
            reasoning: "explanation",
            representation: "dynamical-systems",
        }),
        field,
        name: "autocorrelation",
        narrative: reasonOf(VECTOR_STRINGS.autocorObserved, explained),
        observation: {
            autocorrelation: fixedTo(summary.autocorrelation, ROUNDING.fine),
            pValue: fixedTo(significance.pValue, ROUNDING.fine),
            significant: significance.significant,
        },
        significance: {
            nullModel: NULL_MODEL_LABELS.autocorrelation,
            pValue: significance.pValue,
            significant: significance.significant,
            statistic: significance.statistic,
        },
    });
};

const anomalyRows = function anomalyRows(summary: VectorSummary): Readonly<Record<string, unknown>>[] {
    return summary.outliers.map(([value, z, index]) => {
        const significance = normalSignificance(z);
        return {
            pValue: fixedTo(significance.pValue, ROUNDING.fine),
            record: index,
            significant: significance.significant,
            value: fixedTo(value, ROUNDING.fine),
            z: fixedTo(z, ROUNDING.fine),
        };
    });
};

const anomalyFinding = function anomalyFinding(field: string, summary: VectorSummary): Finding | null {
    if (summary.outliers.length === 0) {
        return null;
    }
    const rows = anomalyRows(summary);
    const extreme = rows.filter((row) => row["significant"] === true).length;
    return explanationFinding({
        analysis: "anomaly",
        explained: VECTOR_STRINGS.anomalyExplained(String(extreme)),
        field,
        name: "anomaly",
        observation: { outliers: rows, significant: extreme },
        observed: VECTOR_STRINGS.anomalyObserved,
        ontology: "probability",
        representation: "number",
    });
};

export const vectorFindings = function vectorFindings(field: string, summary: VectorSummary): Finding[] {
    return withSupport(
        [distributionFinding(field, summary), autocorrelationFinding(field, summary), anomalyFinding(field, summary)],
        summary.count,
    );
};