@mark1russell7/rigor-statistics 0.1.7 → 0.1.9

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.
@@ -652,6 +717,12 @@ function heldValues(name, record, vary, hold, needed) {
652
717
  };
653
718
  return Object.fromEntries(others.map((d) => [d.id, valueOf(d)]));
654
719
  }
720
+ /** This function refuses a bad option `steps`. */
721
+ function checkSteps(name, steps) {
722
+ if (!Number.isSafeInteger(steps) || steps < 2) throw new RangeError(`${name}: steps is a whole number of 2 or more, not ${steps}.`);
723
+ }
724
+ /** This function gives a value inside a range. */
725
+ const clip = (value, [low, high]) => Math.min(high, Math.max(low, value));
655
726
  /** This function gives the values of an axis: each level, or `steps` equal distances on the scale of the dimension. */
656
727
  function axisValues(dimension, steps) {
657
728
  if (dimension.kind !== "continuous") return dimension.levels;
@@ -674,7 +745,7 @@ function matches(dimension, value, held, tolerance) {
674
745
  /** This function examines the request of a view, and it gives the parts that the two views share. */
675
746
  function frameOf(name, record, measure, vary, options) {
676
747
  const { hold = {}, steps = 33, tolerance = 0, width = 1 } = options;
677
- if (!Number.isSafeInteger(steps) || steps < 2) throw new RangeError(`${name}: steps is a whole number of 2 or more, not ${steps}.`);
748
+ checkSteps(name, steps);
678
749
  if (!(Number.isFinite(tolerance) && tolerance >= 0)) throw new RangeError(`${name}: tolerance is a finite number of 0 or more, not ${tolerance}.`);
679
750
  if (!(Number.isFinite(width) && width >= 0)) throw new RangeError(`${name}: width is a finite number of 0 or more, not ${width}.`);
680
751
  const model = record.models.find((m) => m.measure === measure);
@@ -695,33 +766,51 @@ function frameOf(name, record, measure, vary, options) {
695
766
  }
696
767
  const others = record.dimensions.filter((d) => !vary.includes(d.id));
697
768
  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)));
769
+ 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)));
770
+ const matched = placed.filter((p) => typeof p.measures[measure] === "number");
771
+ const missing = placed.filter((p) => typeof p.measures[measure] !== "number");
699
772
  const error = heldOutError(record, measure);
700
773
  const nearest = nearestOf(record.dimensions, record.points, measure);
774
+ const range = record.measures.find((m) => m.id === measure)?.range;
701
775
  const band = (inputs) => {
702
776
  const place = Object.fromEntries(record.dimensions.map((d) => [d.id, whenHolds(d, inputs) ? inputs[d.id] : null]));
703
777
  const value = predictScaled(model, place);
704
- return {
778
+ const fitted = {
705
779
  value: unscale(model, value),
706
780
  low: unscale(model, value - width * error.rmse),
707
- high: unscale(model, value + width * error.rmse),
708
- nearest: nearest(place)
781
+ high: unscale(model, value + width * error.rmse)
782
+ };
783
+ const inside = range === void 0 ? fitted : {
784
+ value: clip(fitted.value, range),
785
+ low: clip(fitted.low, range),
786
+ high: clip(fitted.high, range)
787
+ };
788
+ const moved = inside.value !== fitted.value || inside.low !== fitted.low || inside.high !== fitted.high;
789
+ return {
790
+ ...inside,
791
+ nearest: nearest(place),
792
+ ...moved ? { clipped: true } : {}
709
793
  };
710
794
  };
711
795
  return {
712
796
  held,
713
797
  axes: varied.map((d) => axisValues(d, steps)),
714
798
  matched,
799
+ missing,
715
800
  error,
716
801
  band
717
802
  };
718
803
  }
719
804
  /** This function gives the group field of a measured point of a view. */
720
805
  const groupOf = (point) => point.group === void 0 ? {} : { group: point.group };
