Module 6 · Probability and Random Variables Module demo
Fraud Alert Triage Simulator
One alert, one question.
Transcript
43 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 06 demo · Fraud Alert Triage Simulator
* A card issuer runs two fraud rules on 100,000 synthetic payments a month: rule A (new device) and
* rule B (unusual amount). When an alert fires, what is the chance the payment is really fraud, and
* what loss should the review team budget for?
* Lessons 051–060: outcome spaces, probability rules, conditional probability, independence, Bayes,
* discrete random variables, expected value, variance, joint and conditional distributions, covariance.
* Synthetic counts; not a description of any real fraud system.
*/
// One cell of the outcome space: is it fraud, does rule A fire, does rule B fire, and how many payments.
export type Cell = {fraud: boolean; a: boolean; b: boolean; count: number};
// 051 · the outcome space: eight disjoint cells that together cover every payment
export function outcomeSpace(): Cell[] {
const cells: Cell[] = [];
// fraud payments: A only, B only, both, neither
const fraud = [[true, false, 250], [false, true, 150], [true, true, 550], [false, false, 50]] as const;
// legitimate payments
const legit = [[true, false, 2900], [false, true, 1950], [true, true, 100], [false, false, 94050]] as const;
for (const [a, b, count] of fraud) cells.push({fraud: true, a, b, count});
for (const [a, b, count] of legit) cells.push({fraud: false, a, b, count});
return cells;
}
const total = (cells: Cell[]) => cells.reduce((s, c) => s + c.count, 0);
// probability of an event = its share of the outcome space
export const prob = (cells: Cell[], event: (c: Cell) => boolean) => total(cells.filter(event)) / total(cells);
// 052 · union without double counting: P(A ∪ B) = P(A) + P(B) − P(A ∩ B)
export function union(pa: number, pb: number, both: number) {
if (both < Math.max(0, pa + pb - 1) || both > Math.min(pa, pb)) throw new Error("Impossible intersection");
return {either: pa + pb - both, naive: pa + pb, neither: 1 - (pa + pb - both), onlyA: pa - both, onlyB: pb - both};
}
// 053 · conditional probability: shrink the denominator to the condition
export const conditional = (cells: Cell[], event: (c: Cell) => boolean, given: (c: Cell) => boolean) =>
total(cells.filter(c => given(c) && event(c))) / total(cells.filter(given));
// 054 · independence check: compare the measured joint with the product of the marginals
export const independenceGap = (pa: number, pb: number, both: number) => ({ifIndependent: pa * pb, measured: both, ratio: both / (pa * pb)});
// 055 · Bayes with the base rate
export const bayes = (prior: number, sensitivity: number, falsePositiveRate: number) =>
sensitivity * prior / (sensitivity * prior + falsePositiveRate * (1 - prior));
// 056 / 057 / 058 · a discrete random variable: its expected value, second moment and variance
export function moments(values: number[], p: number[]) {
const total = p.reduce((s, v) => s + v, 0);
if (Math.abs(total - 1) > 1e-9) throw new Error("Probabilities must sum to 1");
const mean = values.reduce((s, x, i) => s + p[i] * x, 0);
const second = values.reduce((s, x, i) => s + p[i] * x * x, 0);
return {contributions: values.map((x, i) => x * p[i]), mean, second, variance: second - mean ** 2, sd: Math.sqrt(second - mean ** 2)};
}
// 059 · joint, marginal and conditional from a joint table (rows: segment; columns: fraud, legitimate)
export function jointTable(counts: number[][]) {
const n = counts.flat().reduce((s, v) => s + v, 0);
const joint = counts.map(r => r.map(v => v / n));
const rowMarginal = joint.map(r => r[0] + r[1]);
const fraudMarginal = joint[0][0] + joint[1][0];
return {n, joint, rowMarginal, fraudMarginal, fraudGivenRow: joint.map((r, i) => r[0] / rowMarginal[i])};
}
// 060 · covariance and correlation of two loss types over shared states
export function covariance(states: {p: number; x: number; y: number}[]) {
const ex = states.reduce((s, a) => s + a.p * a.x, 0), ey = states.reduce((s, a) => s + a.p * a.y, 0);
const vx = states.reduce((s, a) => s + a.p * (a.x - ex) ** 2, 0), vy = states.reduce((s, a) => s + a.p * (a.y - ey) ** 2, 0);
const cov = states.reduce((s, a) => s + a.p * (a.x - ex) * (a.y - ey), 0);
return {ex, ey, vx, vy, cov, corr: cov / Math.sqrt(vx * vy), varTotal: vx + vy + 2 * cov, varIfIndependent: vx + vy};
}
export function runDemo() {
const cells = outcomeSpace();
const payments = total(cells);
const count = (f: (c: Cell) => boolean) => total(cells.filter(f));
const A = (c: Cell) => c.a, B = (c: Cell) => c.b, both = (c: Cell) => c.a && c.b, alert = (c: Cell) => c.a || c.b, fraud = (c: Cell) => c.fraud;
// 051 · the triage policy maps every payment to one of three outcomes
const outcomes = {approved: prob(cells, c => !alert(c)), review: prob(cells, c => alert(c) && !both(c)), declined: prob(cells, both)};
const notDeclined = outcomes.approved + outcomes.review;
const pA = prob(cells, A), pB = prob(cells, B), pAB = prob(cells, both);
const rules = union(pA, pB, pAB);
const prior = prob(cells, fraud);
const sensitivity = conditional(cells, alert, fraud);
const falsePositiveRate = conditional(cells, alert, c => !c.fraud);
const accuracy = prob(cells, c => alert(c) === c.fraud);
// 056–058 · fraud loss on one alerted payment if it were approved without review
const posterior = bayes(prior, sensitivity, falsePositiveRate);
const largeShare = 0.25, small = 120, large = 900;
const lossValues = [0, small, large];
const lossProbs = [1 - posterior, posterior * (1 - largeShare), posterior * largeShare];
const loss = moments(lossValues, lossProbs);
// 059 · the same alerts split by customer segment: rows new / established, columns fraud / legitimate
const segments = jointTable([[500, 900], [450, 4050]]);
// 060 · two loss types on the same day: card-not-present fraud (x) and account takeover (y), in dollars
const dayStates = [{p: 0.55, x: 2000, y: 1500}, {p: 0.25, x: 6000, y: 1000}, {p: 0.15, x: 3000, y: 6000}, {p: 0.05, x: 20000, y: 12000}];
const days = covariance(dayStates);
return {
payments,
cells: cells.map(c => ({...c})),
counts: {fraud: count(fraud), legit: count(c => !c.fraud), A: count(A), B: count(B), both: count(both), alerts: count(alert),
caught: count(c => c.fraud && alert(c)), falseAlarms: count(c => !c.fraud && alert(c)), missed: count(c => c.fraud && !alert(c))},
outcomes, notDeclined,
rules: {pA, pB, pAB, ...rules},
conditionalFraud: {givenBoth: conditional(cells, fraud, both), givenAOnly: conditional(cells, fraud, c => c.a && !c.b),
givenBOnly: conditional(cells, fraud, c => c.b && !c.a), givenAlert: conditional(cells, fraud, alert), givenNoAlert: conditional(cells, fraud, c => !alert(c))},
independence: {...independenceGap(pA, pB, pAB), bGivenA: pAB / pA},
detector: {prior, sensitivity, falsePositiveRate, accuracy, posterior},
loss: {values: lossValues, probs: lossProbs, ...loss, per1000: 1000 * loss.mean, sdPer1000: Math.sqrt(1000) * loss.sd, largeShare},
segments,
days: {...days, states: dayStates, sdTotal: Math.sqrt(days.varTotal), sdIfIndependent: Math.sqrt(days.varIfIndependent)},
};
}
export const checkedResult = {"payments":100000,"cells":[{"fraud":true,"a":true,"b":false,"count":250},{"fraud":true,"a":false,"b":true,"count":150},{"fraud":true,"a":true,"b":true,"count":550},{"fraud":true,"a":false,"b":false,"count":50},{"fraud":false,"a":true,"b":false,"count":2900},{"fraud":false,"a":false,"b":true,"count":1950},{"fraud":false,"a":true,"b":true,"count":100},{"fraud":false,"a":false,"b":false,"count":94050}],"counts":{"fraud":1000,"legit":99000,"A":3800,"B":2750,"both":650,"alerts":5900,"caught":950,"falseAlarms":4950,"missed":50},"outcomes":{"approved":0.941,"review":0.0525,"declined":0.0065},"notDeclined":0.9934999999999999,"rules":{"pA":0.038,"pB":0.0275,"pAB":0.0065,"either":0.059000000000000004,"naive":0.0655,"neither":0.941,"onlyA":0.0315,"onlyB":0.021},"conditionalFraud":{"givenBoth":0.8461538461538461,"givenAOnly":0.07936507936507936,"givenBOnly":0.07142857142857142,"givenAlert":0.16101694915254236,"givenNoAlert":0.0005313496280552603},"independence":{"ifIndependent":0.001045,"measured":0.0065,"ratio":6.220095693779904,"bGivenA":0.17105263157894737},"detector":{"prior":0.01,"sensitivity":0.95,"falsePositiveRate":0.05,"accuracy":0.95,"posterior":0.16101694915254236},"loss":{"values":[0,120,900],"probs":[0.8389830508474576,0.12076271186440676,0.04025423728813559],"contributions":[0,14.491525423728811,36.22881355932203],"mean":50.72033898305084,"second":34344.91525423728,"variance":31772.362467681698,"sd":178.24803636416783,"per1000":50720.33898305084,"sdPer1000":5636.697833632888,"largeShare":0.25},"segments":{"n":5900,"joint":[[0.0847457627118644,0.15254237288135594],[0.07627118644067797,0.6864406779661016]],"rowMarginal":[0.23728813559322035,0.7627118644067796],"fraudMarginal":0.1610169491525424,"fraudGivenRow":[0.3571428571428571,0.1]},"days":{"ex":4050,"ey":2575,"vx":16147500,"vy":7456875,"cov":7421250,"corr":0.6763102722078465,"varTotal":38446875,"varIfIndependent":23604375,"states":[{"p":0.55,"x":2000,"y":1500},{"p":0.25,"x":6000,"y":1000},{"p":0.15,"x":3000,"y":6000},{"p":0.05,"x":20000,"y":12000}],"sdTotal":6200.55441069587,"sdIfIndependent":4858.433389478547}};
// Run this file directly: npx tsx lessons/06-probability-and-random-variables/demo-fraud-alert-triage-simulator.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
{
"payments": 100000,
"cells": [
{
"fraud": true,
"a": true,
"b": false,
"count": 250
},
{
"fraud": true,
"a": false,
"b": true,
"count": 150
},
{
"fraud": true,
"a": true,
"b": true,
"count": 550
},
{
"fraud": true,
"a": false,
"b": false,
"count": 50
},
{
"fraud": false,
"a": true,
"b": false,
"count": 2900
},
{
"fraud": false,
"a": false,
"b": true,
"count": 1950
},
{
"fraud": false,
"a": true,
"b": true,
"count": 100
},
{
"fraud": false,
"a": false,
"b": false,
"count": 94050
}
],
"counts": {
"fraud": 1000,
"legit": 99000,
"A": 3800,
"B": 2750,
"both": 650,
"alerts": 5900,
"caught": 950,
"falseAlarms": 4950,
"missed": 50
},
"outcomes": {
"approved": 0.941,
"review": 0.0525,
"declined": 0.0065
},
"notDeclined": 0.9934999999999999,
"rules": {
"pA": 0.038,
"pB": 0.0275,
"pAB": 0.0065,
"either": 0.059000000000000004,
"naive": 0.0655,
"neither": 0.941,
"onlyA": 0.0315,
"onlyB": 0.021
},
"conditionalFraud": {
"givenBoth": 0.8461538461538461,
"givenAOnly": 0.07936507936507936,
"givenBOnly": 0.07142857142857142,
"givenAlert": 0.16101694915254236,
"givenNoAlert": 0.0005313496280552603
},
"independence": {
"ifIndependent": 0.001045,
"measured": 0.0065,
"ratio": 6.220095693779904,
"bGivenA": 0.17105263157894737
},
"detector": {
"prior": 0.01,
"sensitivity": 0.95,
"falsePositiveRate": 0.05,
"accuracy": 0.95,
"posterior": 0.16101694915254236
},
"loss": {
"values": [
0,
120,
900
],
"probs": [
0.8389830508474576,
0.12076271186440676,
0.04025423728813559
],
"contributions": [
0,
14.491525423728811,
36.22881355932203
],
"mean": 50.72033898305084,
"second": 34344.91525423728,
"variance": 31772.362467681698,
"sd": 178.24803636416783,
"per1000": 50720.33898305084,
"sdPer1000": 5636.697833632888,
"largeShare": 0.25
},
"segments": {
"n": 5900,
"joint": [
[
0.0847457627118644,
0.15254237288135594
],
[
0.07627118644067797,
0.6864406779661016
]
],
"rowMarginal": [
0.23728813559322035,
0.7627118644067796
],
"fraudMarginal": 0.1610169491525424,
"fraudGivenRow": [
0.3571428571428571,
0.1
]
},
"days": {
"ex": 4050,
"ey": 2575,
"vx": 16147500,
"vy": 7456875,
"cov": 7421250,
"corr": 0.6763102722078465,
"varTotal": 38446875,
"varIfIndependent": 23604375,
"states": [
{
"p": 0.55,
"x": 2000,
"y": 1500
},
{
"p": 0.25,
"x": 6000,
"y": 1000
},
{
"p": 0.15,
"x": 3000,
"y": 6000
},
{
"p": 0.05,
"x": 20000,
"y": 12000
}
],
"sdTotal": 6200.55441069587,
"sdIfIndependent": 4858.433389478547
}
}Prefer your own machine? Every file is in the course repository · open it in Codespaces.
What the demo does
Two fraud rules watch 100,000 synthetic card payments. The simulator builds the outcome space, counts alerts without double counting, tests whether the rules are independent, applies Bayes with the base rate, budgets expected loss per 1,000 alerts, and splits risk by segment and by loss type. Every module 06 lesson becomes one simulator feature.
Lessons it combines
- Experiments, Outcomes, Sample Spaces, and Events
- Probability Rules, Complements, Unions, and Intersections
- Conditional Probability
- Independence and Dependence
- Bayes’ Theorem and Base Rates
- Discrete and Continuous Random Variables
- Expected Value
- Variance, Moments, and Moment-Generating Intuition
- Joint, Marginal, and Conditional Distributions
- Covariance and Correlation of Random Variables