# core/aggregators/representation.vector.aggregator.ts

> 143 lines of code and 38 definitions.

Tree: GovLab Patterns
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/patterns#file-patterns-core-aggregators-representation-vector-aggregator-ts
Source text: https://banes-lab.com/source/patterns/core/aggregators/representation.vector.aggregator.ts.txt

Listed in [core/aggregators](https://banes-lab.com/api/source/patterns/core/aggregators.md), after [core/aggregators/representation.tree.aggregator.ts](https://banes-lab.com/source/patterns/core/aggregators/representation.tree.aggregator.ts.md).

## Definitions

- `keepTop` (lexical_declaration, line 19)
- `outliersOf` (lexical_declaration, line 49)
- `observe` (method_definition, line 127, exported)
- `update` (method_definition, line 84, exported)
- `sample` (method_definition, line 90, exported)
- `result` (method_definition, line 98, exported)
- `ANOMALY_K` (lexical_declaration, line 6)
- `DEFAULT_LIMIT` (lexical_declaration, line 7)
- `asNumber` (lexical_declaration, line 9)
- `next` (lexical_declaration, line 20)
- `drop` (lexical_declaration, line 24)
- `current` (lexical_declaration, line 25)
- `collectIndices` (lexical_declaration, line 31)
- `indexOf` (lexical_declaration, line 35)
- `{ mean, stddev, limit }` (lexical_declaration, line 54)
- `scored` (lexical_declaration, line 58)
- `VectorAccumulator` (class_declaration, line 66, exported)
- `field` (public_field_definition, line 67, exported)
- `low` (public_field_definition, line 68, exported)
- `high` (public_field_definition, line 69, exported)
- `count` (public_field_definition, line 70, exported)
- `seen` (public_field_definition, line 71, exported)
- `sum` (public_field_definition, line 72, exported)
- `sumsq` (public_field_definition, line 73, exported)
- `minimum` (public_field_definition, line 74, exported)
- `maximum` (public_field_definition, line 75, exported)
- `prev` (public_field_definition, line 76, exported)
- `cross` (public_field_definition, line 77, exported)
- `consecutive` (public_field_definition, line 78, exported)
- `constructor` (method_definition, line 80, exported)
- `[mean, variance]` (lexical_declaration, line 111, exported)
- `stddev` (lexical_declaration, line 112, exported)
- `autocorrelation` (lexical_declaration, line 113, exported)
- `index` (lexical_declaration, line 128, exported)
- `number` (lexical_declaration, line 130, exported)
- `accumulate` (method_definition, line 139, exported)
- `meanVariance` (method_definition, line 152, exported)
- `mean` (lexical_declaration, line 153, exported)

## Contained in

- [core/aggregators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-aggregators.md)

## Uses

- [core/converters/math.converter.ts](https://banes-lab.com/source/patterns/core/converters/math.converter.ts.md)
- [core/selectors/record.selector.ts](https://banes-lab.com/source/patterns/core/selectors/record.selector.ts.md)

## Linked from

- [core/aggregators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-aggregators.md)
- [core/converters](https://banes-lab.com/anatomy/patterns/folder-patterns-core-converters.md)
- [core/selectors](https://banes-lab.com/anatomy/patterns/folder-patterns-core-selectors.md)

## Source

```typescript
import type { OutlierContext, VectorSummary } from "#types/representation.types";
import type { Rng } from "#types/seed.types";
import { foldRecords } from "#core/selectors/record.selector";
import { gaussian } from "#core/converters/math.converter";

const ANOMALY_K = 16;
const DEFAULT_LIMIT = 10;

const asNumber = function asNumber(value: unknown): number | null {
    if (typeof value === "number") {
        return value;
    }
    if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "number")) {
        return value.reduce((sum: number, item: number) => sum + item, 0);
    }
    return null;
};

const keepTop = function keepTop(heap: readonly [number, number][], entry: [number, number]): [number, number][] {
    const next = [...heap, entry];
    if (next.length <= ANOMALY_K) {
        return next;
    }
    const drop = next.reduce((minIndex, candidate, index) => {
        const current = next[minIndex];
        return current !== undefined && candidate[0] < current[0] ? index : minIndex;
    }, 0);
    return next.filter((_, index) => index !== drop);
};

const collectIndices = function collectIndices(
    low: readonly [number, number][],
    high: readonly [number, number][],
): Map<number, number> {
    const indexOf = new Map<number, number>();
    for (const [value, index] of high) {
        if (!indexOf.has(value)) {
            indexOf.set(value, index);
        }
    }
    for (const [negated, index] of low) {
        if (!indexOf.has(-negated)) {
            indexOf.set(-negated, index);
        }
    }
    return indexOf;
};

const outliersOf = function outliersOf(
    low: readonly [number, number][],
    high: readonly [number, number][],
    context: OutlierContext,
): [number, number, number][] {
    const { mean, stddev, limit } = context;
    if (stddev <= 0) {
        return [];
    }
    const scored = [...collectIndices(low, high).entries()].map(([value, index]): [number, number, number] => [
        value,
        (value - mean) / stddev,
        index,
    ]);
    return scored.toSorted((a, b) => Math.abs(b[1]) - Math.abs(a[1]) || a[0] - b[0]).slice(0, limit);
};

export class VectorAccumulator {
    private readonly field: string;
    private low: [number, number][] = [];
    private high: [number, number][] = [];
    private count = 0;
    private seen = 0;
    private sum = 0;
    private sumsq = 0;
    private minimum = Infinity;
    private maximum = -Infinity;
    private prev: number | null = null;
    private cross = 0;
    private consecutive = 0;

    public constructor(field: string) {
        this.field = field;
    }

    public update(chunk: readonly unknown[]): void {
        foldRecords(chunk, this.field, (value) => {
            this.observe(value);
        });
    }

    public sample(rng: Rng): number | null {
        if (this.count === 0) {
            return null;
        }
        const [mean, variance] = this.meanVariance();
        return gaussian(rng, mean, Math.sqrt(variance));
    }

    public result(limit = DEFAULT_LIMIT): VectorSummary {
        if (this.count === 0) {
            return {
                autocorrelation: 0,
                count: 0,
                field: this.field,
                maximum: 0,
                mean: 0,
                minimum: 0,
                outliers: [],
                stddev: 0,
            };
        }
        const [mean, variance] = this.meanVariance();
        const stddev = Math.sqrt(variance);
        const autocorrelation =
            this.consecutive > 0 && variance > 0 ? (this.cross / this.consecutive - mean * mean) / variance : 0;
        return {
            autocorrelation,
            count: this.count,
            field: this.field,
            maximum: this.maximum,
            mean,
            minimum: this.minimum,
            outliers: outliersOf(this.low, this.high, { limit, mean, stddev }),
            stddev,
        };
    }

    private observe(raw: unknown): void {
        const index = this.seen;
        this.seen += 1;
        const number = asNumber(raw);
        if (number === null) {
            return;
        }
        this.accumulate(number);
        this.high = keepTop(this.high, [number, index]);
        this.low = keepTop(this.low, [-number, index]);
    }

    private accumulate(number: number): void {
        this.count += 1;
        this.sum += number;
        this.sumsq += number * number;
        this.minimum = Math.min(this.minimum, number);
        this.maximum = Math.max(this.maximum, number);
        if (this.prev !== null) {
            this.cross += number * this.prev;
            this.consecutive += 1;
        }
        this.prev = number;
    }

    private meanVariance(): [number, number] {
        const mean = this.sum / this.count;
        return [mean, Math.max(this.sumsq / this.count - mean * mean, 0)];
    }
}
```
