@compstats/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +310 -0
  4. package/dist/3d.d.ts +33 -0
  5. package/dist/3d.d.ts.map +1 -0
  6. package/dist/3d.js +1984 -0
  7. package/dist/3d.js.map +22 -0
  8. package/dist/core/arith.d.ts +133 -0
  9. package/dist/core/arith.d.ts.map +1 -0
  10. package/dist/core/frame.d.ts +98 -0
  11. package/dist/core/frame.d.ts.map +1 -0
  12. package/dist/core/histogram.d.ts +68 -0
  13. package/dist/core/histogram.d.ts.map +1 -0
  14. package/dist/core/kde.d.ts +106 -0
  15. package/dist/core/kde.d.ts.map +1 -0
  16. package/dist/core/logit.d.ts +109 -0
  17. package/dist/core/logit.d.ts.map +1 -0
  18. package/dist/core/matrix.d.ts +135 -0
  19. package/dist/core/matrix.d.ts.map +1 -0
  20. package/dist/core/moderation.d.ts +127 -0
  21. package/dist/core/moderation.d.ts.map +1 -0
  22. package/dist/core/ols.d.ts +76 -0
  23. package/dist/core/ols.d.ts.map +1 -0
  24. package/dist/core/pca.d.ts +99 -0
  25. package/dist/core/pca.d.ts.map +1 -0
  26. package/dist/core/precision.d.ts +10 -0
  27. package/dist/core/precision.d.ts.map +1 -0
  28. package/dist/core/pretty.d.ts +51 -0
  29. package/dist/core/pretty.d.ts.map +1 -0
  30. package/dist/core/regression.d.ts +68 -0
  31. package/dist/core/regression.d.ts.map +1 -0
  32. package/dist/core/rng.d.ts +206 -0
  33. package/dist/core/rng.d.ts.map +1 -0
  34. package/dist/core/sampling.d.ts +157 -0
  35. package/dist/core/sampling.d.ts.map +1 -0
  36. package/dist/core/special.d.ts +92 -0
  37. package/dist/core/special.d.ts.map +1 -0
  38. package/dist/core/tdist.d.ts +53 -0
  39. package/dist/core/tdist.d.ts.map +1 -0
  40. package/dist/core/ttest.d.ts +146 -0
  41. package/dist/core/ttest.d.ts.map +1 -0
  42. package/dist/data/moderationData.d.ts +40 -0
  43. package/dist/data/moderationData.d.ts.map +1 -0
  44. package/dist/data/pcaDegenerate.d.ts +18 -0
  45. package/dist/data/pcaDegenerate.d.ts.map +1 -0
  46. package/dist/index.d.ts +66 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +4195 -0
  49. package/dist/index.js.map +47 -0
  50. package/dist/interactive/controls.d.ts +97 -0
  51. package/dist/interactive/controls.d.ts.map +1 -0
  52. package/dist/interactive/logit.d.ts +57 -0
  53. package/dist/interactive/logit.d.ts.map +1 -0
  54. package/dist/interactive/matrixInverse.d.ts +86 -0
  55. package/dist/interactive/matrixInverse.d.ts.map +1 -0
  56. package/dist/interactive/moderation3d.d.ts +103 -0
  57. package/dist/interactive/moderation3d.d.ts.map +1 -0
  58. package/dist/interactive/pca.d.ts +90 -0
  59. package/dist/interactive/pca.d.ts.map +1 -0
  60. package/dist/interactive/regression.d.ts +57 -0
  61. package/dist/interactive/regression.d.ts.map +1 -0
  62. package/dist/interactive/sampling.d.ts +78 -0
  63. package/dist/interactive/sampling.d.ts.map +1 -0
  64. package/dist/interactive/scatter3d.d.ts +124 -0
  65. package/dist/interactive/scatter3d.d.ts.map +1 -0
  66. package/dist/interactive/tTest.d.ts +69 -0
  67. package/dist/interactive/tTest.d.ts.map +1 -0
  68. package/dist/interactive/target.d.ts +132 -0
  69. package/dist/interactive/target.d.ts.map +1 -0
  70. package/dist/plot/axes.d.ts +128 -0
  71. package/dist/plot/axes.d.ts.map +1 -0
  72. package/dist/plot/draw.d.ts +46 -0
  73. package/dist/plot/draw.d.ts.map +1 -0
  74. package/dist/plot/format.d.ts +33 -0
  75. package/dist/plot/format.d.ts.map +1 -0
  76. package/dist/plot/logit.d.ts +68 -0
  77. package/dist/plot/logit.d.ts.map +1 -0
  78. package/dist/plot/matrixInverse.d.ts +62 -0
  79. package/dist/plot/matrixInverse.d.ts.map +1 -0
  80. package/dist/plot/moderation3d.d.ts +135 -0
  81. package/dist/plot/moderation3d.d.ts.map +1 -0
  82. package/dist/plot/pca.d.ts +79 -0
  83. package/dist/plot/pca.d.ts.map +1 -0
  84. package/dist/plot/plotly.d.ts +209 -0
  85. package/dist/plot/plotly.d.ts.map +1 -0
  86. package/dist/plot/regression.d.ts +52 -0
  87. package/dist/plot/regression.d.ts.map +1 -0
  88. package/dist/plot/sampleCi.d.ts +82 -0
  89. package/dist/plot/sampleCi.d.ts.map +1 -0
  90. package/dist/plot/sampling.d.ts +159 -0
  91. package/dist/plot/sampling.d.ts.map +1 -0
  92. package/dist/plot/scatter3d.d.ts +164 -0
  93. package/dist/plot/scatter3d.d.ts.map +1 -0
  94. package/dist/plot/tTest.d.ts +49 -0
  95. package/dist/plot/tTest.d.ts.map +1 -0
  96. package/dist/plot/target.d.ts +55 -0
  97. package/dist/plot/target.d.ts.map +1 -0
  98. package/package.json +71 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Column-keyed data frames, and the checks R gets for free.
