@mark1russell7/rigor-statistics 0.1.7 → 0.1.8

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
@@ -533,6 +533,10 @@ function predictScaled(model, inputs) {
533
533
  for (const term of model.terms) sum += term.weight * product(term.factors, at, inputs);
534
534
  return sum + 0;
535
535
  }
536
+ /** This function changes a measured value to the scale of a model. A log scale with a floor lifts each smaller value. */
537
+ function toModelScale(value, log, floor) {
538
+ return log ? Math.log10(Math.max(value, floor ?? value)) : value;
539
+ }
536
540
  /** This function changes a value from the scale of the model to the scale of the measure. */
537
541
  function unscale(model, value) {
538
542
  return model.scale === "log" ? 10 ** value : value;
@@ -581,6 +585,67 @@ function heldOutError(record, measure) {
581
585
  skill: baselineMae > 0 ? 1 - mae / baselineMae : null
582
586
  };
583
587
  }
588
+ /** This function gives the stored predictions of a measure in a fold. A fold with no predictions gives `undefined`. */
589
+ function predictionsOf(fold, measure) {
590
+ return fold.predictions !== void 0 && Object.hasOwn(fold.predictions, measure) ? fold.predictions[measure] : void 0;
591
+ }
592
+ /**
593
+ * This function gives the held-out error of a chosen set of points, on the scale of the model. It uses the stored
594
+ * predictions of the option `outOfFold`. Thus a reader can get the error of one cell. The result has the fields of
595
+ * `heldOutError`, and the set of all the points gives the same values to the rounding of the sums.
596
+ *
597
+ * Each point counts at most once. A point with no value of the measure, or with no prediction, adds nothing to `n`.
598
+ * The baseline of a point is the mean of the training points of its fold.
599
+ *
600
+ * The function throws a RangeError for an unknown measure and for a record with no predictions of the measure. It
601
+ * also throws for an unknown id, for a repeated id, and for a set with no point to score.
602
+ */
603
+ function heldOutErrorAt(record, measure, ids) {
604
+ const model = record.models.find((m) => m.measure === measure);
605
+ if (model === void 0) throw new RangeError(`heldOutErrorAt: the record has no model of the measure "${measure}".`);
606
+ if (!record.folds.some((fold) => predictionsOf(fold, measure) !== void 0)) throw new RangeError(`heldOutErrorAt: the record has no predictions of the measure "${measure}". Fit it with the option outOfFold.`);
607
+ const chosen = new Set(ids);
608
+ if (chosen.size !== ids.length) throw new RangeError("heldOutErrorAt: the list of ids has a repeated id.");
609
+ const points = new Map(record.points.map((p) => [p.id, p]));
610
+ for (const id of ids) if (!points.has(id)) throw new RangeError(`heldOutErrorAt: the record has no point "${id}".`);
611
+ const scaled = (point) => {
612
+ const value = point.measures[measure];
613
+ return typeof value === "number" ? toModelScale(value, model.scale === "log", model.floor) : void 0;
614
+ };
615
+ let n = 0;
616
+ let folds = 0;
617
+ let absolute = 0;
618
+ let square = 0;
619
+ let baselineSum = 0;
620
+ for (const fold of record.folds) {
621
+ const predicted = predictionsOf(fold, measure);
622
+ if (predicted === void 0) continue;
623
+ const scored = fold.heldOut.filter((id) => chosen.has(id) && Object.hasOwn(predicted, id));
624
+ if (scored.length === 0) continue;
625
+ const held = new Set(fold.heldOut);
626
+ const baseline = mean(record.points.flatMap((p) => held.has(p.id) || scaled(p) === void 0 ? [] : [scaled(p)]));
627
+ folds += 1;
628
+ for (const id of scored) {
629
+ const y = scaled(points.get(id));
630
+ const error = y - predicted[id];
631
+ n += 1;
632
+ absolute += Math.abs(error);
633
+ square += error * error;
634
+ baselineSum += Math.abs(y - baseline);
635
+ }
636
+ }
637
+ if (n === 0) throw new RangeError(`heldOutErrorAt: no chosen point has a prediction of the measure "${measure}".`);
638
+ const mae = absolute / n;
639
+ const baselineMae = baselineSum / n;
640
+ return {
641
+ n,
642
+ folds,
643
+ mae,
644
+ rmse: Math.sqrt(square / n),
645
+ baselineMae,
646
+ skill: baselineMae > 0 ? 1 - mae / baselineMae : null
647
+ };
648
+ }
584
649
  /**
585
650
  * This function gives the usual value of a dimension: a measured median, or the most common level among `among`. A
586
651
  * tie gives the first level. A dimension with no measured value gives its low end or its first level.
@@ -695,7 +760,9 @@ function frameOf(name, record, measure, vary, options) {
695
760
  }
696
761
  const others = record.dimensions.filter((d) => !vary.includes(d.id));
697
762
  const held = heldValues(name, record, vary, hold, neededLevels(name, varied, vary));
698
- const matched = record.points.filter((p) => typeof p.measures[measure] === "number" && vary.every((id) => p.values[id] !== null) && others.every((d) => matches(d, p.values[d.id], whenHolds(d, p.values) ? held[d.id] : null, tolerance)));
763
+ const placed = record.points.filter((p) => vary.every((id) => p.values[id] !== null) && others.every((d) => matches(d, p.values[d.id], whenHolds(d, p.values) ? held[d.id] : null, tolerance)));
764
+ const matched = placed.filter((p) => typeof p.measures[measure] === "number");
765
+ const missing = placed.filter((p) => typeof p.measures[measure] !== "number");
699
766
  const error = heldOutError(record, measure);
700
767
  const nearest = nearestOf(record.dimensions, record.points, measure);
701
768
  const band = (inputs) => {
@@ -712,16 +779,21 @@ function frameOf(name, record, measure, vary, options) {
712
779
  held,
713
780
  axes: varied.map((d) => axisValues(d, steps)),
714
781
  matched,
782
+ missing,
715
783
  error,
716
784
  band
717
785
  };
718
786
  }
719
787
  /** This function gives the group field of a measured point of a view. */
