@mark1russell7/rigor-statistics 0.1.20 → 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 +40 -1
- package/build/index.d.ts +158 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +422 -1
- package/build/index.js.map +1 -1
- package/package.json +8 -3
package/build/index.js
CHANGED
|
@@ -2882,6 +2882,17 @@ function brier(forecasts) {
|
|
|
2882
2882
|
for (const f of forecasts) sum += (f.p - (f.hit ? 1 : 0)) ** 2;
|
|
2883
2883
|
return sum / forecasts.length;
|
|
2884
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
|
+
}
|
|
2885
2896
|
/** The hit rate of each stated level, from the lowest level. */
|
|
2886
2897
|
function calibration(forecasts) {
|
|
2887
2898
|
checkedForecasts("calibration", forecasts);
|
|
@@ -3287,6 +3298,416 @@ function choose(n, k) {
|
|
|
3287
3298
|
for (let j = 0; j < k; j++) c = c * (n - j) / (j + 1);
|
|
3288
3299
|
return c;
|
|
3289
3300
|
}
|
|
3290
|
-
|
|
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 };
|
|
3291
3712
|
|
|
3292
3713
|
//# sourceMappingURL=index.js.map
|