Module 4 · Location, Ranking, and Exploratory Summaries Module demo
Settlement Delay Dashboard
Where the payouts wait.
Transcript
37 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 04 demo · Settlement Delay Dashboard
* An operations view of how long merchant payouts took to settle last week: twenty payouts across six
* merchants, one of them held for 220 hours. Every summary on the dashboard is one module lesson, and
* the dashboard shows why the mean is not what a typical customer experiences.
* Lessons 031–040. Synthetic example; not financial advice.
*/
export type Payout = {id: string; merchant: string; hours: number};
export type Merchant = {merchant: string; attempts: number; approved: number};
const DELAYS: Record<string, number[]> = {
A: [12, 14, 18, 20],
B: [16, 18, 22],
C: [20, 24, 24, 26],
D: [18, 22, 24],
E: [26, 30, 36],
F: [28, 30, 220],
};
export const payouts: Payout[] = Object.entries(DELAYS).flatMap(([m, hs]) => hs.map((hours, i) => ({id: `${m}${i + 1}`, merchant: m, hours})));
export const merchants: Merchant[] = [
{merchant: "A", attempts: 500, approved: 480},
{merchant: "B", attempts: 250, approved: 230},
{merchant: "C", attempts: 400, approved: 360},
{merchant: "D", attempts: 300, approved: 282},
{merchant: "E", attempts: 200, approved: 170},
{merchant: "F", attempts: 50, approved: 30},
];
const sorted = (x: number[]) => [...x].sort((a, b) => a - b);
const r = (x: number, d = 4) => Math.round(x * 10 ** d) / 10 ** d;
// 031 · count, sum, minimum, maximum and range of a finite, nonempty sample
export function basics(x: number[]) {
if (x.length === 0 || !x.every(Number.isFinite)) throw new Error("Finite, nonempty sample required");
const min = Math.min(...x), max = Math.max(...x);
return {count: x.length, sum: x.reduce((s, v) => s + v, 0), min, max, range: max - min};
}
// 032 · arithmetic mean: the balance point
export const mean = (x: number[]) => x.reduce((s, v) => s + v, 0) / x.length;
// 033 · weighted mean: Σ w·x / Σ w
export function weightedMean(values: number[], weights: number[]): number {
if (values.length !== weights.length) throw new Error("Lengths differ");
return values.reduce((s, v, i) => s + v * weights[i], 0) / weights.reduce((s, w) => s + w, 0);
}
// 034 · median and all tied modes
export function median(x: number[]): number {
const s = sorted(x), mid = Math.floor(s.length / 2);
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
}
export function modes(x: number[]) {
const counts = new Map<number, number>();
for (const v of x) counts.set(v, (counts.get(v) ?? 0) + 1);
const top = Math.max(...counts.values());
return {count: top, values: [...counts].filter(([, c]) => c === top).map(([v]) => v).sort((a, b) => a - b)};
}
// 035 · trimmed and winsorized means with a declared tail share
export function trimWinsor(x: number[], share: number) {
const s = sorted(x), k = Math.floor(s.length * share);
const lo = s[k], hi = s[s.length - 1 - k];
return {k, trimmedMean: mean(s.slice(k, s.length - k)), winsorizedMean: mean(s.map(v => Math.max(lo, Math.min(hi, v)))), lo, hi};
}
// 036 · type-7 quantile (linear interpolation) and nearest-rank, for comparison
export function quantile7(x: number[], p: number) {
const s = sorted(x), h = (s.length - 1) * p, i = Math.floor(h);
return {h, lower: s[i], upper: s[Math.min(i + 1, s.length - 1)], value: s[i] + (h - i) * (s[Math.min(i + 1, s.length - 1)] - s[i])};
}
export function nearestRank(x: number[], p: number) {
const s = sorted(x), rank = Math.ceil(p * s.length);
return {rank, value: s[rank - 1]};
}
// 037 · average ranks for ties and percentile rank 100(r − 1)/(n − 1)
export function averageRanks(x: number[]) {
const ranks = x.map(v => x.filter(y => y < v).length + (x.filter(y => y === v).length + 1) / 2);
return {ranks, percentileRanks: ranks.map(rk => (100 * (rk - 1)) / (x.length - 1))};
}
// 038 · frequency table over half-open bands [lo, hi)
export function frequency(x: number[], edges: number[]) {
return edges.slice(0, -1).map((lo, i) => {
const count = x.filter(v => v >= lo && v < edges[i + 1]).length;
return {lo, hi: edges[i + 1], count, share: count / x.length};
});
}
// 039 · histogram counts with half-open bins, and the ECDF F(t) = #(x ≤ t) / n
export const histogram = (x: number[], width: number, top: number) =>
Array.from({length: Math.ceil(top / width)}, (_, i) => x.filter(v => v >= i * width && v < (i + 1) * width).length);
export const ecdf = (x: number[], t: number) => x.filter(v => v <= t).length / x.length;
// 040 · five-number summary, Tukey fences, observed whiskers and outliers
export function boxPlot(x: number[]) {
const s = sorted(x), q1 = quantile7(s, 0.25).value, med = quantile7(s, 0.5).value, q3 = quantile7(s, 0.75).value;
const iqr = q3 - q1, lo = q1 - 1.5 * iqr, hi = q3 + 1.5 * iqr, inside = s.filter(v => v >= lo && v <= hi);
return {five: [s[0], q1, med, q3, s[s.length - 1]], iqr, fences: [lo, hi], whiskers: [Math.min(...inside), Math.max(...inside)], outliers: s.filter(v => v < lo || v > hi)};
}
export function runDemo() {
const hours = payouts.map(p => p.hours);
const b = basics(hours);
const worst = payouts.reduce((a, p) => (p.hours > a.hours ? p : a));
const withoutWorst = hours.filter(h => h !== worst.hours);
const tw = trimWinsor(hours, 0.1);
const p95 = quantile7(hours, 0.95), p95nr = nearestRank(hours, 0.95), slaHours = 40;
const merchantMedians = merchants.map(m => median(payouts.filter(p => p.merchant === m.merchant).map(p => p.hours)));
const rk = averageRanks(merchantMedians);
const rates = merchants.map(m => m.approved / m.attempts);
const box = boxPlot(hours);
return {
payouts, sortedHours: sorted(hours), merchants: merchants.map(m => m.merchant),
basics: b, worst,
location: {
mean: mean(hours), median: median(hours), modes: modes(hours),
meanWithoutWorst: r(mean(withoutWorst)), medianWithoutWorst: median(withoutWorst),
meanShift: r(mean(hours) - mean(withoutWorst)),
trimShare: 0.1, trimK: tw.k, trimmedMean: tw.trimmedMean, winsorizedMean: tw.winsorizedMean, winsorLo: tw.lo, winsorHi: tw.hi,
},
approval: {
attempts: merchants.map(m => m.attempts), approved: merchants.map(m => m.approved), rates,
unweighted: r(mean(rates)), weighted: r(weightedMean(rates, merchants.map(m => m.attempts))),
totalAttempts: merchants.reduce((s, m) => s + m.attempts, 0), totalApproved: merchants.reduce((s, m) => s + m.approved, 0),
},
percentiles: {
p50: quantile7(hours, 0.5).value, p90: r(quantile7(hours, 0.9).value), p95: r(p95.value), p95h: r(p95.h), p95lower: p95.lower, p95upper: p95.upper,
p95nearestRank: p95nr.value, p95rank: p95nr.rank, slaHours,
slaType7: p95.value <= slaHours ? "met" : "missed", slaNearestRank: p95nr.value <= slaHours ? "met" : "missed",
},
ranks: {merchantMedians, ranks: rk.ranks, percentileRanks: rk.percentileRanks},
frequency: frequency(hours, [0, 24, 48, Infinity]).map(f => ({...f, hi: Number.isFinite(f.hi) ? f.hi : null})),
histogram: {width12: histogram(hours, 12, 228), width48: histogram(hours, 48, 240)},
ecdf: {at24: ecdf(hours, 24), at36: ecdf(hours, 36), at48: ecdf(hours, 48)},
box,
};
}
export const checkedResult = {"payouts":[{"id":"A1","merchant":"A","hours":12},{"id":"A2","merchant":"A","hours":14},{"id":"A3","merchant":"A","hours":18},{"id":"A4","merchant":"A","hours":20},{"id":"B1","merchant":"B","hours":16},{"id":"B2","merchant":"B","hours":18},{"id":"B3","merchant":"B","hours":22},{"id":"C1","merchant":"C","hours":20},{"id":"C2","merchant":"C","hours":24},{"id":"C3","merchant":"C","hours":24},{"id":"C4","merchant":"C","hours":26},{"id":"D1","merchant":"D","hours":18},{"id":"D2","merchant":"D","hours":22},{"id":"D3","merchant":"D","hours":24},{"id":"E1","merchant":"E","hours":26},{"id":"E2","merchant":"E","hours":30},{"id":"E3","merchant":"E","hours":36},{"id":"F1","merchant":"F","hours":28},{"id":"F2","merchant":"F","hours":30},{"id":"F3","merchant":"F","hours":220}],"sortedHours":[12,14,16,18,18,18,20,20,22,22,24,24,24,26,26,28,30,30,36,220],"merchants":["A","B","C","D","E","F"],"basics":{"count":20,"sum":648,"min":12,"max":220,"range":208},"worst":{"id":"F3","merchant":"F","hours":220},"location":{"mean":32.4,"median":23,"modes":{"count":3,"values":[18,24]},"meanWithoutWorst":22.5263,"medianWithoutWorst":22,"meanShift":9.8737,"trimShare":0.1,"trimK":2,"trimmedMean":22.875,"winsorizedMean":22.9,"winsorLo":16,"winsorHi":30},"approval":{"attempts":[500,250,400,300,200,50],"approved":[480,230,360,282,170,30],"rates":[0.96,0.92,0.9,0.94,0.85,0.6],"unweighted":0.8617,"weighted":0.9129,"totalAttempts":1700,"totalApproved":1552},"percentiles":{"p50":23,"p90":30.6,"p95":45.2,"p95h":18.05,"p95lower":36,"p95upper":220,"p95nearestRank":36,"p95rank":19,"slaHours":40,"slaType7":"missed","slaNearestRank":"met"},"ranks":{"merchantMedians":[16,18,24,22,30,30],"ranks":[1,2,4,3,5.5,5.5],"percentileRanks":[0,20,60,40,90,90]},"frequency":[{"lo":0,"hi":24,"count":10,"share":0.5},{"lo":24,"hi":48,"count":9,"share":0.45},{"lo":48,"hi":null,"count":1,"share":0.05}],"histogram":{"width12":[0,10,8,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"width48":[19,0,0,0,1]},"ecdf":{"at24":0.65,"at36":0.95,"at48":0.95},"box":{"five":[12,18,23,26.5,220],"iqr":8.5,"fences":[5.25,39.25],"whiskers":[12,36],"outliers":[220]}};
// Run this file directly: npx tsx lessons/04-location-ranking-and-exploratory-summaries/demo-settlement-delay-dashboard.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
{
"payouts": [
{
"id": "A1",
"merchant": "A",
"hours": 12
},
{
"id": "A2",
"merchant": "A",
"hours": 14
},
{
"id": "A3",
"merchant": "A",
"hours": 18
},
{
"id": "A4",
"merchant": "A",
"hours": 20
},
{
"id": "B1",
"merchant": "B",
"hours": 16
},
{
"id": "B2",
"merchant": "B",
"hours": 18
},
{
"id": "B3",
"merchant": "B",
"hours": 22
},
{
"id": "C1",
"merchant": "C",
"hours": 20
},
{
"id": "C2",
"merchant": "C",
"hours": 24
},
{
"id": "C3",
"merchant": "C",
"hours": 24
},
{
"id": "C4",
"merchant": "C",
"hours": 26
},
{
"id": "D1",
"merchant": "D",
"hours": 18
},
{
"id": "D2",
"merchant": "D",
"hours": 22
},
{
"id": "D3",
"merchant": "D",
"hours": 24
},
{
"id": "E1",
"merchant": "E",
"hours": 26
},
{
"id": "E2",
"merchant": "E",
"hours": 30
},
{
"id": "E3",
"merchant": "E",
"hours": 36
},
{
"id": "F1",
"merchant": "F",
"hours": 28
},
{
"id": "F2",
"merchant": "F",
"hours": 30
},
{
"id": "F3",
"merchant": "F",
"hours": 220
}
],
"sortedHours": [
12,
14,
16,
18,
18,
18,
20,
20,
22,
22,
24,
24,
24,
26,
26,
28,
30,
30,
36,
220
],
"merchants": [
"A",
"B",
"C",
"D",
"E",
"F"
],
"basics": {
"count": 20,
"sum": 648,
"min": 12,
"max": 220,
"range": 208
},
"worst": {
"id": "F3",
"merchant": "F",
"hours": 220
},
"location": {
"mean": 32.4,
"median": 23,
"modes": {
"count": 3,
"values": [
18,
24
]
},
"meanWithoutWorst": 22.5263,
"medianWithoutWorst": 22,
"meanShift": 9.8737,
"trimShare": 0.1,
"trimK": 2,
"trimmedMean": 22.875,
"winsorizedMean": 22.9,
"winsorLo": 16,
"winsorHi": 30
},
"approval": {
"attempts": [
500,
250,
400,
300,
200,
50
],
"approved": [
480,
230,
360,
282,
170,
30
],
"rates": [
0.96,
0.92,
0.9,
0.94,
0.85,
0.6
],
"unweighted": 0.8617,
"weighted": 0.9129,
"totalAttempts": 1700,
"totalApproved": 1552
},
"percentiles": {
"p50": 23,
"p90": 30.6,
"p95": 45.2,
"p95h": 18.05,
"p95lower": 36,
"p95upper": 220,
"p95nearestRank": 36,
"p95rank": 19,
"slaHours": 40,
"slaType7": "missed",
"slaNearestRank": "met"
},
"ranks": {
"merchantMedians": [
16,
18,
24,
22,
30,
30
],
"ranks": [
1,
2,
4,
3,
5.5,
5.5
],
"percentileRanks": [
0,
20,
60,
40,
90,
90
]
},
"frequency": [
{
"lo": 0,
"hi": 24,
"count": 10,
"share": 0.5
},
{
"lo": 24,
"hi": 48,
"count": 9,
"share": 0.45
},
{
"lo": 48,
"hi": null,
"count": 1,
"share": 0.05
}
],
"histogram": {
"width12": [
0,
10,
8,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1
],
"width48": [
19,
0,
0,
0,
1
]
},
"ecdf": {
"at24": 0.65,
"at36": 0.95,
"at48": 0.95
},
"box": {
"five": [
12,
18,
23,
26.5,
220
],
"iqr": 8.5,
"fences": [
5.25,
39.25
],
"whiskers": [
12,
36
],
"outliers": [
220
]
}
}Prefer your own machine? Every file is in the course repository · open it in Codespaces.
What the demo does
An operations dashboard of payout settlement delays: twenty synthetic payouts across six merchants, one held for 220 hours. Every module 04 lesson becomes one summary on the dashboard, and one extreme delay shows why the mean is not what a typical customer experiences.
Lessons it combines
- Count, Sum, Minimum, Maximum, and Range
- Arithmetic Mean
- Weighted Mean
- Median and Mode
- Trimmed and Winsorized Means
- Percentiles, Quantiles, and Quartiles
- Ranks, Ties, and Percentile Rank
- Frequency Tables and Relative Frequency
- Histograms and Empirical Distribution Functions
- Five-Number Summary and Box Plot