@mark1russell7/rigor-statistics 0.1.20 → 0.1.22

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
@@ -2242,6 +2242,168 @@ function fitResponse(data, options = {}) {
2242
2242
  folds: foldRecords
2243
2243
  };
2244
2244
  }
2245
+ // src/response-sobol.ts
2246
+ /**
2247
+ * The Sobol indices of a fitted model of a record of the format `response-records/1`. The first-order index of a
2248
+ * dimension is the share of the variance of the prediction that the dimension causes alone. The total index also
2249
+ * includes each interaction of the dimension with the other dimensions.
2250
+ *
2251
+ * The method is Monte Carlo with two independent matrices A and B of N rows, and one matrix `AB_i` for each dimension `i`.
2252
+ * `AB_i` is A with the column `i` of B (Saltelli and others, 2010). The first-order index uses the estimator of Saltelli
2253
+ * (2010), and the total index uses the estimator of Jansen (1999). The model gets N * (d + 2) evaluations for d dimensions.
2254
+ *
2255
+ * This file uses no Node import, no clock and no `Math.random`, the same as `response.ts`.
2256
+ */
2257
+ /** This function draws one value of a dimension. Each dimension uses the same number of draws: one. */
2258
+ function drawValue(dimension, r) {
2259
+ if (dimension.kind !== "continuous") return dimension.levels[r.below(dimension.levels.length)];
2260
+ const [low, high] = dimension.range;
2261
+ if (dimension.log !== true) return r.range(low, high);
2262
+ const value = 10 ** r.range(Math.log10(low), Math.log10(high));
2263
+ return Math.min(high, Math.max(low, value));
2264
+ }
2265
+ /** This function draws a matrix of `n` rows, row by row, with one draw for each dimension in the order of the record. */
2266
+ function drawMatrix(dimensions, n, r) {
2267
+ return Array.from({ length: n }, () => dimensions.map((d) => drawValue(d, r)));
2268
+ }
2269
+ /** This function gives the inputs of a row. A conditional dimension gets `null` where its condition fails. */
2270
+ function inputsOf(dimensions, row) {
2271
+ const drawn = {};
2272
+ dimensions.forEach((d, i) => {
2273
+ drawn[d.id] = row[i];
2274
+ });
2275
+ const inputs = {};
2276
+ for (const d of dimensions) inputs[d.id] = whenHolds(d, drawn) ? drawn[d.id] : null;
2277
+ return inputs;
2278
+ }
2279
+ /**
2280
+ * This function gives the variance and the indices from the rows `rows`. A variance of 0 gives each index as 0.
2281
+ * The value `constant` is true when each output is equal to the first output.
2282
+ */
2283
+ function estimate(outputs, rows, n, constant) {
2284
+ const d = outputs.ab.length;
2285
+ const at = (j) => rows === null ? j : rows[j];
2286
+ let sum = 0;
2287
+ for (let j = 0; j < n; j++) sum += outputs.a[at(j)] + outputs.b[at(j)];
2288
+ const mean = sum / (2 * n);
2289
+ let square = 0;
2290
+ for (let j = 0; j < n; j++) {
2291
+ const ea = outputs.a[at(j)] - mean;
2292
+ const eb = outputs.b[at(j)] - mean;
2293
+ square += ea * ea + eb * eb;
2294
+ }
2295
+ const variance = constant ? 0 : square / (2 * n);
2296
+ const first = [];
2297
+ const total = [];
2298
+ for (let i = 0; i < d; i++) {
2299
+ const abi = outputs.ab[i];
2300
+ let s = 0;
2301
+ let t = 0;
2302
+ for (let j = 0; j < n; j++) {
2303
+ const fa = outputs.a[at(j)];
2304
+ const fb = outputs.b[at(j)];
2305
+ const fab = abi[at(j)];
2306
+ s += (fb - mean) * (fab - fa);
2307
+ t += (fa - fab) * (fa - fab);
2308
+ }
2309
+ first.push(variance > 0 ? s / n / variance : 0);
2310
+ total.push(variance > 0 ? t / (2 * n) / variance : 0);
2311
+ }
2312
+ return {
2313
+ mean,
2314
+ variance,
2315
+ first,
2316
+ total
2317
+ };
2318
+ }
2319
+ /**
2320
+ * This function gives the first-order and the total Sobol index of each dimension of a record, for the model of
2321
+ * `measure`. The indices are of the prediction of the model, not of the measured points.
2322
+ *
2323
+ * The input distribution:
2324
+ * - Each dimension is independent of the other dimensions.
2325
+ * - A continuous dimension is uniform over its range. With `log`, the logarithm of the value is uniform.
2326
+ * - An ordinal or a categorical dimension is uniform over its levels.
2327
+ * - A conditional dimension gets an independent draw. The model gets `null` for it where `whenHolds` fails for the drawn parent.
2328
+ *
2329
+ * Thus the inputs stay independent, and the indices have their usual meaning. A parent gets the effect of the absence of its child.
2330
+ *
2331
+ * The estimators:
2332
+ * - The first-order index is `mean(f(B) * (f(AB_i) - f(A))) / V` (Saltelli, 2010).
2333
+ * - The total index is `mean((f(A) - f(AB_i)) ** 2) / 2 / V` (Jansen, 1999).
2334
+ * - V is the variance of the outputs of A and B together.
2335
+ * - The variance uses the outputs of A and B less their mean. The first-order sum uses `f(B)` less that mean.
2336
+ * - The total sum uses differences of outputs, so it uses no mean.
2337
+ * - This step keeps the estimators unbiased, and it decreases their error when the mean is large.
2338
+ *
2339
+ * A model with a constant output has the variance 0. Then each index is 0, not NaN. An estimate can be a little
2340
+ * below 0 or above 1, from the Monte Carlo error. The function does not clip it.
2341
+ *
2342
+ * The draws fill A row by row, then B row by row. Each row has one draw for each dimension, in the order of the
2343
+ * record. Thus the same seed gives the same indices. The model can ignore a dimension. Then that dimension gets 0 for both
2344
+ * indices exactly, unless it is the parent of a dimension that the model reads.
2345
+ *
2346
+ * The function throws a RangeError when the record has no model of `measure`, and when N is not a whole number of 2 or more.
2347
+ * `predictResponse` throws for a value that the model refuses, for example a level that the model does not know.
2348
+ */
2349
+ function sobolIndices(record, measure, options = {}) {
2350
+ const model = record.models.find((m) => m.measure === measure);
2351
+ if (model === void 0) throw new RangeError(`sobolIndices: the record has no model of the measure "${measure}".`);
2352
+ const n = options.n ?? 4096;
2353
+ if (!Number.isSafeInteger(n) || n < 2) throw new RangeError(`sobolIndices: n is a whole number of 2 or more, not ${n}.`);
2354
+ const scale = options.scale ?? "measure";
2355
+ const dimensions = record.dimensions;
2356
+ const d = dimensions.length;
2357
+ const r = random(options.seed ?? 1);
2358
+ const a = drawMatrix(dimensions, n, r);
2359
+ const b = drawMatrix(dimensions, n, r);
2360
+ const f = (row) => {
2361
+ const inputs = inputsOf(dimensions, row);
2362
+ return scale === "model" ? predictScaled(model, inputs) : predictResponse(model, inputs);
2363
+ };
2364
+ const fa = Float64Array.from(a, f);
2365
+ const fb = Float64Array.from(b, f);
2366
+ const outputs = {
2367
+ a: fa,
2368
+ b: fb,
2369
+ ab: dimensions.map((_, i) => Float64Array.from(a, (row, j) => f(row.map((v, k) => k === i ? b[j][i] : v))))
2370
+ };
2371
+ const first = fa[0];
2372
+ const constant = fa.every((v) => v === first) && fb.every((v) => v === first);
2373
+ const all = estimate(outputs, null, n, constant);
2374
+ let intervals;
2375
+ if (options.interval !== void 0) {
2376
+ const rows = new Array(n);
2377
+ const flat = (e) => [...e.first, ...e.total];
2378
+ intervals = percentiles("sobolIndices", options.interval, 2 * d, (rr) => {
2379
+ for (let j = 0; j < n; j++) rows[j] = rr.below(n);
2380
+ return flat(estimate(outputs, rows, n, constant));
2381
+ }, () => flat(all));
2382
+ }
2383
+ const indices = dimensions.map((dimension, i) => {
2384
+ const index = {
2385
+ dimension: dimension.id,
2386
+ first: all.first[i],
2387
+ total: all.total[i]
2388
+ };
2389
+ if (intervals === void 0) return index;
2390
+ return {
2391
+ ...index,
2392
+ firstInterval: intervals[i],
2393
+ totalInterval: intervals[d + i]
2394
+ };
2395
+ });
2396
+ return {
2397
+ measure,
2398
+ scale,
2399
+ n,
2400
+ evaluations: n * (d + 2),
2401
+ seed: r.seed,
2402
+ mean: constant ? first : all.mean,
2403
+ variance: all.variance,
2404
+ indices
2405
+ };
2406
+ }
2245
2407
  // src/tests.ts
