@crewhaus/tool-table 0.7.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/README.md +187 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +712 -0
- package/dist/lib/drift.d.ts +185 -0
- package/dist/lib/drift.js +648 -0
- package/dist/lib/profile.d.ts +180 -0
- package/dist/lib/profile.js +332 -0
- package/dist/lib/reconcile.d.ts +123 -0
- package/dist/lib/reconcile.js +336 -0
- package/dist/lib/reshape.d.ts +79 -0
- package/dist/lib/reshape.js +176 -0
- package/dist/paths.d.ts +42 -0
- package/dist/paths.js +158 -0
- package/package.json +47 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Has the data moved since the baseline?
|
|
3
|
+
*
|
|
4
|
+
* Four things go wrong between yesterday's export and today's, and only one
|
|
5
|
+
* of them is visible in a row count: the schema changes, a column's
|
|
6
|
+
* distribution shifts, a column starts arriving empty, or a categorical
|
|
7
|
+
* column grows a value nothing downstream has ever seen. This compares a
|
|
8
|
+
* stored profile against today's and answers all four.
|
|
9
|
+
*
|
|
10
|
+
* THE RULE THAT MAKES ANY OF IT MEAN ANYTHING. A Population Stability Index
|
|
11
|
+
* is a comparison of two histograms over the SAME bins, and the bins have to
|
|
12
|
+
* be the reference's. Bin each side against its own quantiles and every
|
|
13
|
+
* column comes back with ten bins of ten percent on both sides — PSI near
|
|
14
|
+
* zero, however far the distribution actually moved. So the reference profile
|
|
15
|
+
* carries its edges, the current data is binned against those stored edges,
|
|
16
|
+
* and `compareDrift` REFUSES a column whose two sides were not binned
|
|
17
|
+
* identically rather than returning the plausible number.
|
|
18
|
+
*
|
|
19
|
+
* The statistics come from `@crewhaus/tool-math`'s kernel, which is where
|
|
20
|
+
* Mann-Whitney, PSI and the normal tail live for the whole repository. The
|
|
21
|
+
* chi-square below is the one piece that was not already there.
|
|
22
|
+
*/
|
|
23
|
+
import { statsKernel } from "@crewhaus/tool-math";
|
|
24
|
+
import type { ColumnType, TableProfile } from "./profile";
|
|
25
|
+
/** A refusal: the data cannot answer the question asked of it. */
|
|
26
|
+
export declare class DriftError extends Error {
|
|
27
|
+
readonly name = "DriftError";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* P(X > x) for a chi-square variate with `df` degrees of freedom — the upper
|
|
31
|
+
* tail, which is the p-value of every chi-square test.
|
|
32
|
+
*
|
|
33
|
+
* This is Q(df/2, x/2) of the regularized incomplete gamma function, split at
|
|
34
|
+
* `x < a + 1` between the series and the continued fraction because each one
|
|
35
|
+
* loses its digits in the other's region (Press et al., *Numerical Recipes*,
|
|
36
|
+
* §6.2). The kernel in `@crewhaus/tool-math` has the normal tail but not this
|
|
37
|
+
* one; `lib.test.ts` cross-checks the df=1 case against that normal tail,
|
|
38
|
+
* since Q(1/2, x/2) is exactly erfc(sqrt(x/2)) = 2 * P(Z > sqrt(x)).
|
|
39
|
+
*/
|
|
40
|
+
export declare function chiSquareUpperTail(x: number, df: number): number;
|
|
41
|
+
export type ChiSquareResult = {
|
|
42
|
+
readonly chiSquare: number;
|
|
43
|
+
readonly df: number;
|
|
44
|
+
/** `null` when the table has nothing to test — see `note`. */
|
|
45
|
+
readonly p: number | null;
|
|
46
|
+
readonly categories: number;
|
|
47
|
+
/** Cells whose EXPECTED count is under 5, and under 1. Where it frays. */
|
|
48
|
+
readonly cellsBelowFive: number;
|
|
49
|
+
readonly cellsBelowOne: number;
|
|
50
|
+
readonly cells: number;
|
|
51
|
+
/** Cochran's condition: no expected cell under 1, at most a fifth under 5. */
|
|
52
|
+
readonly approximationValid: boolean;
|
|
53
|
+
readonly note: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Chi-square test of homogeneity on the 2 x k table of (reference, current)
|
|
57
|
+
* against category — "are these two samples drawn from the same distribution
|
|
58
|
+
* over these labels?".
|
|
59
|
+
*
|
|
60
|
+
* A category present on only one side stays in the table. It is the single
|
|
61
|
+
* most informative cell there is, and dropping it to avoid a zero is how a
|
|
62
|
+
* column that gained a whole new value tests as unchanged.
|
|
63
|
+
*
|
|
64
|
+
* `approximationValid` is Cochran's rule, and it is NOT advisory: the
|
|
65
|
+
* chi-square statistic's null distribution is only approximately chi-square,
|
|
66
|
+
* and with a handful of rare categories the approximation is off by enough to
|
|
67
|
+
* move a verdict. A caller gating on `p` should gate on this first.
|
|
68
|
+
*/
|
|
69
|
+
export declare function chiSquareHomogeneity(reference: Readonly<Record<string, number>>, current: Readonly<Record<string, number>>): ChiSquareResult;
|
|
70
|
+
export type DriftThresholds = {
|
|
71
|
+
/** Fail when a column's PSI is at or above this. 0.25 is the usual line. */
|
|
72
|
+
readonly psi?: number;
|
|
73
|
+
/** Fail when a VALID distribution p is at or below this. */
|
|
74
|
+
readonly pValue?: number;
|
|
75
|
+
/** Fail when a column's null fraction rose by at least this much, absolute. */
|
|
76
|
+
readonly nullRateIncrease?: number;
|
|
77
|
+
/** Fail when distinct-count ratio leaves [1/r, r]. Must be >= 1. */
|
|
78
|
+
readonly cardinalityRatio?: number;
|
|
79
|
+
/** Fail when row-count ratio leaves [1/r, r]. Must be >= 1. */
|
|
80
|
+
readonly rowCountRatio?: number;
|
|
81
|
+
/** Fail when a column gains more than this many categories. */
|
|
82
|
+
readonly newCategories?: number;
|
|
83
|
+
/** Fail on any added, removed or retyped column. */
|
|
84
|
+
readonly schemaDrift?: boolean;
|
|
85
|
+
};
|
|
86
|
+
export type DriftCompareOptions = {
|
|
87
|
+
/**
|
|
88
|
+
* The share substituted for a bin empty on one side. No default, here or
|
|
89
|
+
* anywhere below: it decides the verdict. A reference bin that empties out
|
|
90
|
+
* reads as PSI 0.466 at 1e-3, 0.218 at 1e-2 and 0.045 at 0.05 — significant,
|
|
91
|
+
* moderate and stable, from one dataset. It belongs in the same config as
|
|
92
|
+
* the threshold it is compared against.
|
|
93
|
+
*/
|
|
94
|
+
readonly epsilon: number;
|
|
95
|
+
readonly thresholds?: DriftThresholds;
|
|
96
|
+
/** Return every PSI bin rather than the three heaviest contributors. */
|
|
97
|
+
readonly includeBins?: boolean;
|
|
98
|
+
};
|
|
99
|
+
export type PsiReport = {
|
|
100
|
+
readonly value: number | null;
|
|
101
|
+
readonly band: "stable" | "moderate" | "significant" | null;
|
|
102
|
+
readonly epsilon: number;
|
|
103
|
+
/** True when a one-sided empty bin took the floor, i.e. PSI depends on it. */
|
|
104
|
+
readonly epsilonApplied: boolean;
|
|
105
|
+
/** Bins actually compared, INCLUDING the two out-of-range bins. */
|
|
106
|
+
readonly binCount: number;
|
|
107
|
+
/** Current values that fell outside the reference's range, by side. */
|
|
108
|
+
readonly outOfRange: {
|
|
109
|
+
readonly below: number;
|
|
110
|
+
readonly above: number;
|
|
111
|
+
};
|
|
112
|
+
readonly bins: ReadonlyArray<statsKernel.PsiBin>;
|
|
113
|
+
readonly note: string;
|
|
114
|
+
};
|
|
115
|
+
export type ColumnDriftResult = {
|
|
116
|
+
readonly column: string;
|
|
117
|
+
readonly kind: "numeric" | "categorical" | "incomparable";
|
|
118
|
+
readonly nulls: {
|
|
119
|
+
readonly reference: number;
|
|
120
|
+
readonly current: number;
|
|
121
|
+
readonly increase: number;
|
|
122
|
+
};
|
|
123
|
+
readonly distinct: {
|
|
124
|
+
readonly reference: number;
|
|
125
|
+
readonly current: number;
|
|
126
|
+
readonly ratio: number | null;
|
|
127
|
+
};
|
|
128
|
+
readonly psi: PsiReport | null;
|
|
129
|
+
readonly mannWhitney: statsKernel.MannWhitneyResult | null;
|
|
130
|
+
readonly chiSquare: ChiSquareResult | null;
|
|
131
|
+
/** Categories in today's data and not the reference's. `null` = cannot say. */
|
|
132
|
+
readonly newCategories: ReadonlyArray<string> | null;
|
|
133
|
+
readonly droppedCategories: ReadonlyArray<string> | null;
|
|
134
|
+
readonly notes: ReadonlyArray<string>;
|
|
135
|
+
};
|
|
136
|
+
export type SchemaDriftReport = {
|
|
137
|
+
readonly added: ReadonlyArray<string>;
|
|
138
|
+
readonly removed: ReadonlyArray<string>;
|
|
139
|
+
readonly retyped: ReadonlyArray<{
|
|
140
|
+
readonly column: string;
|
|
141
|
+
readonly reference: ColumnType;
|
|
142
|
+
readonly current: ColumnType;
|
|
143
|
+
}>;
|
|
144
|
+
/**
|
|
145
|
+
* A column that flipped between being compared as numbers and as labels.
|
|
146
|
+
* Worth separating from `retyped`: `integer` to `decimal` is a retype that
|
|
147
|
+
* still compares fine, and numeric to categorical is one that cannot.
|
|
148
|
+
*/
|
|
149
|
+
readonly kindChanged: ReadonlyArray<{
|
|
150
|
+
readonly column: string;
|
|
151
|
+
readonly reference: string;
|
|
152
|
+
readonly current: string;
|
|
153
|
+
}>;
|
|
154
|
+
};
|
|
155
|
+
export type DriftReport = {
|
|
156
|
+
readonly rows: {
|
|
157
|
+
readonly reference: number;
|
|
158
|
+
readonly current: number;
|
|
159
|
+
readonly ratio: number | null;
|
|
160
|
+
};
|
|
161
|
+
readonly schema: SchemaDriftReport;
|
|
162
|
+
readonly columns: ReadonlyArray<ColumnDriftResult>;
|
|
163
|
+
readonly gate: {
|
|
164
|
+
readonly configured: boolean;
|
|
165
|
+
readonly ok: boolean;
|
|
166
|
+
readonly failures: ReadonlyArray<string>;
|
|
167
|
+
/**
|
|
168
|
+
* Configured thresholds that could not be evaluated at all — the column
|
|
169
|
+
* produced no such measurement. `ok` is false while this is non-empty:
|
|
170
|
+
* a gate that did not get to look is not a gate that passed.
|
|
171
|
+
*/
|
|
172
|
+
readonly unchecked: ReadonlyArray<string>;
|
|
173
|
+
readonly note: string;
|
|
174
|
+
};
|
|
175
|
+
readonly notes: ReadonlyArray<string>;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Compare a stored reference profile against today's.
|
|
179
|
+
*
|
|
180
|
+
* Both must carry a drift capture, and today's must have been captured
|
|
181
|
+
* against the reference's edges — `profileTable`'s `drift.edges` option is
|
|
182
|
+
* what does that. A column captured any other way is reported without a PSI
|
|
183
|
+
* and with the reason, which is the only honest thing to return.
|
|
184
|
+
*/
|
|
185
|
+
export declare function compareDrift(reference: TableProfile, current: TableProfile, options: DriftCompareOptions): DriftReport;
|