@mark1russell7/rigor-statistics 0.1.28 → 0.2.0
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 +35 -0
- package/build/index.d.ts +201 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +680 -12
- package/build/index.js.map +1 -1
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -3842,9 +3842,29 @@ function ratingToStrength(rating, options = {}) {
|
|
|
3842
3842
|
* 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
|
|
3843
3843
|
* convex, so the steps go to its one minimum.
|
|
3844
3844
|
*/
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3845
|
+
/**
|
|
3846
|
+
* This function gives the start of a fit: the log-strengths, and the log of the tie parameter. It throws a RangeError
|
|
3847
|
+
* for a start with the wrong number of items. It also throws for a log-strength that is not finite, or a tie parameter
|
|
3848
|
+
* of 0 or less.
|
|
3849
|
+
*/
|
|
3850
|
+
function startOf(name, items, priorMean, start) {
|
|
3851
|
+
if (start === void 0) return {
|
|
3852
|
+
strengths: new Float64Array(items).fill(priorMean),
|
|
3853
|
+
logTie: 0
|
|
3854
|
+
};
|
|
3855
|
+
if (start.strengths.length !== items) throw new RangeError(`${name}: the start has ${start.strengths.length} log-strengths, and the fit has ${items} items.`);
|
|
3856
|
+
const strengths = Float64Array.from(start.strengths);
|
|
3857
|
+
for (const [i, value] of strengths.entries()) if (!Number.isFinite(value)) throw new RangeError(`${name}: the start log-strength of item ${i} is a finite number, not ${start.strengths[i]}.`);
|
|
3858
|
+
const tie = start.tie ?? 1;
|
|
3859
|
+
if (!(tie > 0 && Number.isFinite(tie))) throw new RangeError(`${name}: the start tie parameter is a finite number above 0, not ${tie}.`);
|
|
3860
|
+
return {
|
|
3861
|
+
strengths,
|
|
3862
|
+
logTie: Math.log(tie)
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
const ARMIJO$1 = 1e-4;
|
|
3866
|
+
const HALVINGS$1 = 60;
|
|
3867
|
+
const ROUNDING$1 = 64 * Number.EPSILON;
|
|
3848
3868
|
function checkedTerms(n, comparisons, ties) {
|
|
3849
3869
|
const terms = [];
|
|
3850
3870
|
for (const [i, comparison] of comparisons.entries()) {
|
|
@@ -3866,7 +3886,7 @@ function checkedTerms(n, comparisons, ties) {
|
|
|
3866
3886
|
}
|
|
3867
3887
|
return terms;
|
|
3868
3888
|
}
|
|
3869
|
-
function positiveOption(label, value) {
|
|
3889
|
+
function positiveOption$1(label, value) {
|
|
3870
3890
|
if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`bradleyTerry: ${label} is a finite number above 0, not ${value}.`);
|
|
3871
3891
|
}
|
|
3872
3892
|
/**
|
|
@@ -3883,11 +3903,12 @@ function bradleyTerry(items, comparisons, options = {}) {
|
|
|
3883
3903
|
const maxIterations = options.maxIterations ?? 100;
|
|
3884
3904
|
const tolerance = options.tolerance ?? 1e-10;
|
|
3885
3905
|
if (!Number.isFinite(priorMean)) throw new RangeError(`bradleyTerry: the prior mean is a finite number, not ${priorMean}.`);
|
|
3886
|
-
positiveOption("the prior SD", priorSd);
|
|
3887
|
-
positiveOption("the tie prior SD", tiePriorSd);
|
|
3888
|
-
positiveOption("the tolerance", tolerance);
|
|
3906
|
+
positiveOption$1("the prior SD", priorSd);
|
|
3907
|
+
positiveOption$1("the tie prior SD", tiePriorSd);
|
|
3908
|
+
positiveOption$1("the tolerance", tolerance);
|
|
3889
3909
|
if (ties !== "davidson" && ties !== "half") throw new RangeError(`bradleyTerry: the tie model is "davidson" or "half", not ${String(ties)}.`);
|
|
3890
3910
|
if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) throw new RangeError(`bradleyTerry: maxIterations is a whole number of 1 or more, not ${maxIterations}.`);
|
|
3911
|
+
const begin = startOf("bradleyTerry", items, priorMean, options.start);
|
|
3891
3912
|
const terms = checkedTerms(items, comparisons, ties);
|
|
3892
3913
|
const fitTie = terms.some((t) => t.tie === 1);
|
|
3893
3914
|
const k = fitTie ? items + 1 : items;
|
|
@@ -3899,8 +3920,9 @@ function bradleyTerry(items, comparisons, options = {}) {
|
|
|
3899
3920
|
priorPrecision: 1 / (priorSd * priorSd),
|
|
3900
3921
|
tiePrecision: 1 / (tiePriorSd * tiePriorSd)
|
|
3901
3922
|
};
|
|
3902
|
-
const x = new Float64Array(k)
|
|
3903
|
-
|
|
3923
|
+
const x = new Float64Array(k);
|
|
3924
|
+
x.set(begin.strengths);
|
|
3925
|
+
if (fitTie) x[items] = begin.logTie;
|
|
3904
3926
|
let iterations = 0;
|
|
3905
3927
|
let converged = false;
|
|
3906
3928
|
let value = objective(model, x);
|
|
@@ -3913,8 +3935,8 @@ function bradleyTerry(items, comparisons, options = {}) {
|
|
|
3913
3935
|
let t = 1;
|
|
3914
3936
|
let next = moved(x, step, t);
|
|
3915
3937
|
let nextValue = objective(model, next);
|
|
3916
|
-
const slack = ROUNDING * (Math.abs(value) + 1);
|
|
3917
|
-
for (let h = 0; h < HALVINGS && !(nextValue <= value + ARMIJO * t * slope + slack); h++) {
|
|
3938
|
+
const slack = ROUNDING$1 * (Math.abs(value) + 1);
|
|
3939
|
+
for (let h = 0; h < HALVINGS$1 && !(nextValue <= value + ARMIJO$1 * t * slope + slack); h++) {
|
|
3918
3940
|
t /= 2;
|
|
3919
3941
|
next = moved(x, step, t);
|
|
3920
3942
|
nextValue = objective(model, next);
|
|
@@ -4056,6 +4078,652 @@ function inverseDiagonal(hessian, k) {
|
|
|
4056
4078
|
}
|
|
4057
4079
|
return out;
|
|
4058
4080
|
}
|
|
4059
|
-
|
|
4081
|
+
// src/bradley-terry-sparse.ts
|
|
4082
|
+
/**
|
|
4083
|
+
* The sparse Bradley-Terry fit. It has the model, the prior and the tie model of `bradleyTerry`, so the two fits have
|
|
4084
|
+
* one optimum. This fit keeps one record for each distinct pair of items, not a matrix of all the items. Thus its
|
|
4085
|
+
* memory is in proportion to the items and the pairs. The dense fit of 10,000 items keeps a Hessian of 800 MB.
|
|
4086
|
+
*
|
|
4087
|
+
* The fit uses Newton steps on the negative log posterior, as `bradleyTerry` does. It does not factor the Hessian.
|
|
4088
|
+
* Preconditioned conjugate gradients solve each Newton system, and the diagonal of the Hessian is the preconditioner.
|
|
4089
|
+
* One product of the Hessian and a vector reads each pair one time.
|
|
4090
|
+
*
|
|
4091
|
+
* The prior makes the objective strongly convex, so the Hessian is positive definite. Thus each step of the solve
|
|
4092
|
+
* gives a direction that goes down. The solve is loose far from the minimum and tight near it (an inexact Newton
|
|
4093
|
+
* method). A line search with the Armijo rule makes the objective smaller at each step. The steps go to the one
|
|
4094
|
+
* minimum, and the fit stops when the gradient puts each parameter inside the tolerance of that minimum.
|
|
4095
|
+
*/
|
|
4096
|
+
/** The largest number of items. The key of a pair is `low * items + high`, and it must be a safe integer. */
|
|
4097
|
+
const MAX_ITEMS = Math.floor(Math.sqrt(Number.MAX_SAFE_INTEGER));
|
|
4098
|
+
const ROUNDING = 64 * Number.EPSILON;
|
|
4099
|
+
const NAME = "bradleyTerrySparse";
|
|
4100
|
+
/** The Armijo constant of the line search: a step gives at least this part of the decrease that the slope forecasts. */
|
|
4101
|
+
const ARMIJO = 1e-4;
|
|
4102
|
+
/** The largest number of halvings of a step in the line search. */
|
|
4103
|
+
const HALVINGS = 60;
|
|
4104
|
+
/** The largest number of conjugate-gradient steps for one Newton system. */
|
|
4105
|
+
const SOLVE_LIMIT = 500;
|
|
4106
|
+
function positiveOption(label, value) {
|
|
4107
|
+
if (!(value > 0 && Number.isFinite(value))) throw new RangeError(`${NAME}: ${label} is a finite number above 0, not ${value}.`);
|
|
4108
|
+
}
|
|
4109
|
+
/** This function checks the comparisons, and sums them for each distinct pair. A comparison with the weight 0 adds nothing. */
|
|
4110
|
+
function pairsOf(items, comparisons, ties) {
|
|
4111
|
+
const index = /* @__PURE__ */ new Map();
|
|
4112
|
+
const low = [];
|
|
4113
|
+
const high = [];
|
|
4114
|
+
const total = [];
|
|
4115
|
+
const net = [];
|
|
4116
|
+
const tie = [];
|
|
4117
|
+
for (const [i, comparison] of comparisons.entries()) {
|
|
4118
|
+
const { a, b, outcome } = comparison;
|
|
4119
|
+
const w = comparison.weight ?? 1;
|
|
4120
|
+
for (const item of [a, b]) if (!Number.isSafeInteger(item) || item < 0 || item >= items) throw new RangeError(`${NAME}: comparison ${i} has the index ${item}, and the indexes are whole numbers from 0 to ${items - 1}.`);
|
|
4121
|
+
if (a === b) throw new RangeError(`${NAME}: comparison ${i} compares the item ${a} with itself.`);
|
|
4122
|
+
if (outcome !== "a" && outcome !== "b" && outcome !== "tie") throw new RangeError(`${NAME}: comparison ${i} has the outcome ${String(outcome)}.`);
|
|
4123
|
+
if (!(w >= 0 && Number.isFinite(w))) throw new RangeError(`${NAME}: comparison ${i} has the weight ${w}, and a weight is a finite number of 0 or more.`);
|
|
4124
|
+
if (w === 0) continue;
|
|
4125
|
+
const lo = Math.min(a, b);
|
|
4126
|
+
const hi = Math.max(a, b);
|
|
4127
|
+
const key = lo * items + hi;
|
|
4128
|
+
let p = index.get(key);
|
|
4129
|
+
if (p === void 0) {
|
|
4130
|
+
p = low.length;
|
|
4131
|
+
index.set(key, p);
|
|
4132
|
+
low.push(lo);
|
|
4133
|
+
high.push(hi);
|
|
4134
|
+
total.push(0);
|
|
4135
|
+
net.push(0);
|
|
4136
|
+
tie.push(0);
|
|
4137
|
+
}
|
|
4138
|
+
total[p] = total[p] + w;
|
|
4139
|
+
if (outcome === "tie") {
|
|
4140
|
+
if (ties === "davidson") tie[p] = tie[p] + w;
|
|
4141
|
+
} else {
|
|
4142
|
+
const winner = outcome === "a" ? a : b;
|
|
4143
|
+
net[p] = net[p] + (winner === lo ? w : -w);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
return {
|
|
4147
|
+
count: low.length,
|
|
4148
|
+
low: Int32Array.from(low),
|
|
4149
|
+
high: Int32Array.from(high),
|
|
4150
|
+
total: Float64Array.from(total),
|
|
4151
|
+
net: Float64Array.from(net),
|
|
4152
|
+
ties: Float64Array.from(tie)
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
/** The connected groups of the items: the group of each item, and the size of each group. */
|
|
4156
|
+
function groupsOf(items, pairs) {
|
|
4157
|
+
const parent = new Int32Array(items);
|
|
4158
|
+
for (let i = 0; i < items; i++) parent[i] = i;
|
|
4159
|
+
const root = (i) => {
|
|
4160
|
+
let r = i;
|
|
4161
|
+
while (parent[r] !== r) r = parent[r];
|
|
4162
|
+
for (let j = i; parent[j] !== r;) {
|
|
4163
|
+
const next = parent[j];
|
|
4164
|
+
parent[j] = r;
|
|
4165
|
+
j = next;
|
|
4166
|
+
}
|
|
4167
|
+
return r;
|
|
4168
|
+
};
|
|
4169
|
+
for (let p = 0; p < pairs.count; p++) {
|
|
4170
|
+
const x = root(pairs.low[p]);
|
|
4171
|
+
const y = root(pairs.high[p]);
|
|
4172
|
+
if (x !== y) parent[Math.max(x, y)] = Math.min(x, y);
|
|
4173
|
+
}
|
|
4174
|
+
const group = new Int32Array(items).fill(-1);
|
|
4175
|
+
const ids = new Int32Array(items).fill(-1);
|
|
4176
|
+
let count = 0;
|
|
4177
|
+
for (let i = 0; i < items; i++) {
|
|
4178
|
+
const r = root(i);
|
|
4179
|
+
if (ids[r] === -1) ids[r] = count++;
|
|
4180
|
+
group[i] = ids[r];
|
|
4181
|
+
}
|
|
4182
|
+
const size = new Float64Array(count);
|
|
4183
|
+
for (let i = 0; i < items; i++) size[group[i]] = size[group[i]] + 1;
|
|
4184
|
+
return {
|
|
4185
|
+
group,
|
|
4186
|
+
size,
|
|
4187
|
+
count
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* The probabilities of the three outcomes of a pair with the difference `d` of the log-strengths, and the log of the
|
|
4192
|
+
* denominator `exp(d / 2) + exp(-d / 2) + nu`. The function writes them into `out`, so the loops make no objects.
|
|
4193
|
+
*/
|
|
4194
|
+
function outcomes(d, logNu, out) {
|
|
4195
|
+
const half = d / 2;
|
|
4196
|
+
const top = Math.max(Math.abs(half), logNu);
|
|
4197
|
+
const ea = Math.exp(half - top);
|
|
4198
|
+
const eb = Math.exp(-half - top);
|
|
4199
|
+
const et = Math.exp(logNu - top);
|
|
4200
|
+
const sum = ea + eb + et;
|
|
4201
|
+
out[0] = ea / sum;
|
|
4202
|
+
out[1] = eb / sum;
|
|
4203
|
+
out[2] = et / sum;
|
|
4204
|
+
out[3] = top + Math.log(sum);
|
|
4205
|
+
}
|
|
4206
|
+
/** This function makes an empty point for `items` items and `count` pairs. */
|
|
4207
|
+
function pointOf(items, count) {
|
|
4208
|
+
return {
|
|
4209
|
+
s: new Float64Array(items),
|
|
4210
|
+
lambda: 0,
|
|
4211
|
+
value: 0,
|
|
4212
|
+
logLikelihood: 0,
|
|
4213
|
+
gradient: new Float64Array(items),
|
|
4214
|
+
tieGradient: 0,
|
|
4215
|
+
norm: 0,
|
|
4216
|
+
curvature: new Float64Array(count),
|
|
4217
|
+
diagonal: new Float64Array(items),
|
|
4218
|
+
cross: new Float64Array(items),
|
|
4219
|
+
tieCurvature: 0
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/** This function writes the objective, the gradient and the Hessian at the parameters of `point` into `point`. */
|
|
4223
|
+
function evaluate(problem, point, out) {
|
|
4224
|
+
const { items, pairs, fitTie, priorMean, p, tau, tieWeight } = problem;
|
|
4225
|
+
const { s, gradient, curvature, diagonal, cross, lambda } = point;
|
|
4226
|
+
const logNu = fitTie ? lambda : Number.NEGATIVE_INFINITY;
|
|
4227
|
+
let prior = 0;
|
|
4228
|
+
for (let i = 0; i < items; i++) {
|
|
4229
|
+
const offset = s[i] - priorMean;
|
|
4230
|
+
prior += offset * offset;
|
|
4231
|
+
gradient[i] = p * offset;
|
|
4232
|
+
diagonal[i] = p;
|
|
4233
|
+
cross[i] = 0;
|
|
4234
|
+
}
|
|
4235
|
+
let logLikelihood = fitTie ? tieWeight * lambda : 0;
|
|
4236
|
+
let tieGradient = fitTie ? tau * lambda - tieWeight : 0;
|
|
4237
|
+
let tieCurvature = fitTie ? tau : 0;
|
|
4238
|
+
for (let q = 0; q < pairs.count; q++) {
|
|
4239
|
+
const lo = pairs.low[q];
|
|
4240
|
+
const hi = pairs.high[q];
|
|
4241
|
+
const total = pairs.total[q];
|
|
4242
|
+
const net = pairs.net[q];
|
|
4243
|
+
const d = s[lo] - s[hi];
|
|
4244
|
+
outcomes(d, logNu, out);
|
|
4245
|
+
const pa = out[0];
|
|
4246
|
+
const pb = out[1];
|
|
4247
|
+
const pt = out[2];
|
|
4248
|
+
const half = (pa - pb) / 2;
|
|
4249
|
+
logLikelihood += net / 2 * d - total * out[3];
|
|
4250
|
+
const g = total * half - net / 2;
|
|
4251
|
+
gradient[lo] = gradient[lo] + g;
|
|
4252
|
+
gradient[hi] = gradient[hi] - g;
|
|
4253
|
+
const c = total * (pa * pb + pt * (1 - pt) / 4);
|
|
4254
|
+
curvature[q] = c;
|
|
4255
|
+
diagonal[lo] = diagonal[lo] + c;
|
|
4256
|
+
diagonal[hi] = diagonal[hi] + c;
|
|
4257
|
+
if (fitTie) {
|
|
4258
|
+
tieGradient += total * pt;
|
|
4259
|
+
tieCurvature += total * pt * (1 - pt);
|
|
4260
|
+
const b = -total * pt * half;
|
|
4261
|
+
cross[lo] = cross[lo] + b;
|
|
4262
|
+
cross[hi] = cross[hi] - b;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
let norm = tieGradient * tieGradient;
|
|
4266
|
+
for (let i = 0; i < items; i++) norm += gradient[i] ** 2;
|
|
4267
|
+
point.value = p * prior / 2 + (fitTie ? tau * lambda * lambda / 2 : 0) - logLikelihood;
|
|
4268
|
+
point.logLikelihood = logLikelihood;
|
|
4269
|
+
point.tieGradient = tieGradient;
|
|
4270
|
+
point.tieCurvature = tieCurvature;
|
|
4271
|
+
point.norm = Math.sqrt(norm);
|
|
4272
|
+
}
|
|
4273
|
+
/**
|
|
4274
|
+
* This function writes the product of the Hessian at `point` and the vector (`v`, `vTie`) into `out`. It gives the
|
|
4275
|
+
* tie part of the product. The product reads each pair one time, and it makes no arrays.
|
|
4276
|
+
*/
|
|
4277
|
+
function multiply(problem, point, v, vTie, out) {
|
|
4278
|
+
const { items, pairs, fitTie, p } = problem;
|
|
4279
|
+
const { curvature, cross } = point;
|
|
4280
|
+
for (let i = 0; i < items; i++) out[i] = p * v[i] + cross[i] * vTie;
|
|
4281
|
+
for (let q = 0; q < pairs.count; q++) {
|
|
4282
|
+
const lo = pairs.low[q];
|
|
4283
|
+
const hi = pairs.high[q];
|
|
4284
|
+
const t = curvature[q] * (v[lo] - v[hi]);
|
|
4285
|
+
out[lo] = out[lo] + t;
|
|
4286
|
+
out[hi] = out[hi] - t;
|
|
4287
|
+
}
|
|
4288
|
+
if (!fitTie) return 0;
|
|
4289
|
+
let tie = point.tieCurvature * vTie;
|
|
4290
|
+
for (let i = 0; i < items; i++) tie += cross[i] * v[i];
|
|
4291
|
+
return tie;
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* This function solves the Newton system `H x = -g` at `point` with preconditioned conjugate gradients. It writes the
|
|
4295
|
+
* log-strength part of `x` into `step`, and it gives the tie part. The solve stops when the norm of the residual is
|
|
4296
|
+
* `forcing` times the norm of the gradient or less. The solve starts at 0, so each step of it gives a descent direction.
|
|
4297
|
+
*/
|
|
4298
|
+
function newtonStep(problem, point, forcing, step, work) {
|
|
4299
|
+
const { items, fitTie } = problem;
|
|
4300
|
+
const { residual: r, scaled: z, direction: d, product: ad } = work;
|
|
4301
|
+
const { gradient, diagonal } = point;
|
|
4302
|
+
let rz = 0;
|
|
4303
|
+
for (let i = 0; i < items; i++) {
|
|
4304
|
+
const ri = -gradient[i];
|
|
4305
|
+
const zi = ri / diagonal[i];
|
|
4306
|
+
r[i] = ri;
|
|
4307
|
+
z[i] = zi;
|
|
4308
|
+
d[i] = zi;
|
|
4309
|
+
rz += ri * zi;
|
|
4310
|
+
}
|
|
4311
|
+
let rTie = fitTie ? -point.tieGradient : 0;
|
|
4312
|
+
let dTie = fitTie ? rTie / point.tieCurvature : 0;
|
|
4313
|
+
rz += rTie * dTie;
|
|
4314
|
+
step.fill(0);
|
|
4315
|
+
let stepTie = 0;
|
|
4316
|
+
const target = forcing * point.norm;
|
|
4317
|
+
const limit = Math.min(SOLVE_LIMIT, items + 1);
|
|
4318
|
+
for (let k = 0; k < limit; k++) {
|
|
4319
|
+
const adTie = multiply(problem, point, d, dTie, ad);
|
|
4320
|
+
let dad = dTie * adTie;
|
|
4321
|
+
for (let i = 0; i < items; i++) dad += d[i] * ad[i];
|
|
4322
|
+
if (!(dad > 0)) break;
|
|
4323
|
+
const alpha = rz / dad;
|
|
4324
|
+
let residual = 0;
|
|
4325
|
+
for (let i = 0; i < items; i++) {
|
|
4326
|
+
step[i] = step[i] + alpha * d[i];
|
|
4327
|
+
const ri = r[i] - alpha * ad[i];
|
|
4328
|
+
r[i] = ri;
|
|
4329
|
+
residual += ri * ri;
|
|
4330
|
+
}
|
|
4331
|
+
stepTie += alpha * dTie;
|
|
4332
|
+
rTie -= alpha * adTie;
|
|
4333
|
+
residual += rTie * rTie;
|
|
4334
|
+
if (Math.sqrt(residual) <= target) break;
|
|
4335
|
+
let next = 0;
|
|
4336
|
+
for (let i = 0; i < items; i++) {
|
|
4337
|
+
const zi = r[i] / diagonal[i];
|
|
4338
|
+
z[i] = zi;
|
|
4339
|
+
next += r[i] * zi;
|
|
4340
|
+
}
|
|
4341
|
+
const zTie = fitTie ? rTie / point.tieCurvature : 0;
|
|
4342
|
+
next += rTie * zTie;
|
|
4343
|
+
const beta = next / rz;
|
|
4344
|
+
rz = next;
|
|
4345
|
+
for (let i = 0; i < items; i++) d[i] = z[i] + beta * d[i];
|
|
4346
|
+
dTie = zTie + beta * dTie;
|
|
4347
|
+
}
|
|
4348
|
+
return stepTie;
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* The sparse Bradley-Terry fit of `comparisons` among `items` items. It has the model, the options and the checks of
|
|
4352
|
+
* `bradleyTerry`, and its memory is in proportion to `items` plus the distinct pairs. The function does not sort or
|
|
4353
|
+
* change its inputs. It throws a RangeError for a bad index, a bad outcome, a bad weight and a bad option.
|
|
4354
|
+
*/
|
|
4355
|
+
function bradleyTerrySparse(items, comparisons, options = {}) {
|
|
4356
|
+
if (!Number.isSafeInteger(items) || items < 1 || items > MAX_ITEMS) throw new RangeError(`${NAME}: the number of items is a whole number from 1 to ${MAX_ITEMS}, not ${items}.`);
|
|
4357
|
+
const priorMean = options.priorMean ?? 0;
|
|
4358
|
+
const priorSd = options.priorSd ?? 1;
|
|
4359
|
+
const ties = options.ties ?? "davidson";
|
|
4360
|
+
const tiePriorSd = options.tiePriorSd ?? 10;
|
|
4361
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
4362
|
+
const tolerance = options.tolerance ?? 1e-8;
|
|
4363
|
+
if (!Number.isFinite(priorMean)) throw new RangeError(`${NAME}: the prior mean is a finite number, not ${priorMean}.`);
|
|
4364
|
+
positiveOption("the prior SD", priorSd);
|
|
4365
|
+
positiveOption("the tie prior SD", tiePriorSd);
|
|
4366
|
+
positiveOption("the tolerance", tolerance);
|
|
4367
|
+
if (ties !== "davidson" && ties !== "half") throw new RangeError(`${NAME}: the tie model is "davidson" or "half", not ${String(ties)}.`);
|
|
4368
|
+
if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) throw new RangeError(`${NAME}: maxIterations is a whole number of 1 or more, not ${maxIterations}.`);
|
|
4369
|
+
const begin = startOf(NAME, items, priorMean, options.start);
|
|
4370
|
+
const pairs = pairsOf(items, comparisons, ties);
|
|
4371
|
+
let tieWeight = 0;
|
|
4372
|
+
let totalWeight = 0;
|
|
4373
|
+
for (let q = 0; q < pairs.count; q++) {
|
|
4374
|
+
tieWeight += pairs.ties[q];
|
|
4375
|
+
totalWeight += pairs.total[q];
|
|
4376
|
+
}
|
|
4377
|
+
const p = 1 / (priorSd * priorSd);
|
|
4378
|
+
const tau = 1 / (tiePriorSd * tiePriorSd);
|
|
4379
|
+
const problem = {
|
|
4380
|
+
items,
|
|
4381
|
+
pairs,
|
|
4382
|
+
fitTie: tieWeight > 0,
|
|
4383
|
+
priorMean,
|
|
4384
|
+
p,
|
|
4385
|
+
tau,
|
|
4386
|
+
tieWeight
|
|
4387
|
+
};
|
|
4388
|
+
const smallest = problem.fitTie ? Math.min(p, tau) : p;
|
|
4389
|
+
const floor = 4 * Number.EPSILON * (totalWeight + 1);
|
|
4390
|
+
let current = pointOf(items, pairs.count);
|
|
4391
|
+
let trial = pointOf(items, pairs.count);
|
|
4392
|
+
current.s.set(begin.strengths);
|
|
4393
|
+
current.lambda = problem.fitTie ? begin.logTie : 0;
|
|
4394
|
+
const out = /* @__PURE__ */ new Float64Array(4);
|
|
4395
|
+
const step = new Float64Array(items);
|
|
4396
|
+
const work = {
|
|
4397
|
+
residual: new Float64Array(items),
|
|
4398
|
+
scaled: new Float64Array(items),
|
|
4399
|
+
direction: new Float64Array(items),
|
|
4400
|
+
product: new Float64Array(items)
|
|
4401
|
+
};
|
|
4402
|
+
evaluate(problem, current, out);
|
|
4403
|
+
let iterations = 0;
|
|
4404
|
+
let bound = current.norm / smallest;
|
|
4405
|
+
let rounded = current.norm <= floor;
|
|
4406
|
+
while (!(bound <= tolerance || rounded) && iterations < maxIterations) {
|
|
4407
|
+
iterations++;
|
|
4408
|
+
const forcing = Math.min(.5, Math.sqrt(current.norm));
|
|
4409
|
+
const stepTie = newtonStep(problem, current, forcing, step, work);
|
|
4410
|
+
let slope = stepTie * current.tieGradient;
|
|
4411
|
+
for (let i = 0; i < items; i++) slope += step[i] * current.gradient[i];
|
|
4412
|
+
const slack = ROUNDING * (Math.abs(current.value) + 1);
|
|
4413
|
+
let t = 1;
|
|
4414
|
+
let accepted = false;
|
|
4415
|
+
for (let h = 0; h <= HALVINGS && !accepted; h++) {
|
|
4416
|
+
for (let i = 0; i < items; i++) trial.s[i] = current.s[i] + t * step[i];
|
|
4417
|
+
trial.lambda = current.lambda + t * stepTie;
|
|
4418
|
+
evaluate(problem, trial, out);
|
|
4419
|
+
accepted = trial.value <= current.value + ARMIJO * t * slope + slack;
|
|
4420
|
+
if (!accepted) t /= 2;
|
|
4421
|
+
}
|
|
4422
|
+
if (!accepted) break;
|
|
4423
|
+
[current, trial] = [trial, current];
|
|
4424
|
+
bound = current.norm / smallest;
|
|
4425
|
+
rounded = current.norm <= floor;
|
|
4426
|
+
}
|
|
4427
|
+
const { group, size } = groupsOf(items, pairs);
|
|
4428
|
+
const se = Array.from(current.s, (_, i) => {
|
|
4429
|
+
const n = size[group[i]];
|
|
4430
|
+
return Math.sqrt(1 / (n * p) + (1 - 1 / n) / current.diagonal[i]);
|
|
4431
|
+
});
|
|
4432
|
+
return {
|
|
4433
|
+
strengths: Array.from(current.s),
|
|
4434
|
+
se,
|
|
4435
|
+
tie: problem.fitTie ? Math.exp(current.lambda) : 0,
|
|
4436
|
+
logLikelihood: current.logLikelihood,
|
|
4437
|
+
iterations,
|
|
4438
|
+
converged: bound <= tolerance || rounded,
|
|
4439
|
+
bound,
|
|
4440
|
+
pairs: pairs.count
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
// src/rating-models.ts
|
|
4444
|
+
/**
|
|
4445
|
+
* The rating models of a log of picks. The Elo and the Glicko-2 replays read the picks in their order. The
|
|
4446
|
+
* Bradley-Terry fit reads the whole log at one time. The choice of a model compares the log-loss of each model on the
|
|
4447
|
+
* last picks of the log. Each function is pure: the same log gives the same result.
|
|
4448
|
+
*/
|
|
4449
|
+
/** The score of the first item of a pick: 1 for a win, 0.5 for a tie and 0 for a loss. */
|
|
4450
|
+
const SCORE = {
|
|
4451
|
+
a: 1,
|
|
4452
|
+
b: 0,
|
|
4453
|
+
tie: .5
|
|
4454
|
+
};
|
|
4455
|
+
/** This function checks the indexes and the outcomes of `comparisons`. With `ordered`, it also refuses a weight other than 1. */
|
|
4456
|
+
function checkedPicks(name, items, comparisons, ordered) {
|
|
4457
|
+
if (!Number.isSafeInteger(items) || items < 0) throw new RangeError(`${name}: the number of items is a whole number of 0 or more, not ${items}.`);
|
|
4458
|
+
for (const [i, { a, b, outcome, weight }] of comparisons.entries()) {
|
|
4459
|
+
for (const index of [a, b]) if (!Number.isSafeInteger(index) || index < 0 || index >= items) throw new RangeError(`${name}: pick ${i} has the index ${index}, and the indexes are whole numbers from 0 to ${items - 1}.`);
|
|
4460
|
+
if (a === b) throw new RangeError(`${name}: pick ${i} compares the item ${a} with itself.`);
|
|
4461
|
+
if (outcome !== "a" && outcome !== "b" && outcome !== "tie") throw new RangeError(`${name}: pick ${i} has the outcome ${String(outcome)}.`);
|
|
4462
|
+
if (ordered && weight !== void 0 && weight !== 1) throw new RangeError(`${name}: pick ${i} has the weight ${weight}. An ordered replay makes one update for each pick, so each weight is 1.`);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
/**
|
|
4466
|
+
* The Elo ratings of `items` items after each pick of `comparisons`, in the order of the list. Each item starts at
|
|
4467
|
+
* `start`. The update of each pick is `eloUpdate` with the options. The order of the picks changes the result.
|
|
4468
|
+
*/
|
|
4469
|
+
function eloReplay(items, comparisons, options = {}) {
|
|
4470
|
+
checkedPicks("eloReplay", items, comparisons, true);
|
|
4471
|
+
const start = options.start ?? 1500;
|
|
4472
|
+
if (!Number.isFinite(start)) throw new RangeError(`eloReplay: the start rating is a finite number, not ${start}.`);
|
|
4473
|
+
eloUpdate(start, start, .5, options);
|
|
4474
|
+
const ratings = new Array(items).fill(start);
|
|
4475
|
+
for (const { a, b, outcome } of comparisons) {
|
|
4476
|
+
const next = eloUpdate(ratings[a], ratings[b], SCORE[outcome], options);
|
|
4477
|
+
ratings[a] = next.a;
|
|
4478
|
+
ratings[b] = next.b;
|
|
4479
|
+
}
|
|
4480
|
+
return ratings;
|
|
4481
|
+
}
|
|
4482
|
+
/**
|
|
4483
|
+
* The Glicko-2 ratings of `items` items after each pick of `comparisons`, in the order of the list. Each pick is one
|
|
4484
|
+
* rating period of its two items. Each item of a pick gets `glicko2Update` against the rating of the other item before
|
|
4485
|
+
* the pick. An item that is not in a pick does not change in that period. The order of the picks changes the result.
|
|
4486
|
+
*/
|
|
4487
|
+
function glicko2Replay(items, comparisons, options = {}) {
|
|
4488
|
+
checkedPicks("glicko2Replay", items, comparisons, true);
|
|
4489
|
+
const start = options.start ?? {
|
|
4490
|
+
rating: 1500,
|
|
4491
|
+
rd: 350,
|
|
4492
|
+
volatility: .06
|
|
4493
|
+
};
|
|
4494
|
+
glicko2Update(start, [], options);
|
|
4495
|
+
const players = Array.from({ length: items }, () => ({
|
|
4496
|
+
rating: start.rating,
|
|
4497
|
+
rd: start.rd,
|
|
4498
|
+
volatility: start.volatility
|
|
4499
|
+
}));
|
|
4500
|
+
for (const { a, b, outcome } of comparisons) {
|
|
4501
|
+
const pa = players[a];
|
|
4502
|
+
const pb = players[b];
|
|
4503
|
+
players[a] = glicko2Update(pa, [{
|
|
4504
|
+
rating: pb.rating,
|
|
4505
|
+
rd: pb.rd,
|
|
4506
|
+
score: SCORE[outcome]
|
|
4507
|
+
}], options);
|
|
4508
|
+
players[b] = glicko2Update(pb, [{
|
|
4509
|
+
rating: pa.rating,
|
|
4510
|
+
rd: pa.rd,
|
|
4511
|
+
score: 1 - SCORE[outcome]
|
|
4512
|
+
}], options);
|
|
4513
|
+
}
|
|
4514
|
+
return players;
|
|
4515
|
+
}
|
|
4516
|
+
/** The default largest number of items of the dense Bradley-Terry fit in `fitRatings`. */
|
|
4517
|
+
const BRADLEY_TERRY_DENSE_LIMIT = 200;
|
|
4518
|
+
/**
|
|
4519
|
+
* The log-strengths of `items` items with one rating model. The model `"bradley-terry"` uses `bradleyTerry`, or
|
|
4520
|
+
* `bradleyTerrySparse` above `denseLimit` items. The models `"elo"` and `"glicko2"` use `eloReplay` and
|
|
4521
|
+
* `glicko2Replay`, and they refuse a weight other than 1. A log of 0 items gives empty lists.
|
|
4522
|
+
*/
|
|
4523
|
+
function fitRatings(model, items, comparisons, options = {}) {
|
|
4524
|
+
checkedPicks("fitRatings", items, comparisons, model !== "bradley-terry");
|
|
4525
|
+
const denseLimit = options.denseLimit ?? 200;
|
|
4526
|
+
if (!(denseLimit >= 0)) throw new RangeError(`fitRatings: denseLimit is a number of 0 or more, not ${denseLimit}.`);
|
|
4527
|
+
if (items === 0) return {
|
|
4528
|
+
strengths: [],
|
|
4529
|
+
se: [],
|
|
4530
|
+
tie: 0
|
|
4531
|
+
};
|
|
4532
|
+
switch (model) {
|
|
4533
|
+
case "bradley-terry": {
|
|
4534
|
+
const fit = items <= denseLimit ? bradleyTerry(items, comparisons, options.bradleyTerry) : bradleyTerrySparse(items, comparisons, options.bradleyTerry);
|
|
4535
|
+
return {
|
|
4536
|
+
strengths: fit.strengths,
|
|
4537
|
+
se: fit.se,
|
|
4538
|
+
tie: fit.tie
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
case "elo": {
|
|
4542
|
+
const ratings = eloReplay(items, comparisons, options.elo);
|
|
4543
|
+
return {
|
|
4544
|
+
strengths: ratings.map((rating) => ratingToStrength(rating, options.elo)),
|
|
4545
|
+
se: ratings.map(() => void 0),
|
|
4546
|
+
tie: 0
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
case "glicko2": {
|
|
4550
|
+
const players = glicko2Replay(items, comparisons, options.glicko2);
|
|
4551
|
+
return {
|
|
4552
|
+
strengths: players.map((player) => ratingToStrength(player.rating)),
|
|
4553
|
+
se: players.map((player) => ratingToStrength(1500 + player.rd)),
|
|
4554
|
+
tie: 0
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
default: throw new RangeError(`fitRatings: the model is "bradley-terry", "elo" or "glicko2", not ${String(model)}.`);
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
/** The lowest probability of a forecast. An outcome with a probability of 0 gives a log-loss of Infinity. */
|
|
4561
|
+
const FORECAST_FLOOR = 1e-9;
|
|
4562
|
+
/**
|
|
4563
|
+
* The choice of a rating model by the log-loss on held-out picks. Each model learns from the first picks of the log
|
|
4564
|
+
* and forecasts the last `holdout` part. All models use the tie parameter of the Bradley-Terry fit of the first picks.
|
|
4565
|
+
* Thus the strengths are the only difference. Two equal scores keep the order of `models`. Each pick has the weight 1.
|
|
4566
|
+
*/
|
|
4567
|
+
function selectRatingModel(items, comparisons, options = {}) {
|
|
4568
|
+
checkedPicks("selectRatingModel", items, comparisons, true);
|
|
4569
|
+
const holdout = options.holdout ?? .2;
|
|
4570
|
+
const minimum = options.minimum ?? 30;
|
|
4571
|
+
const models = options.models ?? [
|
|
4572
|
+
"bradley-terry",
|
|
4573
|
+
"elo",
|
|
4574
|
+
"glicko2"
|
|
4575
|
+
];
|
|
4576
|
+
if (!(holdout > 0 && holdout < 1)) throw new RangeError(`selectRatingModel: the holdout is above 0 and below 1, not ${holdout}.`);
|
|
4577
|
+
if (!Number.isSafeInteger(minimum) || minimum < 0) throw new RangeError(`selectRatingModel: the minimum is a whole number of 0 or more, not ${minimum}.`);
|
|
4578
|
+
if (models.length === 0) throw new RangeError("selectRatingModel: the list of models is empty.");
|
|
4579
|
+
for (const model of models) if (model !== "bradley-terry" && model !== "elo" && model !== "glicko2") throw new RangeError(`selectRatingModel: a model is "bradley-terry", "elo" or "glicko2", not ${String(model)}.`);
|
|
4580
|
+
const tested = Math.floor(comparisons.length * holdout);
|
|
4581
|
+
if (comparisons.length < minimum || tested < 1) return {
|
|
4582
|
+
model: "bradley-terry",
|
|
4583
|
+
scores: [],
|
|
4584
|
+
trained: comparisons.length,
|
|
4585
|
+
tested: 0
|
|
4586
|
+
};
|
|
4587
|
+
const train = comparisons.slice(0, comparisons.length - tested);
|
|
4588
|
+
const test = comparisons.slice(comparisons.length - tested);
|
|
4589
|
+
const bradley = fitRatings("bradley-terry", items, train, options);
|
|
4590
|
+
const scores = models.map((model) => {
|
|
4591
|
+
const { strengths } = model === "bradley-terry" ? bradley : fitRatings(model, items, train, options);
|
|
4592
|
+
return {
|
|
4593
|
+
model,
|
|
4594
|
+
logLoss: logLoss(test.map(({ a, b, outcome }) => {
|
|
4595
|
+
const p = pairProbabilities(strengths[a], strengths[b], bradley.tie)[outcome];
|
|
4596
|
+
return {
|
|
4597
|
+
p: Math.max(p, FORECAST_FLOOR),
|
|
4598
|
+
hit: true
|
|
4599
|
+
};
|
|
4600
|
+
}))
|
|
4601
|
+
};
|
|
4602
|
+
});
|
|
4603
|
+
scores.sort((x, y) => x.logLoss - y.logLoss);
|
|
4604
|
+
return {
|
|
4605
|
+
model: scores[0].model,
|
|
4606
|
+
scores,
|
|
4607
|
+
trained: train.length,
|
|
4608
|
+
tested
|
|
4609
|
+
};
|
|
4610
|
+
}
|
|
4611
|
+
// src/pair-choice.ts
|
|
4612
|
+
/**
|
|
4613
|
+
* The choice of the next pair of a duel. A good pair has a result that is not sure: two items with near ratings, or
|
|
4614
|
+
* items with few picks. The choice uses a seed of `@mark1russell7/rigor-generators`, so a seed gives the same pair again.
|
|
4615
|
+
*/
|
|
4616
|
+
/**
|
|
4617
|
+
* The uncertainty of an item with no standard error and no picks, in rating points. It is the first RD of Glicko-2.
|
|
4618
|
+
* An item with no standard error has the uncertainty `350 / sqrt(1 + picks)`.
|
|
4619
|
+
*/
|
|
4620
|
+
const RATING_START_UNCERTAINTY = 350;
|
|
4621
|
+
/** The largest pool of items for a full search of a pair that is not recent. */
|
|
4622
|
+
const PAIR_FULL_SEARCH = 64;
|
|
4623
|
+
/** The uncertainty of a rating, in rating points: its standard error, or `RATING_START_UNCERTAINTY / sqrt(1 + picks)`. */
|
|
4624
|
+
function ratingUncertainty(item) {
|
|
4625
|
+
return item.se ?? 350 / Math.sqrt(1 + item.picks);
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* The expected information of a pick between two items: `p * (1 - p) * (ua^2 + ub^2)`. Here `p` is `eloExpected` of
|
|
4629
|
+
* the two ratings, and `ua` and `ub` are the uncertainties. The value is high when the result is not sure (a `p` near
|
|
4630
|
+
* 0.5) and the two ratings are not sure.
|
|
4631
|
+
*/
|
|
4632
|
+
function pairInformation(a, b, options = {}) {
|
|
4633
|
+
const p = eloExpected(a.rating, b.rating, options);
|
|
4634
|
+
return p * (1 - p) * (ratingUncertainty(a) ** 2 + ratingUncertainty(b) ** 2);
|
|
4635
|
+
}
|
|
4636
|
+
const pairKey = (a, b) => a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`;
|
|
4637
|
+
/**
|
|
4638
|
+
* The indexes of the second items to examine: the neighbors in the order, and random items. Only the indexes in
|
|
4639
|
+
* `allowed` are candidates.
|
|
4640
|
+
*/
|
|
4641
|
+
function candidatesOf(first, allowed, size, count, draw) {
|
|
4642
|
+
if (allowed.size <= count) return [...allowed].sort((x, y) => x - y);
|
|
4643
|
+
const chosen = /* @__PURE__ */ new Set();
|
|
4644
|
+
const near = Math.ceil(count / 2);
|
|
4645
|
+
for (let step = 1; chosen.size < near && step < size; step++) {
|
|
4646
|
+
if (allowed.has(first - step)) chosen.add(first - step);
|
|
4647
|
+
if (chosen.size < near && allowed.has(first + step)) chosen.add(first + step);
|
|
4648
|
+
}
|
|
4649
|
+
while (chosen.size < count) {
|
|
4650
|
+
const index = draw(size);
|
|
4651
|
+
if (allowed.has(index)) chosen.add(index);
|
|
4652
|
+
}
|
|
4653
|
+
return [...chosen];
|
|
4654
|
+
}
|
|
4655
|
+
/**
|
|
4656
|
+
* The choice of the next pair of a duel from a pool of rated items. The pool goes in the order of the ratings, from
|
|
4657
|
+
* the highest. These are the steps:
|
|
4658
|
+
*
|
|
4659
|
+
* 1. A draw selects the first item. The weight of an item is `1 / (1 + picks)`, so the items with few picks come first.
|
|
4660
|
+
* An item with no permitted pair leaves the draw.
|
|
4661
|
+
* 2. The second item is the candidate with the most information. The candidates are the neighbors of the first item in
|
|
4662
|
+
* the order, and random items, each in a permitted pair. A recent pair loses to each pair that is not recent.
|
|
4663
|
+
* 3. A draw selects the left and the right side, so the side does not change the picks.
|
|
4664
|
+
*
|
|
4665
|
+
* In a pool of `PAIR_FULL_SEARCH` items or fewer, a recent pair comes only when each permitted pair is recent. The
|
|
4666
|
+
* function gives `undefined` when there is no permitted pair. Each step has its own stream of the seed.
|
|
4667
|
+
*/
|
|
4668
|
+
function choosePair(items, options) {
|
|
4669
|
+
const candidates = options.candidates ?? 24;
|
|
4670
|
+
if (!Number.isSafeInteger(candidates) || candidates < 1) throw new RangeError(`choosePair: candidates is a whole number of 1 or more, not ${candidates}.`);
|
|
4671
|
+
for (const item of items) {
|
|
4672
|
+
if (!Number.isFinite(item.rating)) throw new RangeError(`choosePair: the item ${item.id} has the rating ${item.rating}, and a rating is a finite number.`);
|
|
4673
|
+
if (!Number.isSafeInteger(item.picks) || item.picks < 0) throw new RangeError(`choosePair: the item ${item.id} has ${item.picks} picks, and a count of picks is a whole number of 0 or more.`);
|
|
4674
|
+
}
|
|
4675
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4676
|
+
const pool = items.filter((item) => !seen.has(item.id) && seen.add(item.id)).sort((x, y) => y.rating - x.rating);
|
|
4677
|
+
if (pool.length < 2) return void 0;
|
|
4678
|
+
const draw = random(options.seed);
|
|
4679
|
+
const recent = new Set((options.recent ?? []).map(([a, b]) => pairKey(a, b)));
|
|
4680
|
+
const at = (i) => pool[i];
|
|
4681
|
+
const isRecent = (i, j) => recent.has(pairKey(at(i).id, at(j).id));
|
|
4682
|
+
const information = options.information ?? ((a, b) => pairInformation(a, b));
|
|
4683
|
+
const informationOf = (i, j) => {
|
|
4684
|
+
const value = information(at(i), at(j));
|
|
4685
|
+
if (value !== void 0 && !Number.isFinite(value)) throw new RangeError(`choosePair: the information of ${at(i).id} and ${at(j).id} is ${value}, and it is a finite number or undefined.`);
|
|
4686
|
+
return value;
|
|
4687
|
+
};
|
|
4688
|
+
const jitter = draw.fork("jitter");
|
|
4689
|
+
let best;
|
|
4690
|
+
const consider = (i, j, score) => {
|
|
4691
|
+
if (score === void 0) return;
|
|
4692
|
+
const option = {
|
|
4693
|
+
i,
|
|
4694
|
+
j,
|
|
4695
|
+
fresh: !isRecent(i, j),
|
|
4696
|
+
score,
|
|
4697
|
+
tie: jitter.next()
|
|
4698
|
+
};
|
|
4699
|
+
if (best === void 0 || option.fresh && !best.fresh || option.fresh === best.fresh && (option.score > best.score || option.score === best.score && option.tie > best.tie)) best = option;
|
|
4700
|
+
};
|
|
4701
|
+
const weights = pool.map((item) => 1 / (1 + item.picks));
|
|
4702
|
+
const firstDraw = draw.fork("first");
|
|
4703
|
+
let first = -1;
|
|
4704
|
+
const scores = /* @__PURE__ */ new Map();
|
|
4705
|
+
while (first < 0 && weights.some((weight) => weight > 0)) {
|
|
4706
|
+
const index = firstDraw.weighted(weights);
|
|
4707
|
+
for (let j = 0; j < pool.length; j++) {
|
|
4708
|
+
const score = j === index ? void 0 : informationOf(index, j);
|
|
4709
|
+
if (score !== void 0) scores.set(j, score);
|
|
4710
|
+
}
|
|
4711
|
+
if (scores.size > 0) first = index;
|
|
4712
|
+
else weights[index] = 0;
|
|
4713
|
+
}
|
|
4714
|
+
if (first < 0) return void 0;
|
|
4715
|
+
const second = draw.fork("second");
|
|
4716
|
+
const allowed = new Set(scores.keys());
|
|
4717
|
+
for (const j of candidatesOf(first, allowed, pool.length, candidates, (n) => second.below(n))) consider(first, j, scores.get(j));
|
|
4718
|
+
if (!best.fresh && pool.length <= 64) for (let i = 0; i < pool.length; i++) for (let j = i + 1; j < pool.length; j++) consider(i, j, informationOf(i, j));
|
|
4719
|
+
const { i, j, score } = best;
|
|
4720
|
+
const [left, right] = draw.fork("side").chance(.5) ? [at(j), at(i)] : [at(i), at(j)];
|
|
4721
|
+
return {
|
|
4722
|
+
a: left.id,
|
|
4723
|
+
b: right.id,
|
|
4724
|
+
information: score
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
export { BRADLEY_TERRY_DENSE_LIMIT, EMPTY_RUNNING, KENDALL_EXACT, PAIR_FULL_SEARCH, RATING_START_UNCERTAINTY, RESPONSE_LAMBDAS, RESPONSE_MAX_POWER, RESPONSE_MAX_TERMS, RESPONSE_RECORDS_SCHEMA, SIGN_FLIP_EXACT_LIMIT, SIGN_FLIP_EXACT_MAX, U_STATISTIC_TIGHT_MIN_SAMPLE, Z95, addToRunning, bestSmoothLaw, binomialInterval, binomialTest, bootstrap, bootstrapClusters, bootstrapLeastSquares, bootstrapMedian, bootstrapMedianRatio, bootstrapStrata, bootstrapTwo, bradleyTerry, bradleyTerrySparse, brier, calibration, choosePair, cohenKappa, compareRatios, coverageMinSample, eloExpected, eloReplay, eloUpdate, fitRatings, fitResponse, glicko2Idle, glicko2Replay, glicko2Update, heldOutError, heldOutErrorAt, holm, kendallTau, leastSquares, logLoss, mean, median, medianInterval, mergeRunning, modeCount, negativeBinomialInterval, noiseFloor, noiseVerdict, normalCdf, normalQuantile, optionFloor, pairInformation, pairProbabilities, pairedPermutation, parseResponseRecord, partialConjunction, predictResponse, quantile, quantileInterval, quantileSamples, quantileSorted, ranks, ratingToStrength, ratingUncertainty, responseCells, ridgeSolve, runningMean, runningOf, runningSd, sampleTotalInterval, sd, selectRatingModel, signFlipTest, signTest, signTestPower, sliceResponse, smoothLawScores, sobolIndices, sorted, spearman, strengthToRating, summarize, surfaceResponse, tQuantile, twoSamplePermutation, uStatisticInterval, whenHolds, wilson, wilsonSampleSize };
|
|
4060
4728
|
|
|
4061
4729
|
//# sourceMappingURL=index.js.map
|