806
+ /** This function gives the declared reason of a point with no value of a measure, or `null` for no stated reason. */
807
+ const reasonOf = (point, measure) => point.absent !== void 0 && Object.hasOwn(point.absent, measure) ? point.absent[measure] : null;
721
808
  /**
722
809
  * This function gives a view of a measure over one dimension of a parsed record:
723
810
  * - `curve`: the fitted value at each level, or at `steps` values of a continuous dimension, with its band.
724
811
  * - `points`: the measured points that match the held values. This list is empty when no point matches them.
812
+ * - `absent`: the points that match the held values and have no value of the measure, each with its reason. A chart
813
+ * can draw each reason apart.
725
814
  * - `held`: the value of each other dimension.
726
815
  * - `error`: the held-out error.
727
816
  *
@@ -740,30 +829,42 @@ const groupOf = (point) => point.group === void 0 ? {} : { group: point.group };
740
829
  * distance is on the log scale of the model, so the band is a ratio. The band has one width along all the curve. The
741
830
  * band is the mean error of the model, and `nearest` tells how far the place is from a measurement.
742
831
  *
832
+ * A measure with a `range` keeps each place inside it. The fields `value`, `low` and `high` move to the nearest end.
833
+ * The place then has the field `clipped`, and the field `clipped` of the view counts these places. The function
834
+ * `predictResponse` gives the value that the model fitted.
835
+ *
743
836
  * The function throws a RangeError for an unknown measure or dimension, and for a held value that is not good for its
744
837
  * dimension. It also throws for a dimension that is both varied and held, and for a bad option.
745
838
  */
746
839
  function sliceResponse(record, measure, options) {
747
840
  const { vary } = options;
748
- const { held, axes, matched, error, band } = frameOf("sliceResponse", record, measure, [vary], options);
841
+ const { held, axes, matched, missing, error, band } = frameOf("sliceResponse", record, measure, [vary], options);
842
+ const curve = axes[0].map((at) => ({
843
+ at,
844
+ ...band({
845
+ ...held,
846
+ [vary]: at
847
+ })
848
+ }));
749
849
  return {
750
850
  measure,
751
851
  vary,
752
852
  held,
753
- curve: axes[0].map((at) => ({
754
- at,
755
- ...band({
756
- ...held,
757
- [vary]: at
758
- })
759
- })),
853
+ curve,
760
854
  points: matched.map((p) => ({
761
855
  id: p.id,
762
856
  at: p.values[vary],
763
857
  value: p.measures[measure],
764
858
  ...groupOf(p)
765
859
  })),
766
- error
860
+ absent: missing.map((p) => ({
861
+ id: p.id,
862
+ at: p.values[vary],
863
+ reason: reasonOf(p, measure),
864
+ ...groupOf(p)
865
+ })),
866
+ error,
867
+ clipped: curve.filter((place) => place.clipped === true).length
767
868
  };
768
869
  }
769
870
  /**
@@ -775,29 +876,54 @@ function sliceResponse(record, measure, options) {
775
876
  function surfaceResponse(record, measure, options) {
776
877
  const { vary } = options;
777
878
  const [xId, yId] = vary;
778
- const { held, axes, matched, error, band } = frameOf("surfaceResponse", record, measure, vary, options);
879
+ const { held, axes, matched, missing, error, band } = frameOf("surfaceResponse", record, measure, vary, options);
779
880
  const x = axes[0];
780
881
  const y = axes[1];
882
+ const cells = y.map((b) => x.map((a) => band({
883
+ ...held,
884
+ [xId]: a,
885
+ [yId]: b
886
+ })));
781
887
  return {
782
888
  measure,
783
889
  vary,
784
890
  held,
785
891
  x,
786
892
  y,
787
- cells: y.map((b) => x.map((a) => band({
788
- ...held,
789
- [xId]: a,
790
- [yId]: b
791
- }))),
893
+ cells,
792
894
  points: matched.map((p) => ({
793
895
  id: p.id,
794
896
  at: [p.values[xId], p.values[yId]],
795
897
  value: p.measures[measure],
796
898
  ...groupOf(p)
797
899
  })),
798
- error
900
+ absent: missing.map((p) => ({
901
+ id: p.id,
902
+ at: [p.values[xId], p.values[yId]],
903
+ reason: reasonOf(p, measure),
904
+ ...groupOf(p)
905
+ })),
906
+ error,
907
+ clipped: cells.flat().filter((place) => place.clipped === true).length
799
908
  };
800
909
  }
910
+ /**
911
+ * This function gives the number of fitted places of a view before the view starts: the product of the lengths of its
912
+ * axes. A level axis counts its levels, and a continuous axis counts `steps`. The field `nearest` reads each point at
913
+ * each place, so the reads of a view are this count times the number of points. The default of `steps` is the default
914
+ * of a view. The function throws a RangeError for an unknown dimension, for one dimension named two times, and for a
915
+ * bad `steps`.
916
+ */
917
+ function responseCells(record, vary, steps = 33) {
918
+ checkSteps("responseCells", steps);
919
+ const ids = typeof vary === "string" ? [vary] : vary;
920
+ if (new Set(ids).size !== ids.length) throw new RangeError(`responseCells: the two varied dimensions are one dimension, "${ids[0]}".`);
921
+ return ids.reduce((count, id) => {
922
+ const dimension = record.dimensions.find((d) => d.id === id);
923
+ if (dimension === void 0) throw new RangeError(`responseCells: the record has no dimension "${id}".`);
924
+ return count * (dimension.kind === "continuous" ? steps : dimension.levels.length);
925
+ }, 1);
926
+ }
801
927
  // src/response-parse.ts
