Skip to content

Commit 7f5cb0c

Browse files
committed
Check expected values of fairlyAllocateCredit with confidence intervals
1 parent a182c22 commit 7f5cb0c

File tree

3 files changed

+57
-2
lines changed

3 files changed

+57
-2
lines changed

impl/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

impl/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"html-webpack-plugin": "^5.6.3",
1818
"http-server": "^14.1.1",
1919
"prettier": "^3.6.2",
20+
"simple-statistics": "^7.8.8",
2021
"ts-loader": "^9.5.2",
2122
"typescript": "^5.9.2",
2223
"typescript-eslint": "^8.39.0",

impl/src/backend.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fairlyAllocateCredit } from "./backend";
22

33
import { strict as assert } from "assert";
44
import test from "node:test";
5+
import * as simplestatistics from "simple-statistics";
56

67
interface FairlyAllocateCreditTestCase {
78
name: string;
@@ -14,7 +15,32 @@ function noRand(): number {
1415
throw new Error("no rand expected");
1516
}
1617

17-
// TODO: Check that the distribution of credit arrays matches expectations.
18+
type Interval = [min: number, max: number];
19+
20+
// https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
21+
function ppf(q: number, mean: number, stdDev: number): number {
22+
return (
23+
mean +
24+
stdDev * Math.sqrt(2) * simplestatistics.inverseErrorFunction(2 * q - 1)
25+
);
26+
}
27+
28+
function getIntervalApprox(n: number, p: number, alpha: number): Interval {
29+
const mean = n * p;
30+
const variance = mean * (1 - p);
31+
const diff = ppf(1 - alpha / 2, mean, Math.sqrt(variance)) - mean;
32+
return [mean - diff, mean + diff];
33+
}
34+
35+
function getAllIntervals(
36+
n: number,
37+
creditFractions: readonly number[],
38+
alphaTotal: number,
39+
): Interval[] {
40+
const alpha = alphaTotal / creditFractions.length;
41+
return creditFractions.map((cf) => getIntervalApprox(n, cf, alpha));
42+
}
43+
1844
function runFairlyAllocateCreditTest(
1945
tc: Readonly<FairlyAllocateCreditTestCase>,
2046
): void {
@@ -24,7 +50,9 @@ function runFairlyAllocateCreditTest(
2450
(item) => (tc.value * item) / sumCredit,
2551
);
2652

27-
const [rand, k] = tc.needsRand ? [Math.random, 10000] : [noRand, 1];
53+
const [rand, k] = tc.needsRand ? [Math.random, 100000] : [noRand, 1];
54+
55+
const totals = new Array<number>(tc.credit.length).fill(0);
2856

2957
for (let n = 0; n < k; ++n) {
3058
const actualCredit = fairlyAllocateCredit(tc.credit, tc.value, rand);
@@ -40,6 +68,8 @@ function runFairlyAllocateCreditTest(
4068
diff <= 1,
4169
`credit error > 1: actual=${actual}, normalized=${normalized}`,
4270
);
71+
72+
totals[j]! += actual / tc.value;
4373
}
4474

4575
assert.equal(
@@ -49,6 +79,19 @@ function runFairlyAllocateCreditTest(
4979
`actual credit does not sum to value: ${actualCredit.join(", ")}`,
5080
);
5181
}
82+
83+
const intervals = getAllIntervals(
84+
k,
85+
tc.credit.map((c) => c / sumCredit),
86+
0.0001,
87+
);
88+
for (const [j, total] of totals.entries()) {
89+
const [min, max] = intervals[j]!;
90+
assert.ok(
91+
total >= min && total <= max,
92+
`total for credit[${j}] ${total} not in confidence interval [${min}, ${max}]`,
93+
);
94+
}
5295
}
5396

5497
const testCases: FairlyAllocateCreditTestCase[] = [

0 commit comments

Comments
 (0)