@@ -2,6 +2,7 @@ import { fairlyAllocateCredit } from "./backend";
2
2
3
3
import { strict as assert } from "assert" ;
4
4
import test from "node:test" ;
5
+ import { inverseErrorFunction } from "simple-statistics" ;
5
6
6
7
interface FairlyAllocateCreditTestCase {
7
8
name : string ;
@@ -14,17 +15,49 @@ function noRand(): number {
14
15
throw new Error ( "no rand expected" ) ;
15
16
}
16
17
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
+
18
51
function runFairlyAllocateCreditTest (
19
52
tc : Readonly < FairlyAllocateCreditTestCase > ,
20
53
) : void {
21
54
// TODO: replace with precise sum
22
55
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 ] ;
26
59
27
- const [ rand , k ] = tc . needsRand ? [ Math . random , 10000 ] : [ noRand , 1 ] ;
60
+ const totals = new Array < number > ( tc . credit . length ) . fill ( 0 ) ;
28
61
29
62
for ( let n = 0 ; n < k ; ++ n ) {
30
63
const actualCredit = fairlyAllocateCredit ( tc . credit , tc . value , rand ) ;
@@ -34,12 +67,14 @@ function runFairlyAllocateCreditTest(
34
67
for ( const [ j , actual ] of actualCredit . entries ( ) ) {
35
68
assert . ok ( Number . isInteger ( actual ) ) ;
36
69
37
- const normalized = normalizedFloatCredit [ j ] ! ;
70
+ const normalized = normalizedFloatCredit [ j ] ! * tc . value ;
38
71
const diff = Math . abs ( actual - normalized ) ;
39
72
assert . ok (
40
- diff <= 1 ,
41
- `credit error > 1: actual=${ actual } , normalized=${ normalized } ` ,
73
+ diff < 1 ,
74
+ `credit error >= 1: actual=${ actual } , normalized=${ normalized } ` ,
42
75
) ;
76
+
77
+ totals [ j ] ! += actual / tc . value ;
43
78
}
44
79
45
80
assert . equal (
@@ -49,6 +84,25 @@ function runFairlyAllocateCreditTest(
49
84
`actual credit does not sum to value: ${ actualCredit . join ( ", " ) } ` ,
50
85
) ;
51
86
}
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
+ }
52
106
}
53
107
54
108
const testCases : FairlyAllocateCreditTestCase [ ] = [
0 commit comments