3
+ *
4
+ * The 3D functions of the R package take an R data frame, which guarantees
5
+ * two things a JavaScript object does not: every column holds one type, and
6
+ * every column has the same length. `moderation.ts` and the scatter3d module
7
+ * both need those guarantees, so the questions are asked in one place and
8
+ * answered the same way for both.
9
+ *
10
+ * The port follows `../compstatslib/R/scatter3d_helpers.R`:
11
+ *
12
+ * ```r
13
+ * scatter3d_numeric_cols <- function(data) {
14
+ * names(data)[vapply(data, is.numeric, logical(1))]
15
+ * }
16
+ * ```
17
+ *
18
+ * with two departures, both forced by the language and both pinned by tests.
19
+ * R reads a vector's declared type, so `is.numeric(numeric(0))` is TRUE and a
20
+ * column of mixed types cannot exist. A JavaScript array declares nothing, so
21
+ * this module reads the values: **a column is numeric when it holds at least
22
+ * one value and every value is a number.** An empty column therefore is not
23
+ * numeric — it offers no evidence either way, and an empty axis draws nothing
24
+ * — and a column of numbers with one string in it is not numeric either,
25
+ * because fitting it would give `NaN` for every coefficient.
26
+ */
27
+ /**
28
+ * One column of a data frame.
29
+ *
30
+ * Numeric columns carry the statistics. The other two types are here because
31
+ * `plot_scatter3d()` accepts a categorical column for `color`, which R allows
32
+ * to be a factor, a character vector, or a logical vector.
33
+ */
34
+ export type Column = readonly number[] | readonly string[] | readonly boolean[];
35
+ /**
36
+ * A data frame: named columns of equal length.
37
+ *
38
+ * The equal length is a rule, not a type. `frameRows` enforces it.
39
+ */
40
+ export type DataFrame = {
41
+ readonly [name: string]: Column;
42
+ };
43
+ /**
44
+ * Report whether a column holds numbers, and narrow it when it does.
45
+ *
46
+ * @param column The column to inspect.
47
+ * @returns True when the column has at least one value and every value is a
48
+ * number. `NaN` counts as a number, as R's missing values do.
49
+ */
50
+ export declare function isNumericColumn(column: Column): column is readonly number[];
51
+ /**
52
+ * Name the numeric columns, in the order the frame declares them.
53
+ *
54
+ * This is R's `scatter3d_numeric_cols()`. The order matters: the scatter3d
55
+ * default takes the first three names this returns.
56
+ *
57
+ * @param data The frame to inspect.
58
+ * @returns The names of the numeric columns, in insertion order.
59
+ */
60
+ export declare function numericColumns(data: DataFrame): string[];
61
+ /**
62
+ * Refuse a frame that cannot fill three numeric axes.
63
+ *
64
+ * This is R's `scatter3d_require_3_numeric()`, which both the plot and the
65
+ * gadget call with their own name, so that the message says which function
66
+ * the caller reached.
67
+ *
68
+ * @param numeric The numeric column names, from `numericColumns`.
69
+ * @param caller The name to print, such as `plotScatter3d`.
70
+ * @throws RangeError If fewer than three names were given.
71
+ */
72
+ export declare function requireThreeNumericColumns(numeric: readonly string[], caller: string): void;
73
+ /**
74
+ * Return the number of rows, and refuse a frame that has no single answer.
75
+ *
76
+ * @param data The frame to measure.
77
+ * @returns The shared length of the columns. A frame with no columns has no
78
+ * rows.
79
+ * @throws RangeError If two columns have different lengths. An R data frame
80
+ * cannot be built that way, so nothing downstream is written to survive it.
81
+ */
82
+ export declare function frameRows(data: DataFrame): number;
83
+ /**
84
+ * Read one numeric column, or explain why it cannot be used.
85
+ *
86
+ * The wording follows R's own, which names both the column and the argument
87
+ * it arrived through: `Column "b" (passed as \`x\`) is not in \`data\`.`
88
+ *
89
+ * @param data The frame to read.
90
+ * @param name The column name the caller asked for.
91
+ * @param role The option that carried the name, such as `iv` or `mod`. It
92
+ * appears in the error, so the caller learns which argument is wrong.
93
+ * @returns The column.
94
+ * @throws RangeError If the frame has no such column, or the column is not
95
+ * numeric.
96
+ */
97
+ export declare function requireNumericColumn(data: DataFrame, name: string, role: string): readonly number[];
98
+ //# sourceMappingURL=frame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../../src/core/frame.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,OAAO,EAAE,CAAC;AAEhF;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,SAAS,MAAM,EAAE,CAE3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,CAIxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,MAAM,EAAE,MAAM,GACb,IAAI,CAON;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAiBjD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,SAAS,MAAM,EAAE,CAenB"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Counts of values per cell — R's `hist(plot = FALSE)` with its defaults.
3
+ *
4
+ * `plot_sampling()` draws its third panel from this: it accumulates one
5
+ * statistic per sample and calls `hist()` on the running collection, then
6
+ * reads `counts` back to place the panel's label. Verified against R 4.5.3 in
7
+ * `histogram.test.ts`.
8
+ *
9
+ * Three rules carry the behavior, and each one is easy to get subtly wrong:
10
+ *
11
+ * 1. **How many cells.** Sturges' rule, `ceiling(log2(n) + 1)`, is only a
12
+ * suggestion. It goes to `rPretty`, which returns round edges covering the
13
+ * data — often a different count.
14
+ * 2. **Which cell a value belongs to.** Cells are closed on the right,
15
+ * `(edge[k], edge[k+1]]`, so a value sitting on an edge counts into the
16
+ * cell below it. The lowest edge is the exception: nothing sits below it,
17
+ * so it is opened to admit the smallest value.
18
+ * 3. **Values that land on an edge.** Rounding leaves a value that ought to
19
+ * sit on an edge a hair to one side of it. R nudges the edges by a
20
+ * millionth of a cell before counting — outward at the bottom, upward
21
+ * everywhere else — so such a value still lands where it belongs. The
22
+ * reported edges are the unnudged ones.
23
+ */
24
+ /** What the caller may change. R's drawing arguments are not ported. */
25
+ export interface HistogramOptions {
26
+ /**
27
+ * How to place the cell edges.
28
+ *
29
+ * A number asks for about that many cells, which `rPretty` rounds off; an
30
+ * array gives the edges outright. The default is Sturges' rule. R takes the
31
+ * same two forms in one argument, but cannot tell a one-edge array from a
32
+ * request for one cell — this port reads an array as edges, always.
33
+ */
34
+ readonly breaks?: number | readonly number[];
35
+ }
36
+ /** The counted histogram, in the shape of R's `histogram` object. */
37
+ export interface Histogram {
38
+ /** The cell edges, in increasing order. One more than there are cells. */
39
+ readonly breaks: readonly number[];
40
+ /** How many values fell in each cell. */
41
+ readonly counts: readonly number[];
42
+ /** The midpoint of each cell, R's `mids`. */
43
+ readonly mids: readonly number[];
44
+ }
45
+ /**
46
+ * Suggest a cell count by Sturges' rule — R's `nclass.Sturges`.
47
+ *
48
+ * @param values The observations. Only how many there are matters.
49
+ * @returns `ceiling(log2(n) + 1)`, or -Infinity for no values. R returns -Inf
50
+ * there too, which is what makes its `hist()` stop.
51
+ */
52
+ export declare function nclassSturges(values: readonly number[]): number;
53
+ /**
54
+ * Count the values into cells.
55
+ *
56
+ * Values that are not finite are dropped first, as R does — including NaN,
57
+ * which R reads as a missing value and drops here. Note the difference from
58
+ * `kernelDensity`, which refuses NaN, because R's `density()` refuses it.
59
+ *
60
+ * @param values The observations.
61
+ * @param options How to place the cell edges.
62
+ * @returns The edges, the count in each cell, and the cell midpoints.
63
+ * @throws RangeError If no value is finite, if a requested cell count is
64
+ * below one or fractional, if fewer than two edges are given, or if the
65
+ * given edges leave a value uncounted. R stops in all of those cases too.
66
+ */
67
+ export declare function histogram(values: readonly number[], options?: HistogramOptions): Histogram;
68
+ //# sourceMappingURL=histogram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"histogram.d.ts","sourceRoot":"","sources":["../../src/core/histogram.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,yCAAyC;IACzC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,GAAE,gBAAqB,GAC7B,SAAS,CA2BX"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Gaussian kernel density estimate — R's `density()` with its defaults.
3
+ *
4
+ * `plot_sampling()` in the R package draws two of these panels: one over the
5
+ * population and one over the pooled samples. Both calls are `density(x)` with
6
+ * no other argument, so this module ports that default path: the Gaussian
7
+ * kernel, the `nrd0` bandwidth, a 512-point output grid, `cut = 3`, and
8
+ * `ext = 4`. It also ports R's `from` and `to` arguments, which freeze the
9
+ * ends of the reported window — the sampling plot uses them to keep one axis
10
+ * across redraws. Verified against R 4.5.3 in `kde.test.ts`.
11
+ *
12
+ * The estimate is a convolution, and R computes it with an FFT rather than by
13
+ * summing a kernel over every point. This port copies that: the cost is
14
+ * linear in the count of values plus a fixed 1024-point transform, so pooling
15
+ * a million sampled values stays fast. The steps, in R's own order:
16
+ *
17
+ * 1. Pick the bandwidth with `bwNrd0`.
18
+ * 2. Set the reported window to `range(x) ± 3 * bw` (`cut = 3`), and the
19
+ * wider working window to `range(x) ± 7 * bw` (a further `ext = 4`).
20
+ * 3. Spread the mass of the values over 512 equally spaced points of the
21
+ * working window, splitting each value between its two neighbors.
22
+ * 4. Convolve that with a Gaussian of standard deviation `bw`, using a
23
+ * 1024-point FFT, and clamp negative rounding noise to zero.
24
+ * 5. Interpolate the result back onto the reported window.
25
+ *
26
+ * The two windows are different widths and must not be confused. A port that
27
+ * runs the FFT over the reported window gives a visibly narrower curve.
28
+ */
29
+ /** What the caller may change. R's other arguments are not ported. */
30
+ export interface KernelDensityOptions {
31
+ /**
32
+ * The bandwidth, the standard deviation of the kernel.
33
+ *
34
+ * The default is `bwNrd0(values)`, R's `bw = "nrd0"`. Giving the bandwidth
35
+ * skips that selection, and with it the rule that the selection needs two
36
+ * values — R does the same.
37
+ */
38
+ readonly bw?: number;
39
+ /**
40
+ * The low end of the reported window, R's `from`.
41
+ *
42
+ * The default is `min(x) - 3 * bw`, R's `cut = 3`. The working window still
43
+ * reaches 4 bandwidths further out, and `binDist` drops the mass beyond it,
44
+ * as R's `BinDist` does. So a window narrower than the data cuts the tails
45
+ * off, and a wider one pads the curve with near-zero density — the frozen
46
+ * axis the sampling plot needs across redraws.
47
+ */
48
+ readonly from?: number;
49
+ /** The high end of the reported window, R's `to`. Same rules as `from`. */
50
+ readonly to?: number;
51
+ }
52
+ /** The curve, in the shape of R's `density` object. */
53
+ export interface KernelDensityEstimate {
54
+ /**
55
+ * The 512 grid points. They run from `min(x) - 3 * bw` to
56
+ * `max(x) + 3 * bw`, unless the caller froze an end with `from` or `to`.
57
+ */
58
+ readonly x: readonly number[];
59
+ /** The density at each grid point. Never negative. */
60
+ readonly y: readonly number[];
61
+ /** The bandwidth the estimate used. */
62
+ readonly bw: number;
63
+ /**
64
+ * The count of values the caller gave, before any were dropped. R reports
65
+ * the same count, and scales the curve by the share that it kept.
66
+ */
67
+ readonly n: number;
68
+ }
69
+ /**
70
+ * Pick a bandwidth by Silverman's rule, R's `bw.nrd0()`.
71
+ *
72
+ * The rule is `0.9 * min(sd, IQR / 1.34) * n^(-1/5)`. When that smaller of
73
+ * the two spreads is 0 — every value the same, or every value repeated past
74
+ * the quartiles — R walks a chain of fallbacks and takes the first that is
75
+ * not 0: the standard deviation, then the size of the first value, then 1.
76
+ * This keeps the rule from returning a bandwidth of 0, which would leave the
77
+ * estimate undrawable.
78
+ *
79
+ * @param values The observations. All must be finite.
80
+ * @returns The bandwidth. Always positive.
81
+ * @throws RangeError Below two values. R stops with "need at least 2 data
82
+ * points", since the spread of one value has no meaning.
83
+ */
84
+ export declare function bwNrd0(values: readonly number[]): number;
85
+ /**
86
+ * Estimate the density of the values.
87
+ *
88
+ * R takes a matrix here and flattens it. This port takes the flat array, so a
89
+ * caller that pools several samples flattens them first. R flattens a matrix
90
+ * column by column, which for `plot_sampling()` means one whole sample after
91
+ * another.
92
+ *
93
+ * Values that are infinite are dropped, and the curve is scaled by the share
94
+ * of values that remain, as in R. A value that is NaN is refused: R reads it
95
+ * as a missing value and stops.
96
+ *
97
+ * @param values The observations.
98
+ * @param options The bandwidth and the window ends, if the caller sets them.
99
+ * @returns The grid, the density on it, the bandwidth, and the count of
100
+ * values given.
101
+ * @throws RangeError If the bandwidth has to be selected from fewer than two
102
+ * values, if a given bandwidth is not positive and finite, if a given
103
+ * window end is not finite, if a value is NaN, or if no value is finite.
104
+ */
105
+ export declare function kernelDensity(values: readonly number[], options?: KernelDensityOptions): KernelDensityEstimate;
106
+ //# sourceMappingURL=kde.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kde.d.ts","sourceRoot":"","sources":["../../src/core/kde.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAmBH,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAWxD;AAkBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,GAAE,oBAAyB,GACjC,qBAAqB,CAiCvB"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Logistic regression, the statistics half of `plot_logit()` in the R package.
3
+ *
4
+ * R gets these numbers from `glm(formula, family = binomial)`, which fits by
5
+ * iteratively reweighted least squares: each pass builds a working response
6
+ * and a set of weights from the current fit, then solves an ordinary weighted
7
+ * least-squares problem. This module follows `stats::glm.fit` step for step,
8
+ * down to its starting values, its convergence test, and the two clamped link
9
+ * functions R implements in C. Verified against R in `logit.test.ts`.
10
+ *
11
+ * The clamps are not a detail. R's `linkinv` pins a linear predictor beyond
12
+ * ±30 to a probability one machine epsilon away from 0 or 1, which is what
13
+ * keeps a perfectly separated fit — the classroom case where every low x is a
14
+ * 0 and every high x is a 1 — producing finite numbers instead of dividing by
15
+ * zero.
16
+ */
17
+ import type { Point } from "./regression";
18
+ /** The convergence threshold and iteration cap of R's `glm.control()`. */
19
+ export declare const DEFAULT_LOGIT_EPSILON = 1e-8;
20
+ export declare const DEFAULT_LOGIT_MAX_ITERATIONS = 25;
21
+ export interface LogitOptions {
22
+ /**
23
+ * How small the relative change in deviance must be to stop. R's
24
+ * `glm.control(epsilon =)`.
25
+ */
26
+ readonly epsilon?: number;
27
+ /** How many IRLS passes to allow. R's `glm.control(maxit =)`. */
28
+ readonly maxIterations?: number;
29
+ }
30
+ /**
31
+ * The result of a fit.
32
+ *
33
+ * A `null` slope is the equivalent of R's `NA`: with no variation in x the
34
+ * QR aliases the column and R reports "1 not defined because of
35
+ * singularities". `linearRegression` uses the same convention.
36
+ */
37
+ export interface LogitFit {
38
+ /** The linear predictor at x = 0. */
39
+ readonly intercept: number;
40
+ /** The change in the log odds for each unit of x. Null if x is constant. */
41
+ readonly slope: number | null;
42
+ /**
43
+ * The fitted probability of each point, in input order. NaN where the
44
+ * point was dropped for a missing value.
45
+ */
46
+ readonly fitted: readonly number[];
47
+ /**
48
+ * The log odds of each point, in input order. NaN where the point was
49
+ * dropped for a missing value.
50
+ */
51
+ readonly linearPredictors: readonly number[];
52
+ /** Residual deviance of the fitted model. */
53
+ readonly deviance: number;
54
+ /** Deviance of the intercept-only model. */
55
+ readonly nullDeviance: number;
56
+ /** Akaike information criterion, the fit statistic `plot_logit` displays. */
57
+ readonly aic: number;
58
+ /** How many coefficients the fit could identify. */
59
+ readonly rank: number;
60
+ /** How many IRLS passes ran. */
61
+ readonly iterations: number;
62
+ /** Whether the deviance settled before the iteration cap. */
63
+ readonly converged: boolean;
64
+ /**
65
+ * Whether any fitted probability landed within `10 * Number.EPSILON` of 0
66
+ * or 1.
67
+ *
68
+ * This is the condition behind R's "fitted probabilities numerically 0 or 1
69
+ * occurred" warning, which `plot_logit` suppresses. It reports separation:
70
+ * the data admit no maximum likelihood estimate and only the iteration cap
71
+ * stopped the coefficients from growing. A browser library cannot warn, so
72
+ * the fit reports the condition and lets the caller decide.
73
+ */
74
+ readonly saturated: boolean;
75
+ }
76
+ /**
77
+ * Fit the log odds of y as a straight line in x.
78
+ *
79
+ * With no variation in x the fit reports an intercept and no slope, the way
80
+ * R's aliasing does. A single point is that same case: R fits the intercept
81
+ * and reports the slope as `NA`. `plot_logit` never reaches either — it draws
82
+ * the points and returns before fitting fewer than two of them — so the guard
83
+ * belongs to the plot layer and this function reports the honest degenerate
84
+ * fit.
85
+ *
86
+ * A point with a non-finite coordinate is dropped before fitting, R's
87
+ * `na.action = na.omit`; its fitted probability and linear predictor report
88
+ * NaN, keeping input order (R's `na.exclude` padding, as `moderationSurface`
89
+ * does). The 0-or-1 rule below applies to the rows that remain: R's
90
+ * `na.omit` removes an incomplete row before `glm()` ever sees its outcome.
91
+ *
92
+ * @param points The observations. Each complete y must be 0 or 1. The
93
+ * function does not modify them.
94
+ * @param options The convergence controls.
95
+ * @returns The fit, or null if no point is complete.
96
+ * @throws RangeError if a complete row's y is not 0 or 1. R accepts any y in
97
+ * [0, 1] for the binomial family; this port does not, because the points
98
+ * come from clicks and because the AIC below assumes a 0/1 outcome.
99
+ */
100
+ export declare function logisticRegression(points: readonly Point[], options?: LogitOptions): LogitFit | null;
101
+ /**
102
+ * The fitted probability at one x.
103
+ *
104
+ * `plot_logit` draws its curve from 500 of these. An aliased slope holds the
105
+ * curve flat, which is what R's `predict()` does with an `NA` coefficient
106
+ * dropped from the model.
107
+ */
108
+ export declare function predictLogit(fit: LogitFit, x: number): number;
109
+ //# sourceMappingURL=logit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logit.d.ts","sourceRoot":"","sources":["../../src/core/logit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,0EAA0E;AAC1E,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAOD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,OAAO,GAAE,YAAiB,GACzB,QAAQ,GAAG,IAAI,CA0HjB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The determinant and the inverse of a 2x2 matrix.
3
+ *
4
+ * This is the arithmetic half of `plot_matrix_inverse()` in the R package,
5
+ * which builds `A <- matrix(c(x1, y1, x2, y2), nrow = 2)` and calls
6
+ * `solve(A)` before it draws anything. Verified against R in
7
+ * `matrix.test.ts`.
8
+ *
9
+ * Four things are worth knowing before reading the code.
10
+ *
11
+ * **The matrix is two columns.** R fills a matrix column by column, so
12
+ * `(x1, y1)` is column 1 and `(x2, y2)` is column 2. The R plot draws each
13
+ * column as an arrow from the origin, and the parallelogram they span has the
14
+ * determinant as its area. The field names are R's argument names, so nothing
15
+ * has to be transposed on the way in or out.
16
+ *
17
+ * **R's answers come from a factorization, not from the closed form.** Both
18
+ * `det()` and `solve()` factor the matrix first (LAPACK `dgetrf`, partial
19
+ * pivoting), and `det()` then exponentiates a sum of logarithms. The closed
20
+ * forms `x1*y2 - x2*y1` and `[[y2, -x2], [-y1, x1]]/det` give different last
21
+ * bits: for the default matrix of the interactive gadget the closed form gives
22
+ * exactly -3 where R gives -2.9999999999999996. This module follows the
23
+ * factorization, so its numbers are R's.
24
+ *
25
+ * **Two operations there are not the obvious ones.** The factorization scales
26
+ * the column below the pivot by `1 / pivot` instead of dividing by the pivot,
27
+ * and it rounds the rank-one update once (see `fusedMultiplyAdd`). Both
28
+ * change what a near-singular matrix reports: dividing turns R's
29
+ * "computationally singular" into "exactly singular" for four equal entries at
30
+ * `1e-5`, and rounding twice moves a cancelled determinant by a factor of two.
31
+ *
32
+ * **A singular matrix is data, not an error.** R stops with an error, which
33
+ * ends the R function before it draws. A component that redraws while a slider
34
+ * moves cannot throw, so this module reports the singularity in its result and
35
+ * lets the caller decide. The two kinds R distinguishes are kept apart,
36
+ * because they describe different things: an exactly zero pivot, and a matrix
37
+ * that is invertible on paper but too ill-conditioned to invert in doubles.
38
+ */
39
+ /**
40
+ * A 2x2 matrix, held as R's `plot_matrix_inverse` arguments.
41
+ *
42
+ * `(x1, y1)` is the first column and `(x2, y2)` is the second, which is how
43
+ * `matrix(c(x1, y1, x2, y2), nrow = 2)` fills it. Written as a table of rows,
44
+ * the matrix is `[[x1, x2], [y1, y2]]`.
45
+ */
46
+ export interface Matrix2 {
47
+ /** Row 1 of column 1. R's `A[1,1]`. */
48
+ readonly x1: number;
49
+ /** Row 2 of column 1. R's `A[2,1]`. */
50
+ readonly y1: number;
51
+ /** Row 1 of column 2. R's `A[1,2]`. */
52
+ readonly x2: number;
53
+ /** Row 2 of column 2. R's `A[2,2]`. */
54
+ readonly y2: number;
55
+ }
56
+ /**
57
+ * Why a matrix has no inverse, in the two kinds R reports.
58
+ *
59
+ * `"exact"` is R's "Lapack routine dgesv: system is exactly singular:
60
+ * U[i,i] = 0": the factorization found a pivot that is the literal value zero.
61
+ * `"computational"` is R's "system is computationally singular: reciprocal
62
+ * condition number = ...": the factorization completed, but the condition
63
+ * number is below R's tolerance of one machine epsilon.
64
+ */
65
+ export type Singularity = "exact" | "computational";
66
+ /** What `invertMatrix` reports about one matrix. */
67
+ export interface MatrixInversion {
68
+ /**
69
+ * The determinant, as R's `det()` computes it. It is exactly zero for an
70
+ * exactly singular matrix, and it is not a test for singularity: a matrix
71
+ * with a determinant of `2e-16` can still be too ill-conditioned to invert,
72
+ * and one with a determinant of `0.01` inverts without trouble.
73
+ */
74
+ readonly determinant: number;
75
+ /**
76
+ * The inverse, or null if R's `solve()` would have stopped with an error.
77
+ * The port could report the huge and meaningless numbers that a
78
+ * computationally singular matrix produces, and does not: a null says the
79
+ * same thing as R's error, in a form a caller can branch on.
80
+ */
81
+ readonly inverse: Matrix2 | null;
82
+ /** Which kind of singularity, or null if the matrix inverts. */
83
+ readonly singularity: Singularity | null;
84
+ /**
85
+ * The reciprocal condition number in the one-norm:
86
+ * `1 / (norm(A) * norm(inverse))`. R's bar for a usable matrix is
87
+ * `rcond >= Number.EPSILON`, and this port keeps that bar.
88
+ *
89
+ * R's `solve()` reads an estimate of the same number from LAPACK's
90
+ * `dgecon` rather than computing it. Near the bar the two agree to the last
91
+ * bit — every fixture at the edge of singularity does — while for a well
92
+ * conditioned matrix R's estimate can be up to about twice this value,
93
+ * because the estimator only bounds the norm of the inverse from below. The
94
+ * two never disagreed about a matrix over a sweep of 20000 slider settings.
95
+ *
96
+ * It is 0 for an exactly singular matrix. R computes no condition number
97
+ * there — the factorization has already failed — and zero is the limit.
98
+ */
99
+ readonly rcond: number;
100
+ /**
101
+ * Which pivot was exactly zero: the `i` of R's message `U[i,i] = 0`. It is
102
+ * 1 only when the whole first column is zero, and null unless the
103
+ * singularity is exact.
104
+ */
105
+ readonly zeroPivot: 1 | 2 | null;
106
+ }
107
+ /**
108
+ * Return the determinant, as R's `det()` reports it.
109
+ *
110
+ * R factors the matrix and then computes `sign * exp(sum(log(abs(pivot))))`,
111
+ * so an integer determinant does not always come back as an integer. A
112
+ * singular matrix gives a positive zero, whatever its rows: R leaves the sign
113
+ * of the interchange unread on that path.
114
+ *
115
+ * @param matrix The matrix. The function does not modify it.
116
+ * @returns The determinant. It is NaN if an entry is NaN, and it overflows to
117
+ * an infinity for entries large enough, both as R's does.
118
+ */
119
+ export declare function determinant(matrix: Matrix2): number;
120
+ /**
121
+ * Invert the matrix, and report what R's `solve()` would have done with it.
122
+ *
123
+ * The function does not throw. An entry of NaN spreads into every number of
124
+ * the report, which is what R's `solve()` does with it too. An infinite entry
125
+ * gives an infinite norm and so a condition number of zero, and the report
126
+ * says the matrix is computationally singular. R returns an inverse of zeros
127
+ * there instead, but only because its condition test cannot judge a matrix of
128
+ * infinite norm: R's own `rcond()` stops with an error on the same matrix.
129
+ *
130
+ * @param matrix The matrix. The function does not modify it.
131
+ * @returns The inverse and the determinant, or the kind of singularity that
132
+ * stops the matrix from having an inverse.
133
+ */
134
+ export declare function invertMatrix(matrix: Matrix2): MatrixInversion;
135
+ //# sourceMappingURL=matrix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matrix.d.ts","sourceRoot":"","sources":["../../src/core/matrix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAYH;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,eAAe,CAAC;AAEpD,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IACzC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAwC7D"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The fitted surface of a moderated regression.
3
+ *
4
+ * This is the statistics half of `plot_moderation_3d()` in the R package,
5
+ * which fits `lm(formula, data)`, predicts over a 15 by 15 grid of the IV and
6
+ * the moderator, and hands the grid to `lattice::wireframe()`. Verified
7
+ * against R in `moderation.test.ts`.
8
+ *
9
+ * Four things are worth knowing before reading the code.
10
+ *
11
+ * **Columns, not a formula.** R names the model with `y ~ x * z`. TypeScript
12
+ * has no such notation, so the model arrives as column names, per CLAUDE.md:
13
+ * an outcome, an IV, a moderator, an optional list of controls, and a flag
14
+ * for the interaction. The three models the fixtures pin are
15
+ * `{outcome: "y", iv: "x", mod: "z"}` (R's `y ~ x * z`), the same with
16
+ * `interaction: false` (`y ~ x + z`), and the same with `controls: ["w"]`
17
+ * (`y ~ x + z + w + x:z`).
18
+ *
19
+ * **The design follows R's model matrix, not the option order.** R's
20
+ * `model.matrix` puts every main effect before any interaction, whatever
21
+ * order the formula was typed in, so `y ~ x + z + w + x:z` gives the columns
22
+ * `(Intercept), x, z, w, x:z`. This module builds them in that order, which
23
+ * is what lets a caller read the coefficients next to R's.
24
+ *
25
+ * **Controls are held at their mean, and only numbers are accepted.** R's
26
+ * `hold_value()` also handles factors (first level), characters (first in
27
+ * sort order) and logicals (always FALSE). The bundled data has no such
28
+ * column and no doc example uses one, so this port supports the numeric
29
+ * branch alone and refuses the rest, rather than shipping three rules nothing
30
+ * exercises. The other rules are recorded in
31
+ * `.claude/plans/moderation-fixtures.md` section 4 if they are ever wanted.
32
+ *
33
+ * **Missing values leave the fit, as R's do.** `lm()` drops incomplete rows
34
+ * through `na.omit` and `hold_value()` averages with `na.rm = TRUE`; this
35
+ * port does the same, with NaN standing in for `NA` and any non-finite value
36
+ * counting as missing. The details — the `na.exclude`-style NaN padding of
37
+ * `fitted` and `residuals`, the one departure on `zlim` — are on
38
+ * `moderationSurface` itself.
39
+ */
40
+ import { type DataFrame } from "./frame";
41
+ /** Which columns make the model. */
42
+ export interface ModerationOptions {
43
+ /** The column to predict — the vertical axis of the surface. */
44
+ readonly outcome: string;
45
+ /** The predictor on the first horizontal axis. */
46
+ readonly iv: string;
47
+ /** The predictor on the second horizontal axis. */
48
+ readonly mod: string;
49
+ /**
50
+ * Whether the model carries the IV by moderator product. True by default,
51
+ * which is R's `y ~ x * z`. False gives R's additive `y ~ x + z`, whose
52
+ * surface is a plane with no twist.
53
+ */
54
+ readonly interaction?: boolean;
55
+ /**
56
+ * Further predictors to fit but not to plot. Each is held at its own mean
57
+ * over the grid, R's `hold_value()` for a numeric column. Empty by default.
58
+ */
59
+ readonly controls?: readonly string[];
60
+ }
61
+ /** One fitted term, named as R names it. */
62
+ export interface ModerationTerm {
63
+ /**
64
+ * R's coefficient name: `(Intercept)`, a column name, or `iv:mod` for the
65
+ * product term.
66
+ */
67
+ readonly name: string;
68
+ /**
69
+ * The coefficient, or null where R reports `NA` — a column the fit could
70
+ * not tell apart from the columns before it.
71
+ */
72
+ readonly value: number | null;
73
+ }
74
+ /** A fitted model and the surface it predicts. */
75
+ export interface ModerationSurface {
76
+ /** The fitted terms, in R's model-matrix order. */
77
+ readonly coefficients: readonly ModerationTerm[];
78
+ /**
79
+ * The fitted outcome of each data row, in input order. NaN where the row
80
+ * was dropped for a missing value, as R's `na.exclude` pads.
81
+ */
82
+ readonly fitted: readonly number[];
83
+ /**
84
+ * The outcome minus the fit, of each data row, in input order. NaN where
85
+ * the row was dropped for a missing value.
86
+ */
87
+ readonly residuals: readonly number[];
88
+ /** The 15 IV values of the grid, from the column's minimum to its maximum. */
89
+ readonly ivValues: readonly number[];
90
+ /** The 15 moderator values of the grid, over the same span. */
91
+ readonly modValues: readonly number[];
92
+ /**
93
+ * The 225 predicted outcomes, **with the IV varying fastest**: index
94
+ * `j * 15 + i` holds the prediction at `ivValues[i]` and `modValues[j]`.
95
+ * This is the row order of R's `expand.grid(seq_iv, seq_mod)`.
96
+ */
97
+ readonly predictions: readonly number[];
98
+ /**
99
+ * The vertical range to draw, as `[low, high]`: the range of the observed
100
+ * outcome together with the range of the surface. Which of the two reaches
101
+ * further depends on the model — an interaction usually swings the surface
102
+ * past the data at the corners of the grid, while a plane stays inside it.
103
+ */
104
+ readonly zlim: readonly [number, number];
105
+ /** The value each control is held at over the grid: its mean. */
106
+ readonly holds: Readonly<Record<string, number>>;
107
+ }
108
+ /**
109
+ * Fit the model and predict its surface.
110
+ *
111
+ * Rows with a missing (non-finite) value in any model column are dropped
112
+ * before fitting, R's `na.action = na.omit`; their fitted values and
113
+ * residuals report NaN, keeping input order. The grid and zlim span the
114
+ * finite values of their columns, and a control is held at its finite mean —
115
+ * R's `hold_value()` with `na.rm = TRUE`. (R itself computes zlim with no
116
+ * `na.rm` and fails on a missing outcome; the port draws what it can fit, a
117
+ * stated departure.)
118
+ *
119
+ * @param data The frame holding every column the options name.
120
+ * @param options Which column plays which part in the model.
121
+ * @returns The fit, the grid, the surface, and the vertical range.
122
+ * @throws RangeError If a named column is absent, empty, or not numeric, if
123
+ * the IV and the moderator are the same column, if a control repeats
124
+ * another named column, if the frame is ragged, or if no row is complete.
125
+ */
126
+ export declare function moderationSurface(data: DataFrame, options: ModerationOptions): ModerationSurface;
127
+ //# sourceMappingURL=moderation.d.ts.map