720
788
  const groupOf = (point) => point.group === void 0 ? {} : { group: point.group };
789
+ /** This function gives the declared reason of a point with no value of a measure, or `null` for no stated reason. */
790
+ const reasonOf = (point, measure) => point.absent !== void 0 && Object.hasOwn(point.absent, measure) ? point.absent[measure] : null;
721
791
  /**
722
792
  * This function gives a view of a measure over one dimension of a parsed record:
723
793
  * - `curve`: the fitted value at each level, or at `steps` values of a continuous dimension, with its band.
724
794
  * - `points`: the measured points that match the held values. This list is empty when no point matches them.
795
+ * - `absent`: the points that match the held values and have no value of the measure, each with its reason. A chart
796
+ * can draw each reason apart.
725
797
  * - `held`: the value of each other dimension.
726
798
  * - `error`: the held-out error.
727
799
  *
@@ -745,7 +817,7 @@ const groupOf = (point) => point.group === void 0 ? {} : { group: point.group };
745
817
  */
746
818
  function sliceResponse(record, measure, options) {
747
819
  const { vary } = options;
748
- const { held, axes, matched, error, band } = frameOf("sliceResponse", record, measure, [vary], options);
820
+ const { held, axes, matched, missing, error, band } = frameOf("sliceResponse", record, measure, [vary], options);
749
821
  return {
750
822
  measure,
751
823
  vary,
@@ -763,6 +835,12 @@ function sliceResponse(record, measure, options) {
763
835
  value: p.measures[measure],
764
836
  ...groupOf(p)
765
837
  })),
838
+ absent: missing.map((p) => ({
839
+ id: p.id,
840
+ at: p.values[vary],
841
+ reason: reasonOf(p, measure),
842
+ ...groupOf(p)
843
+ })),
766
844
  error
767
845
  };
768
846
  }
@@ -775,7 +853,7 @@ function sliceResponse(record, measure, options) {
775
853
  function surfaceResponse(record, measure, options) {
776
854
  const { vary } = options;
777
855
  const [xId, yId] = vary;
778
- const { held, axes, matched, error, band } = frameOf("surfaceResponse", record, measure, vary, options);
856
+ const { held, axes, matched, missing, error, band } = frameOf("surfaceResponse", record, measure, vary, options);
779
857
  const x = axes[0];
780
858
  const y = axes[1];
781
859
  return {
@@ -795,6 +873,12 @@ function surfaceResponse(record, measure, options) {
795
873
  value: p.measures[measure],
796
874
  ...groupOf(p)
797
875
  })),
876
+ absent: missing.map((p) => ({
877
+ id: p.id,
878
+ at: [p.values[xId], p.values[yId]],
879
+ reason: reasonOf(p, measure),
880
+ ...groupOf(p)
881
+ })),
798
882
  error
799
883
  };
