@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/README.md +31 -1
- package/build/index.d.ts +100 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +271 -56
- package/build/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
955
|
-
function
|
|
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)
|
|
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
|
|
1554
|
-
function
|
|
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
|
|
1760
|
+
const predicted = held.map((r) => predict(r));
|
|
1761
|
+
const errors = held.map((r, i) => rows[r].y - predicted[i]);
|
|
1561
1762
|
return {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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:
|
|
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
|
|
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
|
|
1592
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1680
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|