@compstats/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +127 -110
- package/dist/3d.js +1120 -122
- package/dist/3d.js.map +16 -9
- package/dist/core/arith.d.ts.map +1 -1
- package/dist/core/linalg/cov.d.ts +50 -0
- package/dist/core/linalg/cov.d.ts.map +1 -0
- package/dist/core/linalg/eigen.d.ts +53 -0
- package/dist/core/linalg/eigen.d.ts.map +1 -0
- package/dist/core/linalg/lm.d.ts +78 -0
- package/dist/core/linalg/lm.d.ts.map +1 -0
- package/dist/core/linalg/lu.d.ts +154 -0
- package/dist/core/linalg/lu.d.ts.map +1 -0
- package/dist/core/linalg/matrix.d.ts +131 -0
- package/dist/core/linalg/matrix.d.ts.map +1 -0
- package/dist/core/linalg/modelMatrix.d.ts +69 -0
- package/dist/core/linalg/modelMatrix.d.ts.map +1 -0
- package/dist/core/linalg/namedVector.d.ts +37 -0
- package/dist/core/linalg/namedVector.d.ts.map +1 -0
- package/dist/core/linalg/ops.d.ts +120 -0
- package/dist/core/linalg/ops.d.ts.map +1 -0
- package/dist/core/linalg/prcomp.d.ts +66 -0
- package/dist/core/linalg/prcomp.d.ts.map +1 -0
- package/dist/core/linalg/qr.d.ts +134 -0
- package/dist/core/linalg/qr.d.ts.map +1 -0
- package/dist/core/linalg/vector.d.ts +68 -0
- package/dist/core/linalg/vector.d.ts.map +1 -0
- package/dist/core/moderation.d.ts +6 -3
- package/dist/core/moderation.d.ts.map +1 -1
- package/dist/core/ols.d.ts +4 -7
- package/dist/core/ols.d.ts.map +1 -1
- package/dist/data/moderationData.d.ts +2 -2
- package/dist/data/pcaDegenerate.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1601 -863
- package/dist/index.js.map +17 -11
- package/dist/linalg.d.ts +36 -0
- package/dist/linalg.d.ts.map +1 -0
- package/dist/linalg.js +1860 -0
- package/dist/linalg.js.map +24 -0
- package/dist/plot/moderation3d.d.ts +1 -1
- package/dist/plot/sampling.d.ts +45 -0
- package/dist/plot/sampling.d.ts.map +1 -1
- package/dist/plot/scatter3d.d.ts +1 -1
- package/package.json +16 -5
package/dist/index.js
CHANGED
|
@@ -48,6 +48,9 @@ function meanAbsoluteDeviation(values) {
|
|
|
48
48
|
return mean(values.map((value) => Math.abs(value - center)));
|
|
49
49
|
}
|
|
50
50
|
function fusedMultiplyAdd(a, b, c) {
|
|
51
|
+
if (a * b === 0) {
|
|
52
|
+
return c + a * b;
|
|
53
|
+
}
|
|
51
54
|
const [product, productError] = twoProduct(a, b);
|
|
52
55
|
const [sum2, sumError] = twoSum(c, product);
|
|
53
56
|
const rounded = sum2 + (sumError + productError);
|
|
@@ -630,960 +633,1549 @@ function requireNumericColumn(data, name, role) {
|
|
|
630
633
|
}
|
|
631
634
|
return column;
|
|
632
635
|
}
|
|
633
|
-
// src/core/
|
|
634
|
-
var
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
636
|
+
// src/core/special.ts
|
|
637
|
+
var LANCZOS_G = 607 / 128;
|
|
638
|
+
var LANCZOS_LEAD = 0.9999999999999971;
|
|
639
|
+
var LANCZOS_TAIL = [
|
|
640
|
+
57.15623566586292,
|
|
641
|
+
-59.59796035547549,
|
|
642
|
+
14.136097974741746,
|
|
643
|
+
-0.4919138160976202,
|
|
644
|
+
0.00003399464998481189,
|
|
645
|
+
0.00004652362892704858,
|
|
646
|
+
-0.00009837447530487956,
|
|
647
|
+
0.0001580887032249125,
|
|
648
|
+
-0.00021026444172410488,
|
|
649
|
+
0.00021743961811521265,
|
|
650
|
+
-0.0001643181065367639,
|
|
651
|
+
0.00008441822398385275,
|
|
652
|
+
-0.000026190838401581408,
|
|
653
|
+
0.0000036899182659531625
|
|
654
|
+
];
|
|
655
|
+
var LOG_SQRT_TWO_PI = 0.5 * Math.log(2 * Math.PI);
|
|
656
|
+
function lanczosSeries(x) {
|
|
657
|
+
return LANCZOS_LEAD + sum(LANCZOS_TAIL.map((coefficient, index) => coefficient / (x + index)));
|
|
658
|
+
}
|
|
659
|
+
function logGamma(x) {
|
|
660
|
+
if (!(x > 0)) {
|
|
661
|
+
return Number.NaN;
|
|
640
662
|
}
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
663
|
+
const shifted = x + LANCZOS_G - 0.5;
|
|
664
|
+
return LOG_SQRT_TWO_PI + (x - 0.5) * Math.log(shifted) - shifted + Math.log(lanczosSeries(x));
|
|
665
|
+
}
|
|
666
|
+
function logBeta(a, b) {
|
|
667
|
+
if (!(a > 0) || !(b > 0)) {
|
|
668
|
+
return Number.NaN;
|
|
644
669
|
}
|
|
645
|
-
|
|
646
|
-
|
|
670
|
+
const shiftedSum = a + b + LANCZOS_G - 0.5;
|
|
671
|
+
return LOG_SQRT_TWO_PI - (LANCZOS_G - 0.5) + Math.log(lanczosSeries(a)) + Math.log(lanczosSeries(b)) - Math.log(lanczosSeries(a + b)) + (a - 0.5) * Math.log1p(-b / shiftedSum) + (b - 0.5) * Math.log1p(-a / shiftedSum) - 0.5 * Math.log(shiftedSum);
|
|
672
|
+
}
|
|
673
|
+
var FRACTION_MAX_STEPS = 400;
|
|
674
|
+
var FRACTION_EPSILON = 0.0000000000000003;
|
|
675
|
+
var FRACTION_FLOOR = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001;
|
|
676
|
+
function betaContinuedFraction(x, a, b) {
|
|
677
|
+
const total = a + b;
|
|
678
|
+
const aPlus = a + 1;
|
|
679
|
+
const aMinus = a - 1;
|
|
680
|
+
let c = 1;
|
|
681
|
+
let d = 1 - total * x / aPlus;
|
|
682
|
+
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
683
|
+
d = FRACTION_FLOOR;
|
|
647
684
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
685
|
+
d = 1 / d;
|
|
686
|
+
let value = d;
|
|
687
|
+
for (let step = 1;step <= FRACTION_MAX_STEPS; step += 1) {
|
|
688
|
+
const twice = 2 * step;
|
|
689
|
+
const even = step * (b - step) * x / ((aMinus + twice) * (a + twice));
|
|
690
|
+
d = 1 + even * d;
|
|
691
|
+
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
692
|
+
d = FRACTION_FLOOR;
|
|
651
693
|
}
|
|
652
|
-
|
|
653
|
-
|
|
694
|
+
c = 1 + even / c;
|
|
695
|
+
if (Math.abs(c) < FRACTION_FLOOR) {
|
|
696
|
+
c = FRACTION_FLOOR;
|
|
654
697
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
applyHouseholders(columns, householders, projected, Math.min(rank, rows - 1));
|
|
662
|
-
const solved = backSubstitute(columns, projected, rank);
|
|
663
|
-
const coefficients = new Array(width).fill(null);
|
|
664
|
-
pivot.slice(0, rank).forEach((column, position) => {
|
|
665
|
-
coefficients[column] = solved[position];
|
|
666
|
-
});
|
|
667
|
-
const fitted = design.map((row) => sum(zipWith(row, coefficients, (value, coefficient) => coefficient === null ? 0 : value * coefficient)));
|
|
668
|
-
const residuals = zipWith(y, fitted, (value, fit) => value - fit);
|
|
669
|
-
return { coefficients, fitted, residuals, rank };
|
|
670
|
-
}
|
|
671
|
-
function decompose(columns, tolerance, rows) {
|
|
672
|
-
const width = columns.length;
|
|
673
|
-
const pivot = columns.map((_, column) => column);
|
|
674
|
-
const originalNorms = columns.map((column) => norm(column, 0) || 1);
|
|
675
|
-
const householders = new Array(width).fill(0);
|
|
676
|
-
let live = width;
|
|
677
|
-
for (let step = 0;step < Math.min(rows, width); step++) {
|
|
678
|
-
while (step < live && norm(columns[step], step) < originalNorms[step] * tolerance) {
|
|
679
|
-
cycleToEnd(columns, pivot, originalNorms, step);
|
|
680
|
-
live -= 1;
|
|
698
|
+
d = 1 / d;
|
|
699
|
+
value *= d * c;
|
|
700
|
+
const odd = -(a + step) * (total + step) * x / ((a + twice) * (aPlus + twice));
|
|
701
|
+
d = 1 + odd * d;
|
|
702
|
+
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
703
|
+
d = FRACTION_FLOOR;
|
|
681
704
|
}
|
|
682
|
-
|
|
683
|
-
|
|
705
|
+
c = 1 + odd / c;
|
|
706
|
+
if (Math.abs(c) < FRACTION_FLOOR) {
|
|
707
|
+
c = FRACTION_FLOOR;
|
|
708
|
+
}
|
|
709
|
+
d = 1 / d;
|
|
710
|
+
const delta = d * c;
|
|
711
|
+
value *= delta;
|
|
712
|
+
if (Math.abs(delta - 1) < FRACTION_EPSILON) {
|
|
713
|
+
break;
|
|
684
714
|
}
|
|
685
|
-
householders[step] = reflect(columns, step, rows);
|
|
686
715
|
}
|
|
687
|
-
return
|
|
716
|
+
return value;
|
|
688
717
|
}
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
718
|
+
function incompleteBeta(x, a, b) {
|
|
719
|
+
if (Number.isNaN(x)) {
|
|
720
|
+
return Number.NaN;
|
|
721
|
+
}
|
|
722
|
+
if (x <= 0) {
|
|
693
723
|
return 0;
|
|
694
724
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
column[row] = column[row] / pivotNorm;
|
|
725
|
+
if (x >= 1) {
|
|
726
|
+
return 1;
|
|
698
727
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
728
|
+
return incompleteBetaSplit(x, 1 - x, a, b);
|
|
729
|
+
}
|
|
730
|
+
function incompleteBetaSplit(x, complement, a, b) {
|
|
731
|
+
if (Number.isNaN(x) || Number.isNaN(complement) || !(a > 0) || !(b > 0)) {
|
|
732
|
+
return Number.NaN;
|
|
733
|
+
}
|
|
734
|
+
if (x <= 0) {
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
if (complement <= 0) {
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
const logX = complement < 0.5 ? Math.log1p(-complement) : Math.log(x);
|
|
741
|
+
const logComplement = x < 0.5 ? Math.log1p(-x) : Math.log(complement);
|
|
742
|
+
const front = Math.exp(a * logX + b * logComplement - logBeta(a, b));
|
|
743
|
+
if (x < (a + 1) / (a + b + 2)) {
|
|
744
|
+
return front * betaContinuedFraction(x, a, b) / a;
|
|
745
|
+
}
|
|
746
|
+
return 1 - front * betaContinuedFraction(complement, b, a) / b;
|
|
747
|
+
}
|
|
748
|
+
var GAMMA_MAX_STEPS = 1000;
|
|
749
|
+
var GAMMA_EPSILON = 0.0000000000000003;
|
|
750
|
+
function lowerGammaSeries(a, x) {
|
|
751
|
+
let term = 1 / a;
|
|
752
|
+
let total = term;
|
|
753
|
+
for (let step = 1;step <= GAMMA_MAX_STEPS; step += 1) {
|
|
754
|
+
term *= x / (a + step);
|
|
755
|
+
total += term;
|
|
756
|
+
if (Math.abs(term) < Math.abs(total) * GAMMA_EPSILON) {
|
|
757
|
+
break;
|
|
710
758
|
}
|
|
711
759
|
}
|
|
712
|
-
|
|
713
|
-
return leading;
|
|
760
|
+
return total * Math.exp(-x + a * Math.log(x) - logGamma(a));
|
|
714
761
|
}
|
|
715
|
-
function
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
762
|
+
function upperGammaFraction(a, x) {
|
|
763
|
+
let b = x + 1 - a;
|
|
764
|
+
let c = 1 / FRACTION_FLOOR;
|
|
765
|
+
let d = 1 / b;
|
|
766
|
+
let value = d;
|
|
767
|
+
for (let step = 1;step <= GAMMA_MAX_STEPS; step += 1) {
|
|
768
|
+
const numerator = -step * (step - a);
|
|
769
|
+
b += 2;
|
|
770
|
+
d = numerator * d + b;
|
|
771
|
+
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
772
|
+
d = FRACTION_FLOOR;
|
|
721
773
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
inner += column[row] * response[row];
|
|
774
|
+
c = b + numerator / c;
|
|
775
|
+
if (Math.abs(c) < FRACTION_FLOOR) {
|
|
776
|
+
c = FRACTION_FLOOR;
|
|
726
777
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
778
|
+
d = 1 / d;
|
|
779
|
+
const delta = d * c;
|
|
780
|
+
value *= delta;
|
|
781
|
+
if (Math.abs(delta - 1) < GAMMA_EPSILON) {
|
|
782
|
+
break;
|
|
731
783
|
}
|
|
732
784
|
}
|
|
785
|
+
return value * Math.exp(-x + a * Math.log(x) - logGamma(a));
|
|
733
786
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
let value = response[row];
|
|
738
|
-
for (let column = row + 1;column < rank; column++) {
|
|
739
|
-
value -= columns[column][row] * solved[column];
|
|
740
|
-
}
|
|
741
|
-
solved[row] = value / columns[row][row];
|
|
787
|
+
function upperGamma(a, x) {
|
|
788
|
+
if (x <= 0) {
|
|
789
|
+
return 1;
|
|
742
790
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
moveToEnd(pivot, step);
|
|
748
|
-
moveToEnd(originalNorms, step);
|
|
791
|
+
if (!Number.isFinite(x)) {
|
|
792
|
+
return 0;
|
|
793
|
+
}
|
|
794
|
+
return x < a + 1 ? 1 - lowerGammaSeries(a, x) : upperGammaFraction(a, x);
|
|
749
795
|
}
|
|
750
|
-
function
|
|
751
|
-
|
|
752
|
-
|
|
796
|
+
function normalCdf(z) {
|
|
797
|
+
if (Number.isNaN(z)) {
|
|
798
|
+
return Number.NaN;
|
|
799
|
+
}
|
|
800
|
+
const lower = 0.5 * upperGamma(0.5, 0.5 * z * z);
|
|
801
|
+
return z > 0 ? 1 - lower : lower;
|
|
753
802
|
}
|
|
754
|
-
|
|
755
|
-
|
|
803
|
+
var BELOW_ONE = 1 - Number.EPSILON / 2;
|
|
804
|
+
var INVERSE_MAX_STEPS = 200;
|
|
805
|
+
function inverseGuess(p, a, b) {
|
|
806
|
+
if (a >= 1 && b >= 1) {
|
|
807
|
+
const tail = p < 0.5 ? p : 1 - p;
|
|
808
|
+
const t = Math.sqrt(-2 * Math.log(tail));
|
|
809
|
+
const normal = (p < 0.5 ? -1 : 1) * ((2.30753 + t * 0.27061) / (1 + t * (0.99229 + t * 0.04481)) - t);
|
|
810
|
+
const scale = (normal * normal - 3) / 6;
|
|
811
|
+
const harmonic = 2 / (1 / (2 * a - 1) + 1 / (2 * b - 1));
|
|
812
|
+
const w = normal * Math.sqrt(scale + harmonic) / harmonic - (1 / (2 * b - 1) - 1 / (2 * a - 1)) * (scale + 5 / 6 - 2 / (3 * harmonic));
|
|
813
|
+
return a / (a + b * Math.exp(2 * w));
|
|
814
|
+
}
|
|
815
|
+
const lower = Math.exp(a * Math.log(a / (a + b))) / a;
|
|
816
|
+
const upper = Math.exp(b * Math.log(b / (a + b))) / b;
|
|
817
|
+
const total = lower + upper;
|
|
818
|
+
if (p < lower / total) {
|
|
819
|
+
return Math.pow(a * total * p, 1 / a);
|
|
820
|
+
}
|
|
821
|
+
return 1 - Math.pow(b * total * (1 - p), 1 / b);
|
|
756
822
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
function moderationSurface(data, options) {
|
|
761
|
-
const { outcome, iv, mod, interaction = true, controls = [] } = options;
|
|
762
|
-
if (iv === mod) {
|
|
763
|
-
throw new RangeError(`\`iv\` and \`mod\` must name different columns, both name "${iv}"`);
|
|
823
|
+
function inverseIncompleteBeta(p, a, b) {
|
|
824
|
+
if (Number.isNaN(p) || !(a > 0) || !(b > 0)) {
|
|
825
|
+
return Number.NaN;
|
|
764
826
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (named.has(control)) {
|
|
768
|
-
throw new RangeError(`control "${control}" already names the outcome, the iv, the mod, ` + "or another control");
|
|
769
|
-
}
|
|
770
|
-
named.add(control);
|
|
771
|
-
});
|
|
772
|
-
const rows = frameRows(data);
|
|
773
|
-
const y = requireNumericColumn(data, outcome, "outcome");
|
|
774
|
-
const ivColumn = requireNumericColumn(data, iv, "iv");
|
|
775
|
-
const modColumn = requireNumericColumn(data, mod, "mod");
|
|
776
|
-
const controlColumns = controls.map((control) => requireNumericColumn(data, control, "controls"));
|
|
777
|
-
const modelColumns = [y, ivColumn, modColumn, ...controlColumns];
|
|
778
|
-
const completeRows = y.map((_, row) => row).filter((row) => modelColumns.every((column) => Number.isFinite(column[row])));
|
|
779
|
-
if (completeRows.length === 0) {
|
|
780
|
-
throw new RangeError("the model has no complete rows: every row is missing a value in " + "the outcome, the IV, the moderator, or a control");
|
|
827
|
+
if (p <= 0) {
|
|
828
|
+
return 0;
|
|
781
829
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
{ name: iv, values: ivColumn },
|
|
785
|
-
{ name: mod, values: modColumn },
|
|
786
|
-
...controls.map((control, index) => ({
|
|
787
|
-
name: control,
|
|
788
|
-
values: controlColumns[index]
|
|
789
|
-
})),
|
|
790
|
-
...interaction ? [
|
|
791
|
-
{
|
|
792
|
-
name: `${iv}:${mod}`,
|
|
793
|
-
values: zipWith(ivColumn, modColumn, (a, b) => a * b)
|
|
794
|
-
}
|
|
795
|
-
] : []
|
|
796
|
-
];
|
|
797
|
-
const design = completeRows.map((row) => designColumns.map((column) => column.values[row]));
|
|
798
|
-
const fit = leastSquares(design, completeRows.map((row) => y[row]));
|
|
799
|
-
const coefficients = designColumns.map((column, index) => ({
|
|
800
|
-
name: column.name,
|
|
801
|
-
value: fit.coefficients[index] ?? null
|
|
802
|
-
}));
|
|
803
|
-
const fitted = new Array(rows).fill(Number.NaN);
|
|
804
|
-
const residuals = new Array(rows).fill(Number.NaN);
|
|
805
|
-
completeRows.forEach((row, survivor) => {
|
|
806
|
-
fitted[row] = fit.fitted[survivor];
|
|
807
|
-
residuals[row] = fit.residuals[survivor];
|
|
808
|
-
});
|
|
809
|
-
const ivValues = rSeq(...extent(ivColumn), GRID_STEPS);
|
|
810
|
-
const modValues = rSeq(...extent(modColumn), GRID_STEPS);
|
|
811
|
-
const holds = Object.fromEntries(controls.map((control, index) => [
|
|
812
|
-
control,
|
|
813
|
-
mean(controlColumns[index].filter(Number.isFinite))
|
|
814
|
-
]));
|
|
815
|
-
const weights = coefficients.map((term) => term.value ?? 0);
|
|
816
|
-
const predictions = modValues.flatMap((modValue) => ivValues.map((ivValue) => {
|
|
817
|
-
const gridRow = [
|
|
818
|
-
1,
|
|
819
|
-
ivValue,
|
|
820
|
-
modValue,
|
|
821
|
-
...controls.map((control) => holds[control]),
|
|
822
|
-
...interaction ? [ivValue * modValue] : []
|
|
823
|
-
];
|
|
824
|
-
return sum(zipWith(gridRow, weights, (value, weight) => value * weight));
|
|
825
|
-
}));
|
|
826
|
-
const [dataLow, dataHigh] = extent(y);
|
|
827
|
-
const [surfaceLow, surfaceHigh] = extent(predictions);
|
|
828
|
-
return {
|
|
829
|
-
coefficients,
|
|
830
|
-
fitted,
|
|
831
|
-
residuals,
|
|
832
|
-
ivValues,
|
|
833
|
-
modValues,
|
|
834
|
-
predictions,
|
|
835
|
-
zlim: [Math.min(dataLow, surfaceLow), Math.max(dataHigh, surfaceHigh)],
|
|
836
|
-
holds
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
function rSeq(from, to, length) {
|
|
840
|
-
if (from === to) {
|
|
841
|
-
return new Array(length).fill(from);
|
|
830
|
+
if (p >= 1) {
|
|
831
|
+
return 1;
|
|
842
832
|
}
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
function linearRegression(points) {
|
|
851
|
-
const rows = completePointRows(points);
|
|
852
|
-
if (rows.length === 0) {
|
|
853
|
-
return null;
|
|
833
|
+
const logBetaValue = logBeta(a, b);
|
|
834
|
+
let lower = 0;
|
|
835
|
+
let upper = 1;
|
|
836
|
+
let x = inverseGuess(p, a, b);
|
|
837
|
+
if (!(x > 0) || !(x < 1)) {
|
|
838
|
+
x = 0.5;
|
|
854
839
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
return
|
|
877
|
-
intercept,
|
|
878
|
-
slope,
|
|
879
|
-
correlation,
|
|
880
|
-
ssr,
|
|
881
|
-
sse,
|
|
882
|
-
sst,
|
|
883
|
-
rSquared,
|
|
884
|
-
fitted
|
|
885
|
-
};
|
|
840
|
+
for (let step = 0;step < INVERSE_MAX_STEPS; step += 1) {
|
|
841
|
+
const residual = incompleteBeta(x, a, b) - p;
|
|
842
|
+
if (residual < 0) {
|
|
843
|
+
lower = x;
|
|
844
|
+
} else {
|
|
845
|
+
upper = x;
|
|
846
|
+
}
|
|
847
|
+
const density = Math.exp((a - 1) * Math.log(x) + (b - 1) * Math.log1p(-x) - logBetaValue);
|
|
848
|
+
let next = density > 0 && Number.isFinite(density) ? x - residual / density : Number.NaN;
|
|
849
|
+
if (!(next > lower) || !(next < upper)) {
|
|
850
|
+
next = 0.5 * (lower + upper);
|
|
851
|
+
}
|
|
852
|
+
if (next === x) {
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
const moved = Math.abs(next - x);
|
|
856
|
+
x = next;
|
|
857
|
+
if (moved <= Number.EPSILON * x) {
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return Math.min(x, BELOW_ONE);
|
|
886
862
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
function
|
|
890
|
-
return
|
|
863
|
+
|
|
864
|
+
// src/core/tdist.ts
|
|
865
|
+
function isNonCentral(ncp) {
|
|
866
|
+
return ncp !== undefined && ncp !== 0;
|
|
891
867
|
}
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (
|
|
897
|
-
return
|
|
898
|
-
determinant: determinant2,
|
|
899
|
-
inverse: null,
|
|
900
|
-
singularity: "exact",
|
|
901
|
-
rcond: 0,
|
|
902
|
-
zeroPivot
|
|
903
|
-
};
|
|
868
|
+
function isBadArgument(value, df, ncp) {
|
|
869
|
+
return Number.isNaN(value) || !(df > 0) || ncp !== undefined && Number.isNaN(ncp);
|
|
870
|
+
}
|
|
871
|
+
function dt(x, df, ncp) {
|
|
872
|
+
if (isBadArgument(x, df, ncp)) {
|
|
873
|
+
return Number.NaN;
|
|
904
874
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
inverse: null,
|
|
911
|
-
singularity: "computational",
|
|
912
|
-
rcond,
|
|
913
|
-
zeroPivot: null
|
|
914
|
-
};
|
|
875
|
+
return isNonCentral(ncp) ? nonCentralDensity(x, df, ncp) : centralDensity(x, df);
|
|
876
|
+
}
|
|
877
|
+
function pt(x, df, ncp) {
|
|
878
|
+
if (isBadArgument(x, df, ncp)) {
|
|
879
|
+
return Number.NaN;
|
|
915
880
|
}
|
|
916
|
-
return
|
|
917
|
-
determinant: determinant2,
|
|
918
|
-
inverse,
|
|
919
|
-
singularity: null,
|
|
920
|
-
rcond,
|
|
921
|
-
zeroPivot: null
|
|
922
|
-
};
|
|
881
|
+
return isNonCentral(ncp) ? nonCentralProbability(x, df, ncp) : centralProbability(x, df);
|
|
923
882
|
}
|
|
924
|
-
function
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const belowRight = interchanged ? matrix.x2 : matrix.y2;
|
|
930
|
-
const multiplier = Math.abs(leading) >= SMALLEST_NORMAL ? below * (1 / leading) : below / leading;
|
|
931
|
-
const trailing = fusedMultiplyAdd(-multiplier, upperRight, belowRight);
|
|
932
|
-
return {
|
|
933
|
-
interchanged,
|
|
934
|
-
multiplier,
|
|
935
|
-
pivots: [leading, trailing],
|
|
936
|
-
upperRight
|
|
937
|
-
};
|
|
883
|
+
function qt(p, df, ncp) {
|
|
884
|
+
if (isBadArgument(p, df, ncp) || p < 0 || p > 1) {
|
|
885
|
+
return Number.NaN;
|
|
886
|
+
}
|
|
887
|
+
return isNonCentral(ncp) ? nonCentralQuantile(p, df, ncp) : centralQuantile(p, df);
|
|
938
888
|
}
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
if (zeroPivotOf(factorization) !== null) {
|
|
889
|
+
function centralDensity(x, df) {
|
|
890
|
+
if (!Number.isFinite(x)) {
|
|
942
891
|
return 0;
|
|
943
892
|
}
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
return sign * Math.exp(modulus);
|
|
893
|
+
const logDensity = -0.5 * Math.log(df) - logBeta(0.5, df / 2) - (df + 1) / 2 * Math.log1p(x * x / df);
|
|
894
|
+
return Math.exp(logDensity);
|
|
947
895
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
896
|
+
function centralProbability(x, df) {
|
|
897
|
+
if (x === 0) {
|
|
898
|
+
return 0.5;
|
|
899
|
+
}
|
|
900
|
+
if (x === Number.POSITIVE_INFINITY) {
|
|
951
901
|
return 1;
|
|
952
902
|
}
|
|
953
|
-
|
|
903
|
+
if (x === Number.NEGATIVE_INFINITY) {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
const tail = upperTail(Math.abs(x), df);
|
|
907
|
+
return x < 0 ? tail : 1 - tail;
|
|
954
908
|
}
|
|
955
|
-
function
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const
|
|
961
|
-
return
|
|
962
|
-
x1: withoutNegativeZero(x1),
|
|
963
|
-
y1: withoutNegativeZero(y1),
|
|
964
|
-
x2: withoutNegativeZero(x2),
|
|
965
|
-
y2: withoutNegativeZero(y2)
|
|
966
|
-
};
|
|
909
|
+
function upperTail(t, df) {
|
|
910
|
+
const squared = t * t;
|
|
911
|
+
if (!Number.isFinite(squared)) {
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
const total = df + squared;
|
|
915
|
+
return 0.5 * incompleteBetaSplit(df / total, squared / total, df / 2, 0.5);
|
|
967
916
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
917
|
+
var POLISH_MAX_STEPS = 4;
|
|
918
|
+
function centralQuantile(p, df) {
|
|
919
|
+
if (p === 0.5) {
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
if (p <= 0) {
|
|
923
|
+
return Number.NEGATIVE_INFINITY;
|
|
924
|
+
}
|
|
925
|
+
if (p >= 1) {
|
|
926
|
+
return Number.POSITIVE_INFINITY;
|
|
927
|
+
}
|
|
928
|
+
const tail = p < 0.5 ? p : 1 - p;
|
|
929
|
+
const sign = p < 0.5 ? -1 : 1;
|
|
930
|
+
const twoSided = 2 * tail;
|
|
931
|
+
let squared;
|
|
932
|
+
if (twoSided > 0.5) {
|
|
933
|
+
const near = inverseIncompleteBeta(1 - twoSided, 0.5, df / 2);
|
|
934
|
+
squared = df * near / (1 - near);
|
|
935
|
+
} else {
|
|
936
|
+
const far = inverseIncompleteBeta(twoSided, df / 2, 0.5);
|
|
937
|
+
squared = df * (1 - far) / far;
|
|
938
|
+
}
|
|
939
|
+
return sign * polish(Math.sqrt(squared), tail, df);
|
|
973
940
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
941
|
+
function polish(start, tail, df) {
|
|
942
|
+
let t = start;
|
|
943
|
+
for (let step = 0;step < POLISH_MAX_STEPS; step += 1) {
|
|
944
|
+
const density = centralDensity(t, df);
|
|
945
|
+
if (!(density > 0) || !Number.isFinite(t)) {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
const move = (upperTail(t, df) - tail) / density;
|
|
949
|
+
const next = t + move;
|
|
950
|
+
if (!(next > 0) || !Number.isFinite(next) || Math.abs(move) > 0.25 * t) {
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (next === t) {
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
t = next;
|
|
957
|
+
if (Math.abs(move) <= Number.EPSILON * t) {
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return t;
|
|
976
962
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
963
|
+
var SERIES_MAX_STEPS = 1000;
|
|
964
|
+
var SERIES_ERROR_MAX = 0.000000000001;
|
|
965
|
+
var SERIES_NCP_LIMIT_SQUARED = 2 * Math.LN2 * 1022;
|
|
966
|
+
var SERIES_DF_LIMIT = 400000;
|
|
967
|
+
var SQRT_TWO_OVER_PI = Math.sqrt(2 / Math.PI);
|
|
968
|
+
function nonCentralProbability(x, df, ncp) {
|
|
969
|
+
if (x === Number.POSITIVE_INFINITY) {
|
|
970
|
+
return 1;
|
|
982
971
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
const covariance = sum(zipWith(devX, devY, (dx, dy) => dx * dy)) / divisor;
|
|
992
|
-
const [first, second] = componentsOf(varX, varY, covariance);
|
|
993
|
-
const scores = points.map(() => ({ x: Number.NaN, y: Number.NaN }));
|
|
994
|
-
rows.forEach((row, survivor) => {
|
|
995
|
-
const dx = devX[survivor];
|
|
996
|
-
const dy = devY[survivor];
|
|
997
|
-
scores[row] = {
|
|
998
|
-
x: dx * first.loadings[0] + dy * first.loadings[1],
|
|
999
|
-
y: dx * second.loadings[0] + dy * second.loadings[1]
|
|
1000
|
-
};
|
|
1001
|
-
});
|
|
1002
|
-
return {
|
|
1003
|
-
sdev: [sdevOf(first.variance), sdevOf(second.variance)],
|
|
1004
|
-
rotation: [first.loadings, second.loadings],
|
|
1005
|
-
center: { x: centerX, y: centerY },
|
|
1006
|
-
scores
|
|
1007
|
-
};
|
|
972
|
+
if (x === Number.NEGATIVE_INFINITY) {
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
const reflected = x < 0;
|
|
976
|
+
const t = reflected ? -x : x;
|
|
977
|
+
const delta = reflected ? -ncp : ncp;
|
|
978
|
+
const lower = df > SERIES_DF_LIMIT || delta * delta > SERIES_NCP_LIMIT_SQUARED ? normalApproximation(t, df, delta) : lenthSeries(t, df, delta);
|
|
979
|
+
return reflected ? 1 - lower : lower;
|
|
1008
980
|
}
|
|
1009
|
-
function
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
981
|
+
function normalApproximation(t, df, delta) {
|
|
982
|
+
const shrink = 1 / (4 * df);
|
|
983
|
+
const spread = Math.sqrt(1 + t * t * 2 * shrink);
|
|
984
|
+
return normalCdf((t * (1 - shrink) - delta) / spread);
|
|
985
|
+
}
|
|
986
|
+
function lenthSeries(t, df, delta) {
|
|
987
|
+
const squared = t * t;
|
|
988
|
+
const total = df + squared;
|
|
989
|
+
const x = squared / total;
|
|
990
|
+
const complement = df / total;
|
|
991
|
+
let sum2 = 0;
|
|
992
|
+
if (x > 0) {
|
|
993
|
+
const lambda = delta * delta;
|
|
994
|
+
let oddWeight = 0.5 * Math.exp(-0.5 * lambda);
|
|
995
|
+
let evenWeight = SQRT_TWO_OVER_PI * oddWeight * delta;
|
|
996
|
+
let remaining = 0.5 - oddWeight;
|
|
997
|
+
if (remaining < 0.0000001) {
|
|
998
|
+
remaining = -0.5 * Math.expm1(-0.5 * lambda);
|
|
999
|
+
}
|
|
1000
|
+
let a = 0.5;
|
|
1001
|
+
const b = 0.5 * df;
|
|
1002
|
+
const powered = Math.pow(complement, b);
|
|
1003
|
+
const logBetaValue = logBeta(0.5, b);
|
|
1004
|
+
let oddTerm = incompleteBetaSplit(x, complement, a, b);
|
|
1005
|
+
let oddStep = 2 * powered * Math.exp(a * Math.log(x) - logBetaValue);
|
|
1006
|
+
let evenTerm = 1 - powered;
|
|
1007
|
+
let evenStep = b * x * powered;
|
|
1008
|
+
sum2 = oddWeight * oddTerm + evenWeight * evenTerm;
|
|
1009
|
+
for (let step = 1;step <= SERIES_MAX_STEPS; step += 1) {
|
|
1010
|
+
a += 1;
|
|
1011
|
+
oddTerm -= oddStep;
|
|
1012
|
+
evenTerm -= evenStep;
|
|
1013
|
+
oddStep *= x * (a + b - 1) / a;
|
|
1014
|
+
evenStep *= x * (a + b - 0.5) / (a + 0.5);
|
|
1015
|
+
oddWeight *= lambda / (2 * step);
|
|
1016
|
+
evenWeight *= lambda / (2 * step + 1);
|
|
1017
|
+
remaining -= oddWeight;
|
|
1018
|
+
if (remaining <= 0) {
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
sum2 += oddWeight * oddTerm + evenWeight * evenTerm;
|
|
1022
|
+
if (Math.abs(2 * remaining * (oddTerm - oddStep)) < SERIES_ERROR_MAX) {
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1014
1026
|
}
|
|
1015
|
-
|
|
1016
|
-
const spread = Math.hypot((varX - varY) / 2, covariance);
|
|
1017
|
-
const larger = middle + spread;
|
|
1018
|
-
const smaller = middle - spread;
|
|
1019
|
-
const fromRowX = larger - varX;
|
|
1020
|
-
const fromRowY = larger - varY;
|
|
1021
|
-
const direction = fromRowX >= fromRowY ? [covariance, fromRowX] : [fromRowY, covariance];
|
|
1022
|
-
const leading = signed(unit(direction));
|
|
1023
|
-
const trailing = signed([negated(leading[1]), leading[0]]);
|
|
1024
|
-
return [
|
|
1025
|
-
{ variance: larger, loadings: leading },
|
|
1026
|
-
{ variance: smaller, loadings: trailing }
|
|
1027
|
-
];
|
|
1028
|
-
}
|
|
1029
|
-
function unit(vector) {
|
|
1030
|
-
const length = Math.hypot(vector[0], vector[1]);
|
|
1031
|
-
return [vector[0] / length, vector[1] / length];
|
|
1032
|
-
}
|
|
1033
|
-
function signed(vector) {
|
|
1034
|
-
const dominant = Math.abs(vector[0]) >= Math.abs(vector[1]) ? vector[0] : vector[1];
|
|
1035
|
-
return dominant >= 0 ? vector : [negated(vector[0]), negated(vector[1])];
|
|
1036
|
-
}
|
|
1037
|
-
function negated(value) {
|
|
1038
|
-
return withoutNegativeZero(-value);
|
|
1027
|
+
return Math.min(Math.max(sum2 + normalCdf(-delta), 0), 1);
|
|
1039
1028
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1029
|
+
function nonCentralDensity(x, df, ncp) {
|
|
1030
|
+
if (!Number.isFinite(x)) {
|
|
1031
|
+
return 0;
|
|
1032
|
+
}
|
|
1033
|
+
if (Math.abs(x) > Math.sqrt(df * Number.EPSILON)) {
|
|
1034
|
+
const stepped = x * Math.sqrt((df + 2) / df);
|
|
1035
|
+
const difference = nonCentralProbability(stepped, df + 2, ncp) - nonCentralProbability(x, df, ncp);
|
|
1036
|
+
return df / Math.abs(x) * Math.abs(difference);
|
|
1037
|
+
}
|
|
1038
|
+
return Math.exp(-0.5 * Math.log(df) - logBeta(0.5, df / 2) - 0.5 * ncp * ncp);
|
|
1042
1039
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
var BOUND = 10 * Number.EPSILON;
|
|
1048
|
-
function logisticRegression(points, options = {}) {
|
|
1049
|
-
const {
|
|
1050
|
-
epsilon = DEFAULT_LOGIT_EPSILON,
|
|
1051
|
-
maxIterations = DEFAULT_LOGIT_MAX_ITERATIONS
|
|
1052
|
-
} = options;
|
|
1053
|
-
const rows = completePointRows(points);
|
|
1054
|
-
if (rows.length === 0) {
|
|
1055
|
-
return null;
|
|
1040
|
+
var QUANTILE_MAX_STEPS = 200;
|
|
1041
|
+
function nonCentralQuantile(p, df, ncp) {
|
|
1042
|
+
if (p <= 0) {
|
|
1043
|
+
return Number.NEGATIVE_INFINITY;
|
|
1056
1044
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
throw new RangeError("every outcome must be 0 or 1");
|
|
1045
|
+
if (p >= 1) {
|
|
1046
|
+
return Number.POSITIVE_INFINITY;
|
|
1060
1047
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
let
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
let
|
|
1070
|
-
let
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
tolerance: Math.min(0.0000001, epsilon / 1000)
|
|
1084
|
-
});
|
|
1085
|
-
if (step.coefficients.some((coefficient) => coefficient !== null && !Number.isFinite(coefficient))) {
|
|
1048
|
+
let upper = Math.max(1, ncp);
|
|
1049
|
+
while (Number.isFinite(upper) && nonCentralProbability(upper, df, ncp) < p) {
|
|
1050
|
+
upper *= 2;
|
|
1051
|
+
}
|
|
1052
|
+
let lower = Math.min(-1, -ncp);
|
|
1053
|
+
while (Number.isFinite(lower) && nonCentralProbability(lower, df, ncp) > p) {
|
|
1054
|
+
lower *= 2;
|
|
1055
|
+
}
|
|
1056
|
+
let t = 0.5 * (lower + upper);
|
|
1057
|
+
for (let step = 0;step < QUANTILE_MAX_STEPS; step += 1) {
|
|
1058
|
+
const residual = nonCentralProbability(t, df, ncp) - p;
|
|
1059
|
+
if (residual < 0) {
|
|
1060
|
+
lower = t;
|
|
1061
|
+
} else {
|
|
1062
|
+
upper = t;
|
|
1063
|
+
}
|
|
1064
|
+
const density = nonCentralDensity(t, df, ncp);
|
|
1065
|
+
let next = density > 0 && Number.isFinite(density) ? t - residual / density : Number.NaN;
|
|
1066
|
+
if (!(next > lower) || !(next < upper)) {
|
|
1067
|
+
next = 0.5 * (lower + upper);
|
|
1068
|
+
}
|
|
1069
|
+
if (next === t) {
|
|
1086
1070
|
break;
|
|
1087
1071
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
fitted = predictors.map(linkInverse);
|
|
1092
|
-
deviance = totalDeviance(outcomes, fitted);
|
|
1093
|
-
if (Math.abs(deviance - previousDeviance) / (0.1 + Math.abs(deviance)) < epsilon) {
|
|
1094
|
-
converged = true;
|
|
1072
|
+
const moved = Math.abs(next - t);
|
|
1073
|
+
t = next;
|
|
1074
|
+
if (moved <= Number.EPSILON * Math.abs(t)) {
|
|
1095
1075
|
break;
|
|
1096
1076
|
}
|
|
1097
|
-
previousDeviance = deviance;
|
|
1098
1077
|
}
|
|
1099
|
-
|
|
1100
|
-
const nullDeviance = totalDeviance(outcomes, outcomes.map(() => wholeMean));
|
|
1101
|
-
const paddedFitted = new Array(points.length).fill(Number.NaN);
|
|
1102
|
-
const paddedPredictors = new Array(points.length).fill(Number.NaN);
|
|
1103
|
-
rows.forEach((row, survivor) => {
|
|
1104
|
-
paddedFitted[row] = fitted[survivor];
|
|
1105
|
-
paddedPredictors[row] = predictors[survivor];
|
|
1106
|
-
});
|
|
1107
|
-
return {
|
|
1108
|
-
intercept: coefficients[0] ?? Number.NaN,
|
|
1109
|
-
slope: coefficients[1] ?? null,
|
|
1110
|
-
fitted: paddedFitted,
|
|
1111
|
-
linearPredictors: paddedPredictors,
|
|
1112
|
-
deviance,
|
|
1113
|
-
nullDeviance,
|
|
1114
|
-
aic: deviance + 2 * rank,
|
|
1115
|
-
rank,
|
|
1116
|
-
iterations,
|
|
1117
|
-
converged,
|
|
1118
|
-
saturated: fitted.some((probability) => probability > 1 - BOUND || probability < BOUND)
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
function predictLogit(fit, x) {
|
|
1122
|
-
return linkInverse(fit.intercept + (fit.slope ?? 0) * x);
|
|
1123
|
-
}
|
|
1124
|
-
function logit(probability) {
|
|
1125
|
-
return Math.log(probability / (1 - probability));
|
|
1078
|
+
return t;
|
|
1126
1079
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1080
|
+
|
|
1081
|
+
// src/core/linalg/matrix.ts
|
|
1082
|
+
function matrix(values, options) {
|
|
1083
|
+
const { byrow = false, dimnames } = options;
|
|
1084
|
+
const [nrow, ncol] = extents(values.length, options);
|
|
1085
|
+
const dense = Float64Array.from(values);
|
|
1086
|
+
const data = new Float64Array(nrow * ncol);
|
|
1087
|
+
if (dense.length === 1 && data.length > 1) {
|
|
1088
|
+
data.fill(dense[0]);
|
|
1089
|
+
} else if (byrow) {
|
|
1090
|
+
dense.forEach((value, index) => {
|
|
1091
|
+
const i = Math.floor(index / ncol);
|
|
1092
|
+
const j = index % ncol;
|
|
1093
|
+
data[j * nrow + i] = value;
|
|
1094
|
+
});
|
|
1095
|
+
} else {
|
|
1096
|
+
data.set(dense);
|
|
1097
|
+
}
|
|
1098
|
+
return make(nrow, ncol, data, dimnames ?? null);
|
|
1130
1099
|
}
|
|
1131
|
-
function
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1100
|
+
function extents(length, { nrow, ncol }) {
|
|
1101
|
+
if (nrow === undefined && ncol === undefined) {
|
|
1102
|
+
throw new RangeError("matrix() needs nrow or ncol");
|
|
1134
1103
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1104
|
+
if (nrow !== undefined) {
|
|
1105
|
+
requireExtent(nrow, "nrow");
|
|
1106
|
+
}
|
|
1107
|
+
if (ncol !== undefined) {
|
|
1108
|
+
requireExtent(ncol, "ncol");
|
|
1109
|
+
}
|
|
1110
|
+
const scalar = length === 1;
|
|
1111
|
+
if (nrow !== undefined && ncol !== undefined) {
|
|
1112
|
+
if (!scalar && nrow * ncol !== length) {
|
|
1113
|
+
throw new RangeError(length > nrow * ncol ? "data is too long" : `data length [${length}] is not nrow * ncol [${nrow} * ${ncol}]`);
|
|
1114
|
+
}
|
|
1115
|
+
return [nrow, ncol];
|
|
1116
|
+
}
|
|
1117
|
+
const given = nrow ?? ncol;
|
|
1118
|
+
const name = nrow !== undefined ? "rows" : "columns";
|
|
1119
|
+
if (given === 0) {
|
|
1120
|
+
if (length !== 0) {
|
|
1121
|
+
throw new RangeError("data is too long");
|
|
1122
|
+
}
|
|
1123
|
+
return [0, 0];
|
|
1124
|
+
}
|
|
1125
|
+
const other = scalar ? 1 : length / given;
|
|
1126
|
+
if (!scalar && length % given !== 0) {
|
|
1127
|
+
throw new RangeError(`data length [${length}] is not a multiple of the number of ${name} [${given}]`);
|
|
1128
|
+
}
|
|
1129
|
+
return nrow !== undefined ? [nrow, other] : [other, given];
|
|
1137
1130
|
}
|
|
1138
|
-
function
|
|
1139
|
-
|
|
1131
|
+
function requireExtent(value, name) {
|
|
1132
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
1133
|
+
throw new RangeError(`${name} must be a non-negative integer, got ${value}`);
|
|
1134
|
+
}
|
|
1140
1135
|
}
|
|
1141
|
-
function
|
|
1142
|
-
|
|
1136
|
+
function make(nrow, ncol, data, dimnames) {
|
|
1137
|
+
if (data.length !== nrow * ncol) {
|
|
1138
|
+
throw new RangeError(`data length [${data.length}] is not nrow * ncol [${nrow} * ${ncol}]`);
|
|
1139
|
+
}
|
|
1140
|
+
if (dimnames !== null) {
|
|
1141
|
+
const [rows, columns] = dimnames;
|
|
1142
|
+
if (rows !== null && rows.length !== nrow) {
|
|
1143
|
+
throw new RangeError(`length of dimnames [1] (${rows.length}) not equal to array extent (${nrow})`);
|
|
1144
|
+
}
|
|
1145
|
+
if (columns !== null && columns.length !== ncol) {
|
|
1146
|
+
throw new RangeError(`length of dimnames [2] (${columns.length}) not equal to array extent (${ncol})`);
|
|
1147
|
+
}
|
|
1148
|
+
dimnames = rows === null && columns === null ? null : [rows === null ? null : [...rows], columns === null ? null : [...columns]];
|
|
1149
|
+
}
|
|
1150
|
+
return { nrow, ncol, data, dimnames };
|
|
1143
1151
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1152
|
+
function fromRows(rows) {
|
|
1153
|
+
const nrow = rows.length;
|
|
1154
|
+
const first = rows[0];
|
|
1155
|
+
if (first === undefined || first.length === 0) {
|
|
1156
|
+
throw new RangeError("fromRows() needs at least one row with one value");
|
|
1157
|
+
}
|
|
1158
|
+
const ncol = first.length;
|
|
1159
|
+
const ragged = rows.findIndex((row) => row.length !== ncol);
|
|
1160
|
+
if (ragged !== -1) {
|
|
1161
|
+
throw new RangeError(`every row needs ${ncol} values; row ${ragged} has ${rows[ragged].length}`);
|
|
1162
|
+
}
|
|
1163
|
+
const data = new Float64Array(nrow * ncol);
|
|
1164
|
+
rows.forEach((row, i) => {
|
|
1165
|
+
Float64Array.from(row).forEach((value, j) => {
|
|
1166
|
+
data[j * nrow + i] = value;
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
return make(nrow, ncol, data, null);
|
|
1170
|
+
}
|
|
1171
|
+
function fromColumns(columns) {
|
|
1172
|
+
const ncol = columns.length;
|
|
1173
|
+
const first = columns[0];
|
|
1174
|
+
if (first === undefined || first.length === 0) {
|
|
1175
|
+
throw new RangeError("fromColumns() needs at least one column with one value");
|
|
1176
|
+
}
|
|
1177
|
+
const nrow = first.length;
|
|
1178
|
+
const ragged = columns.findIndex((column) => column.length !== nrow);
|
|
1179
|
+
if (ragged !== -1) {
|
|
1180
|
+
throw new RangeError(`every column needs ${nrow} values; column ${ragged} has ${columns[ragged].length}`);
|
|
1181
|
+
}
|
|
1182
|
+
const data = new Float64Array(nrow * ncol);
|
|
1183
|
+
columns.forEach((column, j) => {
|
|
1184
|
+
data.set(column, j * nrow);
|
|
1185
|
+
});
|
|
1186
|
+
return make(nrow, ncol, data, null);
|
|
1166
1187
|
}
|
|
1167
|
-
function
|
|
1168
|
-
if (!(
|
|
1169
|
-
|
|
1188
|
+
function at(m, i, j) {
|
|
1189
|
+
if (!Number.isInteger(i) || i < 0 || i >= m.nrow) {
|
|
1190
|
+
throw new RangeError(`row index ${i} is outside 0..${m.nrow - 1}`);
|
|
1170
1191
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1192
|
+
if (!Number.isInteger(j) || j < 0 || j >= m.ncol) {
|
|
1193
|
+
throw new RangeError(`column index ${j} is outside 0..${m.ncol - 1}`);
|
|
1194
|
+
}
|
|
1195
|
+
return m.data[j * m.nrow + i];
|
|
1173
1196
|
}
|
|
1174
|
-
function
|
|
1175
|
-
if (!(
|
|
1176
|
-
|
|
1197
|
+
function row(m, i) {
|
|
1198
|
+
if (!Number.isInteger(i) || i < 0 || i >= m.nrow) {
|
|
1199
|
+
throw new RangeError(`row index ${i} is outside 0..${m.nrow - 1}`);
|
|
1177
1200
|
}
|
|
1178
|
-
|
|
1179
|
-
return LOG_SQRT_TWO_PI - (LANCZOS_G - 0.5) + Math.log(lanczosSeries(a)) + Math.log(lanczosSeries(b)) - Math.log(lanczosSeries(a + b)) + (a - 0.5) * Math.log1p(-b / shiftedSum) + (b - 0.5) * Math.log1p(-a / shiftedSum) - 0.5 * Math.log(shiftedSum);
|
|
1201
|
+
return Array.from({ length: m.ncol }, (_, j) => m.data[j * m.nrow + i]);
|
|
1180
1202
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
function betaContinuedFraction(x, a, b) {
|
|
1185
|
-
const total = a + b;
|
|
1186
|
-
const aPlus = a + 1;
|
|
1187
|
-
const aMinus = a - 1;
|
|
1188
|
-
let c = 1;
|
|
1189
|
-
let d = 1 - total * x / aPlus;
|
|
1190
|
-
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
1191
|
-
d = FRACTION_FLOOR;
|
|
1203
|
+
function column(m, j) {
|
|
1204
|
+
if (!Number.isInteger(j) || j < 0 || j >= m.ncol) {
|
|
1205
|
+
throw new RangeError(`column index ${j} is outside 0..${m.ncol - 1}`);
|
|
1192
1206
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1207
|
+
return Array.from(m.data.subarray(j * m.nrow, (j + 1) * m.nrow));
|
|
1208
|
+
}
|
|
1209
|
+
function toRows(m) {
|
|
1210
|
+
return Array.from({ length: m.nrow }, (_, i) => row(m, i));
|
|
1211
|
+
}
|
|
1212
|
+
function toColumns(m) {
|
|
1213
|
+
return Array.from({ length: m.ncol }, (_, j) => column(m, j));
|
|
1214
|
+
}
|
|
1215
|
+
function fromFrame(data, columns) {
|
|
1216
|
+
const nrow = frameRows(data);
|
|
1217
|
+
const names = columns ?? numericColumns(data);
|
|
1218
|
+
const buffer = new Float64Array(nrow * names.length);
|
|
1219
|
+
names.forEach((name, j) => {
|
|
1220
|
+
buffer.set(requireNumericColumn(data, name, "columns"), j * nrow);
|
|
1221
|
+
});
|
|
1222
|
+
return make(nrow, names.length, buffer, names.length === 0 ? null : [null, [...names]]);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/core/linalg/modelMatrix.ts
|
|
1226
|
+
function modelMatrix(data, spec) {
|
|
1227
|
+
const { outcome, intercept = true } = spec;
|
|
1228
|
+
const rowCount = frameRows(data);
|
|
1229
|
+
const terms = orderTerms(spec.terms);
|
|
1230
|
+
const columns = new Map;
|
|
1231
|
+
const read = (name, role) => {
|
|
1232
|
+
const held = columns.get(name);
|
|
1233
|
+
if (held !== undefined) {
|
|
1234
|
+
return held;
|
|
1201
1235
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1236
|
+
const column2 = requireNumericColumn(data, name, role);
|
|
1237
|
+
columns.set(name, column2);
|
|
1238
|
+
return column2;
|
|
1239
|
+
};
|
|
1240
|
+
if (outcome !== undefined) {
|
|
1241
|
+
read(outcome, "outcome");
|
|
1242
|
+
}
|
|
1243
|
+
terms.forEach((factors) => {
|
|
1244
|
+
factors.forEach((name) => read(name, "terms"));
|
|
1245
|
+
});
|
|
1246
|
+
const involved = [...columns.values()];
|
|
1247
|
+
const rows = Array.from({ length: rowCount }, (_, row2) => row2).filter((row2) => involved.every((column2) => Number.isFinite(column2[row2])));
|
|
1248
|
+
const termColumns = terms.map((factors) => rows.map((row2) => factors.reduce((product, name) => product * columns.get(name)[row2], 1)));
|
|
1249
|
+
const design = intercept ? [rows.map(() => 1), ...termColumns] : termColumns;
|
|
1250
|
+
const names = [
|
|
1251
|
+
...intercept ? ["(Intercept)"] : [],
|
|
1252
|
+
...terms.map((factors) => factors.join(":"))
|
|
1253
|
+
];
|
|
1254
|
+
const assign = [
|
|
1255
|
+
...intercept ? [0] : [],
|
|
1256
|
+
...terms.map((_, index) => index + 1)
|
|
1257
|
+
];
|
|
1258
|
+
const n = rows.length;
|
|
1259
|
+
const p = design.length;
|
|
1260
|
+
const buffer = new Float64Array(n * p);
|
|
1261
|
+
design.forEach((column2, j) => {
|
|
1262
|
+
buffer.set(column2, j * n);
|
|
1263
|
+
});
|
|
1264
|
+
return {
|
|
1265
|
+
matrix: make(n, p, buffer, p === 0 ? null : [null, names]),
|
|
1266
|
+
rows,
|
|
1267
|
+
assign,
|
|
1268
|
+
termLabels: names.slice(intercept ? 1 : 0)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function orderTerms(terms) {
|
|
1272
|
+
const seen = new Set;
|
|
1273
|
+
const unique = [];
|
|
1274
|
+
terms.forEach((term) => {
|
|
1275
|
+
const factors = typeof term === "string" ? [term] : term;
|
|
1276
|
+
if (factors.length === 0) {
|
|
1277
|
+
throw new RangeError("an interaction term needs at least one column name");
|
|
1205
1278
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
if (Math.abs(d) < FRACTION_FLOOR) {
|
|
1211
|
-
d = FRACTION_FLOOR;
|
|
1279
|
+
const key = [...factors].sort().join("\x00");
|
|
1280
|
+
if (!seen.has(key)) {
|
|
1281
|
+
seen.add(key);
|
|
1282
|
+
unique.push(factors);
|
|
1212
1283
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1284
|
+
});
|
|
1285
|
+
return unique.map((factors, index) => ({ factors, index })).sort((a, b) => a.factors.length - b.factors.length || a.index - b.index).map(({ factors }) => factors);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/core/linalg/namedVector.ts
|
|
1289
|
+
function namedVector(names, values) {
|
|
1290
|
+
if (names.length !== values.length) {
|
|
1291
|
+
throw new RangeError(`a named vector needs one name per value: ${names.length} names, ${values.length} values`);
|
|
1292
|
+
}
|
|
1293
|
+
return { names: [...names], values: [...values] };
|
|
1294
|
+
}
|
|
1295
|
+
function lookup(v, name) {
|
|
1296
|
+
const index = v.names.indexOf(name);
|
|
1297
|
+
return index === -1 ? undefined : v.values[index];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/core/linalg/ops.ts
|
|
1301
|
+
function t(m) {
|
|
1302
|
+
const { nrow, ncol } = m;
|
|
1303
|
+
const data = new Float64Array(nrow * ncol);
|
|
1304
|
+
for (let j = 0;j < ncol; j++) {
|
|
1305
|
+
for (let i = 0;i < nrow; i++) {
|
|
1306
|
+
data[i * ncol + j] = m.data[j * nrow + i];
|
|
1216
1307
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1308
|
+
}
|
|
1309
|
+
const dimnames = m.dimnames === null ? null : [m.dimnames[1], m.dimnames[0]];
|
|
1310
|
+
return make(ncol, nrow, data, dimnames);
|
|
1311
|
+
}
|
|
1312
|
+
var transpose = t;
|
|
1313
|
+
function matmul(x, y) {
|
|
1314
|
+
const [left, right] = conformProduct(x, y);
|
|
1315
|
+
if (left.ncol !== right.nrow) {
|
|
1316
|
+
throw new RangeError(`non-conformable arguments: ${left.nrow} x ${left.ncol} %*% ${right.nrow} x ${right.ncol}`);
|
|
1317
|
+
}
|
|
1318
|
+
const data = product(left, right);
|
|
1319
|
+
return make(left.nrow, right.ncol, data, productDimnames(left, right));
|
|
1320
|
+
}
|
|
1321
|
+
function conformProduct(x, y) {
|
|
1322
|
+
if (isMatrix(x)) {
|
|
1323
|
+
if (isMatrix(y)) {
|
|
1324
|
+
return [x, y];
|
|
1325
|
+
}
|
|
1326
|
+
return [x, y.length === x.ncol ? asColumn(y) : asRow(y)];
|
|
1327
|
+
}
|
|
1328
|
+
if (isMatrix(y)) {
|
|
1329
|
+
return [x.length === y.nrow ? asRow(x) : asColumn(x), y];
|
|
1330
|
+
}
|
|
1331
|
+
return [asRow(x), x.length === 1 ? asRow(y) : asColumn(y)];
|
|
1332
|
+
}
|
|
1333
|
+
function crossprod(x, y = x) {
|
|
1334
|
+
const left = asColumn(x);
|
|
1335
|
+
const right = isMatrix(y) ? y : y.length === left.nrow ? asColumn(y) : asRow(y);
|
|
1336
|
+
if (left.nrow !== right.nrow) {
|
|
1337
|
+
throw new RangeError(`non-conformable arguments: crossprod of ${left.nrow} x ${left.ncol} and ${right.nrow} x ${right.ncol}`);
|
|
1338
|
+
}
|
|
1339
|
+
return matmul(t(left), right);
|
|
1340
|
+
}
|
|
1341
|
+
function tcrossprod(x, y = x) {
|
|
1342
|
+
const bothVectors = !isMatrix(x) && !isMatrix(y);
|
|
1343
|
+
const left = isMatrix(x) ? x : isMatrix(y) && y.ncol === x.length ? asRow(x) : asColumn(x);
|
|
1344
|
+
const right = isMatrix(y) ? y : !bothVectors && left.nrow === 1 ? asRow(y) : asColumn(y);
|
|
1345
|
+
if (left.ncol !== right.ncol) {
|
|
1346
|
+
throw new RangeError(`non-conformable arguments: tcrossprod of ${left.nrow} x ${left.ncol} and ${right.nrow} x ${right.ncol}`);
|
|
1347
|
+
}
|
|
1348
|
+
return matmul(left, t(right));
|
|
1349
|
+
}
|
|
1350
|
+
function product(x, y) {
|
|
1351
|
+
const { nrow, ncol: inner } = x;
|
|
1352
|
+
const { ncol } = y;
|
|
1353
|
+
const data = new Float64Array(nrow * ncol);
|
|
1354
|
+
for (let j = 0;j < ncol; j++) {
|
|
1355
|
+
for (let k = 0;k < inner; k++) {
|
|
1356
|
+
const factor = y.data[j * y.nrow + k];
|
|
1357
|
+
for (let i = 0;i < nrow; i++) {
|
|
1358
|
+
data[j * nrow + i] = fusedMultiplyAdd(x.data[k * nrow + i], factor, data[j * nrow + i]);
|
|
1359
|
+
}
|
|
1222
1360
|
}
|
|
1223
1361
|
}
|
|
1224
|
-
return
|
|
1362
|
+
return data;
|
|
1225
1363
|
}
|
|
1226
|
-
function
|
|
1227
|
-
|
|
1228
|
-
|
|
1364
|
+
function productDimnames(x, y) {
|
|
1365
|
+
const rows = x.dimnames?.[0] ?? null;
|
|
1366
|
+
const columns = y.dimnames?.[1] ?? null;
|
|
1367
|
+
return rows === null && columns === null ? null : [rows, columns];
|
|
1368
|
+
}
|
|
1369
|
+
function asColumn(value) {
|
|
1370
|
+
if (isMatrix(value)) {
|
|
1371
|
+
return value;
|
|
1229
1372
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1373
|
+
return make(value.length, 1, Float64Array.from(value), null);
|
|
1374
|
+
}
|
|
1375
|
+
function asRow(value) {
|
|
1376
|
+
return make(1, value.length, Float64Array.from(value), null);
|
|
1377
|
+
}
|
|
1378
|
+
function isMatrix(value) {
|
|
1379
|
+
if (Array.isArray(value)) {
|
|
1380
|
+
return false;
|
|
1232
1381
|
}
|
|
1233
|
-
if (
|
|
1234
|
-
return
|
|
1382
|
+
if (typeof value === "object" && value !== null && "nrow" in value && "ncol" in value && "data" in value) {
|
|
1383
|
+
return true;
|
|
1235
1384
|
}
|
|
1236
|
-
|
|
1385
|
+
throw new TypeError("expected a Matrix or an array of numbers");
|
|
1237
1386
|
}
|
|
1238
|
-
function
|
|
1239
|
-
|
|
1240
|
-
|
|
1387
|
+
function cbind(...parts) {
|
|
1388
|
+
const matrices = parts.map(asColumn);
|
|
1389
|
+
const first = matrices[0];
|
|
1390
|
+
if (first === undefined) {
|
|
1391
|
+
throw new RangeError("cbind() needs at least one argument");
|
|
1241
1392
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1393
|
+
const nrow = first.nrow;
|
|
1394
|
+
matrices.forEach((m, index) => {
|
|
1395
|
+
if (m.nrow !== nrow) {
|
|
1396
|
+
throw new RangeError(isMatrix(parts[index]) ? `number of rows of matrices must match (see arg ${index + 1})` : `number of rows of result is not a multiple of vector length (arg ${index + 1})`);
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
const ncol = matrices.reduce((total, m) => total + m.ncol, 0);
|
|
1400
|
+
const data = new Float64Array(nrow * ncol);
|
|
1401
|
+
let offset = 0;
|
|
1402
|
+
matrices.forEach((m) => {
|
|
1403
|
+
data.set(m.data, offset);
|
|
1404
|
+
offset += m.data.length;
|
|
1405
|
+
});
|
|
1406
|
+
const rows = matrices.find((m) => m.dimnames?.[0])?.dimnames?.[0] ?? null;
|
|
1407
|
+
const columns = boundNames(matrices.map((m) => ({ names: m.dimnames?.[1] ?? null, count: m.ncol })));
|
|
1408
|
+
return make(nrow, ncol, data, rows === null && columns === null ? null : [rows, columns]);
|
|
1409
|
+
}
|
|
1410
|
+
function rbind(...parts) {
|
|
1411
|
+
if (parts.length === 0) {
|
|
1412
|
+
throw new RangeError("rbind() needs at least one argument");
|
|
1413
|
+
}
|
|
1414
|
+
const transposed = parts.map((part) => isMatrix(part) ? t(part) : part);
|
|
1415
|
+
try {
|
|
1416
|
+
return t(cbind(...transposed));
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (error instanceof RangeError) {
|
|
1419
|
+
throw new RangeError(error.message.replace("number of rows", "number of columns"));
|
|
1420
|
+
}
|
|
1421
|
+
throw error;
|
|
1244
1422
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1423
|
+
}
|
|
1424
|
+
function boundNames(parts) {
|
|
1425
|
+
if (parts.every((part) => part.names === null)) {
|
|
1426
|
+
return null;
|
|
1247
1427
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (
|
|
1252
|
-
return
|
|
1428
|
+
return parts.flatMap((part) => part.names ?? new Array(part.count).fill(""));
|
|
1429
|
+
}
|
|
1430
|
+
function diag(arg) {
|
|
1431
|
+
if (typeof arg === "number") {
|
|
1432
|
+
return identity(arg);
|
|
1253
1433
|
}
|
|
1254
|
-
|
|
1434
|
+
if (Array.isArray(arg)) {
|
|
1435
|
+
const values = arg;
|
|
1436
|
+
const n2 = values.length;
|
|
1437
|
+
const data = new Float64Array(n2 * n2);
|
|
1438
|
+
values.forEach((value, i) => {
|
|
1439
|
+
data[i * n2 + i] = value;
|
|
1440
|
+
});
|
|
1441
|
+
return make(n2, n2, data, null);
|
|
1442
|
+
}
|
|
1443
|
+
const m = arg;
|
|
1444
|
+
const n = Math.min(m.nrow, m.ncol);
|
|
1445
|
+
return Array.from({ length: n }, (_, i) => m.data[i * m.nrow + i]);
|
|
1255
1446
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
for (let
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1447
|
+
function identity(order) {
|
|
1448
|
+
if (!Number.isInteger(order) || order < 0) {
|
|
1449
|
+
throw new RangeError(`order must be a non-negative integer, got ${order}`);
|
|
1450
|
+
}
|
|
1451
|
+
const data = new Float64Array(order * order);
|
|
1452
|
+
for (let i = 0;i < order; i++) {
|
|
1453
|
+
data[i * order + i] = 1;
|
|
1454
|
+
}
|
|
1455
|
+
return make(order, order, data, null);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/core/linalg/qr.ts
|
|
1459
|
+
var DEFAULT_QR_TOLERANCE = 0.0000001;
|
|
1460
|
+
function qr(x, options = {}) {
|
|
1461
|
+
const { tolerance = DEFAULT_QR_TOLERANCE } = options;
|
|
1462
|
+
if (!(tolerance >= 0)) {
|
|
1463
|
+
throw new RangeError(`tolerance must be a non-negative number, got ${tolerance}`);
|
|
1464
|
+
}
|
|
1465
|
+
if (!isMatrix(x)) {
|
|
1466
|
+
throw new TypeError("expected a Matrix");
|
|
1467
|
+
}
|
|
1468
|
+
if (!x.data.every(Number.isFinite)) {
|
|
1469
|
+
throw new RangeError("NA/NaN/Inf in foreign function call (arg 1)");
|
|
1470
|
+
}
|
|
1471
|
+
const { nrow, ncol } = x;
|
|
1472
|
+
const columns = Array.from({ length: ncol }, (_, j) => Array.from(x.data.subarray(j * nrow, (j + 1) * nrow)));
|
|
1473
|
+
const { householders, pivot, rank } = decompose(columns, tolerance, nrow);
|
|
1474
|
+
const data = new Float64Array(nrow * ncol);
|
|
1475
|
+
columns.forEach((column2, j) => {
|
|
1476
|
+
data.set(column2, j * nrow);
|
|
1477
|
+
});
|
|
1478
|
+
const rows = x.dimnames?.[0] ?? null;
|
|
1479
|
+
const names = x.dimnames?.[1] ?? null;
|
|
1480
|
+
const dimnames = rows === null && names === null ? null : [rows, names === null ? null : pivot.map((from) => names[from])];
|
|
1481
|
+
return { qr: make(nrow, ncol, data, dimnames), qraux: householders, pivot, rank };
|
|
1482
|
+
}
|
|
1483
|
+
function qrCoef(q, y) {
|
|
1484
|
+
if (isMatrix(y)) {
|
|
1485
|
+
requireRows(q, y.nrow);
|
|
1486
|
+
const width = q.qr.ncol;
|
|
1487
|
+
const data = new Float64Array(width * y.ncol);
|
|
1488
|
+
for (let j = 0;j < y.ncol; j++) {
|
|
1489
|
+
const solved = coefficientsOf(q, Array.from(y.data.subarray(j * y.nrow, (j + 1) * y.nrow)));
|
|
1490
|
+
solved.forEach((value, i) => {
|
|
1491
|
+
data[j * width + i] = value ?? Number.NaN;
|
|
1492
|
+
});
|
|
1266
1493
|
}
|
|
1494
|
+
return make(width, y.ncol, data, readerDimnames(originalColumnNames(q), y));
|
|
1267
1495
|
}
|
|
1268
|
-
|
|
1496
|
+
requireRows(q, y.length);
|
|
1497
|
+
return coefficientsOf(q, y);
|
|
1269
1498
|
}
|
|
1270
|
-
function
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1499
|
+
function coefficientsOf(q, y) {
|
|
1500
|
+
const qty = transformed(q, y, true);
|
|
1501
|
+
const solved = backSubstitute(q.qr, qty, q.rank);
|
|
1502
|
+
const coefficients = new Array(q.qr.ncol).fill(null);
|
|
1503
|
+
q.pivot.slice(0, q.rank).forEach((column2, position) => {
|
|
1504
|
+
coefficients[column2] = solved[position];
|
|
1505
|
+
});
|
|
1506
|
+
return coefficients;
|
|
1507
|
+
}
|
|
1508
|
+
function originalColumnNames(q) {
|
|
1509
|
+
const pivoted = q.qr.dimnames?.[1] ?? null;
|
|
1510
|
+
if (pivoted === null) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
const names = new Array(pivoted.length);
|
|
1514
|
+
q.pivot.forEach((original, position) => {
|
|
1515
|
+
names[original] = pivoted[position];
|
|
1516
|
+
});
|
|
1517
|
+
return names;
|
|
1518
|
+
}
|
|
1519
|
+
function qrFitted(q, y) {
|
|
1520
|
+
return perColumn(q, y, (column2) => transformed(q, transformed(q, column2, true).map((value, index) => index < q.rank ? value : 0), false));
|
|
1521
|
+
}
|
|
1522
|
+
function qrResid(q, y) {
|
|
1523
|
+
return perColumn(q, y, (column2) => transformed(q, transformed(q, column2, true).map((value, index) => index < q.rank ? 0 : value), false));
|
|
1524
|
+
}
|
|
1525
|
+
function qrQty(q, y) {
|
|
1526
|
+
return perColumn(q, y, (column2) => transformed(q, column2, true));
|
|
1527
|
+
}
|
|
1528
|
+
function qrQy(q, y) {
|
|
1529
|
+
return perColumn(q, y, (column2) => transformed(q, column2, false));
|
|
1530
|
+
}
|
|
1531
|
+
function perColumn(q, y, read) {
|
|
1532
|
+
if (isMatrix(y)) {
|
|
1533
|
+
requireRows(q, y.nrow);
|
|
1534
|
+
const data = new Float64Array(y.nrow * y.ncol);
|
|
1535
|
+
for (let j = 0;j < y.ncol; j++) {
|
|
1536
|
+
data.set(read(Array.from(y.data.subarray(j * y.nrow, (j + 1) * y.nrow))), j * y.nrow);
|
|
1281
1537
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1538
|
+
return make(y.nrow, y.ncol, data, readerDimnames(null, y));
|
|
1539
|
+
}
|
|
1540
|
+
requireRows(q, y.length);
|
|
1541
|
+
return read(y);
|
|
1542
|
+
}
|
|
1543
|
+
function readerDimnames(rows, y) {
|
|
1544
|
+
const columns = y.dimnames?.[1] ?? null;
|
|
1545
|
+
return rows === null && columns === null ? null : [rows, columns];
|
|
1546
|
+
}
|
|
1547
|
+
function transformed(q, y, transpose2) {
|
|
1548
|
+
const result = [...y];
|
|
1549
|
+
const count = reflectorCount(q);
|
|
1550
|
+
if (transpose2) {
|
|
1551
|
+
for (let step = 0;step < count; step++) {
|
|
1552
|
+
applyReflector(q, step, result);
|
|
1285
1553
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
if (Math.abs(delta - 1) < GAMMA_EPSILON) {
|
|
1290
|
-
break;
|
|
1554
|
+
} else {
|
|
1555
|
+
for (let step = count - 1;step >= 0; step--) {
|
|
1556
|
+
applyReflector(q, step, result);
|
|
1291
1557
|
}
|
|
1292
1558
|
}
|
|
1293
|
-
return
|
|
1559
|
+
return result;
|
|
1294
1560
|
}
|
|
1295
|
-
function
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1561
|
+
function qrQ(q) {
|
|
1562
|
+
const { nrow, ncol } = q.qr;
|
|
1563
|
+
const width = Math.min(nrow, ncol);
|
|
1564
|
+
const data = new Float64Array(nrow * width);
|
|
1565
|
+
for (let j = 0;j < width; j++) {
|
|
1566
|
+
const unit = new Array(nrow).fill(0);
|
|
1567
|
+
unit[j] = 1;
|
|
1568
|
+
data.set(transformed(q, unit, false), j * nrow);
|
|
1569
|
+
}
|
|
1570
|
+
return make(nrow, width, data, null);
|
|
1571
|
+
}
|
|
1572
|
+
function qrR(q) {
|
|
1573
|
+
const { nrow, ncol } = q.qr;
|
|
1574
|
+
const height = Math.min(nrow, ncol);
|
|
1575
|
+
const data = new Float64Array(height * ncol);
|
|
1576
|
+
for (let j = 0;j < ncol; j++) {
|
|
1577
|
+
for (let i = 0;i <= Math.min(j, height - 1); i++) {
|
|
1578
|
+
data[j * height + i] = q.qr.data[j * nrow + i];
|
|
1579
|
+
}
|
|
1301
1580
|
}
|
|
1302
|
-
|
|
1581
|
+
const rows = q.qr.dimnames?.[0]?.slice(0, height) ?? null;
|
|
1582
|
+
const columns = q.qr.dimnames?.[1] ?? null;
|
|
1583
|
+
return make(height, ncol, data, rows === null && columns === null ? null : [rows, columns]);
|
|
1303
1584
|
}
|
|
1304
|
-
function
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1585
|
+
function requireRows(q, rows) {
|
|
1586
|
+
if (rows !== q.qr.nrow) {
|
|
1587
|
+
throw new RangeError("'qr' and 'y' must have the same number of rows");
|
|
1307
1588
|
}
|
|
1308
|
-
const lower = 0.5 * upperGamma(0.5, 0.5 * z * z);
|
|
1309
|
-
return z > 0 ? 1 - lower : lower;
|
|
1310
1589
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const scale = (normal * normal - 3) / 6;
|
|
1319
|
-
const harmonic = 2 / (1 / (2 * a - 1) + 1 / (2 * b - 1));
|
|
1320
|
-
const w = normal * Math.sqrt(scale + harmonic) / harmonic - (1 / (2 * b - 1) - 1 / (2 * a - 1)) * (scale + 5 / 6 - 2 / (3 * harmonic));
|
|
1321
|
-
return a / (a + b * Math.exp(2 * w));
|
|
1590
|
+
function reflectorCount(q) {
|
|
1591
|
+
return Math.min(q.rank, q.qr.nrow - 1);
|
|
1592
|
+
}
|
|
1593
|
+
function applyReflector(q, step, vector) {
|
|
1594
|
+
const leading = q.qraux[step];
|
|
1595
|
+
if (leading === 0) {
|
|
1596
|
+
return;
|
|
1322
1597
|
}
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1598
|
+
const { nrow } = q.qr;
|
|
1599
|
+
const column2 = q.qr.data.subarray(step * nrow, (step + 1) * nrow);
|
|
1600
|
+
let inner = leading * vector[step];
|
|
1601
|
+
for (let row2 = step + 1;row2 < nrow; row2++) {
|
|
1602
|
+
inner = fusedMultiplyAdd(column2[row2], vector[row2], inner);
|
|
1328
1603
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
return Number.NaN;
|
|
1604
|
+
const factor = -inner / leading;
|
|
1605
|
+
vector[step] = fusedMultiplyAdd(factor, leading, vector[step]);
|
|
1606
|
+
for (let row2 = step + 1;row2 < nrow; row2++) {
|
|
1607
|
+
vector[row2] = fusedMultiplyAdd(factor, column2[row2], vector[row2]);
|
|
1334
1608
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1609
|
+
}
|
|
1610
|
+
function decompose(columns, tolerance, rows) {
|
|
1611
|
+
const width = columns.length;
|
|
1612
|
+
const pivot = columns.map((_, column2) => column2);
|
|
1613
|
+
const qraux = columns.map((column2) => norm(column2, 0));
|
|
1614
|
+
const lastNorms = [...qraux];
|
|
1615
|
+
const originalNorms = qraux.map((value) => value || 1);
|
|
1616
|
+
let k = width + 1;
|
|
1617
|
+
for (let step = 0;step < Math.min(rows, width); step++) {
|
|
1618
|
+
while (step + 1 < k && qraux[step] < originalNorms[step] * tolerance) {
|
|
1619
|
+
moveToEnd(columns, step);
|
|
1620
|
+
moveToEnd(pivot, step);
|
|
1621
|
+
moveToEnd(qraux, step);
|
|
1622
|
+
moveToEnd(lastNorms, step);
|
|
1623
|
+
moveToEnd(originalNorms, step);
|
|
1624
|
+
k -= 1;
|
|
1625
|
+
}
|
|
1626
|
+
if (step === rows - 1) {
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
reflect(columns, qraux, lastNorms, step, rows);
|
|
1337
1630
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1631
|
+
return { householders: qraux, pivot, rank: Math.min(k - 1, rows) };
|
|
1632
|
+
}
|
|
1633
|
+
function reflect(columns, qraux, lastNorms, step, rows) {
|
|
1634
|
+
const column2 = columns[step];
|
|
1635
|
+
const length = norm(column2, step);
|
|
1636
|
+
if (length === 0) {
|
|
1637
|
+
return;
|
|
1340
1638
|
}
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
let
|
|
1344
|
-
|
|
1345
|
-
if (!(x > 0) || !(x < 1)) {
|
|
1346
|
-
x = 0.5;
|
|
1639
|
+
const pivotNorm = column2[step] < 0 ? -length : length;
|
|
1640
|
+
const reciprocal = 1 / pivotNorm;
|
|
1641
|
+
for (let row2 = step;row2 < rows; row2++) {
|
|
1642
|
+
column2[row2] = column2[row2] * reciprocal;
|
|
1347
1643
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1644
|
+
const leading = 1 + column2[step];
|
|
1645
|
+
column2[step] = leading;
|
|
1646
|
+
for (let index = step + 1;index < columns.length; index++) {
|
|
1647
|
+
const other = columns[index];
|
|
1648
|
+
let inner = 0;
|
|
1649
|
+
for (let row2 = step;row2 < rows; row2++) {
|
|
1650
|
+
inner = fusedMultiplyAdd(column2[row2], other[row2], inner);
|
|
1354
1651
|
}
|
|
1355
|
-
const
|
|
1356
|
-
let
|
|
1357
|
-
|
|
1358
|
-
next = 0.5 * (lower + upper);
|
|
1652
|
+
const factor = -inner / leading;
|
|
1653
|
+
for (let row2 = step;row2 < rows; row2++) {
|
|
1654
|
+
other[row2] = fusedMultiplyAdd(factor, column2[row2], other[row2]);
|
|
1359
1655
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1656
|
+
const running = qraux[index];
|
|
1657
|
+
if (running !== 0) {
|
|
1658
|
+
const ratio = Math.abs(other[step]) / running;
|
|
1659
|
+
const remaining = Math.max(1 - ratio * ratio, 0);
|
|
1660
|
+
if (Math.abs(remaining) < 0.000001) {
|
|
1661
|
+
qraux[index] = norm(other, step + 1);
|
|
1662
|
+
lastNorms[index] = qraux[index];
|
|
1663
|
+
} else {
|
|
1664
|
+
qraux[index] = running * Math.sqrt(remaining);
|
|
1665
|
+
}
|
|
1362
1666
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1667
|
+
}
|
|
1668
|
+
qraux[step] = leading;
|
|
1669
|
+
column2[step] = -pivotNorm;
|
|
1670
|
+
}
|
|
1671
|
+
function backSubstitute(compact, response, rank) {
|
|
1672
|
+
const { nrow } = compact;
|
|
1673
|
+
const entry = (i, j) => compact.data[j * nrow + i];
|
|
1674
|
+
const solved = response.slice(0, rank);
|
|
1675
|
+
for (let column2 = rank - 1;column2 >= 0; column2--) {
|
|
1676
|
+
const value = solved[column2] / entry(column2, column2);
|
|
1677
|
+
solved[column2] = value;
|
|
1678
|
+
for (let row2 = 0;row2 < column2; row2++) {
|
|
1679
|
+
solved[row2] = fusedMultiplyAdd(-value, entry(row2, column2), solved[row2]);
|
|
1367
1680
|
}
|
|
1368
1681
|
}
|
|
1369
|
-
return
|
|
1682
|
+
return solved;
|
|
1683
|
+
}
|
|
1684
|
+
function moveToEnd(track, from) {
|
|
1685
|
+
const [moved] = track.splice(from, 1);
|
|
1686
|
+
track.push(moved);
|
|
1687
|
+
}
|
|
1688
|
+
function norm(column2, from) {
|
|
1689
|
+
let squares = 0;
|
|
1690
|
+
for (let row2 = from;row2 < column2.length; row2++) {
|
|
1691
|
+
const value = column2[row2];
|
|
1692
|
+
squares = fusedMultiplyAdd(value, value, squares);
|
|
1693
|
+
}
|
|
1694
|
+
return Math.sqrt(squares);
|
|
1370
1695
|
}
|
|
1371
1696
|
|
|
1372
|
-
// src/core/
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1697
|
+
// src/core/linalg/lm.ts
|
|
1698
|
+
function lm(data, options) {
|
|
1699
|
+
const { outcome, intercept = true, tolerance = DEFAULT_QR_TOLERANCE } = options;
|
|
1700
|
+
const design = modelMatrix(data, options);
|
|
1701
|
+
const { rows } = design;
|
|
1702
|
+
const n = rows.length;
|
|
1703
|
+
if (n === 0) {
|
|
1704
|
+
throw new RangeError("0 (non-NA) cases");
|
|
1705
|
+
}
|
|
1706
|
+
const outcomeColumn = data[outcome];
|
|
1707
|
+
const y = rows.map((row2) => outcomeColumn[row2]);
|
|
1708
|
+
const factored = qr(design.matrix, { tolerance });
|
|
1709
|
+
const coefficients = qrCoef(factored, y);
|
|
1710
|
+
const residuals = qrResid(factored, y);
|
|
1711
|
+
const fitted = zipWith(y, residuals, (value, residual) => value - residual);
|
|
1712
|
+
const names = design.matrix.dimnames?.[1] ?? [];
|
|
1713
|
+
const { rank } = factored;
|
|
1714
|
+
const dfResidual = n - rank;
|
|
1715
|
+
const interceptCount = intercept ? 1 : 0;
|
|
1716
|
+
const rss = sum(residuals.map((r) => r * r));
|
|
1717
|
+
const centered = intercept ? mean(fitted) : 0;
|
|
1718
|
+
const mss = sum(fitted.map((f) => (f - centered) * (f - centered)));
|
|
1719
|
+
const resvar = rss / dfResidual;
|
|
1720
|
+
const numdf = rank - interceptCount;
|
|
1721
|
+
const rSquared = numdf > 0 ? mss / (mss + rss) : 0;
|
|
1722
|
+
const adjRSquared = numdf > 0 ? 1 - (1 - rSquared) * ((n - interceptCount) / dfResidual) : 0;
|
|
1723
|
+
const standardErrors = standardErrorsOf(factored, resvar, names.length);
|
|
1724
|
+
const tValues = zipWith(coefficients, standardErrors, (b, se) => b === null || se === null ? null : b / se);
|
|
1725
|
+
const pValues = tValues.map((tv) => tv === null ? null : 2 * pt(-Math.abs(tv), dfResidual));
|
|
1726
|
+
return {
|
|
1727
|
+
coefficients: namedVector(names, coefficients),
|
|
1728
|
+
standardErrors: namedVector(names, standardErrors),
|
|
1729
|
+
tValues: namedVector(names, tValues),
|
|
1730
|
+
pValues: namedVector(names, pValues),
|
|
1731
|
+
fitted: padded(fitted, rows, outcomeColumn.length),
|
|
1732
|
+
residuals: padded(residuals, rows, outcomeColumn.length),
|
|
1733
|
+
rank,
|
|
1734
|
+
dfResidual,
|
|
1735
|
+
rSquared,
|
|
1736
|
+
adjRSquared,
|
|
1737
|
+
sigma: Math.sqrt(resvar),
|
|
1738
|
+
fStatistic: numdf > 0 ? { value: mss / numdf / resvar, numdf, dendf: dfResidual } : null,
|
|
1739
|
+
rows,
|
|
1740
|
+
termLabels: design.termLabels
|
|
1741
|
+
};
|
|
1375
1742
|
}
|
|
1376
|
-
function
|
|
1377
|
-
|
|
1743
|
+
function standardErrorsOf(factored, resvar, width) {
|
|
1744
|
+
const { rank, pivot } = factored;
|
|
1745
|
+
const { nrow } = factored.qr;
|
|
1746
|
+
const r = (i, j) => factored.qr.data[j * nrow + i];
|
|
1747
|
+
const inverse = Array.from({ length: rank }, (_, k) => {
|
|
1748
|
+
const x = new Array(rank).fill(0);
|
|
1749
|
+
x[k] = 1 / r(k, k);
|
|
1750
|
+
for (let i = k - 1;i >= 0; i--) {
|
|
1751
|
+
let total = 0;
|
|
1752
|
+
for (let j = i + 1;j <= k; j++) {
|
|
1753
|
+
total += r(i, j) * x[j];
|
|
1754
|
+
}
|
|
1755
|
+
x[i] = -total / r(i, i);
|
|
1756
|
+
}
|
|
1757
|
+
return x;
|
|
1758
|
+
});
|
|
1759
|
+
const diagonal = Array.from({ length: rank }, (_, i) => sum(inverse.map((columnOfInverse) => columnOfInverse[i] ** 2)));
|
|
1760
|
+
const errors = new Array(width).fill(null);
|
|
1761
|
+
pivot.slice(0, rank).forEach((original, position) => {
|
|
1762
|
+
errors[original] = Math.sqrt(diagonal[position] * resvar);
|
|
1763
|
+
});
|
|
1764
|
+
return errors;
|
|
1765
|
+
}
|
|
1766
|
+
function padded(values, rows, length) {
|
|
1767
|
+
const out = new Array(length).fill(Number.NaN);
|
|
1768
|
+
rows.forEach((row2, index) => {
|
|
1769
|
+
out[row2] = values[index];
|
|
1770
|
+
});
|
|
1771
|
+
return out;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// src/core/moderation.ts
|
|
1775
|
+
var GRID_STEPS = 15;
|
|
1776
|
+
function moderationSurface(data, options) {
|
|
1777
|
+
const { outcome, iv, mod, interaction = true, controls = [] } = options;
|
|
1778
|
+
if (iv === mod) {
|
|
1779
|
+
throw new RangeError(`\`iv\` and \`mod\` must name different columns, both name "${iv}"`);
|
|
1780
|
+
}
|
|
1781
|
+
const named = new Set([outcome, iv, mod]);
|
|
1782
|
+
controls.forEach((control) => {
|
|
1783
|
+
if (named.has(control)) {
|
|
1784
|
+
throw new RangeError(`control "${control}" already names the outcome, the iv, the mod, ` + "or another control");
|
|
1785
|
+
}
|
|
1786
|
+
named.add(control);
|
|
1787
|
+
});
|
|
1788
|
+
frameRows(data);
|
|
1789
|
+
const y = requireNumericColumn(data, outcome, "outcome");
|
|
1790
|
+
const ivColumn = requireNumericColumn(data, iv, "iv");
|
|
1791
|
+
const modColumn = requireNumericColumn(data, mod, "mod");
|
|
1792
|
+
const controlColumns = controls.map((control) => requireNumericColumn(data, control, "controls"));
|
|
1793
|
+
const modelColumns = [y, ivColumn, modColumn, ...controlColumns];
|
|
1794
|
+
const anyComplete = y.some((_, row2) => modelColumns.every((column2) => Number.isFinite(column2[row2])));
|
|
1795
|
+
if (!anyComplete) {
|
|
1796
|
+
throw new RangeError("the model has no complete rows: every row is missing a value in " + "the outcome, the IV, the moderator, or a control");
|
|
1797
|
+
}
|
|
1798
|
+
const fit = lm(data, {
|
|
1799
|
+
outcome,
|
|
1800
|
+
terms: [iv, mod, ...controls, ...interaction ? [[iv, mod]] : []]
|
|
1801
|
+
});
|
|
1802
|
+
const { fitted, residuals } = fit;
|
|
1803
|
+
const coefficients = fit.coefficients.names.map((name, index) => ({
|
|
1804
|
+
name,
|
|
1805
|
+
value: fit.coefficients.values[index] ?? null
|
|
1806
|
+
}));
|
|
1807
|
+
const ivValues = rSeq(...extent(ivColumn), GRID_STEPS);
|
|
1808
|
+
const modValues = rSeq(...extent(modColumn), GRID_STEPS);
|
|
1809
|
+
const holds = Object.fromEntries(controls.map((control, index) => [
|
|
1810
|
+
control,
|
|
1811
|
+
mean(controlColumns[index].filter(Number.isFinite))
|
|
1812
|
+
]));
|
|
1813
|
+
const weights = coefficients.map((term) => term.value ?? 0);
|
|
1814
|
+
const predictions = modValues.flatMap((modValue) => ivValues.map((ivValue) => {
|
|
1815
|
+
const gridRow = [
|
|
1816
|
+
1,
|
|
1817
|
+
ivValue,
|
|
1818
|
+
modValue,
|
|
1819
|
+
...controls.map((control) => holds[control]),
|
|
1820
|
+
...interaction ? [ivValue * modValue] : []
|
|
1821
|
+
];
|
|
1822
|
+
return sum(zipWith(gridRow, weights, (value, weight) => value * weight));
|
|
1823
|
+
}));
|
|
1824
|
+
const [dataLow, dataHigh] = extent(y);
|
|
1825
|
+
const [surfaceLow, surfaceHigh] = extent(predictions);
|
|
1826
|
+
return {
|
|
1827
|
+
coefficients,
|
|
1828
|
+
fitted,
|
|
1829
|
+
residuals,
|
|
1830
|
+
ivValues,
|
|
1831
|
+
modValues,
|
|
1832
|
+
predictions,
|
|
1833
|
+
zlim: [Math.min(dataLow, surfaceLow), Math.max(dataHigh, surfaceHigh)],
|
|
1834
|
+
holds
|
|
1835
|
+
};
|
|
1378
1836
|
}
|
|
1379
|
-
function
|
|
1380
|
-
if (
|
|
1381
|
-
return
|
|
1837
|
+
function rSeq(from, to, length) {
|
|
1838
|
+
if (from === to) {
|
|
1839
|
+
return new Array(length).fill(from);
|
|
1382
1840
|
}
|
|
1383
|
-
|
|
1841
|
+
const by = (to - from) / (length - 1);
|
|
1842
|
+
return Array.from({ length }, (_, index) => index === 0 ? from : index === length - 1 ? to : from + index * by);
|
|
1384
1843
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
return isNonCentral(ncp) ? nonCentralProbability(x, df, ncp) : centralProbability(x, df);
|
|
1844
|
+
// src/core/regression.ts
|
|
1845
|
+
function completePointRows(points) {
|
|
1846
|
+
return points.flatMap((point, row2) => Number.isFinite(point.x) && Number.isFinite(point.y) ? [row2] : []);
|
|
1390
1847
|
}
|
|
1391
|
-
function
|
|
1392
|
-
|
|
1393
|
-
|
|
1848
|
+
function linearRegression(points) {
|
|
1849
|
+
const rows = completePointRows(points);
|
|
1850
|
+
if (rows.length === 0) {
|
|
1851
|
+
return null;
|
|
1394
1852
|
}
|
|
1395
|
-
|
|
1853
|
+
const xs = rows.map((row2) => points[row2].x);
|
|
1854
|
+
const ys = rows.map((row2) => points[row2].y);
|
|
1855
|
+
const meanX = mean(xs);
|
|
1856
|
+
const meanY = mean(ys);
|
|
1857
|
+
const devX = xs.map((x) => x - meanX);
|
|
1858
|
+
const devY = ys.map((y) => y - meanY);
|
|
1859
|
+
const sumSquaresX = sum(devX.map((d) => d * d));
|
|
1860
|
+
const sumSquaresY = sum(devY.map((d) => d * d));
|
|
1861
|
+
const sumProducts = sum(zipWith(devX, devY, (dx, dy) => dx * dy));
|
|
1862
|
+
const slope = sumSquaresX === 0 ? null : sumProducts / sumSquaresX;
|
|
1863
|
+
const intercept = slope === null ? meanY : meanY - slope * meanX;
|
|
1864
|
+
const correlation = sumSquaresX === 0 || sumSquaresY === 0 ? null : sumProducts / Math.sqrt(sumSquaresX * sumSquaresY);
|
|
1865
|
+
const fittedComplete = xs.map((x) => slope === null ? intercept : intercept + slope * x);
|
|
1866
|
+
const ssr = sum(fittedComplete.map((f) => (f - meanY) * (f - meanY)));
|
|
1867
|
+
const sse = sum(zipWith(ys, fittedComplete, (y, f) => (y - f) * (y - f)));
|
|
1868
|
+
const sst = sumSquaresY;
|
|
1869
|
+
const rSquared = sst === 0 ? null : ssr / sst;
|
|
1870
|
+
const fitted = new Array(points.length).fill(Number.NaN);
|
|
1871
|
+
rows.forEach((row2, survivor) => {
|
|
1872
|
+
fitted[row2] = fittedComplete[survivor];
|
|
1873
|
+
});
|
|
1874
|
+
return {
|
|
1875
|
+
intercept,
|
|
1876
|
+
slope,
|
|
1877
|
+
correlation,
|
|
1878
|
+
ssr,
|
|
1879
|
+
sse,
|
|
1880
|
+
sst,
|
|
1881
|
+
rSquared,
|
|
1882
|
+
fitted
|
|
1883
|
+
};
|
|
1396
1884
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const logDensity = -0.5 * Math.log(df) - logBeta(0.5, df / 2) - (df + 1) / 2 * Math.log1p(x * x / df);
|
|
1402
|
-
return Math.exp(logDensity);
|
|
1885
|
+
// src/core/matrix.ts
|
|
1886
|
+
var SMALLEST_NORMAL = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022250738585072014;
|
|
1887
|
+
function determinant(matrix2) {
|
|
1888
|
+
return determinantOf(factorize(matrix2));
|
|
1403
1889
|
}
|
|
1404
|
-
function
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (
|
|
1409
|
-
return
|
|
1890
|
+
function invertMatrix(matrix2) {
|
|
1891
|
+
const factorization = factorize(matrix2);
|
|
1892
|
+
const determinant2 = determinantOf(factorization);
|
|
1893
|
+
const zeroPivot = zeroPivotOf(factorization);
|
|
1894
|
+
if (zeroPivot !== null) {
|
|
1895
|
+
return {
|
|
1896
|
+
determinant: determinant2,
|
|
1897
|
+
inverse: null,
|
|
1898
|
+
singularity: "exact",
|
|
1899
|
+
rcond: 0,
|
|
1900
|
+
zeroPivot
|
|
1901
|
+
};
|
|
1410
1902
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1903
|
+
const inverse = solveForIdentity(factorization);
|
|
1904
|
+
const rcond = 1 / (oneNorm(matrix2) * oneNorm(inverse));
|
|
1905
|
+
if (rcond < Number.EPSILON) {
|
|
1906
|
+
return {
|
|
1907
|
+
determinant: determinant2,
|
|
1908
|
+
inverse: null,
|
|
1909
|
+
singularity: "computational",
|
|
1910
|
+
rcond,
|
|
1911
|
+
zeroPivot: null
|
|
1912
|
+
};
|
|
1413
1913
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1914
|
+
return {
|
|
1915
|
+
determinant: determinant2,
|
|
1916
|
+
inverse,
|
|
1917
|
+
singularity: null,
|
|
1918
|
+
rcond,
|
|
1919
|
+
zeroPivot: null
|
|
1920
|
+
};
|
|
1416
1921
|
}
|
|
1417
|
-
function
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1922
|
+
function factorize(matrix2) {
|
|
1923
|
+
const interchanged = Math.abs(matrix2.y1) > Math.abs(matrix2.x1);
|
|
1924
|
+
const leading = interchanged ? matrix2.y1 : matrix2.x1;
|
|
1925
|
+
const upperRight = interchanged ? matrix2.y2 : matrix2.x2;
|
|
1926
|
+
const below = interchanged ? matrix2.x1 : matrix2.y1;
|
|
1927
|
+
const belowRight = interchanged ? matrix2.x2 : matrix2.y2;
|
|
1928
|
+
const multiplier = Math.abs(leading) >= SMALLEST_NORMAL ? below * (1 / leading) : below / leading;
|
|
1929
|
+
const trailing = fusedMultiplyAdd(-multiplier, upperRight, belowRight);
|
|
1930
|
+
return {
|
|
1931
|
+
interchanged,
|
|
1932
|
+
multiplier,
|
|
1933
|
+
pivots: [leading, trailing],
|
|
1934
|
+
upperRight
|
|
1935
|
+
};
|
|
1424
1936
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
if (
|
|
1937
|
+
function determinantOf(factorization) {
|
|
1938
|
+
const { interchanged, pivots } = factorization;
|
|
1939
|
+
if (zeroPivotOf(factorization) !== null) {
|
|
1428
1940
|
return 0;
|
|
1429
1941
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (p >= 1) {
|
|
1434
|
-
return Number.POSITIVE_INFINITY;
|
|
1435
|
-
}
|
|
1436
|
-
const tail = p < 0.5 ? p : 1 - p;
|
|
1437
|
-
const sign = p < 0.5 ? -1 : 1;
|
|
1438
|
-
const twoSided = 2 * tail;
|
|
1439
|
-
let squared;
|
|
1440
|
-
if (twoSided > 0.5) {
|
|
1441
|
-
const near = inverseIncompleteBeta(1 - twoSided, 0.5, df / 2);
|
|
1442
|
-
squared = df * near / (1 - near);
|
|
1443
|
-
} else {
|
|
1444
|
-
const far = inverseIncompleteBeta(twoSided, df / 2, 0.5);
|
|
1445
|
-
squared = df * (1 - far) / far;
|
|
1446
|
-
}
|
|
1447
|
-
return sign * polish(Math.sqrt(squared), tail, df);
|
|
1448
|
-
}
|
|
1449
|
-
function polish(start, tail, df) {
|
|
1450
|
-
let t = start;
|
|
1451
|
-
for (let step = 0;step < POLISH_MAX_STEPS; step += 1) {
|
|
1452
|
-
const density = centralDensity(t, df);
|
|
1453
|
-
if (!(density > 0) || !Number.isFinite(t)) {
|
|
1454
|
-
break;
|
|
1455
|
-
}
|
|
1456
|
-
const move = (upperTail(t, df) - tail) / density;
|
|
1457
|
-
const next = t + move;
|
|
1458
|
-
if (!(next > 0) || !Number.isFinite(next) || Math.abs(move) > 0.25 * t) {
|
|
1459
|
-
break;
|
|
1460
|
-
}
|
|
1461
|
-
if (next === t) {
|
|
1462
|
-
break;
|
|
1463
|
-
}
|
|
1464
|
-
t = next;
|
|
1465
|
-
if (Math.abs(move) <= Number.EPSILON * t) {
|
|
1466
|
-
break;
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
return t;
|
|
1942
|
+
const modulus = sum(pivots.map((pivot) => Math.log(Math.abs(pivot))));
|
|
1943
|
+
const sign = pivots.reduce((carried, pivot) => pivot < 0 ? -carried : carried, interchanged ? -1 : 1);
|
|
1944
|
+
return sign * Math.exp(modulus);
|
|
1470
1945
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
var SERIES_DF_LIMIT = 400000;
|
|
1475
|
-
var SQRT_TWO_OVER_PI = Math.sqrt(2 / Math.PI);
|
|
1476
|
-
function nonCentralProbability(x, df, ncp) {
|
|
1477
|
-
if (x === Number.POSITIVE_INFINITY) {
|
|
1946
|
+
function zeroPivotOf(factorization) {
|
|
1947
|
+
const [leading, trailing] = factorization.pivots;
|
|
1948
|
+
if (leading === 0) {
|
|
1478
1949
|
return 1;
|
|
1479
1950
|
}
|
|
1480
|
-
|
|
1481
|
-
return 0;
|
|
1482
|
-
}
|
|
1483
|
-
const reflected = x < 0;
|
|
1484
|
-
const t = reflected ? -x : x;
|
|
1485
|
-
const delta = reflected ? -ncp : ncp;
|
|
1486
|
-
const lower = df > SERIES_DF_LIMIT || delta * delta > SERIES_NCP_LIMIT_SQUARED ? normalApproximation(t, df, delta) : lenthSeries(t, df, delta);
|
|
1487
|
-
return reflected ? 1 - lower : lower;
|
|
1951
|
+
return trailing === 0 ? 2 : null;
|
|
1488
1952
|
}
|
|
1489
|
-
function
|
|
1490
|
-
const
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1953
|
+
function solveForIdentity(factorization) {
|
|
1954
|
+
const { interchanged } = factorization;
|
|
1955
|
+
const [firstTop, firstBottom] = interchanged ? [0, 1] : [1, 0];
|
|
1956
|
+
const [secondTop, secondBottom] = interchanged ? [1, 0] : [0, 1];
|
|
1957
|
+
const [x1, y1] = solveColumn(factorization, firstTop, firstBottom);
|
|
1958
|
+
const [x2, y2] = solveColumn(factorization, secondTop, secondBottom);
|
|
1959
|
+
return {
|
|
1960
|
+
x1: withoutNegativeZero(x1),
|
|
1961
|
+
y1: withoutNegativeZero(y1),
|
|
1962
|
+
x2: withoutNegativeZero(x2),
|
|
1963
|
+
y2: withoutNegativeZero(y2)
|
|
1964
|
+
};
|
|
1493
1965
|
}
|
|
1494
|
-
function
|
|
1495
|
-
const
|
|
1496
|
-
const
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
let a = 0.5;
|
|
1509
|
-
const b = 0.5 * df;
|
|
1510
|
-
const powered = Math.pow(complement, b);
|
|
1511
|
-
const logBetaValue = logBeta(0.5, b);
|
|
1512
|
-
let oddTerm = incompleteBetaSplit(x, complement, a, b);
|
|
1513
|
-
let oddStep = 2 * powered * Math.exp(a * Math.log(x) - logBetaValue);
|
|
1514
|
-
let evenTerm = 1 - powered;
|
|
1515
|
-
let evenStep = b * x * powered;
|
|
1516
|
-
sum2 = oddWeight * oddTerm + evenWeight * evenTerm;
|
|
1517
|
-
for (let step = 1;step <= SERIES_MAX_STEPS; step += 1) {
|
|
1518
|
-
a += 1;
|
|
1519
|
-
oddTerm -= oddStep;
|
|
1520
|
-
evenTerm -= evenStep;
|
|
1521
|
-
oddStep *= x * (a + b - 1) / a;
|
|
1522
|
-
evenStep *= x * (a + b - 0.5) / (a + 0.5);
|
|
1523
|
-
oddWeight *= lambda / (2 * step);
|
|
1524
|
-
evenWeight *= lambda / (2 * step + 1);
|
|
1525
|
-
remaining -= oddWeight;
|
|
1526
|
-
if (remaining <= 0) {
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
sum2 += oddWeight * oddTerm + evenWeight * evenTerm;
|
|
1530
|
-
if (Math.abs(2 * remaining * (oddTerm - oddStep)) < SERIES_ERROR_MAX) {
|
|
1531
|
-
break;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1966
|
+
function solveColumn(factorization, top, bottom) {
|
|
1967
|
+
const { multiplier, pivots, upperRight } = factorization;
|
|
1968
|
+
const lower = fusedMultiplyAdd(-multiplier, top, bottom) / pivots[1];
|
|
1969
|
+
const upper = fusedMultiplyAdd(-upperRight, lower, top) / pivots[0];
|
|
1970
|
+
return [upper, lower];
|
|
1971
|
+
}
|
|
1972
|
+
function oneNorm(matrix2) {
|
|
1973
|
+
return Math.max(Math.abs(matrix2.x1) + Math.abs(matrix2.y1), Math.abs(matrix2.x2) + Math.abs(matrix2.y2));
|
|
1974
|
+
}
|
|
1975
|
+
// src/core/pca.ts
|
|
1976
|
+
function principalComponents(points) {
|
|
1977
|
+
const rows = completePointRows(points);
|
|
1978
|
+
if (rows.length === 0) {
|
|
1979
|
+
return null;
|
|
1534
1980
|
}
|
|
1535
|
-
|
|
1981
|
+
const complete = rows.map((row2) => points[row2]);
|
|
1982
|
+
const centerX = mean(complete.map((point) => point.x));
|
|
1983
|
+
const centerY = mean(complete.map((point) => point.y));
|
|
1984
|
+
const devX = complete.map((point) => point.x - centerX);
|
|
1985
|
+
const devY = complete.map((point) => point.y - centerY);
|
|
1986
|
+
const divisor = Math.max(1, complete.length - 1);
|
|
1987
|
+
const varX = sum(devX.map((d) => d * d)) / divisor;
|
|
1988
|
+
const varY = sum(devY.map((d) => d * d)) / divisor;
|
|
1989
|
+
const covariance = sum(zipWith(devX, devY, (dx, dy) => dx * dy)) / divisor;
|
|
1990
|
+
const [first, second] = componentsOf(varX, varY, covariance);
|
|
1991
|
+
const scores = points.map(() => ({ x: Number.NaN, y: Number.NaN }));
|
|
1992
|
+
rows.forEach((row2, survivor) => {
|
|
1993
|
+
const dx = devX[survivor];
|
|
1994
|
+
const dy = devY[survivor];
|
|
1995
|
+
scores[row2] = {
|
|
1996
|
+
x: dx * first.loadings[0] + dy * first.loadings[1],
|
|
1997
|
+
y: dx * second.loadings[0] + dy * second.loadings[1]
|
|
1998
|
+
};
|
|
1999
|
+
});
|
|
2000
|
+
return {
|
|
2001
|
+
sdev: [sdevOf(first.variance), sdevOf(second.variance)],
|
|
2002
|
+
rotation: [first.loadings, second.loadings],
|
|
2003
|
+
center: { x: centerX, y: centerY },
|
|
2004
|
+
scores
|
|
2005
|
+
};
|
|
1536
2006
|
}
|
|
1537
|
-
function
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
const stepped = x * Math.sqrt((df + 2) / df);
|
|
1543
|
-
const difference = nonCentralProbability(stepped, df + 2, ncp) - nonCentralProbability(x, df, ncp);
|
|
1544
|
-
return df / Math.abs(x) * Math.abs(difference);
|
|
2007
|
+
function componentsOf(varX, varY, covariance) {
|
|
2008
|
+
if (covariance === 0) {
|
|
2009
|
+
const alongX = { variance: varX, loadings: [1, 0] };
|
|
2010
|
+
const alongY = { variance: varY, loadings: [0, 1] };
|
|
2011
|
+
return varX >= varY ? [alongX, alongY] : [alongY, alongX];
|
|
1545
2012
|
}
|
|
1546
|
-
|
|
2013
|
+
const middle = (varX + varY) / 2;
|
|
2014
|
+
const spread = Math.hypot((varX - varY) / 2, covariance);
|
|
2015
|
+
const larger = middle + spread;
|
|
2016
|
+
const smaller = middle - spread;
|
|
2017
|
+
const fromRowX = larger - varX;
|
|
2018
|
+
const fromRowY = larger - varY;
|
|
2019
|
+
const direction = fromRowX >= fromRowY ? [covariance, fromRowX] : [fromRowY, covariance];
|
|
2020
|
+
const leading = signed(unit(direction));
|
|
2021
|
+
const trailing = signed([negated(leading[1]), leading[0]]);
|
|
2022
|
+
return [
|
|
2023
|
+
{ variance: larger, loadings: leading },
|
|
2024
|
+
{ variance: smaller, loadings: trailing }
|
|
2025
|
+
];
|
|
1547
2026
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
2027
|
+
function unit(vector) {
|
|
2028
|
+
const length = Math.hypot(vector[0], vector[1]);
|
|
2029
|
+
return [vector[0] / length, vector[1] / length];
|
|
2030
|
+
}
|
|
2031
|
+
function signed(vector) {
|
|
2032
|
+
const dominant = Math.abs(vector[0]) >= Math.abs(vector[1]) ? vector[0] : vector[1];
|
|
2033
|
+
return dominant >= 0 ? vector : [negated(vector[0]), negated(vector[1])];
|
|
2034
|
+
}
|
|
2035
|
+
function negated(value) {
|
|
2036
|
+
return withoutNegativeZero(-value);
|
|
2037
|
+
}
|
|
2038
|
+
function sdevOf(variance) {
|
|
2039
|
+
return Math.sqrt(Math.max(0, variance));
|
|
2040
|
+
}
|
|
2041
|
+
// src/core/ols.ts
|
|
2042
|
+
var DEFAULT_LEAST_SQUARES_TOLERANCE = 0.0000001;
|
|
2043
|
+
function leastSquares(design, y, options = {}) {
|
|
2044
|
+
const { weights, tolerance = DEFAULT_LEAST_SQUARES_TOLERANCE } = options;
|
|
2045
|
+
const rows = design.length;
|
|
2046
|
+
if (rows === 0) {
|
|
2047
|
+
throw new RangeError("least squares needs at least one row");
|
|
1555
2048
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2049
|
+
const width = design[0].length;
|
|
2050
|
+
if (design.some((row2) => row2.length !== width)) {
|
|
2051
|
+
throw new RangeError("every design row needs the same number of columns");
|
|
1559
2052
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
lower *= 2;
|
|
2053
|
+
if (y.length !== rows) {
|
|
2054
|
+
throw new RangeError(`the response has ${y.length} values but the design has ${rows} rows`);
|
|
1563
2055
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if (residual < 0) {
|
|
1568
|
-
lower = t;
|
|
1569
|
-
} else {
|
|
1570
|
-
upper = t;
|
|
2056
|
+
if (weights !== undefined) {
|
|
2057
|
+
if (weights.length !== rows) {
|
|
2058
|
+
throw new RangeError(`there are ${weights.length} weights but ${rows} rows`);
|
|
1571
2059
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
if (!(next > lower) || !(next < upper)) {
|
|
1575
|
-
next = 0.5 * (lower + upper);
|
|
2060
|
+
if (weights.some((weight) => !(weight >= 0))) {
|
|
2061
|
+
throw new RangeError("weights cannot be negative or missing");
|
|
1576
2062
|
}
|
|
1577
|
-
|
|
2063
|
+
}
|
|
2064
|
+
const scale = weights?.map((weight) => Math.sqrt(weight));
|
|
2065
|
+
const scaled = (value, row2) => scale === undefined ? value : value * scale[row2];
|
|
2066
|
+
const buffer = new Float64Array(rows * width);
|
|
2067
|
+
design.forEach((row2, index) => {
|
|
2068
|
+
for (let column2 = 0;column2 < width; column2++) {
|
|
2069
|
+
buffer[column2 * rows + index] = scaled(row2[column2], index);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
const factored = qr(make(rows, width, buffer, null), { tolerance });
|
|
2073
|
+
const coefficients = qrCoef(factored, y.map(scaled));
|
|
2074
|
+
const fitted = design.map((row2) => sum(zipWith(row2, coefficients, (value, coefficient) => coefficient === null ? 0 : value * coefficient)));
|
|
2075
|
+
const residuals = zipWith(y, fitted, (value, fit) => value - fit);
|
|
2076
|
+
return { coefficients, fitted, residuals, rank: factored.rank };
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// src/core/logit.ts
|
|
2080
|
+
var DEFAULT_LOGIT_EPSILON = 0.00000001;
|
|
2081
|
+
var DEFAULT_LOGIT_MAX_ITERATIONS = 25;
|
|
2082
|
+
var LINK_THRESHOLD = 30;
|
|
2083
|
+
var BOUND = 10 * Number.EPSILON;
|
|
2084
|
+
function logisticRegression(points, options = {}) {
|
|
2085
|
+
const {
|
|
2086
|
+
epsilon = DEFAULT_LOGIT_EPSILON,
|
|
2087
|
+
maxIterations = DEFAULT_LOGIT_MAX_ITERATIONS
|
|
2088
|
+
} = options;
|
|
2089
|
+
const rows = completePointRows(points);
|
|
2090
|
+
if (rows.length === 0) {
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
const complete = rows.map((row2) => points[row2]);
|
|
2094
|
+
if (complete.some((point) => point.y !== 0 && point.y !== 1)) {
|
|
2095
|
+
throw new RangeError("every outcome must be 0 or 1");
|
|
2096
|
+
}
|
|
2097
|
+
const outcomes = complete.map((point) => point.y);
|
|
2098
|
+
const design = complete.map((point) => [1, point.x]);
|
|
2099
|
+
let fitted = outcomes.map((outcome) => (outcome + 0.5) / 2);
|
|
2100
|
+
let predictors = fitted.map(logit);
|
|
2101
|
+
let previousDeviance = totalDeviance(outcomes, fitted);
|
|
2102
|
+
let deviance = previousDeviance;
|
|
2103
|
+
let coefficients = [Number.NaN, null];
|
|
2104
|
+
let rank = 0;
|
|
2105
|
+
let iterations = 0;
|
|
2106
|
+
let converged = false;
|
|
2107
|
+
for (let iteration = 1;iteration <= maxIterations; iteration++) {
|
|
2108
|
+
iterations = iteration;
|
|
2109
|
+
const working = predictors.map((predictor, row2) => {
|
|
2110
|
+
const probability = fitted[row2];
|
|
2111
|
+
const slope = linkSlope(predictor);
|
|
2112
|
+
return {
|
|
2113
|
+
weight: slope * slope / (probability * (1 - probability)),
|
|
2114
|
+
response: predictor + (outcomes[row2] - probability) / slope
|
|
2115
|
+
};
|
|
2116
|
+
});
|
|
2117
|
+
const step = leastSquares(design, working.map((row2) => row2.response), {
|
|
2118
|
+
weights: working.map((row2) => row2.weight),
|
|
2119
|
+
tolerance: Math.min(0.0000001, epsilon / 1000)
|
|
2120
|
+
});
|
|
2121
|
+
if (step.coefficients.some((coefficient) => coefficient !== null && !Number.isFinite(coefficient))) {
|
|
1578
2122
|
break;
|
|
1579
2123
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2124
|
+
coefficients = step.coefficients;
|
|
2125
|
+
rank = step.rank;
|
|
2126
|
+
predictors = design.map((row2) => sum(zipWith(row2, coefficients, (value, coefficient) => coefficient === null ? 0 : value * coefficient)));
|
|
2127
|
+
fitted = predictors.map(linkInverse);
|
|
2128
|
+
deviance = totalDeviance(outcomes, fitted);
|
|
2129
|
+
if (Math.abs(deviance - previousDeviance) / (0.1 + Math.abs(deviance)) < epsilon) {
|
|
2130
|
+
converged = true;
|
|
1583
2131
|
break;
|
|
1584
2132
|
}
|
|
2133
|
+
previousDeviance = deviance;
|
|
1585
2134
|
}
|
|
1586
|
-
|
|
2135
|
+
const wholeMean = mean(outcomes);
|
|
2136
|
+
const nullDeviance = totalDeviance(outcomes, outcomes.map(() => wholeMean));
|
|
2137
|
+
const paddedFitted = new Array(points.length).fill(Number.NaN);
|
|
2138
|
+
const paddedPredictors = new Array(points.length).fill(Number.NaN);
|
|
2139
|
+
rows.forEach((row2, survivor) => {
|
|
2140
|
+
paddedFitted[row2] = fitted[survivor];
|
|
2141
|
+
paddedPredictors[row2] = predictors[survivor];
|
|
2142
|
+
});
|
|
2143
|
+
return {
|
|
2144
|
+
intercept: coefficients[0] ?? Number.NaN,
|
|
2145
|
+
slope: coefficients[1] ?? null,
|
|
2146
|
+
fitted: paddedFitted,
|
|
2147
|
+
linearPredictors: paddedPredictors,
|
|
2148
|
+
deviance,
|
|
2149
|
+
nullDeviance,
|
|
2150
|
+
aic: deviance + 2 * rank,
|
|
2151
|
+
rank,
|
|
2152
|
+
iterations,
|
|
2153
|
+
converged,
|
|
2154
|
+
saturated: fitted.some((probability) => probability > 1 - BOUND || probability < BOUND)
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function predictLogit(fit, x) {
|
|
2158
|
+
return linkInverse(fit.intercept + (fit.slope ?? 0) * x);
|
|
2159
|
+
}
|
|
2160
|
+
function logit(probability) {
|
|
2161
|
+
return Math.log(probability / (1 - probability));
|
|
2162
|
+
}
|
|
2163
|
+
function linkInverse(predictor) {
|
|
2164
|
+
const odds = predictor < -LINK_THRESHOLD ? Number.EPSILON : predictor > LINK_THRESHOLD ? 1 / Number.EPSILON : Math.exp(predictor);
|
|
2165
|
+
return odds / (1 + odds);
|
|
2166
|
+
}
|
|
2167
|
+
function linkSlope(predictor) {
|
|
2168
|
+
if (predictor > LINK_THRESHOLD || predictor < -LINK_THRESHOLD) {
|
|
2169
|
+
return Number.EPSILON;
|
|
2170
|
+
}
|
|
2171
|
+
const odds = Math.exp(predictor);
|
|
2172
|
+
return odds / ((1 + odds) * (1 + odds));
|
|
2173
|
+
}
|
|
2174
|
+
function totalDeviance(outcomes, probabilities) {
|
|
2175
|
+
return sum(zipWith(outcomes, probabilities, (outcome, probability) => 2 * (surprise(outcome, probability) + surprise(1 - outcome, 1 - probability))));
|
|
2176
|
+
}
|
|
2177
|
+
function surprise(outcome, probability) {
|
|
2178
|
+
return outcome !== 0 ? outcome * Math.log(outcome / probability) : 0;
|
|
1587
2179
|
}
|
|
1588
2180
|
// src/core/ttest.ts
|
|
1589
2181
|
var DEFAULT_T_TEST_OPTIONS = {
|
|
@@ -1596,27 +2188,27 @@ var FILL_UPPER_QUANTILE = 0.999;
|
|
|
1596
2188
|
function tTestStats(options = {}) {
|
|
1597
2189
|
const { diff, sd: sd2, n, alpha } = { ...DEFAULT_T_TEST_OPTIONS, ...options };
|
|
1598
2190
|
const df = n - 1;
|
|
1599
|
-
const
|
|
2191
|
+
const t2 = diff / (sd2 / Math.sqrt(n));
|
|
1600
2192
|
const criticalValue = qt(1 - alpha, df);
|
|
1601
|
-
const beta = pt(criticalValue, df,
|
|
2193
|
+
const beta = pt(criticalValue, df, t2);
|
|
1602
2194
|
const power = 1 - beta;
|
|
1603
|
-
const altMedian = qt(0.5, df,
|
|
1604
|
-
const altFillFrom = qt(beta, df,
|
|
2195
|
+
const altMedian = qt(0.5, df, t2);
|
|
2196
|
+
const altFillFrom = qt(beta, df, t2);
|
|
1605
2197
|
return {
|
|
1606
2198
|
diff,
|
|
1607
2199
|
sd: sd2,
|
|
1608
2200
|
n,
|
|
1609
2201
|
alpha,
|
|
1610
2202
|
df,
|
|
1611
|
-
t,
|
|
2203
|
+
t: t2,
|
|
1612
2204
|
criticalValue,
|
|
1613
2205
|
beta,
|
|
1614
2206
|
power,
|
|
1615
2207
|
altMedian,
|
|
1616
|
-
altMedianDensity: dt(altMedian, df,
|
|
2208
|
+
altMedianDensity: dt(altMedian, df, t2),
|
|
1617
2209
|
altFill: {
|
|
1618
2210
|
from: altFillFrom,
|
|
1619
|
-
to: qt(FILL_UPPER_QUANTILE, df,
|
|
2211
|
+
to: qt(FILL_UPPER_QUANTILE, df, t2)
|
|
1620
2212
|
},
|
|
1621
2213
|
nullFill: {
|
|
1622
2214
|
from: criticalValue,
|
|
@@ -2701,8 +3293,8 @@ var HEAD_LENGTH = 24;
|
|
|
2701
3293
|
function matrixInverseScale(width, height) {
|
|
2702
3294
|
return createScale({ width, height, x: WORLD, y: WORLD });
|
|
2703
3295
|
}
|
|
2704
|
-
function plotMatrixInverse(target,
|
|
2705
|
-
const inversion = invertMatrix(
|
|
3296
|
+
function plotMatrixInverse(target, matrix2) {
|
|
3297
|
+
const inversion = invertMatrix(matrix2);
|
|
2706
3298
|
const { ctx, width, height } = resolveTarget(target);
|
|
2707
3299
|
if (inversion.inverse === null) {
|
|
2708
3300
|
return inversion;
|
|
@@ -2713,22 +3305,22 @@ function plotMatrixInverse(target, matrix) {
|
|
|
2713
3305
|
const { area } = scale;
|
|
2714
3306
|
ctx.save();
|
|
2715
3307
|
clipToArea(ctx, area);
|
|
2716
|
-
drawMatrix(ctx, scale,
|
|
3308
|
+
drawMatrix(ctx, scale, matrix2, MATRIX_FILL);
|
|
2717
3309
|
drawMatrix(ctx, scale, inversion.inverse, INVERSE_FILL);
|
|
2718
3310
|
ctx.restore();
|
|
2719
3311
|
return inversion;
|
|
2720
3312
|
}
|
|
2721
|
-
function drawMatrix(ctx, scale,
|
|
2722
|
-
drawParallelogram(ctx, scale,
|
|
2723
|
-
drawColumnArrow(ctx, scale,
|
|
2724
|
-
drawColumnArrow(ctx, scale,
|
|
3313
|
+
function drawMatrix(ctx, scale, matrix2, fill) {
|
|
3314
|
+
drawParallelogram(ctx, scale, matrix2, fill);
|
|
3315
|
+
drawColumnArrow(ctx, scale, matrix2.x1, matrix2.y1);
|
|
3316
|
+
drawColumnArrow(ctx, scale, matrix2.x2, matrix2.y2);
|
|
2725
3317
|
}
|
|
2726
|
-
function drawParallelogram(ctx, scale,
|
|
3318
|
+
function drawParallelogram(ctx, scale, matrix2, fill) {
|
|
2727
3319
|
const corners = [
|
|
2728
3320
|
[0, 0],
|
|
2729
|
-
[
|
|
2730
|
-
[
|
|
2731
|
-
[
|
|
3321
|
+
[matrix2.x1, matrix2.y1],
|
|
3322
|
+
[matrix2.x1 + matrix2.x2, matrix2.y1 + matrix2.y2],
|
|
3323
|
+
[matrix2.x2, matrix2.y2],
|
|
2732
3324
|
[0, 0]
|
|
2733
3325
|
];
|
|
2734
3326
|
ctx.save();
|
|
@@ -2932,29 +3524,29 @@ function drawRows(ctx, scale, rows, colors) {
|
|
|
2932
3524
|
ctx.setLineDash([]);
|
|
2933
3525
|
ctx.lineWidth = SPAN_WIDTH;
|
|
2934
3526
|
ctx.strokeStyle = colors.span99;
|
|
2935
|
-
for (const { interval, row } of rows) {
|
|
2936
|
-
drawSpan(ctx, scale, interval.ci99.low, interval.ci99.high,
|
|
3527
|
+
for (const { interval, row: row2 } of rows) {
|
|
3528
|
+
drawSpan(ctx, scale, interval.ci99.low, interval.ci99.high, row2);
|
|
2937
3529
|
}
|
|
2938
3530
|
ctx.strokeStyle = colors.span95;
|
|
2939
|
-
for (const { interval, row } of rows) {
|
|
2940
|
-
drawSpan(ctx, scale, interval.ci95.low, interval.ci95.high,
|
|
3531
|
+
for (const { interval, row: row2 } of rows) {
|
|
3532
|
+
drawSpan(ctx, scale, interval.ci95.low, interval.ci95.high, row2);
|
|
2941
3533
|
}
|
|
2942
3534
|
ctx.fillStyle = colors.point;
|
|
2943
|
-
for (const { interval, row } of rows) {
|
|
2944
|
-
drawDiamond(ctx, scale, interval.mean,
|
|
3535
|
+
for (const { interval, row: row2 } of rows) {
|
|
3536
|
+
drawDiamond(ctx, scale, interval.mean, row2);
|
|
2945
3537
|
}
|
|
2946
3538
|
ctx.restore();
|
|
2947
3539
|
}
|
|
2948
|
-
function drawSpan(ctx, scale, low, high,
|
|
2949
|
-
const y = scale.toPixelY(
|
|
3540
|
+
function drawSpan(ctx, scale, low, high, row2) {
|
|
3541
|
+
const y = scale.toPixelY(row2);
|
|
2950
3542
|
ctx.beginPath();
|
|
2951
3543
|
ctx.moveTo(scale.toPixelX(low), y);
|
|
2952
3544
|
ctx.lineTo(scale.toPixelX(high), y);
|
|
2953
3545
|
ctx.stroke();
|
|
2954
3546
|
}
|
|
2955
|
-
function drawDiamond(ctx, scale,
|
|
2956
|
-
const x = scale.toPixelX(
|
|
2957
|
-
const y = scale.toPixelY(
|
|
3547
|
+
function drawDiamond(ctx, scale, at2, row2) {
|
|
3548
|
+
const x = scale.toPixelX(at2);
|
|
3549
|
+
const y = scale.toPixelY(row2);
|
|
2958
3550
|
ctx.beginPath();
|
|
2959
3551
|
ctx.moveTo(x, y - POINT_RADIUS2);
|
|
2960
3552
|
ctx.lineTo(x + POINT_RADIUS2, y);
|
|
@@ -2962,12 +3554,12 @@ function drawDiamond(ctx, scale, at, row) {
|
|
|
2962
3554
|
ctx.lineTo(x - POINT_RADIUS2, y);
|
|
2963
3555
|
ctx.fill();
|
|
2964
3556
|
}
|
|
2965
|
-
function drawPopulationMean(ctx, scale,
|
|
2966
|
-
if (!Number.isFinite(
|
|
3557
|
+
function drawPopulationMean(ctx, scale, at2) {
|
|
3558
|
+
if (!Number.isFinite(at2)) {
|
|
2967
3559
|
return;
|
|
2968
3560
|
}
|
|
2969
3561
|
const { area } = scale;
|
|
2970
|
-
const x = scale.toPixelX(
|
|
3562
|
+
const x = scale.toPixelX(at2);
|
|
2971
3563
|
ctx.save();
|
|
2972
3564
|
ctx.setLineDash([]);
|
|
2973
3565
|
ctx.strokeStyle = MEAN_LINE_COLOR;
|
|
@@ -2989,6 +3581,12 @@ var SAMPLE_CURVE_COLOR = "rgba(179, 179, 179, 0.5)";
|
|
|
2989
3581
|
var BAR_COLOR = "lightgray";
|
|
2990
3582
|
var CURVE_WIDTH2 = 2;
|
|
2991
3583
|
var SAMPLE_CURVE_WIDTH = 1;
|
|
3584
|
+
var MARK_COLOR = "cornflowerblue";
|
|
3585
|
+
var SAMPLE_MARK_COLOR = "rgba(100, 149, 237, 0.5)";
|
|
3586
|
+
var MARK_WIDTH = 2;
|
|
3587
|
+
var SAMPLE_MARK_WIDTH = 1;
|
|
3588
|
+
var MARK_TICK = 4;
|
|
3589
|
+
var CARET_SIZE = 6;
|
|
2992
3590
|
var LABEL_FONT = "bold 11px sans-serif";
|
|
2993
3591
|
var COUNT_FONT = "11px sans-serif";
|
|
2994
3592
|
var LABEL_LINE_HEIGHT = 13;
|
|
@@ -3017,27 +3615,75 @@ function plotSampling(target, population, options = {}) {
|
|
|
3017
3615
|
theta = mean,
|
|
3018
3616
|
rng = seededRng(Math.floor(Math.random() * 4294967296)),
|
|
3019
3617
|
state = null,
|
|
3020
|
-
densityWindow = "data"
|
|
3618
|
+
densityWindow = "data",
|
|
3619
|
+
mark
|
|
3021
3620
|
} = options;
|
|
3022
3621
|
const window = frozenWindow(population, state);
|
|
3023
3622
|
const grid = densityWindow === "frozen" ? { from: window.min, to: window.max } : {};
|
|
3024
3623
|
const drawable = population.length >= 2;
|
|
3025
3624
|
const draw = drawable ? drawSamples(rng, population, { sampleSize, reps, theta }) : { samples: [], thetas: [] };
|
|
3026
3625
|
const sampleTheta = [...state?.sampleTheta ?? [], ...draw.thetas];
|
|
3027
|
-
clearSurface(ctx, width, height);
|
|
3028
|
-
drawDensityPanel(ctx, width, height, "population", window, drawable ? kernelDensity(population, grid) : null, [], "Population Distribution");
|
|
3029
3626
|
const pooled = draw.samples.flat();
|
|
3030
|
-
|
|
3627
|
+
const marks = markNumbers(mark, theta, pooled, sampleTheta);
|
|
3628
|
+
clearSurface(ctx, width, height);
|
|
3629
|
+
drawDensityPanel(ctx, width, height, "population", window, drawable ? kernelDensity(population, grid) : null, [], "Population Distribution", populationMarks(mark), mark !== undefined && mark.populationValue === null ? `no true ${mark.label}` : null);
|
|
3630
|
+
drawDensityPanel(ctx, width, height, "samples", window, pooled.length >= 2 ? kernelDensity(pooled, grid) : null, draw.samples.filter((sample) => sample.length >= 2).map((sample) => kernelDensity(sample, grid)), "Sample Distribution", sampleMarks(mark, draw.samples, draw.thetas, pooled, marks), null);
|
|
3031
3631
|
const countable = sampleTheta.filter((theta2) => theta2 >= window.min && theta2 <= window.max);
|
|
3032
3632
|
const counted = countable.length > 0 ? histogram(countable) : null;
|
|
3033
|
-
drawStatisticPanel(ctx, width, height, window, counted, sampleTheta.length);
|
|
3633
|
+
drawStatisticPanel(ctx, width, height, window, counted, sampleTheta.length, marks === null || marks.pile === null ? [] : [{ kind: "location", value: marks.pile, center: 0, faint: false }]);
|
|
3034
3634
|
return {
|
|
3035
3635
|
state: { xMin: window.min, xMax: window.max, sampleTheta },
|
|
3036
3636
|
samples: draw.samples,
|
|
3037
3637
|
thetas: draw.thetas,
|
|
3038
|
-
histogram: counted
|
|
3638
|
+
histogram: counted,
|
|
3639
|
+
marks
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
function markNumbers(mark, theta, pooled, sampleTheta) {
|
|
3643
|
+
if (mark === undefined) {
|
|
3644
|
+
return null;
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
population: mark.populationValue,
|
|
3648
|
+
sample: pooled.length > 0 ? theta(pooled) : null,
|
|
3649
|
+
pile: sampleTheta.length > 0 ? mean(sampleTheta) : null
|
|
3039
3650
|
};
|
|
3040
3651
|
}
|
|
3652
|
+
function populationMarks(mark) {
|
|
3653
|
+
if (mark === undefined || mark.populationValue === null) {
|
|
3654
|
+
return [];
|
|
3655
|
+
}
|
|
3656
|
+
if (mark.kind === "location") {
|
|
3657
|
+
return [
|
|
3658
|
+
{ kind: "location", value: mark.populationValue, center: 0, faint: false }
|
|
3659
|
+
];
|
|
3660
|
+
}
|
|
3661
|
+
const center = mark.populationCenter;
|
|
3662
|
+
if (center === null || center === undefined) {
|
|
3663
|
+
return [];
|
|
3664
|
+
}
|
|
3665
|
+
return [{ kind: "spread", value: mark.populationValue, center, faint: false }];
|
|
3666
|
+
}
|
|
3667
|
+
function sampleMarks(mark, samples, thetas, pooled, marks) {
|
|
3668
|
+
if (mark === undefined || marks === null || marks.sample === null) {
|
|
3669
|
+
return [];
|
|
3670
|
+
}
|
|
3671
|
+
const perSample = samples.map((sample, index) => ({
|
|
3672
|
+
kind: mark.kind,
|
|
3673
|
+
value: thetas[index],
|
|
3674
|
+
center: sample.length > 0 ? mean(sample) : 0,
|
|
3675
|
+
faint: true
|
|
3676
|
+
}));
|
|
3677
|
+
return [
|
|
3678
|
+
...perSample,
|
|
3679
|
+
{
|
|
3680
|
+
kind: mark.kind,
|
|
3681
|
+
value: marks.sample,
|
|
3682
|
+
center: pooled.length > 0 ? mean(pooled) : 0,
|
|
3683
|
+
faint: false
|
|
3684
|
+
}
|
|
3685
|
+
];
|
|
3686
|
+
}
|
|
3041
3687
|
function frozenWindow(population, state) {
|
|
3042
3688
|
if (state !== null) {
|
|
3043
3689
|
return { min: state.xMin, max: state.xMax };
|
|
@@ -3048,29 +3694,37 @@ function frozenWindow(population, state) {
|
|
|
3048
3694
|
const [min, max] = extent(population);
|
|
3049
3695
|
return { min, max };
|
|
3050
3696
|
}
|
|
3051
|
-
function drawDensityPanel(ctx, width, height, panel, window, main, perSample, label) {
|
|
3697
|
+
function drawDensityPanel(ctx, width, height, panel, window, main, perSample, label, marks, note) {
|
|
3052
3698
|
const peak = main === null ? 1 : highestOf(main.y);
|
|
3053
3699
|
const scale = samplingScale(width, height, panel, window, peak);
|
|
3054
3700
|
drawAxes(ctx, scale, { frame: false, yAxis: false });
|
|
3055
3701
|
drawLabel(ctx, scale, [label], peak / 2, LABEL_FONT);
|
|
3056
|
-
if (
|
|
3057
|
-
|
|
3702
|
+
if (note !== null) {
|
|
3703
|
+
drawMarkNote(ctx, scale, note, peak / 2);
|
|
3058
3704
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3705
|
+
if (main !== null) {
|
|
3706
|
+
drawCurve2(ctx, scale, main, CURVE_COLOR2, CURVE_WIDTH2, panel === "population");
|
|
3707
|
+
for (const estimate of perSample) {
|
|
3708
|
+
drawCurve2(ctx, scale, estimate, SAMPLE_CURVE_COLOR, SAMPLE_CURVE_WIDTH, false);
|
|
3709
|
+
}
|
|
3710
|
+
if (panel === "samples") {
|
|
3711
|
+
drawCurve2(ctx, scale, main, CURVE_COLOR2, CURVE_WIDTH2, false);
|
|
3712
|
+
}
|
|
3062
3713
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3714
|
+
for (const mark of marks) {
|
|
3715
|
+
drawMark(ctx, scale, window, mark);
|
|
3065
3716
|
}
|
|
3066
3717
|
}
|
|
3067
|
-
function drawStatisticPanel(ctx, width, height, window, counted, drawnSoFar) {
|
|
3718
|
+
function drawStatisticPanel(ctx, width, height, window, counted, drawnSoFar, marks) {
|
|
3068
3719
|
const tallest = counted === null ? 1 : highestOf(counted.counts);
|
|
3069
3720
|
const scale = samplingScale(width, height, "statistic", window, tallest);
|
|
3070
3721
|
drawAxes(ctx, scale, { frame: false, yAxis: false });
|
|
3071
3722
|
if (counted !== null) {
|
|
3072
3723
|
drawBars(ctx, scale, counted);
|
|
3073
3724
|
}
|
|
3725
|
+
for (const mark of marks) {
|
|
3726
|
+
drawMark(ctx, scale, window, mark);
|
|
3727
|
+
}
|
|
3074
3728
|
drawLabel(ctx, scale, ["Sampling Statistic ", `( ${drawnSoFar} )`], tallest / 2, COUNT_FONT);
|
|
3075
3729
|
}
|
|
3076
3730
|
function drawCurve2(ctx, scale, estimate, color, lineWidth, dotted) {
|
|
@@ -3108,7 +3762,91 @@ function drawBars(ctx, scale, counted) {
|
|
|
3108
3762
|
});
|
|
3109
3763
|
ctx.restore();
|
|
3110
3764
|
}
|
|
3111
|
-
function
|
|
3765
|
+
function drawMark(ctx, scale, window, mark) {
|
|
3766
|
+
const color = mark.faint ? SAMPLE_MARK_COLOR : MARK_COLOR;
|
|
3767
|
+
const lineWidth = mark.faint ? SAMPLE_MARK_WIDTH : MARK_WIDTH;
|
|
3768
|
+
if (mark.kind === "location") {
|
|
3769
|
+
if (isInside(window, mark.value)) {
|
|
3770
|
+
drawMarkLine(ctx, scale, mark.value, color, lineWidth);
|
|
3771
|
+
} else {
|
|
3772
|
+
drawCaret(ctx, scale, mark.value > window.max, color);
|
|
3773
|
+
}
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
const low = mark.center - mark.value;
|
|
3777
|
+
const high = mark.center + mark.value;
|
|
3778
|
+
drawMarkSpan(ctx, scale, low, high, color, lineWidth);
|
|
3779
|
+
if (!isInside(window, low)) {
|
|
3780
|
+
drawCaret(ctx, scale, false, color);
|
|
3781
|
+
}
|
|
3782
|
+
if (!isInside(window, high)) {
|
|
3783
|
+
drawCaret(ctx, scale, true, color);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
function isInside(window, value) {
|
|
3787
|
+
return value >= window.min && value <= window.max;
|
|
3788
|
+
}
|
|
3789
|
+
function drawMarkLine(ctx, scale, value, color, lineWidth) {
|
|
3790
|
+
const { area } = scale;
|
|
3791
|
+
const px = scale.toPixelX(value);
|
|
3792
|
+
ctx.save();
|
|
3793
|
+
clipToArea(ctx, area);
|
|
3794
|
+
ctx.setLineDash([]);
|
|
3795
|
+
ctx.strokeStyle = color;
|
|
3796
|
+
ctx.lineWidth = lineWidth;
|
|
3797
|
+
ctx.beginPath();
|
|
3798
|
+
ctx.moveTo(px, area.top);
|
|
3799
|
+
ctx.lineTo(px, area.bottom);
|
|
3800
|
+
ctx.stroke();
|
|
3801
|
+
ctx.restore();
|
|
3802
|
+
}
|
|
3803
|
+
function drawMarkSpan(ctx, scale, low, high, color, lineWidth) {
|
|
3804
|
+
const { area } = scale;
|
|
3805
|
+
const y = (area.top + area.bottom) / 2;
|
|
3806
|
+
const left = scale.toPixelX(low);
|
|
3807
|
+
const right = scale.toPixelX(high);
|
|
3808
|
+
ctx.save();
|
|
3809
|
+
clipToArea(ctx, area);
|
|
3810
|
+
ctx.setLineDash([]);
|
|
3811
|
+
ctx.strokeStyle = color;
|
|
3812
|
+
ctx.lineWidth = lineWidth;
|
|
3813
|
+
ctx.beginPath();
|
|
3814
|
+
ctx.moveTo(left, y);
|
|
3815
|
+
ctx.lineTo(right, y);
|
|
3816
|
+
ctx.moveTo(left, y - MARK_TICK);
|
|
3817
|
+
ctx.lineTo(left, y + MARK_TICK);
|
|
3818
|
+
ctx.moveTo(right, y - MARK_TICK);
|
|
3819
|
+
ctx.lineTo(right, y + MARK_TICK);
|
|
3820
|
+
ctx.stroke();
|
|
3821
|
+
ctx.restore();
|
|
3822
|
+
}
|
|
3823
|
+
function drawCaret(ctx, scale, atRight, color) {
|
|
3824
|
+
const { area } = scale;
|
|
3825
|
+
const y = (area.top + area.bottom) / 2;
|
|
3826
|
+
const tip = atRight ? area.right : area.left;
|
|
3827
|
+
const base = atRight ? tip - CARET_SIZE : tip + CARET_SIZE;
|
|
3828
|
+
ctx.save();
|
|
3829
|
+
clipToArea(ctx, area);
|
|
3830
|
+
ctx.setLineDash([]);
|
|
3831
|
+
ctx.fillStyle = color;
|
|
3832
|
+
ctx.beginPath();
|
|
3833
|
+
ctx.moveTo(tip, y);
|
|
3834
|
+
ctx.lineTo(base, y - CARET_SIZE / 2);
|
|
3835
|
+
ctx.lineTo(base, y + CARET_SIZE / 2);
|
|
3836
|
+
ctx.fill();
|
|
3837
|
+
ctx.restore();
|
|
3838
|
+
}
|
|
3839
|
+
function drawMarkNote(ctx, scale, note, at2) {
|
|
3840
|
+
ctx.save();
|
|
3841
|
+
ctx.setLineDash([]);
|
|
3842
|
+
ctx.fillStyle = MARK_COLOR;
|
|
3843
|
+
ctx.font = COUNT_FONT;
|
|
3844
|
+
ctx.textAlign = "left";
|
|
3845
|
+
ctx.textBaseline = "middle";
|
|
3846
|
+
ctx.fillText(note, scale.area.left, scale.toPixelY(at2) + LABEL_LINE_HEIGHT);
|
|
3847
|
+
ctx.restore();
|
|
3848
|
+
}
|
|
3849
|
+
function drawLabel(ctx, scale, lines, at2, font) {
|
|
3112
3850
|
ctx.save();
|
|
3113
3851
|
ctx.setLineDash([]);
|
|
3114
3852
|
ctx.fillStyle = TEXT_COLOR2;
|
|
@@ -3116,7 +3854,7 @@ function drawLabel(ctx, scale, lines, at, font) {
|
|
|
3116
3854
|
ctx.textAlign = "left";
|
|
3117
3855
|
ctx.textBaseline = "middle";
|
|
3118
3856
|
lines.forEach((line, index) => {
|
|
3119
|
-
ctx.fillText(line, scale.area.left, scale.toPixelY(
|
|
3857
|
+
ctx.fillText(line, scale.area.left, scale.toPixelY(at2) + index * LABEL_LINE_HEIGHT);
|
|
3120
3858
|
});
|
|
3121
3859
|
ctx.restore();
|
|
3122
3860
|
}
|
|
@@ -4191,5 +4929,5 @@ export {
|
|
|
4191
4929
|
DEFAULT_LEAST_SQUARES_TOLERANCE
|
|
4192
4930
|
};
|
|
4193
4931
|
|
|
4194
|
-
//# debugId=
|
|
4932
|
+
//# debugId=6C77280FC4EBBC8264756E2164756E21
|
|
4195
4933
|
//# sourceMappingURL=index.js.map
|