802
928
  /**
803
929
  * The validator of the format `response-records/1`. The functions here read unknown data and write faults. Thus a page
@@ -901,9 +1027,7 @@ function checkDimension(value, path, faults) {
901
1027
  need(!Object.hasOwn(object, "levels"), at(path, "levels"), "a continuous dimension has a range, not levels.", faults);
902
1028
  need(!Object.hasOwn(object, "range") || !log || !isNumber(peek(object["range"], "0")) || peek(object["range"], "0") > 0, at(path, "range"), "a log dimension has a range above 0.", faults);
903
1029
  need(Object.hasOwn(object, "range"), at(path, "range"), "this field is missing.", faults);
904
- const range = object["range"];
905
- const good = Array.isArray(range) && range.length === 2 && isNumber(range[0]) && isNumber(range[1]) && range[0] < range[1];
906
- if (Object.hasOwn(object, "range")) need(good, at(path, "range"), `this is a low and a high finite number, with low less than high, not ${shown(range)}.`, faults);
1030
+ if (Object.hasOwn(object, "range")) checkRange(object["range"], at(path, "range"), faults);
907
1031
  } else if (kind === "ordinal" || kind === "categorical") {
908
1032
  need(!Object.hasOwn(object, "range"), at(path, "range"), `a dimension of the kind ${kind} has levels, not a range.`, faults);
909
1033
  need(kind === "ordinal" || !Object.hasOwn(object, "log"), at(path, "log"), "a categorical dimension has no log scale.", faults);
@@ -937,6 +1061,10 @@ function checkWhen(value, path, dimensions, faults) {
937
1061
  if (!Array.isArray(known)) return;
938
1062
  levels.forEach((level, i) => need(known.includes(level), `${at(path, "levels")}[${i}]`, `the dimension ${shown(id)} has no level ${shown(level)}.`, faults));
939
1063
  }
1064
+ /** This function examines a range. */
1065
+ function checkRange(range, path, faults) {
1066
+ need(Array.isArray(range) && range.length === 2 && isNumber(range[0]) && isNumber(range[1]) && range[0] < range[1], path, `this is a low and a high finite number, with low less than high, not ${shown(range)}.`, faults);
1067
+ }
940
1068
  /** This function examines one measure. */
941
1069
  function checkMeasure(value, path, faults) {
942
1070
  const object = fields(value, path, [
@@ -944,20 +1072,46 @@ function checkMeasure(value, path, faults) {
944
1072
  "label",
945
1073
  "unit",
946
1074
  "direction"
947
- ], [], faults);
1075
+ ], ["range"], faults);
948
1076
  if (object === void 0) return;
949
1077
  check(object, "id", path, isId, "text with one character or more", faults);
950
1078
  check(object, "label", path, isString, "text", faults);
951
1079
  check(object, "unit", path, isString, "text", faults);
952
1080
  check(object, "direction", path, (v) => v === "higher" || v === "lower" || v === "none", `"higher", "lower" or "none"`, faults);
1081
+ if (Object.hasOwn(object, "range")) checkRange(object["range"], at(path, "range"), faults);
1082
+ }
1083
+ /** This function refuses a measured value outside the range of its measure. */
1084
+ function checkMeasured(point, path, measures, faults) {
1085
+ for (const { id, range } of measures) {
1086
+ const value = peek(peek(point, "measures"), id);
1087
+ if (range === void 0 || typeof value !== "number") continue;
1088
+ need(value >= range[0] && value <= range[1], at(at(path, "measures"), id), `this value is outside the range of the measure, [${range[0]}, ${range[1]}].`, faults);
1089
+ }
953
1090
  }
