@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/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$1(name, value) {
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$1("signTest", better);
2279
- whole$1("signTest", worse);
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$1("signTestPower", n);
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$1("wilson", k);
2301
- whole$1("wilson", n);
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$1("partialConjunction", need);
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$1("wilsonSampleSize", max);
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 value comes from the pinned
2982
- * counts of the tests. No measured `m` obeys the rule, so the value is infinite. The TSDoc of `uStatisticInterval`
2983
- * gives the rule and the counts.
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 rule for `U_STATISTIC_TIGHT_MIN_SAMPLE` is the smallest `m` of the table with 190 of 200 or more in each cell.
3045
- * No `m` of the table obeys the rule. The last `m` with a count below 190 is 108, the largest `m` of the table. The
3046
- * table has no larger `m`, so the constant is infinite. Thus the method `"auto"` uses the method `"conservative"` at
3047
- * each `m` less than `n`.
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
- export { KENDALL_EXACT, RESPONSE_LAMBDAS, RESPONSE_MAX_POWER, RESPONSE_MAX_TERMS, RESPONSE_RECORDS_SCHEMA, U_STATISTIC_TIGHT_MIN_SAMPLE, Z95, bestSmoothLaw, binomialInterval, bootstrap, bootstrapClusters, bootstrapLeastSquares, bootstrapMedian, bootstrapMedianRatio, bootstrapTwo, brier, calibration, cohenKappa, compareRatios, fitResponse, heldOutError, heldOutErrorAt, holm, kendallTau, leastSquares, mean, median, medianInterval, modeCount, negativeBinomialInterval, noiseFloor, noiseVerdict, optionFloor, pairedPermutation, parseResponseRecord, partialConjunction, predictResponse, quantile, quantileInterval, quantileSamples, quantileSorted, ranks, responseCells, ridgeSolve, sampleTotalInterval, sd, signTest, signTestPower, sliceResponse, smoothLawScores, sorted, spearman, summarize, surfaceResponse, tQuantile, twoSamplePermutation, uStatisticInterval, whenHolds, wilson, wilsonSampleSize };
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