2246
2408
  /**
2247
2409
  * The tests of a difference: the sign test, the paired permutation test, the Wilson interval of a proportion and
@@ -2882,6 +3044,17 @@ function brier(forecasts) {
2882
3044
  for (const f of forecasts) sum += (f.p - (f.hit ? 1 : 0)) ** 2;
2883
3045
  return sum / forecasts.length;
2884
3046
  }
3047
+ /**
3048
+ * The log-loss of predictions: the mean of `-ln(p)` for a hit and `-ln(1 - p)` for a miss. 0 is perfect, and
3049
+ * `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,
3050
+ * give the probability of the observed outcome as `p`, with `hit` true.
3051
+ */
3052
+ function logLoss(forecasts) {
3053
+ checkedForecasts("logLoss", forecasts);
3054
+ let sum = 0;
3055
+ for (const f of forecasts) sum -= Math.log(f.hit ? f.p : 1 - f.p);
3056
+ return sum / forecasts.length;
3057
+ }
2885
3058
  /** The hit rate of each stated level, from the lowest level. */
2886
3059
  function calibration(forecasts) {
2887
3060
  checkedForecasts("calibration", forecasts);
@@ -3287,6 +3460,416 @@ function choose(n, k) {
3287
3460
  for (let j = 0; j < k; j++) c = c * (n - j) / (j + 1);
3288
3461
  return c;
3289
3462
  }
3290
- 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, brier, calibration, cohenKappa, compareRatios, coverageMinSample, 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 };
3463
+ // src/rating.ts
3464
+ function finite(name, label, value) {
3465
+ if (!Number.isFinite(value)) throw new RangeError(`${name}: ${label} is a finite number, not ${value}.`);
3466
+ }
3467
+ function positive(name, label, value) {
3468
+ if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`${name}: ${label} is a finite number above 0, not ${value}.`);
3469
+ }
3470
+ function checkedScale(name, options) {
3471
+ const center = options.center ?? 1500;
3472
+ const scale = options.scale ?? 400;
3473
+ const base = options.base ?? 10;
3474
+ finite(name, "the center", center);
3475
+ positive(name, "the scale", scale);
3476
+ if (!(base > 1 && Number.isFinite(base))) throw new RangeError(`${name}: the base is a finite number above 1, not ${base}.`);
3477
+ return {
3478
+ center,
3479
+ scale,
3480
+ base
3481
+ };
3482
+ }
3483
+ /**
3484
+ * The expected score of the first item against the second item: `1 / (1 + base ** ((rb - ra) / scale))`. A win
3485
+ * counts 1, a tie 0.5 and a loss 0. The expected score of the second item is 1 minus this value.
3486
+ */
3487
+ function eloExpected(ra, rb, options = {}) {
3488
+ finite("eloExpected", "the first rating", ra);
3489
+ finite("eloExpected", "the second rating", rb);
3490
+ const { scale, base } = checkedScale("eloExpected", options);
3491
+ return 1 / (1 + base ** ((rb - ra) / scale));
3492
+ }
3493
+ /**
3494
+ * 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
3495
+ * and 0 for a loss. Each rating moves by `k` times the difference between the score and the expected score. Thus
3496
+ * the sum of the two ratings does not change.
3497
+ */
3498
+ function eloUpdate(ra, rb, score, options = {}) {
3499
+ if (!(score >= 0 && score <= 1)) throw new RangeError(`eloUpdate: the score is from 0 to 1, not ${score}.`);
3500
+ const k = options.k ?? 32;
3501
+ if (!(k >= 0 && Number.isFinite(k))) throw new RangeError(`eloUpdate: k is a finite number of 0 or more, not ${k}.`);
3502
+ const expected = eloExpected(ra, rb, options);
3503
+ const change = k * (score - expected);
3504
+ return {
3505
+ a: ra + change,
3506
+ b: rb - change,
3507
+ expected
3508
+ };
3509
+ }
3510
+ /** The factor between the Glicko scale and the Glicko-2 scale: `400 / ln(10)`, to 4 places as in the paper. */
3511
+ const GLICKO2_FACTOR = 173.7178;
3512
+ const GLICKO2_CENTER = 1500;
3513
+ const VOLATILITY_STEPS = 1e3;
3514
+ function checkedRating(name, player) {
3515
+ finite(name, "the rating", player.rating);
3516
+ positive(name, "the RD", player.rd);
3517
+ positive(name, "the volatility", player.volatility);
3518
+ }
3519
+ /**
3520
+ * The Glicko-2 update of one player after one rating period (Glickman, "Example of the Glicko-2 system", 2012). The
3521
+ * function uses the Illinois algorithm for the new volatility. The games use the ratings of the opponents from the
3522
+ * start of the period. An empty list of games gives `glicko2Idle(player)`.
3523
+ */
3524
+ function glicko2Update(player, games, options = {}) {
3525
+ checkedRating("glicko2Update", player);
3526
+ const tau = options.tau ?? .5;
3527
+ const tolerance = options.tolerance ?? 1e-6;
3528
+ positive("glicko2Update", "tau", tau);
3529
+ positive("glicko2Update", "the tolerance", tolerance);
3530
+ for (const game of games) {
3531
+ finite("glicko2Update", "the rating of an opponent", game.rating);
3532
+ positive("glicko2Update", "the RD of an opponent", game.rd);
3533
+ if (!(game.score >= 0 && game.score <= 1)) throw new RangeError(`glicko2Update: a score is from 0 to 1, not ${game.score}.`);
3534
+ }
3535
+ if (games.length === 0) return glicko2Idle(player);
3536
+ const mu = (player.rating - GLICKO2_CENTER) / GLICKO2_FACTOR;
3537
+ const phi = player.rd / GLICKO2_FACTOR;
3538
+ const sigma = player.volatility;
3539
+ let information = 0;
3540
+ let improvement = 0;
3541
+ for (const game of games) {
3542
+ const muJ = (game.rating - GLICKO2_CENTER) / GLICKO2_FACTOR;
3543
+ const phiJ = game.rd / GLICKO2_FACTOR;
3544
+ const g = 1 / Math.sqrt(1 + 3 * phiJ * phiJ / (Math.PI * Math.PI));
3545
+ const e = 1 / (1 + Math.exp(-g * (mu - muJ)));
3546
+ information += g * g * e * (1 - e);
3547
+ improvement += g * (game.score - e);
3548
+ }
3549
+ const v = 1 / information;
3550
+ const newSigma = newVolatility(phi, sigma, v, v * improvement, tau, tolerance);
3551
+ const phiStar = Math.sqrt(phi * phi + newSigma * newSigma);
3552
+ const newPhi = 1 / Math.sqrt(1 / (phiStar * phiStar) + 1 / v);
3553
+ const newMu = mu + newPhi * newPhi * improvement;
3554
+ return {
3555
+ rating: GLICKO2_FACTOR * newMu + GLICKO2_CENTER,
3556
+ rd: GLICKO2_FACTOR * newPhi,
3557
+ volatility: newSigma
3558
+ };
3559
+ }
3560
+ /** Step 5 of the paper: the new volatility, by the Illinois algorithm on `x = ln(sigma^2)`. */
3561
+ function newVolatility(phi, sigma, v, delta, tau, tolerance) {
3562
+ const a = Math.log(sigma * sigma);
3563
+ const phi2 = phi * phi;
3564
+ const delta2 = delta * delta;
3565
+ const f = (x) => {
3566
+ const ex = Math.exp(x);
3567
+ const sum = phi2 + v + ex;
3568
+ return ex * (delta2 - phi2 - v - ex) / (2 * sum * sum) - (x - a) / (tau * tau);
3569
+ };
3570
+ let lowEnd = a;
3571
+ let highEnd;
3572
+ if (delta2 > phi2 + v) highEnd = Math.log(delta2 - phi2 - v);
3573
+ else {
3574
+ let k = 1;
3575
+ while (f(a - k * tau) < 0 && k < VOLATILITY_STEPS) k++;
3576
+ highEnd = a - k * tau;
3577
+ }
3578
+ let fLow = f(lowEnd);
3579
+ let fHigh = f(highEnd);
3580
+ for (let step = 0; Math.abs(highEnd - lowEnd) > tolerance && step < VOLATILITY_STEPS; step++) {
3581
+ const c = lowEnd + (lowEnd - highEnd) * fLow / (fHigh - fLow);
3582
+ const fC = f(c);
3583
+ if (fC * fHigh <= 0) {
3584
+ lowEnd = highEnd;
3585
+ fLow = fHigh;
3586
+ } else fLow /= 2;
3587
+ highEnd = c;
3588
+ fHigh = fC;
3589
+ }
3590
+ return Math.exp(lowEnd / 2);
3591
+ }
3592
+ /**
3593
+ * The Glicko-2 rating of a player after a rating period with no games. The rating and the volatility stay the same.
3594
+ * The RD grows to `sqrt(RD^2 + (173.7178 * volatility)^2)`.
3595
+ */
3596
+ function glicko2Idle(player) {
3597
+ checkedRating("glicko2Idle", player);
3598
+ const phi = player.rd / GLICKO2_FACTOR;
3599
+ const rd = GLICKO2_FACTOR * Math.sqrt(phi * phi + player.volatility * player.volatility);
3600
+ return {
3601
+ rating: player.rating,
3602
+ rd,
3603
+ volatility: player.volatility
3604
+ };
3605
+ }
3606
+ /**
3607
+ * This function gives the three probabilities of a pair from the log-strengths `sa` and `sb` and the Davidson tie
3608
+ * parameter `nu`. The denominator is `exp(d / 2) + exp(-d / 2) + nu`, with `d = sa - sb`. The function does no
3609
+ * checks, because the fit uses it in its inner loop.
3610
+ */
3611
+ function pairOutcomes(sa, sb, nu) {
3612
+ const half = (sa - sb) / 2;
3613
+ const top = Math.max(Math.abs(half), nu > 0 ? Math.log(nu) : Number.NEGATIVE_INFINITY);
3614
+ const ea = Math.exp(half - top);
3615
+ const eb = Math.exp(-half - top);
3616
+ const et = nu > 0 ? Math.exp(Math.log(nu) - top) : 0;
3617
+ const sum = ea + eb + et;
3618
+ return {
3619
+ a: ea / sum,
3620
+ b: eb / sum,
3621
+ tie: et / sum
3622
+ };
3623
+ }
3624
+ /**
3625
+ * The probabilities of a pair under the Bradley-Terry model with Davidson (1970) ties. The inputs are the
3626
+ * log-strengths of the two items and the tie parameter `tie` of `bradleyTerry`. With `tie` 0, the probability that
3627
+ * the first item wins is `1 / (1 + exp(sb - sa))`. A constant added to the two strengths changes no probability.
3628
+ */
3629
+ function pairProbabilities(sa, sb, tie = 0) {
3630
+ finite("pairProbabilities", "the first strength", sa);
3631
+ finite("pairProbabilities", "the second strength", sb);
3632
+ if (!(tie >= 0 && Number.isFinite(tie))) throw new RangeError(`pairProbabilities: the tie parameter is a finite number of 0 or more, not ${tie}.`);
3633
+ return pairOutcomes(sa, sb, tie);
3634
+ }
3635
+ /**
3636
+ * The rating of a log-strength: `center + scale * strength / ln(base)`. The defaults give
3637
+ * `1500 + 400 * log10(e) * strength`. Thus `eloExpected` of two ratings is the Bradley-Terry win probability.
3638
+ */
3639
+ function strengthToRating(strength, options = {}) {
3640
+ finite("strengthToRating", "the strength", strength);
3641
+ const { center, scale, base } = checkedScale("strengthToRating", options);
3642
+ return center + scale * strength / Math.log(base);
3643
+ }
3644
+ /** The log-strength of a rating. It is the inverse of `strengthToRating` with the same options. */
3645
+ function ratingToStrength(rating, options = {}) {
3646
+ finite("ratingToStrength", "the rating", rating);
3647
+ const { center, scale, base } = checkedScale("ratingToStrength", options);
3648
+ return (rating - center) * Math.log(base) / scale;
3649
+ }
3650
+ // src/bradley-terry.ts
3651
+ /**
3652
+ * The Bradley-Terry fit of pairwise comparisons among `n` items. Each item has a log-strength. The fit is the
3653
+ * maximum a posteriori estimate with a normal (ridge) prior on each log-strength. Thus an item with only wins has a
3654
+ * finite strength, and the strengths have one solution.
3655
+ *
3656
+ * 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
3657
+ * convex, so the steps go to its one minimum.
3658
+ */
3659
+ const ARMIJO = 1e-4;
3660
+ const HALVINGS = 60;
3661
+ const ROUNDING = 64 * Number.EPSILON;
3662
+ function checkedTerms(n, comparisons, ties) {
3663
+ const terms = [];
3664
+ for (const [i, comparison] of comparisons.entries()) {
3665
+ const { a, b, outcome } = comparison;
3666
+ const w = comparison.weight ?? 1;
3667
+ 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}.`);
3668
+ if (a === b) throw new RangeError(`bradleyTerry: comparison ${i} compares the item ${a} with itself.`);
3669
+ if (outcome !== "a" && outcome !== "b" && outcome !== "tie") throw new RangeError(`bradleyTerry: comparison ${i} has the outcome ${String(outcome)}.`);
3670
+ 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.`);
3671
+ if (w === 0) continue;
3672
+ const c = outcome === "a" ? .5 : outcome === "b" ? -.5 : 0;
3673
+ terms.push({
3674
+ a,
3675
+ b,
3676
+ w,
3677
+ c,
3678
+ tie: outcome === "tie" && ties === "davidson" ? 1 : 0
3679
+ });
3680
+ }
3681
+ return terms;
3682
+ }
3683
+ function positiveOption(label, value) {
3684
+ if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`bradleyTerry: ${label} is a finite number above 0, not ${value}.`);
3685
+ }
3686
+ /**
3687
+ * The Bradley-Terry fit of `comparisons` among `items` items. The function does not sort or change its inputs. An
3688
+ * empty list of comparisons gives the prior for each item. The function throws a RangeError for a bad index, a bad
3689
+ * outcome, a bad weight and a bad option.
3690
+ */
3691
+ function bradleyTerry(items, comparisons, options = {}) {
3692
+ if (!Number.isSafeInteger(items) || items < 1) throw new RangeError(`bradleyTerry: the number of items is a whole number of 1 or more, not ${items}.`);
3693
+ const priorMean = options.priorMean ?? 0;
3694
+ const priorSd = options.priorSd ?? 1;
3695
+ const ties = options.ties ?? "davidson";
3696
+ const tiePriorSd = options.tiePriorSd ?? 10;
3697
+ const maxIterations = options.maxIterations ?? 100;
3698
+ const tolerance = options.tolerance ?? 1e-10;
3699
+ if (!Number.isFinite(priorMean)) throw new RangeError(`bradleyTerry: the prior mean is a finite number, not ${priorMean}.`);
3700
+ positiveOption("the prior SD", priorSd);
3701
+ positiveOption("the tie prior SD", tiePriorSd);
3702
+ positiveOption("the tolerance", tolerance);
3703
+ if (ties !== "davidson" && ties !== "half") throw new RangeError(`bradleyTerry: the tie model is "davidson" or "half", not ${String(ties)}.`);
3704
+ if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) throw new RangeError(`bradleyTerry: maxIterations is a whole number of 1 or more, not ${maxIterations}.`);
3705
+ const terms = checkedTerms(items, comparisons, ties);
3706
+ const fitTie = terms.some((t) => t.tie === 1);
3707
+ const k = fitTie ? items + 1 : items;
3708
+ const model = {
3709
+ items,
3710
+ terms,
3711
+ fitTie,
3712
+ priorMean,
3713
+ priorPrecision: 1 / (priorSd * priorSd),
3714
+ tiePrecision: 1 / (tiePriorSd * tiePriorSd)
3715
+ };
3716
+ const x = new Float64Array(k).fill(priorMean);
3717
+ if (fitTie) x[items] = 0;
3718
+ let iterations = 0;
3719
+ let converged = false;
3720
+ let value = objective(model, x);
3721
+ while (iterations < maxIterations) {
3722
+ iterations++;
3723
+ const { gradient, hessian } = derivatives(model, x);
3724
+ const step = solve(hessian, gradient, k);
3725
+ let slope = 0;
3726
+ for (let i = 0; i < k; i++) slope += gradient[i] * step[i];
3727
+ let t = 1;
3728
+ let next = moved(x, step, t);
3729
+ let nextValue = objective(model, next);
3730
+ const slack = ROUNDING * (Math.abs(value) + 1);
3731
+ for (let h = 0; h < HALVINGS && !(nextValue <= value + ARMIJO * t * slope + slack); h++) {
3732
+ t /= 2;
3733
+ next = moved(x, step, t);
3734
+ nextValue = objective(model, next);
3735
+ }
3736
+ let largest = 0;
3737
+ for (let i = 0; i < k; i++) largest = Math.max(largest, Math.abs(step[i]));
3738
+ if (nextValue <= value + slack) {
3739
+ x.set(next);
3740
+ value = nextValue;
3741
+ }
3742
+ if (largest <= tolerance) {
3743
+ converged = true;
3744
+ break;
3745
+ }
3746
+ }
3747
+ const { hessian } = derivatives(model, x);
3748
+ const variance = inverseDiagonal(hessian, k);
3749
+ const nu = fitTie ? Math.exp(x[items]) : 0;
3750
+ let logLikelihood = 0;
3751
+ for (const term of terms) logLikelihood += term.w * termLogLikelihood(term, x[term.a], x[term.b], nu);
3752
+ return {
3753
+ strengths: Array.from(x.subarray(0, items)),
3754
+ se: Array.from(variance.subarray(0, items), (v) => Math.sqrt(v)),
3755
+ tie: nu,
3756
+ logLikelihood,
3757
+ iterations,
3758
+ converged
3759
+ };
3760
+ }
3761
+ function moved(x, step, t) {
3762
+ const out = new Float64Array(x.length);
3763
+ for (let i = 0; i < x.length; i++) out[i] = x[i] + t * step[i];
3764
+ return out;
3765
+ }
3766
+ /** The log of the denominator `exp(d / 2) + exp(-d / 2) + nu`, with no overflow. */
3767
+ function logDenominator(d, logNu) {
3768
+ const half = Math.abs(d) / 2;
3769
+ const top = Math.max(half, logNu);
3770
+ return top + Math.log(Math.exp(half - top) + Math.exp(-half - top) + Math.exp(logNu - top));
3771
+ }
3772
+ /** 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. */
3773
+ function termLogLikelihood(term, sa, sb, nu) {
3774
+ const d = sa - sb;
3775
+ const logNu = nu > 0 ? Math.log(nu) : Number.NEGATIVE_INFINITY;
3776
+ const logD = logDenominator(d, logNu);
3777
+ return term.tie === 1 ? logNu - logD : term.c * d - logD;
3778
+ }
3779
+ /** The negative log posterior at `x`. */
3780
+ function objective(model, x) {
3781
+ const logNu = model.fitTie ? x[model.items] : Number.NEGATIVE_INFINITY;
3782
+ let sum = 0;
3783
+ for (const term of model.terms) {
3784
+ const d = x[term.a] - x[term.b];
3785
+ sum += term.w * (logDenominator(d, logNu) - term.c * d - (term.tie === 1 ? logNu : 0));
3786
+ }
3787
+ for (let i = 0; i < model.items; i++) sum += model.priorPrecision * (x[i] - model.priorMean) ** 2 / 2;
3788
+ if (model.fitTie) sum += model.tiePrecision * logNu * logNu / 2;
3789
+ return sum;
3790
+ }
3791
+ /** The gradient and the Hessian (in row order) of the negative log posterior at `x`. */
3792
+ function derivatives(model, x) {
3793
+ const k = x.length;
3794
+ const n = model.items;
3795
+ const gradient = new Float64Array(k);
3796
+ const hessian = new Float64Array(k * k);
3797
+ const nu = model.fitTie ? Math.exp(x[n]) : 0;
3798
+ for (const term of model.terms) {
3799
+ const { a, b, w } = term;
3800
+ const p = pairOutcomes(x[a], x[b], nu);
3801
+ const q = (p.a - p.b) / 2;
3802
+ const gd = w * (q - term.c);
3803
+ const hdd = w * ((p.a + p.b) / 4 - q * q);
3804
+ gradient[a] = gradient[a] + gd;
3805
+ gradient[b] = gradient[b] - gd;
3806
+ hessian[a * k + a] = hessian[a * k + a] + hdd;
3807
+ hessian[b * k + b] = hessian[b * k + b] + hdd;
3808
+ hessian[a * k + b] = hessian[a * k + b] - hdd;
3809
+ hessian[b * k + a] = hessian[b * k + a] - hdd;
3810
+ if (model.fitTie) {
3811
+ const hdl = -w * q * p.tie;
3812
+ gradient[n] = gradient[n] + w * (p.tie - term.tie);
3813
+ hessian[n * k + n] = hessian[n * k + n] + w * p.tie * (1 - p.tie);
3814
+ hessian[a * k + n] = hessian[a * k + n] + hdl;
3815
+ hessian[n * k + a] = hessian[n * k + a] + hdl;
3816
+ hessian[b * k + n] = hessian[b * k + n] - hdl;
3817
+ hessian[n * k + b] = hessian[n * k + b] - hdl;
3818
+ }
3819
+ }
3820
+ for (let i = 0; i < n; i++) {
3821
+ gradient[i] = gradient[i] + model.priorPrecision * (x[i] - model.priorMean);
3822
+ hessian[i * k + i] = hessian[i * k + i] + model.priorPrecision;
3823
+ }
3824
+ if (model.fitTie) {
3825
+ gradient[n] = gradient[n] + model.tiePrecision * x[n];
3826
+ hessian[n * k + n] = hessian[n * k + n] + model.tiePrecision;
3827
+ }
3828
+ return {
3829
+ gradient,
3830
+ hessian
3831
+ };
3832
+ }
3833
+ /** The Cholesky factor of a copy of the Hessian. The priors make the Hessian positive definite. */
3834
+ function factored(hessian, k) {
3835
+ const l = Float64Array.from(hessian);
3836
+ if (!factor(l, k)) throw new RangeError("bradleyTerry: the Hessian is not positive definite. Make the prior SD smaller.");
3837
+ return l;
3838
+ }
3839
+ /** The Newton step: the solution of `hessian * step = -gradient`. */
3840
+ function solve(hessian, gradient, k) {
3841
+ const l = factored(hessian, k);
3842
+ const y = new Float64Array(k);
3843
+ for (let i = 0; i < k; i++) {
3844
+ let s = -gradient[i];
3845
+ for (let j = 0; j < i; j++) s -= l[i * k + j] * y[j];
3846
+ y[i] = s / l[i * k + i];
3847
+ }
3848
+ for (let i = k - 1; i >= 0; i--) {
3849
+ let s = y[i];
3850
+ for (let j = i + 1; j < k; j++) s -= l[j * k + i] * y[j];
3851
+ y[i] = s / l[i * k + i];
3852
+ }
3853
+ return y;
3854
+ }
3855
+ /** The diagonal of the inverse of the Hessian: the column sums of the squares of the inverse of its Cholesky factor. */
3856
+ function inverseDiagonal(hessian, k) {
3857
+ const l = factored(hessian, k);
3858
+ const out = new Float64Array(k);
3859
+ const column = new Float64Array(k);
3860
+ for (let c = 0; c < k; c++) {
3861
+ column.fill(0);
3862
+ for (let i = c; i < k; i++) {
3863
+ let s = i === c ? 1 : 0;
3864
+ for (let j = c; j < i; j++) s -= l[i * k + j] * column[j];
3865
+ column[i] = s / l[i * k + i];
3866
+ }
3867
+ let sum = 0;
3868
+ for (let i = c; i < k; i++) sum += column[i] ** 2;
3869
+ out[c] = sum;
3870
+ }
3871
+ return out;
3872
+ }
3873
+ 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, sobolIndices, sorted, spearman, strengthToRating, summarize, surfaceResponse, tQuantile, twoSamplePermutation, uStatisticInterval, whenHolds, wilson, wilsonSampleSize };
3291
3874
 
3292
3875
  //# sourceMappingURL=index.js.map