800
884
  }
@@ -951,13 +1035,30 @@ function checkMeasure(value, path, faults) {
951
1035
  check(object, "unit", path, isString, "text", faults);
952
1036
  check(object, "direction", path, (v) => v === "higher" || v === "lower" || v === "none", `"higher", "lower" or "none"`, faults);
953
1037
  }
954
- /** This function examines one point, with the dimensions and the measures of the record. */
955
- function checkPoint(value, path, dimensions, measures, faults) {
1038
+ /** This function examines one declared reason. */
1039
+ function checkReason(value, path, faults) {
1040
+ const object = fields(value, path, ["id", "label"], [], faults);
1041
+ if (object === void 0) return;
1042
+ check(object, "id", path, isId, "text with one character or more", faults);
1043
+ check(object, "label", path, isString, "text", faults);
1044
+ }
1045
+ /** This function examines the reasons of a point: each has a declaration, and each is on a measure with no number. */
1046
+ function checkAbsent(value, path, measuresOfPoint, measures, reasons, faults) {
1047
+ const absent = fields(value, path, [], [...measures], faults);
1048
+ if (absent === void 0) return;
1049
+ for (const measure of measures) {
1050
+ if (!Object.hasOwn(absent, measure)) continue;
1051
+ need(reasons.has(absent[measure]), at(path, measure), `the record declares no reason ${shown(absent[measure])}.`, faults);
1052
+ need(typeof peek(measuresOfPoint, measure) !== "number", at(path, measure), "this measure has a number, so it has no reason.", faults);
1053
+ }
1054
+ }
1055
+ /** This function examines one point, with the dimensions, the measures and the reasons of the record. */
1056
+ function checkPoint(value, path, dimensions, measures, reasons, faults) {
956
1057
  const object = fields(value, path, [
957
1058
  "id",
958
1059
  "values",
959
1060
  "measures"
960
- ], ["group"], faults);
1061
+ ], ["group", "absent"], faults);
961
1062
  if (object === void 0) return;
962
1063
  check(object, "id", path, isId, "text with one character or more", faults);
963
1064
  check(object, "group", path, isId, "text with one character or more", faults);
@@ -982,6 +1083,7 @@ function checkPoint(value, path, dimensions, measures, faults) {
982
1083
  else need(present, at(inside, dimension.id), `this is null when the dimension "${parent}" is ${shown(values[parent])}.`, faults);
983
1084
  }
984
1085
  }
1086
+ if (Object.hasOwn(object, "absent")) checkAbsent(object["absent"], at(path, "absent"), object["measures"], measures, reasons, faults);
985
1087
  if (Object.hasOwn(object, "measures")) {
986
1088
  const inside = at(path, "measures");
987
1089
  const values = fields(object["measures"], inside, [...measures], [], faults);
@@ -1003,7 +1105,7 @@ function responseDataFaults(value, required, optional) {
1003
1105
  "measures",
1004
1106
  "points",
1005
1107
  ...required
1006
- ], optional, faults);
1108
+ ], ["reasons", ...optional], faults);
1007
1109
  if (object === void 0) return faults;
1008
1110
  check(object, "schema", "", (v) => v === RESPONSE_RECORDS_SCHEMA, `"${RESPONSE_RECORDS_SCHEMA}"`, faults);
1009
1111
  check(object, "name", "", isId, "text with one character or more", faults);
@@ -1016,9 +1118,12 @@ function responseDataFaults(value, required, optional) {
1016
1118
  const measures = Object.hasOwn(object, "measures") ? list(object["measures"], "measures", 1, faults) : [];
1017
1119
  measures.forEach((m, i) => checkMeasure(m, `measures[${i}]`, faults));
1018
1120
  const measureIds = uniqueIds(measures, "measures", faults);
1121
+ const reasons = Object.hasOwn(object, "reasons") ? list(object["reasons"], "reasons", 0, faults) : [];
1122
+ reasons.forEach((r, i) => checkReason(r, `reasons[${i}]`, faults));
1123
+ const reasonIds = uniqueIds(reasons, "reasons", faults);
1019
1124
  if (faults.length > 0) return faults;
1020
1125
  const points = list(object["points"], "points", 1, faults);
1021
- points.forEach((p, i) => checkPoint(p, `points[${i}]`, dimensions, measureIds, faults));
1126
+ points.forEach((p, i) => checkPoint(p, `points[${i}]`, dimensions, measureIds, reasonIds, faults));
1022
1127
  uniqueIds(points, "points", faults);
1023
1128
  return faults;
1024
1129
  }