954
- /** This function examines one point, with the dimensions and the measures of the record. */
955
- function checkPoint(value, path, dimensions, measures, faults) {
1091
+ /** This function examines one declared reason. */
1092
+ function checkReason(value, path, faults) {
1093
+ const object = fields(value, path, ["id", "label"], [], faults);
1094
+ if (object === void 0) return;
1095
+ check(object, "id", path, isId, "text with one character or more", faults);
1096
+ check(object, "label", path, isString, "text", faults);
1097
+ }
1098
+ /** This function examines the reasons of a point: each has a declaration, and each is on a measure with no number. */
1099
+ function checkAbsent(value, path, measuresOfPoint, measures, reasons, faults) {
1100
+ const absent = fields(value, path, [], [...measures], faults);
1101
+ if (absent === void 0) return;
1102
+ for (const measure of measures) {
1103
+ if (!Object.hasOwn(absent, measure)) continue;
1104
+ need(reasons.has(absent[measure]), at(path, measure), `the record declares no reason ${shown(absent[measure])}.`, faults);
1105
+ need(typeof peek(measuresOfPoint, measure) !== "number", at(path, measure), "this measure has a number, so it has no reason.", faults);
1106
+ }
1107
+ }
1108
+ /** This function examines one point, with the dimensions, the measures and the reasons of the record. */
1109
+ function checkPoint(value, path, dimensions, measures, reasons, faults) {
956
1110
  const object = fields(value, path, [
957
1111
  "id",
958
1112
  "values",
959
1113
  "measures"
960
- ], ["group"], faults);
1114
+ ], ["group", "absent"], faults);
961
1115
  if (object === void 0) return;
962
1116
  check(object, "id", path, isId, "text with one character or more", faults);
963
1117
  check(object, "group", path, isId, "text with one character or more", faults);
@@ -982,6 +1136,7 @@ function checkPoint(value, path, dimensions, measures, faults) {
982
1136
  else need(present, at(inside, dimension.id), `this is null when the dimension "${parent}" is ${shown(values[parent])}.`, faults);
983
1137
  }
984
1138
  }
1139
+ if (Object.hasOwn(object, "absent")) checkAbsent(object["absent"], at(path, "absent"), object["measures"], measures, reasons, faults);
985
1140
  if (Object.hasOwn(object, "measures")) {
986
1141
  const inside = at(path, "measures");
987
1142
  const values = fields(object["measures"], inside, [...measures], [], faults);
@@ -1003,7 +1158,7 @@ function responseDataFaults(value, required, optional) {
1003
1158
  "measures",
1004
1159
  "points",
1005
1160
  ...required
1006
- ], optional, faults);
1161
+ ], ["reasons", ...optional], faults);
1007
1162
  if (object === void 0) return faults;
1008
1163
  check(object, "schema", "", (v) => v === RESPONSE_RECORDS_SCHEMA, `"${RESPONSE_RECORDS_SCHEMA}"`, faults);
1009
1164
  check(object, "name", "", isId, "text with one character or more", faults);
@@ -1016,10 +1171,15 @@ function responseDataFaults(value, required, optional) {
1016
1171
  const measures = Object.hasOwn(object, "measures") ? list(object["measures"], "measures", 1, faults) : [];
1017
1172
  measures.forEach((m, i) => checkMeasure(m, `measures[${i}]`, faults));
1018
1173
  const measureIds = uniqueIds(measures, "measures", faults);
1174
+ const reasons = Object.hasOwn(object, "reasons") ? list(object["reasons"], "reasons", 0, faults) : [];
1175
+ reasons.forEach((r, i) => checkReason(r, `reasons[${i}]`, faults));
1176
+ const reasonIds = uniqueIds(reasons, "reasons", faults);
1019
1177
  if (faults.length > 0) return faults;
1020
1178
  const points = list(object["points"], "points", 1, faults);
1021
- points.forEach((p, i) => checkPoint(p, `points[${i}]`, dimensions, measureIds, faults));
1179
+ points.forEach((p, i) => checkPoint(p, `points[${i}]`, dimensions, measureIds, reasonIds, faults));
1022
1180
  uniqueIds(points, "points", faults);
1181
+ if (faults.length > 0) return faults;
1182
+ points.forEach((p, i) => checkMeasured(p, `points[${i}]`, measures, faults));
1023
1183
  return faults;
1024
1184
  }
