Skip to content

Commit bca39a1

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

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
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: 62 additions & 8 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 { inverseErrorFunction } from "simple-statistics";
56

67
interface FairlyAllocateCreditTestCase {
78
name: string;
@@ -14,17 +15,49 @@ 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/Probit
21+
function normalPpf(q: number, stdDev: number): number {
22+
return stdDev * Math.sqrt(2) * inverseErrorFunction(2 * q - 1);
23+
}
24+
25+
const minNForIntervalApprox = 1000;
26+
27+
function getIntervalApprox(n: number, p: number, alpha: number): Interval {
28+
if (n < minNForIntervalApprox) {
29+
throw new RangeError(`n must be >= ${minNForIntervalApprox}`);
30+
}
31+
32+
// Approximates a binomial distribution with a normal distribution which is a bit
33+
// simpler as it is symmetric.
34+
const mean = n * p;
35+
const variance = mean * (1 - p);
36+
const diff = normalPpf(1 - alpha / 2, Math.sqrt(variance));
37+
return [mean - diff, mean + diff];
38+
}
39+
40+
function getAllIntervals(
41+
n: number,
42+
creditFractions: readonly number[],
43+
alphaTotal: number,
44+
): Interval[] {
45+
// We are testing one hypothesis per dimension, so divide `alphaTotal` by
46+
// the number of dimensions: https://en.wikipedia.org/wiki/Bonferroni_correction
47+
const alpha = alphaTotal / creditFractions.length;
48+
return creditFractions.map((cf) => getIntervalApprox(n, cf, alpha));
49+
}
50+
1851
function runFairlyAllocateCreditTest(
1952
tc: Readonly<FairlyAllocateCreditTestCase>,
2053
): void {
2154
// TODO: replace with precise sum
2255
const sumCredit = tc.credit.reduce((a, b) => a + b, 0);
23-
const normalizedFloatCredit = tc.credit.map(
24-
(item) => (tc.value * item) / sumCredit,
25-
);
56+
const normalizedFloatCredit = tc.credit.map((item) => item / sumCredit);
57+
58+
const [rand, k] = tc.needsRand ? [Math.random, 1000] : [noRand, 1];
2659

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

2962
for (let n = 0; n < k; ++n) {
3063
const actualCredit = fairlyAllocateCredit(tc.credit, tc.value, rand);
@@ -34,12 +67,14 @@ function runFairlyAllocateCreditTest(
3467
for (const [j, actual] of actualCredit.entries()) {
3568
assert.ok(Number.isInteger(actual));
3669

37-
const normalized = normalizedFloatCredit[j]!;
70+
const normalized = normalizedFloatCredit[j]! * tc.value;
3871
const diff = Math.abs(actual - normalized);
3972
assert.ok(
40-
diff <= 1,
41-
`credit error > 1: actual=${actual}, normalized=${normalized}`,
73+
diff < 1,
74+
`credit error >= 1: actual=${actual}, normalized=${normalized}`,
4275
);
76+
77+
totals[j]! += actual / tc.value;
4378
}
4479

4580
assert.equal(
@@ -49,6 +84,25 @@ function runFairlyAllocateCreditTest(
4984
`actual credit does not sum to value: ${actualCredit.join(", ")}`,
5085
);
5186
}
87+
88+
const alpha = 0.00001; // Probability of test failing at random.
89+
90+
const intervals: Interval[] =
91+
k > 1
92+
? getAllIntervals(
93+
k,
94+
normalizedFloatCredit.map((c) => c - Math.floor(c)),
95+
alpha,
96+
)
97+
: normalizedFloatCredit.map((c) => [c, c]);
98+
99+
for (const [j, total] of totals.entries()) {
100+
const [min, max] = intervals[j]!;
101+
assert.ok(
102+
total >= min && total <= max,
103+
`total for credit[${j}] ${total} not in ${1 - alpha} confidence interval [${min}, ${max}]`,
104+
);
105+
}
52106
}
53107

54108
const testCases: FairlyAllocateCreditTestCase[] = [

0 commit comments

Comments
 (0)