Module 5 · Dispersion, Shape, and Robust Statistics Module demo
Anomaly Alert Engine
An engine that scores every settlement.
Transcript
47 sentences · select one to jump thereCode lab
Run it yourself
The demo source in one language. Edit it, run TypeScript and Python right here, and compare with the expected output.
/**
* Fintech Math Bootcamp · Module 05 demo · Anomaly Alert Engine
* A payments team watches how long each merchant's card payments take to settle. When a new settlement
* arrives, the engine asks: how unusual is this, compared with the merchant's own history?
* Lessons 041–050: deviations, range and IQR, the two MADs, variance and standard deviation,
* coefficient of variation, z-score versus robust z-score, skewness and kurtosis.
* Synthetic data; an alert is a request for review, never proof of fraud.
*/
export type Summary = {n: number; mean: number; median: number; min: number; max: number};
const sum = (x: number[]) => x.reduce((s, v) => s + v, 0);
export const mean = (x: number[]) => sum(x) / x.length;
// 034 · median, reused as the robust centre
export function median(x: number[]): number {
const s = [...x].sort((a, b) => a - b), m = Math.floor(s.length / 2);
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
}
// 041 · deviation, absolute deviation and squared deviation from a centre
export const deviations = (x: number[], center: number) => x.map(v => v - center);
export const absolute = (d: number[]) => d.map(Math.abs);
export const squared = (d: number[]) => d.map(v => v * v);
// 042 · range and interquartile range (linear interpolation between order statistics)
export function quantile(x: number[], p: number): number {
const s = [...x].sort((a, b) => a - b), h = (s.length - 1) * p, i = Math.floor(h);
return s[i] + (h - i) * (s[Math.min(i + 1, s.length - 1)] - s[i]);
}
export const range = (x: number[]) => Math.max(...x) - Math.min(...x);
export const iqr = (x: number[]) => quantile(x, 0.75) - quantile(x, 0.25);
// 043 · mean absolute deviation: average distance from the mean
export const meanAbsDev = (x: number[]) => mean(absolute(deviations(x, mean(x))));
// 044 · median absolute deviation: median distance from the median (raw), and its normal-scaled version
export const rawMAD = (x: number[]) => median(absolute(deviations(x, median(x))));
export const scaledMAD = (x: number[]) => 1.4826 * rawMAD(x);
// 045 · population and sample variance from the same sum of squared deviations
export function variances(x: number[]) {
if (x.length < 2) throw new Error("Sample variance needs n >= 2");
const ss = sum(squared(deviations(x, mean(x))));
return {ss, population: ss / x.length, sample: ss / (x.length - 1)};
}
// 046 · standard deviation returns the spread to the original unit (hours, dollars)
export const sampleSD = (x: number[]) => Math.sqrt(variances(x).sample);
// 047 · coefficient of variation: spread relative to scale, for positive quantities
export function cv(x: number[]): number {
const m = mean(x);
if (m === 0) throw new Error("CV undefined at zero mean");
return sampleSD(x) / Math.abs(m);
}
// 048 · classic z-score (mean, sample SD) and robust z-score (median, raw MAD, 0.6745 scaling)
export const zScore = (value: number, history: number[]) => (value - mean(history)) / sampleSD(history);
export function robustZ(value: number, history: number[]): number | null {
const mad = rawMAD(history);
return mad === 0 ? null : 0.67448975 * (value - median(history)) / mad;
}
// 049 · moment skewness; 050 · kurtosis and excess kurtosis (population moments)
export function shape(x: number[]) {
const m = mean(x), mk = (k: number) => mean(x.map(v => (v - m) ** k));
const kurtosis = mk(4) / mk(2) ** 2;
return {skewness: mk(3) / mk(2) ** 1.5, kurtosis, excess: kurtosis - 3};
}
export const Z_LIMIT = 3, ROBUST_LIMIT = 3.5;
// The engine: score one new settlement against the merchant's own history.
export function score(value: number, history: number[]) {
const z = zScore(value, history), rz = robustZ(value, history);
return {value, z, robustZ: rz, classicAlert: Math.abs(z) > Z_LIMIT, robustAlert: rz !== null && Math.abs(rz) > ROBUST_LIMIT};
}
const r = (v: number, d = 4) => Math.round(v * 10 ** d) / 10 ** d;
export function runDemo() {
// Settlement delay in hours for Maple Street Bakery's last twelve card batches.
// Batch 12 got stuck over a long weekend: 30 hours.
const delays = [2, 3, 2, 4, 3, 2, 3, 5, 2, 3, 4, 30];
const m = mean(delays), med = median(delays);
const dev = deviations(delays, m);
const v = variances(delays);
const withoutStuck = delays.slice(0, -1);
// three new batches arrive and are scored against the history
const incoming = [4, 12, 1].map(x => score(x, delays));
const target = incoming[1];
// two merchants of very different scale: average ticket in dollars
const bakeryTickets = [6.5, 9, 12, 7.5, 14, 8, 5.5, 10.5];
const furnitureTickets = [980, 1450, 1210, 890, 1320, 1150, 1580, 1020];
const merchant = (name: string, x: number[]) => ({name, tickets: x, mean: mean(x), sd: sampleSD(x), cv: cv(x)});
const sh = shape(delays), shClean = shape(withoutStuck);
return {
delays, n: delays.length, mean: m, median: med,
deviations: dev.map(d => r(d)), absolute: absolute(dev).map(d => r(d)), squared: squared(dev).map(d => r(d)),
range: range(delays), q25: quantile(delays, 0.25), q75: quantile(delays, 0.75), iqr: iqr(delays),
meanAbsDev: meanAbsDev(delays), rawMAD: rawMAD(delays), scaledMAD: scaledMAD(delays),
ss: v.ss, populationVariance: v.population, sampleVariance: v.sample,
populationSD: Math.sqrt(v.population), sampleSD: Math.sqrt(v.sample),
limits: {z: Z_LIMIT, robust: ROBUST_LIMIT},
incoming, target,
// what the classic score would say if the stuck batch had never inflated the spread
clean: {mean: mean(withoutStuck), sd: sampleSD(withoutStuck), z: zScore(target.value, withoutStuck)},
merchants: [merchant('Maple Street Bakery', bakeryTickets), merchant('Oakline Furniture', furnitureTickets)],
shape: sh, shapeWithoutStuck: shClean,
};
}
export const checkedResult = {"delays":[2,3,2,4,3,2,3,5,2,3,4,30],"n":12,"mean":5.25,"median":3,"deviations":[-3.25,-2.25,-3.25,-1.25,-2.25,-3.25,-2.25,-0.25,-3.25,-2.25,-1.25,24.75],"absolute":[3.25,2.25,3.25,1.25,2.25,3.25,2.25,0.25,3.25,2.25,1.25,24.75],"squared":[10.5625,5.0625,10.5625,1.5625,5.0625,10.5625,5.0625,0.0625,10.5625,5.0625,1.5625,612.5625],"range":28,"q25":2,"q75":4,"iqr":2,"meanAbsDev":4.125,"rawMAD":1,"scaledMAD":1.4826,"ss":678.25,"populationVariance":56.520833333333336,"sampleVariance":61.65909090909091,"populationSD":7.5180338741810235,"sampleSD":7.852330285277798,"limits":{"z":3,"robust":3.5},"incoming":[{"value":4,"z":-0.1591884134501581,"robustZ":0.67448975,"classicAlert":false,"robustAlert":false},{"value":12,"z":0.8596174326308538,"robustZ":6.07040775,"classicAlert":false,"robustAlert":true},{"value":1,"z":-0.5412406057305376,"robustZ":-1.3489795,"classicAlert":false,"robustAlert":false}],"target":{"value":12,"z":0.8596174326308538,"robustZ":6.07040775,"classicAlert":false,"robustAlert":true},"clean":{"mean":3,"sd":1,"z":9},"merchants":[{"name":"Maple Street Bakery","tickets":[6.5,9,12,7.5,14,8,5.5,10.5],"mean":9.125,"sd":2.8753881725529062,"cv":0.3151110326085377},{"name":"Oakline Furniture","tickets":[980,1450,1210,890,1320,1150,1580,1020],"mean":1200,"sd":239.28464818525833,"cv":0.19940387348771527}],"shape":{"skewness":2.9366174393368616,"kurtosis":9.802634215757388,"excess":6.802634215757388},"shapeWithoutStuck":{"skewness":0.6292853089020909,"kurtosis":2.4200000000000004,"excess":-0.5799999999999996}};
// Run this file directly: npx tsx lessons/05-dispersion-shape-and-robust-statistics/demo-anomaly-alert-engine.ts
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop()!)) {
console.log(JSON.stringify(runDemo(), null, 2));
}
Your output
Press Run to execute the code in your browser.
Expected output
{
"delays": [
2,
3,
2,
4,
3,
2,
3,
5,
2,
3,
4,
30
],
"n": 12,
"mean": 5.25,
"median": 3,
"deviations": [
-3.25,
-2.25,
-3.25,
-1.25,
-2.25,
-3.25,
-2.25,
-0.25,
-3.25,
-2.25,
-1.25,
24.75
],
"absolute": [
3.25,
2.25,
3.25,
1.25,
2.25,
3.25,
2.25,
0.25,
3.25,
2.25,
1.25,
24.75
],
"squared": [
10.5625,
5.0625,
10.5625,
1.5625,
5.0625,
10.5625,
5.0625,
0.0625,
10.5625,
5.0625,
1.5625,
612.5625
],
"range": 28,
"q25": 2,
"q75": 4,
"iqr": 2,
"meanAbsDev": 4.125,
"rawMAD": 1,
"scaledMAD": 1.4826,
"ss": 678.25,
"populationVariance": 56.520833333333336,
"sampleVariance": 61.65909090909091,
"populationSD": 7.5180338741810235,
"sampleSD": 7.852330285277798,
"limits": {
"z": 3,
"robust": 3.5
},
"incoming": [
{
"value": 4,
"z": -0.1591884134501581,
"robustZ": 0.67448975,
"classicAlert": false,
"robustAlert": false
},
{
"value": 12,
"z": 0.8596174326308538,
"robustZ": 6.07040775,
"classicAlert": false,
"robustAlert": true
},
{
"value": 1,
"z": -0.5412406057305376,
"robustZ": -1.3489795,
"classicAlert": false,
"robustAlert": false
}
],
"target": {
"value": 12,
"z": 0.8596174326308538,
"robustZ": 6.07040775,
"classicAlert": false,
"robustAlert": true
},
"clean": {
"mean": 3,
"sd": 1,
"z": 9
},
"merchants": [
{
"name": "Maple Street Bakery",
"tickets": [
6.5,
9,
12,
7.5,
14,
8,
5.5,
10.5
],
"mean": 9.125,
"sd": 2.8753881725529062,
"cv": 0.3151110326085377
},
{
"name": "Oakline Furniture",
"tickets": [
980,
1450,
1210,
890,
1320,
1150,
1580,
1020
],
"mean": 1200,
"sd": 239.28464818525833,
"cv": 0.19940387348771527
}
],
"shape": {
"skewness": 2.9366174393368616,
"kurtosis": 9.802634215757388,
"excess": 6.802634215757388
},
"shapeWithoutStuck": {
"skewness": 0.6292853089020909,
"kurtosis": 2.4200000000000004,
"excess": -0.5799999999999996
}
}Prefer your own machine? Every file is in the course repository · open it in Codespaces.
What the demo does
An engine scores new card settlements against each merchant's own history: deviations, range and IQR, the two MADs, variance and standard deviation, the coefficient of variation, classic versus robust z-scores, and skewness and kurtosis as tail warnings. Every module 05 lesson becomes one engine feature.
Lessons it combines
- Deviation, Absolute Deviation, and Squared Deviation
- Range and Interquartile Range
- Mean Absolute Deviation
- Median Absolute Deviation
- Population and Sample Variance
- Population and Sample Standard Deviation
- Coefficient of Variation and Scale Comparability
- Z-Score, Robust Z-Score, and Standardization
- Skewness and Tail Asymmetry
- Kurtosis, Excess Kurtosis, and Tail Weight