@@ -1230,6 +1335,24 @@ function checkFoldError(value, path, count, faults) {
1230
1335
  "baselineMae"
1231
1336
  ]) check(object, key, path, isNotNegative, "a finite number of 0 or more", faults);
1232
1337
  }
1338
+ /** This function examines the stored predictions of a fold against its held-out points and its errors. */
1339
+ function checkPredictions(value, path, held, errors, measures, faults) {
1340
+ const predictions = fields(value, path, [], measures, faults);
1341
+ if (predictions === void 0) return;
1342
+ for (const measure of measures) {
1343
+ if (!Object.hasOwn(predictions, measure)) continue;
1344
+ const inside = at(path, measure);
1345
+ const stored = fields(predictions[measure], inside, [], Object.keys(peek(predictions, measure) ?? {}), faults);
1346
+ if (stored === void 0) continue;
1347
+ need(peek(errors, measure) !== void 0, inside, "this fold has no error of this measure.", faults);
1348
+ const scored = new Set(held.filter((p) => typeof p.measures[measure] === "number").map((p) => p.id));
1349
+ for (const id of Object.keys(stored)) {
1350
+ need(scored.has(id), at(inside, id), `this fold has no held-out point ${shown(id)} with a value of the measure.`, faults);
1351
+ check(stored, id, inside, isNumber, "a finite number", faults);
1352
+ }
1353
+ for (const id of scored) need(Object.hasOwn(stored, id), at(inside, id), "this held-out point has no prediction.", faults);
1354
+ }
1355
+ }
1233
1356
  /** This function adds the points of a fold to `foldOfPoint`, and it gives them. Each bad id gives a fault. */
