@mark1russell7/rigor-statistics 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/build/index.d.ts +272 -8
- package/build/index.d.ts.map +1 -1
- package/build/index.js +579 -19
- package/build/index.js.map +1 -1
- package/package.json +8 -3
package/build/index.js
CHANGED
|
@@ -2247,7 +2247,7 @@ function fitResponse(data, options = {}) {
|
|
|
2247
2247
|
* The tests of a difference: the sign test, the paired permutation test, the Wilson interval of a proportion and
|
|
2248
2248
|
* the Holm correction for several tests.
|
|
2249
2249
|
*/
|
|
2250
|
-
function whole$
|
|
2250
|
+
function whole$2(name, value) {
|
|
2251
2251
|
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name}: ${value} is not a whole number of 0 or more.`);
|
|
2252
2252
|
}
|
|
2253
2253
|
/** The log of each binomial term C(n, k) / 2^n, for each k from 0 to n. */
|
|
@@ -2275,8 +2275,8 @@ function upperTail(k, n) {
|
|
|
2275
2275
|
* two times the tail of the larger count, at most 1. The one-sided p is equal to `signTestP` of packing.
|
|
2276
2276
|
*/
|
|
2277
2277
|
function signTest(better, worse, sides = 1) {
|
|
2278
|
-
whole$
|
|
2279
|
-
whole$
|
|
2278
|
+
whole$2("signTest", better);
|
|
2279
|
+
whole$2("signTest", worse);
|
|
2280
2280
|
const n = better + worse;
|
|
2281
2281
|
if (n === 0) return 1;
|
|
2282
2282
|
if (sides === 1) return upperTail(better, n);
|
|
@@ -2287,7 +2287,7 @@ function signTest(better, worse, sides = 1) {
|
|
|
2287
2287
|
* n wins out of n are not enough. Use it before a run, to know how many inputs a claim needs.
|
|
2288
2288
|
*/
|
|
2289
2289
|
function signTestPower(n, level = .05, sides = 2) {
|
|
2290
|
-
whole$
|
|
2290
|
+
whole$2("signTestPower", n);
|
|
2291
2291
|
for (let k = Math.ceil(n / 2); k <= n; k++) if (signTest(k, n - k, sides) < level) return k;
|
|
2292
2292
|
}
|
|
2293
2293
|
/** The z of a two-sided 95% interval. */
|
|
@@ -2297,8 +2297,8 @@ const Z95 = 1.959963984540054;
|
|
|
2297
2297
|
* a proportion near 0 or 1, where the normal interval does not. For n = 0 the interval is 0 to 1.
|
|
2298
2298
|
*/
|
|
2299
2299
|
function wilson(k, n, z = Z95) {
|
|
2300
|
-
whole$
|
|
2301
|
-
whole$
|
|
2300
|
+
whole$2("wilson", k);
|
|
2301
|
+
whole$2("wilson", n);
|
|
2302
2302
|
if (k > n) throw new RangeError(`wilson: k ${k} is more than n ${n}.`);
|
|
2303
2303
|
if (n === 0) return {
|
|
2304
2304
|
low: 0,
|
|
@@ -2345,7 +2345,7 @@ function holm(pValues, level = .05) {
|
|
|
2345
2345
|
*/
|
|
2346
2346
|
function partialConjunction(pValues, need) {
|
|
2347
2347
|
for (const p of pValues) if (!(p >= 0 && p <= 1)) throw new RangeError(`partialConjunction: each p is from 0 to 1, not ${p}.`);
|
|
2348
|
-
whole$
|
|
2348
|
+
whole$2("partialConjunction", need);
|
|
2349
2349
|
const m = pValues.length;
|
|
2350
2350
|
if (need < 1 || need > m) throw new RangeError(`partialConjunction: need is from 1 to the count of tests ${m}, not ${need}.`);
|
|
2351
2351
|
const sorted = [...pValues].sort((a, b) => a - b);
|
|
@@ -2448,7 +2448,7 @@ function twoSamplePermutation(a, b, options = {}) {
|
|
|
2448
2448
|
*/
|
|
2449
2449
|
function wilsonSampleSize(target, expected, z = Z95, max = 1e6) {
|
|
2450
2450
|
if (!(target >= 0 && target <= 1 && expected >= 0 && expected <= 1)) throw new RangeError(`wilsonSampleSize: target ${target} and expected ${expected} must be from 0 to 1.`);
|
|
2451
|
-
whole$
|
|
2451
|
+
whole$2("wilsonSampleSize", max);
|
|
2452
2452
|
if (expected <= target) return void 0;
|
|
2453
2453
|
for (let n = 1; n <= max; n++) if (wilson(Math.round(expected * n), n, z).low >= target) return n;
|
|
2454
2454
|
}
|
|
@@ -2566,7 +2566,7 @@ function kendallTau(a, b, seed = 1) {
|
|
|
2566
2566
|
* The intervals that come from a discrete distribution. The interval of a median or of a quantile comes from the order
|
|
2567
2567
|
* statistics. The central interval of a count comes from a binomial or a negative binomial distribution.
|
|
2568
2568
|
*/
|
|
2569
|
-
function whole(name, value) {
|
|
2569
|
+
function whole$1(name, value) {
|
|
2570
2570
|
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name}: ${value} is not a whole number of 0 or more.`);
|
|
2571
2571
|
}
|
|
2572
2572
|
function levelOf(name, level) {
|
|
@@ -2675,7 +2675,7 @@ function quantileSamples(p, level = .95) {
|
|
|
2675
2675
|
* or less. The two tails are separate sums, so that a small upper tail keeps its digits.
|
|
2676
2676
|
*/
|
|
2677
2677
|
function binomialInterval(n, p, level = .95) {
|
|
2678
|
-
whole("binomialInterval", n);
|
|
2678
|
+
whole$1("binomialInterval", n);
|
|
2679
2679
|
if (!(p >= 0 && p <= 1)) throw new RangeError(`binomialInterval: p is from 0 to 1, not ${p}.`);
|
|
2680
2680
|
const tail = levelOf("binomialInterval", level);
|
|
2681
2681
|
const terms = binomialTerms(n, p);
|
|
@@ -2700,12 +2700,47 @@ function binomialInterval(n, p, level = .95) {
|
|
|
2700
2700
|
};
|
|
2701
2701
|
}
|
|
2702
2702
|
/**
|
|
2703
|
+
* This function gives the p of the exact one-sided binomial test of `k` successes in `n` trials, with
|
|
2704
|
+
* X ~ Binomial(n, p). The side `"less"` gives `P(X <= k)`. A small value rejects the claim "the chance is `p` or
|
|
2705
|
+
* more". The side `"greater"` gives `P(X >= k)`. A small value rejects the claim "the chance is `p` or less".
|
|
2706
|
+
*
|
|
2707
|
+
* The sum starts at the largest term of the tail, so a small tail keeps its digits. At `p = 1/2`, the side
|
|
2708
|
+
* `"greater"` is equal to the one-sided `signTest(k, n - k)`.
|
|
2709
|
+
*
|
|
2710
|
+
* @param k - The count of successes, a whole number from 0 to `n`.
|
|
2711
|
+
* @param n - The count of trials, a whole number of 0 or more.
|
|
2712
|
+
* @param p - The chance of a success under the claim, from 0 to 1.
|
|
2713
|
+
* @param side - The side of the tail.
|
|
2714
|
+
* @returns The p of the test, from 0 to 1.
|
|
2715
|
+
*/
|
|
2716
|
+
function binomialTest(k, n, p, side) {
|
|
2717
|
+
whole$1("binomialTest", k);
|
|
2718
|
+
whole$1("binomialTest", n);
|
|
2719
|
+
if (k > n) throw new RangeError(`binomialTest: k ${k} is more than n ${n}.`);
|
|
2720
|
+
if (!(p >= 0 && p <= 1)) throw new RangeError(`binomialTest: p is from 0 to 1, not ${p}.`);
|
|
2721
|
+
if (side !== "less" && side !== "greater") throw new RangeError(`binomialTest: side is "less" or "greater", not ${String(side)}.`);
|
|
2722
|
+
const [from, to] = side === "less" ? [0, k] : [k, n];
|
|
2723
|
+
if (from === 0 && to === n) return 1;
|
|
2724
|
+
if (p === 0) return from === 0 ? 1 : 0;
|
|
2725
|
+
if (p === 1) return to === n ? 1 : 0;
|
|
2726
|
+
const [logP, logQ] = [Math.log(p), Math.log1p(-p)];
|
|
2727
|
+
const logs = [];
|
|
2728
|
+
let logChoose = 0;
|
|
2729
|
+
for (let i = 0; i <= to; i++) {
|
|
2730
|
+
if (i > 0) logChoose += Math.log((n - i + 1) / i);
|
|
2731
|
+
if (i >= from) logs.push(logChoose + i * logP + (n - i) * logQ);
|
|
2732
|
+
}
|
|
2733
|
+
const top = Math.max(...logs);
|
|
2734
|
+
const sum = logs.reduce((s, l) => s + Math.exp(l - top), 0);
|
|
2735
|
+
return Math.min(1, Math.exp(top) * sum);
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2703
2738
|
* The central interval of a negative binomial count. X is the count of failures before `successes` successes, and
|
|
2704
2739
|
* `p` is the chance of a success at each try. `low` is the smallest k with P(X <= k) > (1 - level) / 2.
|
|
2705
2740
|
* `high` is the smallest k with P(X <= k) >= 1 - (1 - level) / 2.
|
|
2706
2741
|
*/
|
|
2707
2742
|
function negativeBinomialInterval(successes, p, level = .95) {
|
|
2708
|
-
whole("negativeBinomialInterval", successes);
|
|
2743
|
+
whole$1("negativeBinomialInterval", successes);
|
|
2709
2744
|
if (!(p > 0 && p <= 1)) throw new RangeError(`negativeBinomialInterval: p is more than 0 and 1 or less, not ${p}.`);
|
|
2710
2745
|
const tail = levelOf("negativeBinomialInterval", level);
|
|
2711
2746
|
const logQ = Math.log1p(-p);
|
|
@@ -2847,6 +2882,17 @@ function brier(forecasts) {
|
|
|
2847
2882
|
for (const f of forecasts) sum += (f.p - (f.hit ? 1 : 0)) ** 2;
|
|
2848
2883
|
return sum / forecasts.length;
|
|
2849
2884
|
}
|
|
2885
|
+
/**
|
|
2886
|
+
* The log-loss of predictions: the mean of `-ln(p)` for a hit and `-ln(1 - p)` for a miss. 0 is perfect, and
|
|
2887
|
+
* `ln(2)` is a constant 0.5. Lower is better. A p of 0 for a hit gives Infinity. For an outcome of three values,
|
|
2888
|
+
* give the probability of the observed outcome as `p`, with `hit` true.
|
|
2889
|
+
*/
|
|
2890
|
+
function logLoss(forecasts) {
|
|
2891
|
+
checkedForecasts("logLoss", forecasts);
|
|
2892
|
+
let sum = 0;
|
|
2893
|
+
for (const f of forecasts) sum -= Math.log(f.hit ? f.p : 1 - f.p);
|
|
2894
|
+
return sum / forecasts.length;
|
|
2895
|
+
}
|
|
2850
2896
|
/** The hit rate of each stated level, from the lowest level. */
|
|
2851
2897
|
function calibration(forecasts) {
|
|
2852
2898
|
checkedForecasts("calibration", forecasts);
|
|
@@ -2972,15 +3018,109 @@ function optionFloor(options) {
|
|
|
2972
3018
|
}
|
|
2973
3019
|
throw new RangeError(`optionFloor: no count under 10,000,000 for sigma ${sigma} and delta ${delta}.`);
|
|
2974
3020
|
}
|
|
3021
|
+
// src/coverage.ts
|
|
3022
|
+
/**
|
|
3023
|
+
* The rule that finds the smallest sample at which an interval method keeps its level. The input is the hits of the
|
|
3024
|
+
* method on the true value in seeded cells. The bar is fixed before the run.
|
|
3025
|
+
*/
|
|
3026
|
+
function whole(name, value, min) {
|
|
3027
|
+
if (!Number.isSafeInteger(value) || value < min) throw new RangeError(`coverageMinSample: ${name} is a whole number of ${min} or more, not ${value}.`);
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* This function applies a fixed bar of coverage to the cells of a study. It gives the smallest sample size `m` from
|
|
3031
|
+
* which an interval method keeps its level. The method qualifies at `m` when the cells with a sample of `m` or more
|
|
3032
|
+
* pass two tests:
|
|
3033
|
+
*
|
|
3034
|
+
* 1. No cell rejects the coverage `level`. Each cell has the exact one-sided binomial test `P(X <= hits)`, with
|
|
3035
|
+
* X ~ Binomial(draws, level). The family of these tests gets the Holm correction at `alpha`.
|
|
3036
|
+
* 2. The Wilson lower end of the pooled coverage, the sum of the hits over the sum of the draws, is `pooledLow`
|
|
3037
|
+
* or more.
|
|
3038
|
+
*
|
|
3039
|
+
* Each step tells the result of the two tests at its `m`. A step does not depend on the steps of other sizes. At a
|
|
3040
|
+
* small `m`, the pool also holds the cells of the larger sizes. Thus a small `m` can pass while a larger `m` fails
|
|
3041
|
+
* alone, for example near `m = n`.
|
|
3042
|
+
*
|
|
3043
|
+
* Thus the set of sizes is closed upward. The result is the smallest `m` of the cells such that each size of `m` or
|
|
3044
|
+
* more qualifies. The field `blocking` gives the largest size that fails. When the largest size fails, the result is
|
|
3045
|
+
* infinite.
|
|
3046
|
+
*
|
|
3047
|
+
* @param cells - The hits and the draws of each cell. Each cell has its own sample size.
|
|
3048
|
+
* @param bar - The level, the alpha of the Holm family and the pooled lower end.
|
|
3049
|
+
* @returns The smallest size from which each size qualifies, the size that blocks, and the result at each size.
|
|
3050
|
+
*/
|
|
3051
|
+
function coverageMinSample(cells, bar) {
|
|
3052
|
+
const { level, alpha, pooledLow } = bar;
|
|
3053
|
+
const z = bar.z ?? 1.959963984540054;
|
|
3054
|
+
if (!(level > 0 && level < 1)) throw new RangeError(`coverageMinSample: level is more than 0 and less than 1, not ${level}.`);
|
|
3055
|
+
if (!(alpha > 0 && alpha < 1)) throw new RangeError(`coverageMinSample: alpha is more than 0 and less than 1, not ${alpha}.`);
|
|
3056
|
+
if (!(pooledLow >= 0 && pooledLow <= 1)) throw new RangeError(`coverageMinSample: pooledLow is from 0 to 1, not ${pooledLow}.`);
|
|
3057
|
+
if (!(z > 0 && Number.isFinite(z))) throw new RangeError(`coverageMinSample: z is a finite number more than 0, not ${z}.`);
|
|
3058
|
+
for (const cell of cells) {
|
|
3059
|
+
whole("sample", cell.sample, 1);
|
|
3060
|
+
whole("draws", cell.draws, 1);
|
|
3061
|
+
whole("hits", cell.hits, 0);
|
|
3062
|
+
if (cell.hits > cell.draws) throw new RangeError(`coverageMinSample: the hits ${cell.hits} of the cell ${cell.name} are more than its draws ${cell.draws}.`);
|
|
3063
|
+
}
|
|
3064
|
+
const p = cells.map((cell) => binomialTest(cell.hits, cell.draws, level, "less"));
|
|
3065
|
+
const steps = [...new Set(cells.map((cell) => cell.sample))].sort((a, b) => a - b).map((m) => {
|
|
3066
|
+
const at = cells.map((cell, i) => ({
|
|
3067
|
+
cell,
|
|
3068
|
+
p: p[i]
|
|
3069
|
+
})).filter(({ cell }) => cell.sample >= m);
|
|
3070
|
+
const corrected = holm(at.map(({ p: value }) => value), alpha);
|
|
3071
|
+
const rejected = at.filter((_, i) => corrected[i]?.reject === true).map(({ cell }) => cell.name);
|
|
3072
|
+
const smallestAdjusted = Math.min(...corrected.map((c) => c.adjusted));
|
|
3073
|
+
let hits = 0;
|
|
3074
|
+
let draws = 0;
|
|
3075
|
+
for (const { cell } of at) {
|
|
3076
|
+
hits += cell.hits;
|
|
3077
|
+
draws += cell.draws;
|
|
3078
|
+
}
|
|
3079
|
+
const pooled = wilson(hits, draws, z);
|
|
3080
|
+
const cellOk = rejected.length === 0;
|
|
3081
|
+
const pooledOk = pooled.low >= pooledLow;
|
|
3082
|
+
const reason = cellOk ? pooledOk ? "qualifies" : "pooled-low" : pooledOk ? "cell-rejected" : "cell-rejected-and-pooled-low";
|
|
3083
|
+
return {
|
|
3084
|
+
sample: m,
|
|
3085
|
+
cells: at.length,
|
|
3086
|
+
rejected,
|
|
3087
|
+
smallestAdjusted,
|
|
3088
|
+
hits,
|
|
3089
|
+
draws,
|
|
3090
|
+
pooled,
|
|
3091
|
+
qualifies: cellOk && pooledOk,
|
|
3092
|
+
reason
|
|
3093
|
+
};
|
|
3094
|
+
});
|
|
3095
|
+
let minSample = Number.POSITIVE_INFINITY;
|
|
3096
|
+
let blocking;
|
|
3097
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
3098
|
+
const step = steps[i];
|
|
3099
|
+
if (!step.qualifies) {
|
|
3100
|
+
blocking = step.sample;
|
|
3101
|
+
break;
|
|
3102
|
+
}
|
|
3103
|
+
minSample = step.sample;
|
|
3104
|
+
}
|
|
3105
|
+
return blocking === void 0 ? {
|
|
3106
|
+
minSample,
|
|
3107
|
+
steps
|
|
3108
|
+
} : {
|
|
3109
|
+
minSample,
|
|
3110
|
+
blocking,
|
|
3111
|
+
steps
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
2975
3114
|
// src/u-statistic.ts
|
|
2976
3115
|
/**
|
|
2977
3116
|
* The interval of a U-statistic from a uniform sample of `m` of `n` units, drawn without replacement. A kernel `h`
|
|
2978
3117
|
* gives a value to each set of `r` units. The total of the population is the sum of `h` over all its sets of `r`.
|
|
2979
3118
|
*/
|
|
2980
3119
|
/**
|
|
2981
|
-
* The smallest sample `m` at which the method `"auto"` uses the method `"tight"`. The
|
|
2982
|
-
*
|
|
2983
|
-
*
|
|
3120
|
+
* The smallest sample `m` at which the method `"auto"` uses the method `"tight"`. The calibration study
|
|
3121
|
+
* `u-statistic-interval` gives the value with `coverageMinSample`. The value is the smallest `m` of the grid such that
|
|
3122
|
+
* each `m` of the grid from this value up passes the bar. Until a run of the study gives a value, the value is
|
|
3123
|
+
* infinite. The TSDoc of `uStatisticInterval` gives the bar of the study.
|
|
2984
3124
|
*/
|
|
2985
3125
|
const U_STATISTIC_TIGHT_MIN_SAMPLE = Number.POSITIVE_INFINITY;
|
|
2986
3126
|
/**
|
|
@@ -3041,10 +3181,20 @@ const U_STATISTIC_TIGHT_MIN_SAMPLE = Number.POSITIVE_INFINITY;
|
|
|
3041
3181
|
* The method `"auto"` needs `range`. It uses the method `"tight"` when `m` is `U_STATISTIC_TIGHT_MIN_SAMPLE` or more
|
|
3042
3182
|
* and the variance is more than 0. Otherwise, it uses the method `"conservative"`.
|
|
3043
3183
|
*
|
|
3044
|
-
* The
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
*
|
|
3184
|
+
* The calibration study `u-statistic-interval` sets `U_STATISTIC_TIGHT_MIN_SAMPLE`. Its bar was fixed before its run.
|
|
3185
|
+
* The study draws 1000 samples in each cell of the 3 kernels and the 17 cells of the table. The method `"tight"`
|
|
3186
|
+
* qualifies at `m` when the cells with a sample of `m` or more pass two tests.
|
|
3187
|
+
*
|
|
3188
|
+
* First, no cell rejects the coverage 0.95 in a one-sided exact binomial test at alpha 0.05, with the Holm
|
|
3189
|
+
* correction over these cells. Second, the Wilson lower end of the pooled coverage of these cells is 0.94 or more.
|
|
3190
|
+
* `coverageMinSample` applies this bar.
|
|
3191
|
+
*
|
|
3192
|
+
* The set of sizes is closed upward. The constant is the smallest `m` of the grid such that each `m` of the grid from
|
|
3193
|
+
* this value up qualifies. A small `m` pools the good cells of the larger sizes, so it can pass while a larger `m`
|
|
3194
|
+
* fails alone. The upward rule keeps the method `"auto"` away from a size that failed.
|
|
3195
|
+
*
|
|
3196
|
+
* When the largest `m` fails, the constant is infinite. Until a run of the study gives a value, the constant is infinite. Thus the method `"auto"`
|
|
3197
|
+
* uses the method `"conservative"` at each `m` less than `n`.
|
|
3048
3198
|
*
|
|
3049
3199
|
* With `range`, the interval of the mean stays in `[a, b]`, and each total is in the range of a sum of
|
|
3050
3200
|
* `C(m - 1, r - 1)` kernel values.
|
|
@@ -3148,6 +3298,416 @@ function choose(n, k) {
|
|
|
3148
3298
|
for (let j = 0; j < k; j++) c = c * (n - j) / (j + 1);
|
|
3149
3299
|
return c;
|
|
3150
3300
|
}
|
|
3151
|
-
|
|
3301
|
+
// src/rating.ts
|
|
3302
|
+
function finite(name, label, value) {
|
|
3303
|
+
if (!Number.isFinite(value)) throw new RangeError(`${name}: ${label} is a finite number, not ${value}.`);
|
|
3304
|
+
}
|
|
3305
|
+
function positive(name, label, value) {
|
|
3306
|
+
if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`${name}: ${label} is a finite number above 0, not ${value}.`);
|
|
3307
|
+
}
|
|
3308
|
+
function checkedScale(name, options) {
|
|
3309
|
+
const center = options.center ?? 1500;
|
|
3310
|
+
const scale = options.scale ?? 400;
|
|
3311
|
+
const base = options.base ?? 10;
|
|
3312
|
+
finite(name, "the center", center);
|
|
3313
|
+
positive(name, "the scale", scale);
|
|
3314
|
+
if (!(base > 1 && Number.isFinite(base))) throw new RangeError(`${name}: the base is a finite number above 1, not ${base}.`);
|
|
3315
|
+
return {
|
|
3316
|
+
center,
|
|
3317
|
+
scale,
|
|
3318
|
+
base
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* The expected score of the first item against the second item: `1 / (1 + base ** ((rb - ra) / scale))`. A win
|
|
3323
|
+
* counts 1, a tie 0.5 and a loss 0. The expected score of the second item is 1 minus this value.
|
|
3324
|
+
*/
|
|
3325
|
+
function eloExpected(ra, rb, options = {}) {
|
|
3326
|
+
finite("eloExpected", "the first rating", ra);
|
|
3327
|
+
finite("eloExpected", "the second rating", rb);
|
|
3328
|
+
const { scale, base } = checkedScale("eloExpected", options);
|
|
3329
|
+
return 1 / (1 + base ** ((rb - ra) / scale));
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* The Elo update of a pair after one result. The `score` is the score of the first item: 1 for a win, 0.5 for a tie
|
|
3333
|
+
* and 0 for a loss. Each rating moves by `k` times the difference between the score and the expected score. Thus
|
|
3334
|
+
* the sum of the two ratings does not change.
|
|
3335
|
+
*/
|
|
3336
|
+
function eloUpdate(ra, rb, score, options = {}) {
|
|
3337
|
+
if (!(score >= 0 && score <= 1)) throw new RangeError(`eloUpdate: the score is from 0 to 1, not ${score}.`);
|
|
3338
|
+
const k = options.k ?? 32;
|
|
3339
|
+
if (!(k >= 0 && Number.isFinite(k))) throw new RangeError(`eloUpdate: k is a finite number of 0 or more, not ${k}.`);
|
|
3340
|
+
const expected = eloExpected(ra, rb, options);
|
|
3341
|
+
const change = k * (score - expected);
|
|
3342
|
+
return {
|
|
3343
|
+
a: ra + change,
|
|
3344
|
+
b: rb - change,
|
|
3345
|
+
expected
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
/** The factor between the Glicko scale and the Glicko-2 scale: `400 / ln(10)`, to 4 places as in the paper. */
|
|
3349
|
+
const GLICKO2_FACTOR = 173.7178;
|
|
3350
|
+
const GLICKO2_CENTER = 1500;
|
|
3351
|
+
const VOLATILITY_STEPS = 1e3;
|
|
3352
|
+
function checkedRating(name, player) {
|
|
3353
|
+
finite(name, "the rating", player.rating);
|
|
3354
|
+
positive(name, "the RD", player.rd);
|
|
3355
|
+
positive(name, "the volatility", player.volatility);
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* The Glicko-2 update of one player after one rating period (Glickman, "Example of the Glicko-2 system", 2012). The
|
|
3359
|
+
* function uses the Illinois algorithm for the new volatility. The games use the ratings of the opponents from the
|
|
3360
|
+
* start of the period. An empty list of games gives `glicko2Idle(player)`.
|
|
3361
|
+
*/
|
|
3362
|
+
function glicko2Update(player, games, options = {}) {
|
|
3363
|
+
checkedRating("glicko2Update", player);
|
|
3364
|
+
const tau = options.tau ?? .5;
|
|
3365
|
+
const tolerance = options.tolerance ?? 1e-6;
|
|
3366
|
+
positive("glicko2Update", "tau", tau);
|
|
3367
|
+
positive("glicko2Update", "the tolerance", tolerance);
|
|
3368
|
+
for (const game of games) {
|
|
3369
|
+
finite("glicko2Update", "the rating of an opponent", game.rating);
|
|
3370
|
+
positive("glicko2Update", "the RD of an opponent", game.rd);
|
|
3371
|
+
if (!(game.score >= 0 && game.score <= 1)) throw new RangeError(`glicko2Update: a score is from 0 to 1, not ${game.score}.`);
|
|
3372
|
+
}
|
|
3373
|
+
if (games.length === 0) return glicko2Idle(player);
|
|
3374
|
+
const mu = (player.rating - GLICKO2_CENTER) / GLICKO2_FACTOR;
|
|
3375
|
+
const phi = player.rd / GLICKO2_FACTOR;
|
|
3376
|
+
const sigma = player.volatility;
|
|
3377
|
+
let information = 0;
|
|
3378
|
+
let improvement = 0;
|
|
3379
|
+
for (const game of games) {
|
|
3380
|
+
const muJ = (game.rating - GLICKO2_CENTER) / GLICKO2_FACTOR;
|
|
3381
|
+
const phiJ = game.rd / GLICKO2_FACTOR;
|
|
3382
|
+
const g = 1 / Math.sqrt(1 + 3 * phiJ * phiJ / (Math.PI * Math.PI));
|
|
3383
|
+
const e = 1 / (1 + Math.exp(-g * (mu - muJ)));
|
|
3384
|
+
information += g * g * e * (1 - e);
|
|
3385
|
+
improvement += g * (game.score - e);
|
|
3386
|
+
}
|
|
3387
|
+
const v = 1 / information;
|
|
3388
|
+
const newSigma = newVolatility(phi, sigma, v, v * improvement, tau, tolerance);
|
|
3389
|
+
const phiStar = Math.sqrt(phi * phi + newSigma * newSigma);
|
|
3390
|
+
const newPhi = 1 / Math.sqrt(1 / (phiStar * phiStar) + 1 / v);
|
|
3391
|
+
const newMu = mu + newPhi * newPhi * improvement;
|
|
3392
|
+
return {
|
|
3393
|
+
rating: GLICKO2_FACTOR * newMu + GLICKO2_CENTER,
|
|
3394
|
+
rd: GLICKO2_FACTOR * newPhi,
|
|
3395
|
+
volatility: newSigma
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
/** Step 5 of the paper: the new volatility, by the Illinois algorithm on `x = ln(sigma^2)`. */
|
|
3399
|
+
function newVolatility(phi, sigma, v, delta, tau, tolerance) {
|
|
3400
|
+
const a = Math.log(sigma * sigma);
|
|
3401
|
+
const phi2 = phi * phi;
|
|
3402
|
+
const delta2 = delta * delta;
|
|
3403
|
+
const f = (x) => {
|
|
3404
|
+
const ex = Math.exp(x);
|
|
3405
|
+
const sum = phi2 + v + ex;
|
|
3406
|
+
return ex * (delta2 - phi2 - v - ex) / (2 * sum * sum) - (x - a) / (tau * tau);
|
|
3407
|
+
};
|
|
3408
|
+
let lowEnd = a;
|
|
3409
|
+
let highEnd;
|
|
3410
|
+
if (delta2 > phi2 + v) highEnd = Math.log(delta2 - phi2 - v);
|
|
3411
|
+
else {
|
|
3412
|
+
let k = 1;
|
|
3413
|
+
while (f(a - k * tau) < 0 && k < VOLATILITY_STEPS) k++;
|
|
3414
|
+
highEnd = a - k * tau;
|
|
3415
|
+
}
|
|
3416
|
+
let fLow = f(lowEnd);
|
|
3417
|
+
let fHigh = f(highEnd);
|
|
3418
|
+
for (let step = 0; Math.abs(highEnd - lowEnd) > tolerance && step < VOLATILITY_STEPS; step++) {
|
|
3419
|
+
const c = lowEnd + (lowEnd - highEnd) * fLow / (fHigh - fLow);
|
|
3420
|
+
const fC = f(c);
|
|
3421
|
+
if (fC * fHigh <= 0) {
|
|
3422
|
+
lowEnd = highEnd;
|
|
3423
|
+
fLow = fHigh;
|
|
3424
|
+
} else fLow /= 2;
|
|
3425
|
+
highEnd = c;
|
|
3426
|
+
fHigh = fC;
|
|
3427
|
+
}
|
|
3428
|
+
return Math.exp(lowEnd / 2);
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* The Glicko-2 rating of a player after a rating period with no games. The rating and the volatility stay the same.
|
|
3432
|
+
* The RD grows to `sqrt(RD^2 + (173.7178 * volatility)^2)`.
|
|
3433
|
+
*/
|
|
3434
|
+
function glicko2Idle(player) {
|
|
3435
|
+
checkedRating("glicko2Idle", player);
|
|
3436
|
+
const phi = player.rd / GLICKO2_FACTOR;
|
|
3437
|
+
const rd = GLICKO2_FACTOR * Math.sqrt(phi * phi + player.volatility * player.volatility);
|
|
3438
|
+
return {
|
|
3439
|
+
rating: player.rating,
|
|
3440
|
+
rd,
|
|
3441
|
+
volatility: player.volatility
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* This function gives the three probabilities of a pair from the log-strengths `sa` and `sb` and the Davidson tie
|
|
3446
|
+
* parameter `nu`. The denominator is `exp(d / 2) + exp(-d / 2) + nu`, with `d = sa - sb`. The function does no
|
|
3447
|
+
* checks, because the fit uses it in its inner loop.
|
|
3448
|
+
*/
|
|
3449
|
+
function pairOutcomes(sa, sb, nu) {
|
|
3450
|
+
const half = (sa - sb) / 2;
|
|
3451
|
+
const top = Math.max(Math.abs(half), nu > 0 ? Math.log(nu) : Number.NEGATIVE_INFINITY);
|
|
3452
|
+
const ea = Math.exp(half - top);
|
|
3453
|
+
const eb = Math.exp(-half - top);
|
|
3454
|
+
const et = nu > 0 ? Math.exp(Math.log(nu) - top) : 0;
|
|
3455
|
+
const sum = ea + eb + et;
|
|
3456
|
+
return {
|
|
3457
|
+
a: ea / sum,
|
|
3458
|
+
b: eb / sum,
|
|
3459
|
+
tie: et / sum
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* The probabilities of a pair under the Bradley-Terry model with Davidson (1970) ties. The inputs are the
|
|
3464
|
+
* log-strengths of the two items and the tie parameter `tie` of `bradleyTerry`. With `tie` 0, the probability that
|
|
3465
|
+
* the first item wins is `1 / (1 + exp(sb - sa))`. A constant added to the two strengths changes no probability.
|
|
3466
|
+
*/
|
|
3467
|
+
function pairProbabilities(sa, sb, tie = 0) {
|
|
3468
|
+
finite("pairProbabilities", "the first strength", sa);
|
|
3469
|
+
finite("pairProbabilities", "the second strength", sb);
|
|
3470
|
+
if (!(tie >= 0 && Number.isFinite(tie))) throw new RangeError(`pairProbabilities: the tie parameter is a finite number of 0 or more, not ${tie}.`);
|
|
3471
|
+
return pairOutcomes(sa, sb, tie);
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* The rating of a log-strength: `center + scale * strength / ln(base)`. The defaults give
|
|
3475
|
+
* `1500 + 400 * log10(e) * strength`. Thus `eloExpected` of two ratings is the Bradley-Terry win probability.
|
|
3476
|
+
*/
|
|
3477
|
+
function strengthToRating(strength, options = {}) {
|
|
3478
|
+
finite("strengthToRating", "the strength", strength);
|
|
3479
|
+
const { center, scale, base } = checkedScale("strengthToRating", options);
|
|
3480
|
+
return center + scale * strength / Math.log(base);
|
|
3481
|
+
}
|
|
3482
|
+
/** The log-strength of a rating. It is the inverse of `strengthToRating` with the same options. */
|
|
3483
|
+
function ratingToStrength(rating, options = {}) {
|
|
3484
|
+
finite("ratingToStrength", "the rating", rating);
|
|
3485
|
+
const { center, scale, base } = checkedScale("ratingToStrength", options);
|
|
3486
|
+
return (rating - center) * Math.log(base) / scale;
|
|
3487
|
+
}
|
|
3488
|
+
// src/bradley-terry.ts
|
|
3489
|
+
/**
|
|
3490
|
+
* The Bradley-Terry fit of pairwise comparisons among `n` items. Each item has a log-strength. The fit is the
|
|
3491
|
+
* maximum a posteriori estimate with a normal (ridge) prior on each log-strength. Thus an item with only wins has a
|
|
3492
|
+
* finite strength, and the strengths have one solution.
|
|
3493
|
+
*
|
|
3494
|
+
* Ties use the Davidson (1970) model, or count as half a win and half a loss. The fit uses Newton steps with a line search on the negative log posterior. This function is
|
|
3495
|
+
* convex, so the steps go to its one minimum.
|
|
3496
|
+
*/
|
|
3497
|
+
const ARMIJO = 1e-4;
|
|
3498
|
+
const HALVINGS = 60;
|
|
3499
|
+
const ROUNDING = 64 * Number.EPSILON;
|
|
3500
|
+
function checkedTerms(n, comparisons, ties) {
|
|
3501
|
+
const terms = [];
|
|
3502
|
+
for (const [i, comparison] of comparisons.entries()) {
|
|
3503
|
+
const { a, b, outcome } = comparison;
|
|
3504
|
+
const w = comparison.weight ?? 1;
|
|
3505
|
+
for (const index of [a, b]) if (!Number.isSafeInteger(index) || index < 0 || index >= n) throw new RangeError(`bradleyTerry: comparison ${i} has the index ${index}, and the indexes are whole numbers from 0 to ${n - 1}.`);
|
|
3506
|
+
if (a === b) throw new RangeError(`bradleyTerry: comparison ${i} compares the item ${a} with itself.`);
|
|
3507
|
+
if (outcome !== "a" && outcome !== "b" && outcome !== "tie") throw new RangeError(`bradleyTerry: comparison ${i} has the outcome ${String(outcome)}.`);
|
|
3508
|
+
if (!(w >= 0 && Number.isFinite(w))) throw new RangeError(`bradleyTerry: comparison ${i} has the weight ${w}, and a weight is a finite number of 0 or more.`);
|
|
3509
|
+
if (w === 0) continue;
|
|
3510
|
+
const c = outcome === "a" ? .5 : outcome === "b" ? -.5 : 0;
|
|
3511
|
+
terms.push({
|
|
3512
|
+
a,
|
|
3513
|
+
b,
|
|
3514
|
+
w,
|
|
3515
|
+
c,
|
|
3516
|
+
tie: outcome === "tie" && ties === "davidson" ? 1 : 0
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3519
|
+
return terms;
|
|
3520
|
+
}
|
|
3521
|
+
function positiveOption(label, value) {
|
|
3522
|
+
if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`bradleyTerry: ${label} is a finite number above 0, not ${value}.`);
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* The Bradley-Terry fit of `comparisons` among `items` items. The function does not sort or change its inputs. An
|
|
3526
|
+
* empty list of comparisons gives the prior for each item. The function throws a RangeError for a bad index, a bad
|
|
3527
|
+
* outcome, a bad weight and a bad option.
|
|
3528
|
+
*/
|
|
3529
|
+
function bradleyTerry(items, comparisons, options = {}) {
|
|
3530
|
+
if (!Number.isSafeInteger(items) || items < 1) throw new RangeError(`bradleyTerry: the number of items is a whole number of 1 or more, not ${items}.`);
|
|
3531
|
+
const priorMean = options.priorMean ?? 0;
|
|
3532
|
+
const priorSd = options.priorSd ?? 1;
|
|
3533
|
+
const ties = options.ties ?? "davidson";
|
|
3534
|
+
const tiePriorSd = options.tiePriorSd ?? 10;
|
|
3535
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
3536
|
+
const tolerance = options.tolerance ?? 1e-10;
|
|
3537
|
+
if (!Number.isFinite(priorMean)) throw new RangeError(`bradleyTerry: the prior mean is a finite number, not ${priorMean}.`);
|
|
3538
|
+
positiveOption("the prior SD", priorSd);
|
|
3539
|
+
positiveOption("the tie prior SD", tiePriorSd);
|
|
3540
|
+
positiveOption("the tolerance", tolerance);
|
|
3541
|
+
if (ties !== "davidson" && ties !== "half") throw new RangeError(`bradleyTerry: the tie model is "davidson" or "half", not ${String(ties)}.`);
|
|
3542
|
+
if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) throw new RangeError(`bradleyTerry: maxIterations is a whole number of 1 or more, not ${maxIterations}.`);
|
|
3543
|
+
const terms = checkedTerms(items, comparisons, ties);
|
|
3544
|
+
const fitTie = terms.some((t) => t.tie === 1);
|
|
3545
|
+
const k = fitTie ? items + 1 : items;
|
|
3546
|
+
const model = {
|
|
3547
|
+
items,
|
|
3548
|
+
terms,
|
|
3549
|
+
fitTie,
|
|
3550
|
+
priorMean,
|
|
3551
|
+
priorPrecision: 1 / (priorSd * priorSd),
|
|
3552
|
+
tiePrecision: 1 / (tiePriorSd * tiePriorSd)
|
|
3553
|
+
};
|
|
3554
|
+
const x = new Float64Array(k).fill(priorMean);
|
|
3555
|
+
if (fitTie) x[items] = 0;
|
|
3556
|
+
let iterations = 0;
|
|
3557
|
+
let converged = false;
|
|
3558
|
+
let value = objective(model, x);
|
|
3559
|
+
while (iterations < maxIterations) {
|
|
3560
|
+
iterations++;
|
|
3561
|
+
const { gradient, hessian } = derivatives(model, x);
|
|
3562
|
+
const step = solve(hessian, gradient, k);
|
|
3563
|
+
let slope = 0;
|
|
3564
|
+
for (let i = 0; i < k; i++) slope += gradient[i] * step[i];
|
|
3565
|
+
let t = 1;
|
|
3566
|
+
let next = moved(x, step, t);
|
|
3567
|
+
let nextValue = objective(model, next);
|
|
3568
|
+
const slack = ROUNDING * (Math.abs(value) + 1);
|
|
3569
|
+
for (let h = 0; h < HALVINGS && !(nextValue <= value + ARMIJO * t * slope + slack); h++) {
|
|
3570
|
+
t /= 2;
|
|
3571
|
+
next = moved(x, step, t);
|
|
3572
|
+
nextValue = objective(model, next);
|
|
3573
|
+
}
|
|
3574
|
+
let largest = 0;
|
|
3575
|
+
for (let i = 0; i < k; i++) largest = Math.max(largest, Math.abs(step[i]));
|
|
3576
|
+
if (nextValue <= value + slack) {
|
|
3577
|
+
x.set(next);
|
|
3578
|
+
value = nextValue;
|
|
3579
|
+
}
|
|
3580
|
+
if (largest <= tolerance) {
|
|
3581
|
+
converged = true;
|
|
3582
|
+
break;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
const { hessian } = derivatives(model, x);
|
|
3586
|
+
const variance = inverseDiagonal(hessian, k);
|
|
3587
|
+
const nu = fitTie ? Math.exp(x[items]) : 0;
|
|
3588
|
+
let logLikelihood = 0;
|
|
3589
|
+
for (const term of terms) logLikelihood += term.w * termLogLikelihood(term, x[term.a], x[term.b], nu);
|
|
3590
|
+
return {
|
|
3591
|
+
strengths: Array.from(x.subarray(0, items)),
|
|
3592
|
+
se: Array.from(variance.subarray(0, items), (v) => Math.sqrt(v)),
|
|
3593
|
+
tie: nu,
|
|
3594
|
+
logLikelihood,
|
|
3595
|
+
iterations,
|
|
3596
|
+
converged
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
function moved(x, step, t) {
|
|
3600
|
+
const out = new Float64Array(x.length);
|
|
3601
|
+
for (let i = 0; i < x.length; i++) out[i] = x[i] + t * step[i];
|
|
3602
|
+
return out;
|
|
3603
|
+
}
|
|
3604
|
+
/** The log of the denominator `exp(d / 2) + exp(-d / 2) + nu`, with no overflow. */
|
|
3605
|
+
function logDenominator(d, logNu) {
|
|
3606
|
+
const half = Math.abs(d) / 2;
|
|
3607
|
+
const top = Math.max(half, logNu);
|
|
3608
|
+
return top + Math.log(Math.exp(half - top) + Math.exp(-half - top) + Math.exp(logNu - top));
|
|
3609
|
+
}
|
|
3610
|
+
/** The log-likelihood of one comparison with weight 1. A `"half"` tie has the coefficient 0: the mean of the two log-probabilities of a win. */
|
|
3611
|
+
function termLogLikelihood(term, sa, sb, nu) {
|
|
3612
|
+
const d = sa - sb;
|
|
3613
|
+
const logNu = nu > 0 ? Math.log(nu) : Number.NEGATIVE_INFINITY;
|
|
3614
|
+
const logD = logDenominator(d, logNu);
|
|
3615
|
+
return term.tie === 1 ? logNu - logD : term.c * d - logD;
|
|
3616
|
+
}
|
|
3617
|
+
/** The negative log posterior at `x`. */
|
|
3618
|
+
function objective(model, x) {
|
|
3619
|
+
const logNu = model.fitTie ? x[model.items] : Number.NEGATIVE_INFINITY;
|
|
3620
|
+
let sum = 0;
|
|
3621
|
+
for (const term of model.terms) {
|
|
3622
|
+
const d = x[term.a] - x[term.b];
|
|
3623
|
+
sum += term.w * (logDenominator(d, logNu) - term.c * d - (term.tie === 1 ? logNu : 0));
|
|
3624
|
+
}
|
|
3625
|
+
for (let i = 0; i < model.items; i++) sum += model.priorPrecision * (x[i] - model.priorMean) ** 2 / 2;
|
|
3626
|
+
if (model.fitTie) sum += model.tiePrecision * logNu * logNu / 2;
|
|
3627
|
+
return sum;
|
|
3628
|
+
}
|
|
3629
|
+
/** The gradient and the Hessian (in row order) of the negative log posterior at `x`. */
|
|
3630
|
+
function derivatives(model, x) {
|
|
3631
|
+
const k = x.length;
|
|
3632
|
+
const n = model.items;
|
|
3633
|
+
const gradient = new Float64Array(k);
|
|
3634
|
+
const hessian = new Float64Array(k * k);
|
|
3635
|
+
const nu = model.fitTie ? Math.exp(x[n]) : 0;
|
|
3636
|
+
for (const term of model.terms) {
|
|
3637
|
+
const { a, b, w } = term;
|
|
3638
|
+
const p = pairOutcomes(x[a], x[b], nu);
|
|
3639
|
+
const q = (p.a - p.b) / 2;
|
|
3640
|
+
const gd = w * (q - term.c);
|
|
3641
|
+
const hdd = w * ((p.a + p.b) / 4 - q * q);
|
|
3642
|
+
gradient[a] = gradient[a] + gd;
|
|
3643
|
+
gradient[b] = gradient[b] - gd;
|
|
3644
|
+
hessian[a * k + a] = hessian[a * k + a] + hdd;
|
|
3645
|
+
hessian[b * k + b] = hessian[b * k + b] + hdd;
|
|
3646
|
+
hessian[a * k + b] = hessian[a * k + b] - hdd;
|
|
3647
|
+
hessian[b * k + a] = hessian[b * k + a] - hdd;
|
|
3648
|
+
if (model.fitTie) {
|
|
3649
|
+
const hdl = -w * q * p.tie;
|
|
3650
|
+
gradient[n] = gradient[n] + w * (p.tie - term.tie);
|
|
3651
|
+
hessian[n * k + n] = hessian[n * k + n] + w * p.tie * (1 - p.tie);
|
|
3652
|
+
hessian[a * k + n] = hessian[a * k + n] + hdl;
|
|
3653
|
+
hessian[n * k + a] = hessian[n * k + a] + hdl;
|
|
3654
|
+
hessian[b * k + n] = hessian[b * k + n] - hdl;
|
|
3655
|
+
hessian[n * k + b] = hessian[n * k + b] - hdl;
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
for (let i = 0; i < n; i++) {
|
|
3659
|
+
gradient[i] = gradient[i] + model.priorPrecision * (x[i] - model.priorMean);
|
|
3660
|
+
hessian[i * k + i] = hessian[i * k + i] + model.priorPrecision;
|
|
3661
|
+
}
|
|
3662
|
+
if (model.fitTie) {
|
|
3663
|
+
gradient[n] = gradient[n] + model.tiePrecision * x[n];
|
|
3664
|
+
hessian[n * k + n] = hessian[n * k + n] + model.tiePrecision;
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
gradient,
|
|
3668
|
+
hessian
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
/** The Cholesky factor of a copy of the Hessian. The priors make the Hessian positive definite. */
|
|
3672
|
+
function factored(hessian, k) {
|
|
3673
|
+
const l = Float64Array.from(hessian);
|
|
3674
|
+
if (!factor(l, k)) throw new RangeError("bradleyTerry: the Hessian is not positive definite. Make the prior SD smaller.");
|
|
3675
|
+
return l;
|
|
3676
|
+
}
|
|
3677
|
+
/** The Newton step: the solution of `hessian * step = -gradient`. */
|
|
3678
|
+
function solve(hessian, gradient, k) {
|
|
3679
|
+
const l = factored(hessian, k);
|
|
3680
|
+
const y = new Float64Array(k);
|
|
3681
|
+
for (let i = 0; i < k; i++) {
|
|
3682
|
+
let s = -gradient[i];
|
|
3683
|
+
for (let j = 0; j < i; j++) s -= l[i * k + j] * y[j];
|
|
3684
|
+
y[i] = s / l[i * k + i];
|
|
3685
|
+
}
|
|
3686
|
+
for (let i = k - 1; i >= 0; i--) {
|
|
3687
|
+
let s = y[i];
|
|
3688
|
+
for (let j = i + 1; j < k; j++) s -= l[j * k + i] * y[j];
|
|
3689
|
+
y[i] = s / l[i * k + i];
|
|
3690
|
+
}
|
|
3691
|
+
return y;
|
|
3692
|
+
}
|
|
3693
|
+
/** The diagonal of the inverse of the Hessian: the column sums of the squares of the inverse of its Cholesky factor. */
|
|
3694
|
+
function inverseDiagonal(hessian, k) {
|
|
3695
|
+
const l = factored(hessian, k);
|
|
3696
|
+
const out = new Float64Array(k);
|
|
3697
|
+
const column = new Float64Array(k);
|
|
3698
|
+
for (let c = 0; c < k; c++) {
|
|
3699
|
+
column.fill(0);
|
|
3700
|
+
for (let i = c; i < k; i++) {
|
|
3701
|
+
let s = i === c ? 1 : 0;
|
|
3702
|
+
for (let j = c; j < i; j++) s -= l[i * k + j] * column[j];
|
|
3703
|
+
column[i] = s / l[i * k + i];
|
|
3704
|
+
}
|
|
3705
|
+
let sum = 0;
|
|
3706
|
+
for (let i = c; i < k; i++) sum += column[i] ** 2;
|
|
3707
|
+
out[c] = sum;
|
|
3708
|
+
}
|
|
3709
|
+
return out;
|
|
3710
|
+
}
|
|
3711
|
+
export { KENDALL_EXACT, RESPONSE_LAMBDAS, RESPONSE_MAX_POWER, RESPONSE_MAX_TERMS, RESPONSE_RECORDS_SCHEMA, U_STATISTIC_TIGHT_MIN_SAMPLE, Z95, bestSmoothLaw, binomialInterval, binomialTest, bootstrap, bootstrapClusters, bootstrapLeastSquares, bootstrapMedian, bootstrapMedianRatio, bootstrapTwo, bradleyTerry, brier, calibration, cohenKappa, compareRatios, coverageMinSample, eloExpected, eloUpdate, fitResponse, glicko2Idle, glicko2Update, heldOutError, heldOutErrorAt, holm, kendallTau, leastSquares, logLoss, mean, median, medianInterval, modeCount, negativeBinomialInterval, noiseFloor, noiseVerdict, optionFloor, pairProbabilities, pairedPermutation, parseResponseRecord, partialConjunction, predictResponse, quantile, quantileInterval, quantileSamples, quantileSorted, ranks, ratingToStrength, responseCells, ridgeSolve, sampleTotalInterval, sd, signTest, signTestPower, sliceResponse, smoothLawScores, sorted, spearman, strengthToRating, summarize, surfaceResponse, tQuantile, twoSamplePermutation, uStatisticInterval, whenHolds, wilson, wilsonSampleSize };
|
|
3152
3712
|
|
|
3153
3713
|
//# sourceMappingURL=index.js.map
|