1025
1185
  /** This function examines one input of a ridge model against the dimensions of the record. */
@@ -1230,6 +1390,24 @@ function checkFoldError(value, path, count, faults) {
1230
1390
  "baselineMae"
1231
1391
  ]) check(object, key, path, isNotNegative, "a finite number of 0 or more", faults);
1232
1392
  }
1393
+ /** This function examines the stored predictions of a fold against its held-out points and its errors. */
1394
+ function checkPredictions(value, path, held, errors, measures, faults) {
1395
+ const predictions = fields(value, path, [], measures, faults);
1396
+ if (predictions === void 0) return;
1397
+ for (const measure of measures) {
1398
+ if (!Object.hasOwn(predictions, measure)) continue;
1399
+ const inside = at(path, measure);
1400
+ const stored = fields(predictions[measure], inside, [], Object.keys(peek(predictions, measure) ?? {}), faults);
1401
+ if (stored === void 0) continue;
1402
+ need(peek(errors, measure) !== void 0, inside, "this fold has no error of this measure.", faults);
1403
+ const scored = new Set(held.filter((p) => typeof p.measures[measure] === "number").map((p) => p.id));
1404
+ for (const id of Object.keys(stored)) {
1405
+ need(scored.has(id), at(inside, id), `this fold has no held-out point ${shown(id)} with a value of the measure.`, faults);
1406
+ check(stored, id, inside, isNumber, "a finite number", faults);
1407
+ }
1408
+ for (const id of scored) need(Object.hasOwn(stored, id), at(inside, id), "this held-out point has no prediction.", faults);
1409
+ }
1410
+ }
1233
1411
  /** This function adds the points of a fold to `foldOfPoint`, and it gives them. Each bad id gives a fault. */
