@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
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @crewhaus/tool-table
|
|
2
|
+
|
|
3
|
+
Making a messy export usable.
|
|
4
|
+
|
|
5
|
+
| Tool | Answers |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `TableProfile` | what is actually in this file |
|
|
8
|
+
| `DataDriftCheck` | has today's feed moved away from the baseline |
|
|
9
|
+
| `TableDiff` | what changed since yesterday's export |
|
|
10
|
+
| `RecordLinkage` | is this the same customer as that one |
|
|
11
|
+
| `ContactNormalize` | what do these identifiers look like canonically |
|
|
12
|
+
| `TableReshape` | pivot between wide and long |
|
|
13
|
+
| `TableShard` | split a file too big for one pass |
|
|
14
|
+
| `FixedWidthParse` | read a positional layout with no delimiter |
|
|
15
|
+
|
|
16
|
+
The first questions about any data file are always the same, and the usual
|
|
17
|
+
way to answer them is to read the first fifty rows into a context window and
|
|
18
|
+
guess. Fifty rows do not tell you that column nine is empty in the last
|
|
19
|
+
thousand, or that the id you were about to join on repeats.
|
|
20
|
+
|
|
21
|
+
Files are read here rather than passed in: a table worth these tools is one
|
|
22
|
+
too big to paste, which is the whole saving. CSV parsing comes from
|
|
23
|
+
`@crewhaus/tool-data` — two readers in one repository would disagree about
|
|
24
|
+
quoting and embedded newlines on the same file, in different tools.
|
|
25
|
+
|
|
26
|
+
## It refuses to guess where guessing is invisible
|
|
27
|
+
|
|
28
|
+
Each of these produces output that looks right when it is wrong, so each is
|
|
29
|
+
reported instead:
|
|
30
|
+
|
|
31
|
+
- **A duplicate key in a diff.** With a key that appears twice there is no
|
|
32
|
+
fact about which row became which. Picking one produces a reconciliation
|
|
33
|
+
that looks authoritative and is arbitrary.
|
|
34
|
+
- **A pivot collision.** Going long to wide, a repeated (identifier,
|
|
35
|
+
variable) pair does not say which value wins. Silently taking the last
|
|
36
|
+
makes a pivot that looks complete and is wrong in a way nothing downstream
|
|
37
|
+
can detect.
|
|
38
|
+
- **A short line in a fixed-width file.** Padding it turns a truncated record
|
|
39
|
+
into one with empty trailing fields, which read as real data.
|
|
40
|
+
- **A record matching two others.** That is a question, not two matches, so
|
|
41
|
+
each record is used once and the leftovers are listed.
|
|
42
|
+
|
|
43
|
+
A candidate key must be unique **and** complete: a column with distinct
|
|
44
|
+
values and some nulls cannot key a join, and offering it would be worse than
|
|
45
|
+
offering nothing.
|
|
46
|
+
|
|
47
|
+
## Matching carries its evidence
|
|
48
|
+
|
|
49
|
+
`RecordLinkage` returns, for every accepted pair, which field matched and how
|
|
50
|
+
closely. "These are the same person" is a claim somebody will have to defend,
|
|
51
|
+
and a bare score cannot be defended. Pairs above the review floor but below
|
|
52
|
+
the accept floor come back separately rather than being decided.
|
|
53
|
+
|
|
54
|
+
Rules can normalize before comparing, with the same canonicalization
|
|
55
|
+
`ContactNormalize` applies — without it, "Dr Jane Smith" and "Smith Jane"
|
|
56
|
+
score far apart on an edit distance while obviously being the same person.
|
|
57
|
+
|
|
58
|
+
Gmail's dots and `+tags` fold **only** for Gmail-family domains, because they
|
|
59
|
+
are a Gmail feature and not a rule of email: treating `a.b@other.com` as
|
|
60
|
+
`ab@other.com` merges two different people. A phone number with no country
|
|
61
|
+
code stays national and says so rather than being given one. Every fold is
|
|
62
|
+
named, and the original is never discarded.
|
|
63
|
+
|
|
64
|
+
## Bounds are on the work, not on a proxy for it
|
|
65
|
+
|
|
66
|
+
Edit distance is quadratic in the length of what it compares, so the real
|
|
67
|
+
cost of a linkage run is comparisons **times** length squared. A cap on the
|
|
68
|
+
number of comparisons alone bounds the wrong quantity: thirty records against
|
|
69
|
+
thirty, with three-thousand-character values, is only nine hundred
|
|
70
|
+
comparisons and took forty-five seconds. Fuzzy comparison looks at the first
|
|
71
|
+
256 characters, which is far more than the names, companies and addresses
|
|
72
|
+
this exists to match, and the same run now takes four milliseconds.
|
|
73
|
+
|
|
74
|
+
Pivoting on a high-cardinality column is refused rather than answered — an id
|
|
75
|
+
or a timestamp gives a column per row, which is a table nobody can read
|
|
76
|
+
rather than an error anybody notices. And `TableShard` returns the plan
|
|
77
|
+
without the shard bodies once they exceed a few megabytes, because returning
|
|
78
|
+
them at that size is not a result, it is the file again.
|
|
79
|
+
|
|
80
|
+
## Positions are 1-based and inclusive
|
|
81
|
+
|
|
82
|
+
`FixedWidthParse` takes the positions exactly as a layout document, copybook
|
|
83
|
+
or mainframe spec states them. Converting in your head is how a field ends up
|
|
84
|
+
one character off down the whole file.
|
|
85
|
+
|
|
86
|
+
## Drift, and the one thing a baseline has to carry
|
|
87
|
+
|
|
88
|
+
`DataDriftCheck` compares today's file against a `TableProfile` taken earlier
|
|
89
|
+
and reports schema drift, distribution shift, null-rate and cardinality jumps,
|
|
90
|
+
categories that are new today, and the row-count ratio.
|
|
91
|
+
|
|
92
|
+
The Population Stability Index at the centre of it is only meaningful when
|
|
93
|
+
today's numbers are binned against the **baseline's** bin edges. Bin each side
|
|
94
|
+
against its own quantiles and both come back as ten deciles of ten percent, so
|
|
95
|
+
the index reads near zero however far the data moved — and that is not an edge
|
|
96
|
+
case, it is what happens every time. In this package's own tests, a column
|
|
97
|
+
moved bodily from 0-100 to 500-600 scores **11.46** against the stored edges
|
|
98
|
+
and **exactly 0** against fresh ones.
|
|
99
|
+
|
|
100
|
+
So the edges are captured when the baseline is taken, and cannot be recovered
|
|
101
|
+
afterwards:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
TableProfile { file: "month.csv", driftProfile: {} } # stores edges, category
|
|
105
|
+
# counts and a value sample
|
|
106
|
+
DataDriftCheck { referenceProfile: "month.json", file: "today.csv", epsilon: 1e-3 }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A baseline profiled without `driftProfile` is **refused**, with the command
|
|
110
|
+
that fixes it, rather than answered from edges derived on the spot. So is a
|
|
111
|
+
column whose two sides were somehow binned differently.
|
|
112
|
+
|
|
113
|
+
`driftProfile` is opt-in because the capture is machine fodder — a 500-value
|
|
114
|
+
sample per numeric column makes the answer a person asked for worse. A profile
|
|
115
|
+
taken without it is byte-for-byte what it always was.
|
|
116
|
+
|
|
117
|
+
## Epsilon is an input, not a constant
|
|
118
|
+
|
|
119
|
+
An empty bin makes the PSI log term infinite, so an empty bin's share is
|
|
120
|
+
floored at `epsilon`. That floor decides the verdict. For a ten-bin baseline
|
|
121
|
+
where one bin empties out:
|
|
122
|
+
|
|
123
|
+
| epsilon | PSI | band |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| 0.001 | 0.4664 | significant |
|
|
126
|
+
| 0.01 | 0.2178 | moderate |
|
|
127
|
+
| 0.05 | 0.0452 | stable |
|
|
128
|
+
|
|
129
|
+
Three ship decisions from one dataset. `epsilon` is therefore a **required**
|
|
130
|
+
input with no default anywhere in the stack, it is echoed in the result, and a
|
|
131
|
+
PSI that leaned on the floor says so. Put it in the same config as the
|
|
132
|
+
threshold it is compared against.
|
|
133
|
+
|
|
134
|
+
Values beyond the baseline's range get their own two bins rather than being
|
|
135
|
+
clamped into the end ones: a distribution that walked off the edge of the chart
|
|
136
|
+
is the loudest drift signal there is, and clamping is how it reads as stable.
|
|
137
|
+
|
|
138
|
+
## What the drift check refuses to say
|
|
139
|
+
|
|
140
|
+
- **New categories, when the stored list was capped.** With a truncated list
|
|
141
|
+
there is no telling a genuinely new value from one that was always there and
|
|
142
|
+
merely rare. "Three new payment methods appeared" is a sentence somebody
|
|
143
|
+
acts on, so a wrong one is worse than none.
|
|
144
|
+
- **A p-value the approximation does not support.** The rank test carries the
|
|
145
|
+
kernel's own `normalApproximationValid`, and an invalid p cannot trip the
|
|
146
|
+
`pValue` gate — below eight values a side it is a rank ordering, not a rate.
|
|
147
|
+
The chi-square carries Cochran's condition for the same reason.
|
|
148
|
+
- **A distribution comparison across a type flip.** A column that arrives as
|
|
149
|
+
numbers and returns as labels is reported as `kindChanged`; its null rate and
|
|
150
|
+
cardinality are still compared, because those still mean something.
|
|
151
|
+
- **Anything, when `failOn` is absent.** `gate.ok` is then true because nothing
|
|
152
|
+
was checked, and it says so in as many words.
|
|
153
|
+
- **A comparison across a repeated header.** Two columns with one name give no
|
|
154
|
+
fact about which is which, so they are named in the notes and left
|
|
155
|
+
uncompared rather than matched to whichever came last.
|
|
156
|
+
|
|
157
|
+
A refusal is not a pass. Every threshold above that could not be evaluated —
|
|
158
|
+
PSI on a column that stopped parsing, new categories on a capped list, a p the
|
|
159
|
+
approximation does not support, any per-column gate on a column that vanished
|
|
160
|
+
or whose name repeats — lands in `gate.unchecked`, and `gate.ok` is **false**
|
|
161
|
+
while that list is non-empty. `failures` still means "measured and breached",
|
|
162
|
+
so the two are told apart; `unchecked` means the gate never got to look, which
|
|
163
|
+
is the one thing "ok" must never be allowed to mean.
|
|
164
|
+
|
|
165
|
+
Statistics come from `@crewhaus/tool-math`'s kernel — Mann-Whitney, PSI and the
|
|
166
|
+
normal tail live there for the whole repository. The chi-square test of
|
|
167
|
+
homogeneity and its incomplete-gamma tail are the one piece that was not
|
|
168
|
+
already there; the df=1 case is pinned against that kernel's normal tail, which
|
|
169
|
+
is an exact identity and so an independent oracle.
|
|
170
|
+
|
|
171
|
+
One JSON wrinkle worth knowing: the out-of-range bins are bounded by infinity,
|
|
172
|
+
and `JSON.stringify` turns those into `null`. The bin's `label` (`"(-inf, 0)"`,
|
|
173
|
+
`"[99, +inf)"`) is the field to read.
|
|
174
|
+
|
|
175
|
+
## What it does not do
|
|
176
|
+
|
|
177
|
+
- **It does not write.** Everything here reads; `TableShard` returns the
|
|
178
|
+
shard bodies and the caller writes them.
|
|
179
|
+
- **It does not query.** For selection, aggregation and joins there are
|
|
180
|
+
`jsonQuery`, `tableQuery` and `tableAggregate` in `@crewhaus/tool-data`,
|
|
181
|
+
and `sqlQuery` in `@crewhaus/tool-sql`.
|
|
182
|
+
- **It does not read Parquet, ORC, Avro or Excel.** Delimited text and fixed
|
|
183
|
+
width only; `xlsxRead` in `@crewhaus/tool-docs` handles spreadsheets.
|
|
184
|
+
- **It does not clean data.** Nothing here corrects a value, fills a null or
|
|
185
|
+
resolves an inconsistency — it tells you they are there.
|
|
186
|
+
- **It does not explain drift.** `DataDriftCheck` says which column moved and
|
|
187
|
+
by how much. Why it moved is upstream of here.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
2
|
+
export declare const tableProfile: RegisteredTool;
|
|
3
|
+
export declare const dataDriftCheck: RegisteredTool;
|
|
4
|
+
export declare const tableDiff: RegisteredTool;
|
|
5
|
+
export declare const recordLinkage: RegisteredTool;
|
|
6
|
+
export declare const contactNormalize: RegisteredTool;
|
|
7
|
+
export declare const tableReshape: RegisteredTool;
|
|
8
|
+
export declare const tableShard: RegisteredTool;
|
|
9
|
+
export declare const fixedWidthParse: RegisteredTool;
|
|
10
|
+
/** Every tool this package registers, in the order a catalog should list them. */
|
|
11
|
+
export declare const TABLE_TOOLS: ReadonlyArray<RegisteredTool>;
|