1234
1357
  function heldPoints(value, path, f, points, foldOfPoint, faults) {
1235
1358
  const held = [];
@@ -1265,15 +1388,18 @@ function checkFolds(value, data, faults) {
1265
1388
  const sizes = groupSizes(data.points);
1266
1389
  const foldOfPoint = /* @__PURE__ */ new Map();
1267
1390
  const covered = /* @__PURE__ */ new Set();
1268
- list(value, "folds", 1, faults).forEach((item, f) => {
1391
+ const folds = list(value, "folds", 1, faults);
1392
+ const predicted = measures.filter((m) => folds.some((fold) => peek(peek(fold, "predictions"), m) !== void 0));
1393
+ folds.forEach((item, f) => {
1269
1394
  const path = `folds[${f}]`;
1270
- const object = fields(item, path, ["heldOut", "errors"], [], faults);
1395
+ const object = fields(item, path, ["heldOut", "errors"], ["predictions"], faults);
1271
1396
  if (object === void 0) return;
1272
1397
  const held = Object.hasOwn(object, "heldOut") ? heldPoints(object["heldOut"], at(path, "heldOut"), f, points, foldOfPoint, faults) : [];
1273
1398
  for (const [group, count] of groupSizes(held)) {
1274
1399
  const size = sizes.get(group);
1275
1400
  need(count === size, at(path, "heldOut"), `the ${group} has ${count} of its ${size} points in this fold, so it is on both sides of the split.`, faults);
1276
1401
  }
1402
+ if (Object.hasOwn(object, "predictions")) checkPredictions(object["predictions"], at(path, "predictions"), held, object["errors"], measures, faults);
1277
1403
  if (!Object.hasOwn(object, "errors")) return;
1278
1404
  const errors = fields(object["errors"], at(path, "errors"), [], measures, faults);
1279
1405
  if (errors === void 0) return;
@@ -1282,6 +1408,7 @@ function checkFolds(value, data, faults) {
1282
1408
  const count = held.filter((p) => p.measures[measure] !== null).length;
1283
1409
  checkFoldError(errors[measure], at(at(path, "errors"), measure), count, faults);
1284
1410
  if (count > 0) covered.add(measure);
1411
+ need(!predicted.includes(measure) || peek(object["predictions"], measure) !== void 0, at(path, "predictions"), `another fold has predictions of the measure "${measure}", so this fold needs them too.`, faults);
1285
1412
  }
1286
1413
  });
1287
1414
  for (const measure of measures) need(covered.has(measure), "folds", `no fold has a held-out error of the measure "${measure}".`, faults);
@@ -1300,6 +1427,15 @@ function checkFolds(value, data, faults) {
1300
1427
  * - All the points of a group are in one fold, or all are in no fold. Thus no group is on both sides of a split.
1301
1428
  * - The count `n` of an error is the number of held-out points with a value of the measure.
1302
1429
  * - Each measure has an error in one fold or more.
1430
+ *
1431
+ * These rules apply to the predictions of a fold:
1432
+ * - Each prediction is a finite number of a held-out point of the fold with a value of the measure.
1433
+ * - The fold has a prediction of each such point, and an error of the measure.
1434
+ * - When one fold has predictions of a measure, each fold with an error of the measure has them.
1435
+ *
1436
+ * These rules apply to the reasons of a point:
1437
+ * - Each reason is on a known measure with no number. A measure with `null` and no reason is good.
1438
+ * - Each reason has a declaration in the field `reasons` of the record.
1303
1439
  */
1304
1440
  function parseResponseRecord(value) {
1305
1441
  try {
@@ -1433,6 +1569,15 @@ function expansion(dimensions, degree, interactions) {
1433
1569
  });
1434
1570
  return terms;
1435
1571
  }
1572
+ /** This function gives the terms that are not 0 at one point or more. A fold gives a term with no point the weight 0. */
1573
+ function seenTerms(terms, dimensions, points) {
1574
+ const inputs = checkedInputs(dimensions, terms);
1575
+ const places = points.map((point) => ({
1576
+ at: coordinates(inputs, point.values),
1577
+ values: point.values
1578
+ }));
1579
+ return terms.filter((term) => places.some(({ at, values }) => product(term, at, values) !== 0));
1580
+ }
1436
1581
  /** This function gives the inputs of a ridge fit: one for each dimension of a term. It throws for a bad term. */
1437
1582
  function checkedInputs(dimensions, terms) {
1438
1583
  const used = new Set(terms.flatMap((t) => t.map((f) => f.dimension)));
@@ -1550,19 +1695,23 @@ function tableLearner(rows, dimensions) {
1550
1695
  return (r) => cells.get(keys[r]) ?? fallback;
1551
1696
  };
1552
1697
  }
1553
- /** This function gives the error of a fold. A fold with no held-out row or no training row has no error. */
1554
- function foldError(rows, fold, learner) {
1698
+ /** This function gives the score of a fold. A fold with no held-out row or no training row has no score. */
1699
+ function foldScore(rows, fold, learner) {
1555
1700
  const train = rows.filter((row) => row.fold !== fold).map((row) => row.y);
1556
1701
  const held = rows.flatMap((row, r) => row.fold === fold ? [r] : []);
1557
1702
  if (held.length === 0 || train.length === 0) return void 0;
1558
1703
  const predict = learner(fold);
1559
1704
  const baseline = mean(train);
1560
- const errors = held.map((r) => rows[r].y - predict(r));
1705
+ const predicted = held.map((r) => predict(r));
1706
+ const errors = held.map((r, i) => rows[r].y - predicted[i]);
1561
1707
  return {
1562
- n: held.length,
1563
- mae: mean(errors.map(Math.abs)),
1564
- rmse: Math.sqrt(mean(errors.map((e) => e * e))),
1565
- baselineMae: mean(held.map((r) => Math.abs(rows[r].y - baseline)))
1708
+ error: {
1709
+ n: held.length,
1710
+ mae: mean(errors.map(Math.abs)),
1711
+ rmse: Math.sqrt(mean(errors.map((e) => e * e))),
1712
+ baselineMae: mean(held.map((r) => Math.abs(rows[r].y - baseline)))
1713
+ },
1714
+ predictions: Object.fromEntries(held.map((r, i) => [rows[r].point.id, predicted[i]]))
1566
1715
  };
1567
1716
  }
1568
1717
  /** This function gives the rows of a measure. It throws a RangeError for a value that a log scale cannot read. */
@@ -1573,12 +1722,12 @@ function rowsOf(points, measure, log, floor, foldOf) {
1573
1722
  if (log && floor === void 0 && value <= 0) throw new RangeError(`fitResponse: the point "${point.id}" has the value ${value} of the log measure "${measure}". Give logFloor.`);
1574
1723
  return [{
1575
1724
  point,
1576
- y: log ? Math.log10(Math.max(value, floor ?? value)) : value,
1725
+ y: toModelScale(value, log, floor),
1577
1726
  fold: foldOf.get(groupKey(point))
1578
1727
  }];
1579
1728
  });
1580
1729
  }
1581
- /** This function gives the model of one measure on all its rows, and the error of each fold. */
1730
+ /** This function gives the model of one measure on all its rows, and the score of each fold. */
1582
1731
  function fitMeasure(rows, measure, log, floor, plan) {
1583
1732
  if (rows.length === 0) throw new RangeError(`fitResponse: no point has a value of the measure "${measure.id}".`);
1584
1733
  const base = {
@@ -1588,8 +1737,8 @@ function fitMeasure(rows, measure, log, floor, plan) {
1588
1737
  };
1589
1738
  const ridge = plan.kind === "ridge" ? ridgeLearner(rows, plan) : void 0;
1590
1739
  const learner = ridge?.learner ?? tableLearner(rows, plan.ids);
1591
- const errors = Array.from({ length: plan.folds }, (_, f) => foldError(rows, f, learner));
1592
- if (errors.every((e) => e === void 0)) throw new RangeError(`fitResponse: all the values of the measure "${measure.id}" are in one fold. Give more groups.`);
1740
+ const scores = Array.from({ length: plan.folds }, (_, f) => foldScore(rows, f, learner));
1741
+ if (scores.every((s) => s === void 0)) throw new RangeError(`fitResponse: all the values of the measure "${measure.id}" are in one fold. Give more groups.`);
1593
1742
  const conditional = plan.conditional.length === 0 ? {} : { conditional: plan.conditional };
1594
1743
  if (ridge === void 0) return {
1595
1744
  model: {
@@ -1600,7 +1749,7 @@ function fitMeasure(rows, measure, log, floor, plan) {
1600
1749
  cells: tableCells(rows, plan.ids),
1601
1750
  fallback: mean(rows.map((r) => r.y))
1602
1751
  },
1603
- errors
1752
+ scores
1604
1753
  };
1605
1754
  const { lambda, beta } = ridge.final();
1606
1755
  if (!beta.every(Number.isFinite)) throw new RangeError(`fitResponse: the fit of the measure "${measure.id}" with lambda ${lambda} is singular. Give a larger lambda.`);
@@ -1617,7 +1766,7 @@ function fitMeasure(rows, measure, log, floor, plan) {
1617
1766
  intercept: beta[0],
1618
1767
  terms
1619
1768
  },
1620
- errors
1769
+ scores
1621
1770
  };
1622
1771
  }
1623
1772
  /** This function examines the options that do not depend on the data, and it gives the number of folds and the penalties. */
@@ -1659,7 +1808,10 @@ function kindOf(dimensions, model) {
1659
1808
  * The penalty: each fold of a ridge fit chooses from `lambdas` with the other folds. Thus the held-out error of a
1660
1809
  * fold is not optimistic. The model of the record chooses with all the folds.
1661
1810
  *
1662
- * The errors: a fold scores the model of the other folds on its held-out points, on the scale of the model.
1811
+ * The errors: a fold scores the model of the other folds on its held-out points, on the scale of the model. With the
1812
+ * option `outOfFold`, the fold also stores the prediction of each of these points.
1813
+ *
1814
+ * The reasons: a measure with a stated reason is `null` for the fit. The field `reasons` of the data stays in the record.
1663
1815
  *
1664
1816
  * The result does not depend on the order of the points, except for the field `points`, which keeps the order of
1665
1817
  * the data. The same data and options give the same bytes in one JavaScript engine.
@@ -1676,8 +1828,10 @@ function fitResponse(data, options = {}) {
1676
1828
  const log = new Set(options.log ?? []);
1677
1829
  for (const id of log) if (!measures.some((m) => m.id === id)) throw new RangeError(`fitResponse: the option log names the unknown measure "${id}".`);
1678
1830
  const kind = kindOf(dimensions, options.model ?? "auto");
1679
- const terms = kind === "table" ? [] : options.terms ?? expansion(dimensions, degree, options.interactions ?? true);
1680
- if (terms.length + 1 > 1024) throw new RangeError(`fitResponse: the expansion has ${terms.length} terms, and the limit is 1023. Set interactions to false, or give terms.`);
1831
+ const interactions = options.interactions ?? true;
1832
+ const expanded = kind === "table" ? [] : options.terms ?? expansion(dimensions, degree, interactions !== false);
1833
+ const terms = interactions === "seen" && options.terms === void 0 ? seenTerms(expanded, dimensions, data.points) : expanded;
1834
+ if (terms.length + 1 > 1024) throw new RangeError(`fitResponse: the expansion has ${terms.length} terms, and the limit is 1023. Set interactions to false or "seen", or give terms.`);
1681
1835
  const inputs = checkedInputs(dimensions, terms);
1682
1836
  const points = [...data.points].sort((a, b) => byText(a.id, b.id));
1683
1837
  const groups = [...new Set(points.map(groupKey))].sort(byText);
@@ -1695,18 +1849,24 @@ function fitResponse(data, options = {}) {
1695
1849
  inputs
1696
1850
  };
1697
1851
  const fits = measures.map((m) => fitMeasure(rowsOf(points, m.id, log.has(m.id), options.logFloor, foldOf), m, log.has(m.id), options.logFloor, plan));
1698
- const foldRecords = Array.from({ length: folds }, (_, f) => ({
1699
- heldOut: points.filter((p) => foldOf.get(groupKey(p)) === f).map((p) => p.id),
1700
- errors: Object.fromEntries(measures.flatMap((m, i) => {
1701
- const error = fits[i]?.errors[f];
1702
- return error === void 0 ? [] : [[m.id, error]];
1703
- }))
1704
- }));
1852
+ const foldRecords = Array.from({ length: folds }, (_, f) => {
1853
+ const scored = measures.flatMap((m, i) => {
1854
+ const score = fits[i]?.scores[f];
1855
+ return score === void 0 ? [] : [[m.id, score]];
1856
+ });
1857
+ return {
1858
+ heldOut: points.filter((p) => foldOf.get(groupKey(p)) === f).map((p) => p.id),
1859
+ errors: Object.fromEntries(scored.map(([id, score]) => [id, score.error])),
1860
+ ...options.outOfFold === true ? { predictions: Object.fromEntries(scored.map(([id, score]) => [id, score.predictions])) } : {}
1861
+ };
1862
+ });
1863
+ const reasons = data.reasons === void 0 ? {} : { reasons: data.reasons };
1705
1864
  return {
1706
1865
  schema: RESPONSE_RECORDS_SCHEMA,
1707
1866
  name: data.name,
1708
1867
  dimensions,
1709
1868
  measures,
1869
+ ...reasons,
1710
1870
  points: data.points,
1711
1871
  models: fits.map((fit) => fit.model),
1712
1872
  folds: foldRecords
@@ -2167,6 +2327,6 @@ function calibration(forecasts) {
2167
2327
  interval: wilson(hits, n)
2168
2328
  }));
2169
2329
  }
2170
- export { KENDALL_EXACT, RESPONSE_LAMBDAS, RESPONSE_MAX_POWER, RESPONSE_MAX_TERMS, RESPONSE_RECORDS_SCHEMA, Z95, binomialInterval, bootstrap, bootstrapClusters, bootstrapLeastSquares, bootstrapMedian, bootstrapMedianRatio, bootstrapTwo, brier, calibration, compareRatios, fitResponse, heldOutError, holm, kendallTau, leastSquares, mean, median, medianInterval, negativeBinomialInterval, noiseFloor, noiseVerdict, pairedPermutation, parseResponseRecord, predictResponse, quantile, quantileSorted, ranks, ridgeSolve, sd, signTest, signTestPower, sliceResponse, sorted, spearman, summarize, surfaceResponse, wilson };
2330
+ export { KENDALL_EXACT, RESPONSE_LAMBDAS, RESPONSE_MAX_POWER, RESPONSE_MAX_TERMS, RESPONSE_RECORDS_SCHEMA, Z95, binomialInterval, bootstrap, bootstrapClusters, bootstrapLeastSquares, bootstrapMedian, bootstrapMedianRatio, bootstrapTwo, brier, calibration, compareRatios, fitResponse, heldOutError, heldOutErrorAt, holm, kendallTau, leastSquares, mean, median, medianInterval, negativeBinomialInterval, noiseFloor, noiseVerdict, pairedPermutation, parseResponseRecord, predictResponse, quantile, quantileSorted, ranks, ridgeSolve, sd, signTest, signTestPower, sliceResponse, sorted, spearman, summarize, surfaceResponse, wilson };
2171
2331
 
2172
2332
  //# sourceMappingURL=index.js.map