1234
1412
  function heldPoints(value, path, f, points, foldOfPoint, faults) {
1235
1413
  const held = [];
@@ -1265,15 +1443,18 @@ function checkFolds(value, data, faults) {
1265
1443
  const sizes = groupSizes(data.points);
1266
1444
  const foldOfPoint = /* @__PURE__ */ new Map();
1267
1445
  const covered = /* @__PURE__ */ new Set();
1268
- list(value, "folds", 1, faults).forEach((item, f) => {
1446
+ const folds = list(value, "folds", 1, faults);
1447
+ const predicted = measures.filter((m) => folds.some((fold) => peek(peek(fold, "predictions"), m) !== void 0));
1448
+ folds.forEach((item, f) => {
1269
1449
  const path = `folds[${f}]`;
1270
- const object = fields(item, path, ["heldOut", "errors"], [], faults);
1450
+ const object = fields(item, path, ["heldOut", "errors"], ["predictions"], faults);
1271
1451
  if (object === void 0) return;
1272
1452
  const held = Object.hasOwn(object, "heldOut") ? heldPoints(object["heldOut"], at(path, "heldOut"), f, points, foldOfPoint, faults) : [];
1273
1453
  for (const [group, count] of groupSizes(held)) {
1274
1454
  const size = sizes.get(group);
1275
1455
  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
1456
  }
1457
+ if (Object.hasOwn(object, "predictions")) checkPredictions(object["predictions"], at(path, "predictions"), held, object["errors"], measures, faults);
1277
1458
  if (!Object.hasOwn(object, "errors")) return;
1278
1459
  const errors = fields(object["errors"], at(path, "errors"), [], measures, faults);
1279
1460
  if (errors === void 0) return;
@@ -1282,6 +1463,7 @@ function checkFolds(value, data, faults) {
1282
1463
  const count = held.filter((p) => p.measures[measure] !== null).length;
1283
1464
  checkFoldError(errors[measure], at(at(path, "errors"), measure), count, faults);
1284
1465
  if (count > 0) covered.add(measure);
1466
+ 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
1467
  }
1286
1468
  });
1287
1469
  for (const measure of measures) need(covered.has(measure), "folds", `no fold has a held-out error of the measure "${measure}".`, faults);
@@ -1300,6 +1482,15 @@ function checkFolds(value, data, faults) {
1300
1482
  * - 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
1483
  * - The count `n` of an error is the number of held-out points with a value of the measure.
1302
1484
  * - Each measure has an error in one fold or more.
1485
+ *
1486
+ * These rules apply to the predictions of a fold:
1487
+ * - Each prediction is a finite number of a held-out point of the fold with a value of the measure.
1488
+ * - The fold has a prediction of each such point, and an error of the measure.
1489
+ * - When one fold has predictions of a measure, each fold with an error of the measure has them.
1490
+ *
1491
+ * These rules apply to the reasons of a point:
1492
+ * - Each reason is on a known measure with no number. A measure with `null` and no reason is good.
1493
+ * - Each reason has a declaration in the field `reasons` of the record.
1303
1494
  */
1304
1495
  function parseResponseRecord(value) {
1305
1496
  try {
@@ -1433,6 +1624,15 @@ function expansion(dimensions, degree, interactions) {
1433
1624
  });
1434
1625
  return terms;
1435
1626
  }
1627
+ /** 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. */
1628
+ function seenTerms(terms, dimensions, points) {
1629
+ const inputs = checkedInputs(dimensions, terms);
1630
+ const places = points.map((point) => ({
1631
+ at: coordinates(inputs, point.values),
1632
+ values: point.values
1633
+ }));
1634
+ return terms.filter((term) => places.some(({ at, values }) => product(term, at, values) !== 0));
1635
+ }
1436
1636
  /** This function gives the inputs of a ridge fit: one for each dimension of a term. It throws for a bad term. */
1437
1637
  function checkedInputs(dimensions, terms) {
1438
1638
  const used = new Set(terms.flatMap((t) => t.map((f) => f.dimension)));
@@ -1550,19 +1750,23 @@ function tableLearner(rows, dimensions) {
1550
1750
  return (r) => cells.get(keys[r]) ?? fallback;
1551
1751
  };
1552
1752
  }
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) {
1753
+ /** This function gives the score of a fold. A fold with no held-out row or no training row has no score. */
1754
+ function foldScore(rows, fold, learner) {
1555
1755
  const train = rows.filter((row) => row.fold !== fold).map((row) => row.y);
1556
1756
  const held = rows.flatMap((row, r) => row.fold === fold ? [r] : []);
1557
1757
  if (held.length === 0 || train.length === 0) return void 0;
1558
1758
  const predict = learner(fold);
1559
1759
  const baseline = mean(train);
1560
- const errors = held.map((r) => rows[r].y - predict(r));
1760
+ const predicted = held.map((r) => predict(r));
1761
+ const errors = held.map((r, i) => rows[r].y - predicted[i]);
1561
1762
  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)))
1763
+ error: {
1764
+ n: held.length,
1765
+ mae: mean(errors.map(Math.abs)),
1766
+ rmse: Math.sqrt(mean(errors.map((e) => e * e))),
1767
+ baselineMae: mean(held.map((r) => Math.abs(rows[r].y - baseline)))
1768
+ },
1769
+ predictions: Object.fromEntries(held.map((r, i) => [rows[r].point.id, predicted[i]]))
1566
1770
  };
1567
1771
  }
1568
1772
  /** This function gives the rows of a measure. It throws a RangeError for a value that a log scale cannot read. */
@@ -1573,12 +1777,12 @@ function rowsOf(points, measure, log, floor, foldOf) {
1573
1777
  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
1778
  return [{
1575
1779
  point,
1576
- y: log ? Math.log10(Math.max(value, floor ?? value)) : value,
1780
+ y: toModelScale(value, log, floor),
1577
1781
  fold: foldOf.get(groupKey(point))
1578
1782
  }];
1579
1783
  });
1580
1784
  }
1581
- /** This function gives the model of one measure on all its rows, and the error of each fold. */
1785
+ /** This function gives the model of one measure on all its rows, and the score of each fold. */
1582
1786
  function fitMeasure(rows, measure, log, floor, plan) {
1583
1787
  if (rows.length === 0) throw new RangeError(`fitResponse: no point has a value of the measure "${measure.id}".`);
1584
1788
  const base = {
@@ -1588,8 +1792,8 @@ function fitMeasure(rows, measure, log, floor, plan) {
1588
1792
  };
1589
1793
  const ridge = plan.kind === "ridge" ? ridgeLearner(rows, plan) : void 0;
1590
1794
  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.`);
1795
+ const scores = Array.from({ length: plan.folds }, (_, f) => foldScore(rows, f, learner));
1796
+ 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
1797
  const conditional = plan.conditional.length === 0 ? {} : { conditional: plan.conditional };
1594
1798
  if (ridge === void 0) return {
1595
1799
  model: {
@@ -1600,7 +1804,7 @@ function fitMeasure(rows, measure, log, floor, plan) {
1600
1804
  cells: tableCells(rows, plan.ids),
1601
1805
  fallback: mean(rows.map((r) => r.y))
1602
1806
  },
1603
- errors
1807
+ scores
1604
1808
  };
1605
1809
  const { lambda, beta } = ridge.final();
1606
1810
  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 +1821,7 @@ function fitMeasure(rows, measure, log, floor, plan) {
1617
1821
  intercept: beta[0],
1618
1822
  terms
1619
1823
  },
1620
- errors
1824
+ scores
1621
1825
  };
1622
1826
  }
1623
1827
  /** This function examines the options that do not depend on the data, and it gives the number of folds and the penalties. */
@@ -1659,7 +1863,10 @@ function kindOf(dimensions, model) {
1659
1863
  * The penalty: each fold of a ridge fit chooses from `lambdas` with the other folds. Thus the held-out error of a
1660
1864
  * fold is not optimistic. The model of the record chooses with all the folds.
1661
1865
  *
1662
- * The errors: a fold scores the model of the other folds on its held-out points, on the scale of the model.
1866
+ * The errors: a fold scores the model of the other folds on its held-out points, on the scale of the model. With the
1867
+ * option `outOfFold`, the fold also stores the prediction of each of these points.
1868
+ *
1869
+ * The reasons: a measure with a stated reason is `null` for the fit. The field `reasons` of the data stays in the record.
1663
1870
  *
1664
1871
  * The result does not depend on the order of the points, except for the field `points`, which keeps the order of
1665
1872
  * the data. The same data and options give the same bytes in one JavaScript engine.
@@ -1676,8 +1883,10 @@ function fitResponse(data, options = {}) {
1676
1883
  const log = new Set(options.log ?? []);
1677
1884
  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
1885
  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.`);
1886
+ const interactions = options.interactions ?? true;
1887
+ const expanded = kind === "table" ? [] : options.terms ?? expansion(dimensions, degree, interactions !== false);
1888
+ const terms = interactions === "seen" && options.terms === void 0 ? seenTerms(expanded, dimensions, data.points) : expanded;
1889
+ 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
1890
  const inputs = checkedInputs(dimensions, terms);
1682
1891
  const points = [...data.points].sort((a, b) => byText(a.id, b.id));
1683
1892
  const groups = [...new Set(points.map(groupKey))].sort(byText);
@@ -1695,18 +1904,24 @@ function fitResponse(data, options = {}) {
1695
1904
  inputs
1696
1905
  };
1697
1906
  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
- }));
1907
+ const foldRecords = Array.from({ length: folds }, (_, f) => {
1908
+ const scored = measures.flatMap((m, i) => {
1909
+ const score = fits[i]?.scores[f];
1910
+ return score === void 0 ? [] : [[m.id, score]];
1911
+ });
1912
+ return {
1913
+ heldOut: points.filter((p) => foldOf.get(groupKey(p)) === f).map((p) => p.id),
1914
+ errors: Object.fromEntries(scored.map(([id, score]) => [id, score.error])),
1915
+ ...options.outOfFold === true ? { predictions: Object.fromEntries(scored.map(([id, score]) => [id, score.predictions])) } : {}
1916
+ };
1917
+ });
1918
+ const reasons = data.reasons === void 0 ? {} : { reasons: data.reasons };
1705
1919
  return {
1706
1920
  schema: RESPONSE_RECORDS_SCHEMA,
1707
1921
  name: data.name,
1708
1922
  dimensions,
1709
1923
  measures,
1924
+ ...reasons,
1710
1925
  points: data.points,
1711
1926
  models: fits.map((fit) => fit.model),
1712
1927
  folds: foldRecords
@@ -2167,6 +2382,6 @@ function calibration(forecasts) {
2167
2382
  interval: wilson(hits, n)
2168
2383
  }));
2169
2384
  }
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 };
2385
+ 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, responseCells, ridgeSolve, sd, signTest, signTestPower, sliceResponse, sorted, spearman, summarize, surfaceResponse, whenHolds, wilson };
2171
2386
 
2172
2387
  //# sourceMappingURL=index.js.map