abkit 0.1.0__tar.gz

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.
abkit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hadi Mohammadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
abkit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,262 @@
1
+ Metadata-Version: 2.4
2
+ Name: abkit
3
+ Version: 0.1.0
4
+ Summary: Audit an A/B-test readout before you ship the decision
5
+ Author: Hadi Mohammadi
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mohammadi-hadi/abkit
8
+ Project-URL: Issues, https://github.com/mohammadi-hadi/abkit/issues
9
+ Keywords: ab-testing,experimentation,srm,statistics,audit
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.26
21
+ Requires-Dist: pydantic>=2.5
22
+ Requires-Dist: matplotlib>=3.8
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: ruff>=0.4; extra == "dev"
26
+ Requires-Dist: mypy>=1.10; extra == "dev"
27
+ Requires-Dist: scipy>=1.12; extra == "dev"
28
+ Requires-Dist: statsmodels>=0.14; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # abkit
32
+
33
+ Audit an A/B-test readout before you ship the decision.
34
+
35
+ [![ci](https://github.com/mohammadi-hadi/abkit/actions/workflows/ci.yml/badge.svg)](https://github.com/mohammadi-hadi/abkit/actions/workflows/ci.yml)
36
+
37
+ An experiment readout can say "B wins, p = 0.03, ship it" while the traffic
38
+ split was broken, three whale customers carried the whole effect, the analyst
39
+ stopped at the first significant refresh, the winning metric was one of forty
40
+ tried, or the experiment never had the power to estimate what it now claims.
41
+ None of that shows up in the p-value. abkit reads the per-unit data and the
42
+ intended design, measures each of these failure modes, and flags whatever
43
+ leaves the range a healthy experiment could occupy.
44
+
45
+ ## The audit in one table
46
+
47
+ Eight synthetic experiments, seven implanted defects. Bold values are flags;
48
+ each lands on the row where its defect was implanted, and the clean row
49
+ carries none.
50
+
51
+ <!-- abkit:demo -->
52
+ | experiment | implanted defect | SRM p | contam. | fragility | look FPR | novelty gap | uncorrected | exagg. | flags |
53
+ |---|---|---|---|---|---|---|---|---|---|
54
+ | clean | nothing | 0.89 | 0 | 20 | - | -0.02 | - | 1.0 | none |
55
+ | traffic-leak | 3% of traffic diverted to treatment | **4.1e-14** | 0 | 20 | - | -0.02 | - | 1.0 | sample ratio mismatch |
56
+ | double-dipper | 25 units assigned to both arms | 0.67 | **25** | 20 | - | -0.03 | - | 1.0 | assignment contamination |
57
+ | whale-driven | 3 extreme units carry the significance | 0.11 | 0 | **1** | - | 0.26 | - | 1.2 | outlier fragility |
58
+ | peeker | null effect, stopped at the first significant look | 0.55 | 0 | 8 | **0.19** | -0.17 | - | 1.2 | peeking |
59
+ | fading-novelty | early-half effect 0.30, late-half effect 0.00 | 0.041 | 0 | 20 | - | **0.31** | - | 1.0 | novelty |
60
+ | metric-fisher | null effect, 40 secondary metrics tested | 0.054 | 0 | 0 | - | -0.11 | **2** | 3.4 | uncorrected winners |
61
+ | underpowered-winner | true effect 0.1, powered for 0.14 of that chance | 0.91 | 0 | 8 | - | -0.38 | - | **2.7** | winner's curse |
62
+ <!-- /abkit:demo -->
63
+
64
+ The row to sit with is `peeker`: a true null effect, analyzed after every
65
+ batch of 250 units and stopped the moment z crossed 1.96. Its final readout
66
+ is indistinguishable from a real win — p < 0.05, decent sample size — but the
67
+ schedule it followed has a 19% false-positive rate, not 5%. The audit flags
68
+ the process, which is the only place that defect lives.
69
+
70
+ ![peeking](results/figures/peeking.png)
71
+
72
+ ![exaggeration](results/figures/exaggeration.png)
73
+
74
+ `make demo` regenerates the table, the [full report](results/report.md) and
75
+ the figures from fixed seeds; CI rebuilds them from pinned dependencies and
76
+ fails if a committed number differs from what the code produces.
77
+
78
+ ## 4,873 real experiments, audited
79
+
80
+ [`examples/upworthy/`](examples/upworthy/) runs abkit's aggregate checks over
81
+ the exploratory split of the [Upworthy Research Archive](https://doi.org/10.1038/s41597-021-00934-7)
82
+ — 4,873 real headline A/B tests from 2013–2015. Committed results:
83
+ [examples/upworthy/results/upworthy.md](examples/upworthy/results/upworthy.md).
84
+
85
+ - **16% of real tests fail the sample-ratio check** at p < 0.001 against the
86
+ platform's equal-allocation design — and not because packages were added
87
+ mid-test; the rate is, if anything, higher in tests whose variants all
88
+ launched together. Those readouts carried unknown exposure bias.
89
+ - **10.9% of tests have a "significant winner"** (best headline beats the
90
+ runner-up at raw p < 0.05), but Benjamini–Hochberg across all 4,873
91
+ winner comparisons keeps only **2.7%** of them.
92
+ - The median significant winning lift (0.85 CTR points, power 0.62) is
93
+ expected to **overstate its true effect by 1.27x** even when the winner is
94
+ real — the winner's curse, measured on real experiments.
95
+
96
+ ## Install
97
+
98
+ ```
99
+ pip install git+https://github.com/mohammadi-hadi/abkit
100
+ ```
101
+
102
+ Python 3.11+. Runtime dependencies: numpy, pydantic, matplotlib.
103
+
104
+ ## Quickstart
105
+
106
+ Log one JSON object per randomized unit, and the design the experiment was
107
+ supposed to follow:
108
+
109
+ ```json
110
+ {"unit_id": "u1", "arm": "treatment", "metrics": {"revenue": 12.4, "conversion": 1.0}, "pre": {"revenue": 9.1}, "t": 1718040000}
111
+ ```
112
+
113
+ ```json
114
+ {
115
+ "split": {"control": 0.5, "treatment": 0.5},
116
+ "control": "control",
117
+ "treatment": "treatment",
118
+ "primary_metric": "revenue",
119
+ "alpha": 0.05,
120
+ "mde": 0.5,
121
+ "looks": [{"n": 20000, "z": 1.31}, {"n": 40000, "z": 2.11}]
122
+ }
123
+ ```
124
+
125
+ ```python
126
+ from abkit import load_units, load_design, run_audit
127
+
128
+ audit = run_audit(load_units("units.jsonl"), load_design("design.json"))
129
+ for flag in audit.flags:
130
+ print(flag.name, "--", flag.detail)
131
+ ```
132
+
133
+ Or from the shell, with an exit code CI can gate on:
134
+
135
+ ```
136
+ abkit report units.jsonl --design design.json --out audit --fail-on-flags
137
+ ```
138
+
139
+ Alongside `report.md` the audit writes `report.json` for pipelines that gate
140
+ on specific numbers. Any check that could not run is listed with the exact
141
+ fields to log to enable it — a skip describes the log file, not the
142
+ experiment.
143
+
144
+ ## What it checks
145
+
146
+ | check | question it answers | needs |
147
+ |---|---|---|
148
+ | sample ratio mismatch | did the split the readout assumes actually happen? | arms + intended split |
149
+ | assignment contamination | is any unit in more than one arm? | unit ids |
150
+ | covariate balance | were the groups equivalent before the treatment? | `pre` covariates |
151
+ | outlier fragility | how many extreme units does significance rest on? | per-unit primary metric |
152
+ | uncorrected winners | do the claimed wins survive multiple-testing control? | 2+ metrics |
153
+ | peeking | what false-positive rate did the stopping rule really have? | `design.looks` |
154
+ | novelty | is the single reported effect averaging over a changing one? | `t` per unit |
155
+ | winner's curse | how exaggerated is a significant estimate at this power? | `design.mde` (or observed) |
156
+ | variance ratio | does the treatment move the distribution, not just the mean? | per-unit primary metric |
157
+ | CUPED headroom | how much tighter could the CI have been? | `pre` covariates |
158
+
159
+ ## How the numbers are defended
160
+
161
+ - **Cross-checked implementations.** The chi-square survival function matches
162
+ scipy to 1e-10 across df 1–10, Welch's statistic matches
163
+ `scipy.stats.ttest_ind`, the two-proportion z matches statsmodels
164
+ `proportions_ztest`, and Benjamini–Hochberg reproduces statsmodels
165
+ `multipletests` decisions exactly. The library itself depends on none of
166
+ them.
167
+ - **Known closed-form results.** The Gelman–Carlin power / Type-S /
168
+ exaggeration triple is checked against numerical integration to 1e-6, and
169
+ the peeking simulator reproduces the classical repeated-significance rates
170
+ (0.083 at 2 looks, 0.142 at 5, 0.193 at 10) within Monte-Carlo tolerance.
171
+ - **Validation by implantation.** The synthetic experiments have defect
172
+ dials, and the tests require each probe to recover its dial — an implanted
173
+ 0.3 covariate shift must read as SMD 0.3 — to stay monotone in it, and to
174
+ stay silent on the clean experiment.
175
+ - **Drift-checked results.** The demo table, report and README numbers are
176
+ regenerated by CI from pinned dependencies and diffed against the
177
+ committed copy.
178
+
179
+ ## Design notes
180
+
181
+ - **Innocent bands, not point nulls.** With enough data an SMD of 0.02
182
+ excludes zero while meaning nothing. Band probes declare the interval a
183
+ healthy experiment could plausibly occupy — SMD and novelty gap within
184
+ ±0.10 — and flag only when the whole 95% bootstrap CI leaves it. Exact
185
+ procedures use the field's conventional thresholds: SRM at p < 0.001,
186
+ contamination at zero tolerance. All constants live in one place
187
+ ([`probes.py`](src/abkit/probes.py)) and are easy to disagree with.
188
+ - **The stopping rule is audited, not the final p.** A peeked experiment's
189
+ final test statistic looks ordinary; what is broken is the procedure that
190
+ produced it. The peeking probe Monte-Carlos the analyst's actual look
191
+ schedule under the null and reports the false-positive rate that schedule
192
+ had.
193
+ - **Fragility is counted in units, not sigmas.** "Removing two customers
194
+ overturns the decision" is a statement a stakeholder can act on; a
195
+ leave-one-out influence statistic is not.
196
+ - **Exaggeration needs a basis effect.** With `design.mde` the winner's-curse
197
+ probe reports power and exaggeration at the effect the experiment was
198
+ designed for; without it, the observed effect is used and labeled an
199
+ optimistic bound, since conditioning on significance inflates it.
200
+ - **Stratified bootstrap.** Units are resampled within their arm, never
201
+ across, so group sizes — which determine every statistic's sampling
202
+ distribution — are preserved.
203
+ - **Deterministic to the digit.** Seeded PCG64 everywhere, no BLAS in any
204
+ statistic's path, and a pinned drift environment: the same data gives the
205
+ same report on any machine.
206
+
207
+ ## Limitations
208
+
209
+ - An audit is bounded by what was logged. No pre-period covariates, no
210
+ balance or CUPED checks; no look history, no peeking check; no per-unit
211
+ data at all, and only the aggregate checks (`abkit.stats`) apply.
212
+ - The innocent bands are judgment calls, not derivations.
213
+ - The peeking probe assumes the recorded looks are all the looks there were.
214
+ - Fragility uses a greedy removal path; it upper-bounds, and in adversarial
215
+ cases may not find, the true minimal flip set.
216
+ - Implantation shows the probes detect the mechanisms simulated; real
217
+ experiments can fail in ways not simulated here.
218
+ - The Upworthy analysis treats the archive's documented equal allocation as
219
+ the intended split; if some tests intentionally used unequal splits, their
220
+ SRM flags are misattributed.
221
+
222
+ ## Related work
223
+
224
+ The failure modes measured here are the standard ones from the online
225
+ experimentation literature: sample-ratio mismatch and its taxonomy
226
+ ([Fabijan et al., 2019](https://doi.org/10.1145/3292500.3330722)), trustworthy
227
+ experimentation practice ([Kohavi, Tang & Xu, 2020](https://experimentguide.com/)),
228
+ peeking and always-valid inference ([Johari et al., 2017](https://doi.org/10.1145/3097983.3097992)),
229
+ variance reduction with pre-experiment data — CUPED
230
+ ([Deng et al., 2013](https://doi.org/10.1145/2433396.2433413)),
231
+ Type-M/Type-S errors ([Gelman & Carlin, 2014](https://doi.org/10.1177/1745691614551642)),
232
+ the winner's curse in A/B testing ([Lee & Shen, 2018](https://doi.org/10.1145/3219819.3219905)),
233
+ and experimentation culture at scale
234
+ ([Kaufman, Pitchforth & Vermeer, 2017](https://arxiv.org/abs/1710.08217)).
235
+ The real-data case study uses the Upworthy Research Archive
236
+ ([Matias, Munger & Watts, 2021](https://doi.org/10.1038/s41597-021-00934-7)).
237
+ abkit's contribution is packaging the checks as one auditable tool with
238
+ uncertainty on every number and validation by implantation.
239
+
240
+ Companion projects: [judgekit](https://github.com/mohammadi-hadi/judgekit)
241
+ applies the same audit-before-you-trust pattern to LLM judges;
242
+ [trajectory-judge](https://github.com/mohammadi-hadi/trajectory-judge)
243
+ measures what outcome-only judges miss on agent trajectories.
244
+
245
+ ## Citation
246
+
247
+ If abkit is useful in your work, please cite it (see
248
+ [CITATION.cff](CITATION.cff)):
249
+
250
+ ```bibtex
251
+ @software{mohammadi_abkit,
252
+ author = {Mohammadi, Hadi},
253
+ title = {abkit: audit an A/B-test readout before you ship the decision},
254
+ url = {https://github.com/mohammadi-hadi/abkit},
255
+ version = {0.1.0},
256
+ year = {2026}
257
+ }
258
+ ```
259
+
260
+ ## License
261
+
262
+ MIT
abkit-0.1.0/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # abkit
2
+
3
+ Audit an A/B-test readout before you ship the decision.
4
+
5
+ [![ci](https://github.com/mohammadi-hadi/abkit/actions/workflows/ci.yml/badge.svg)](https://github.com/mohammadi-hadi/abkit/actions/workflows/ci.yml)
6
+
7
+ An experiment readout can say "B wins, p = 0.03, ship it" while the traffic
8
+ split was broken, three whale customers carried the whole effect, the analyst
9
+ stopped at the first significant refresh, the winning metric was one of forty
10
+ tried, or the experiment never had the power to estimate what it now claims.
11
+ None of that shows up in the p-value. abkit reads the per-unit data and the
12
+ intended design, measures each of these failure modes, and flags whatever
13
+ leaves the range a healthy experiment could occupy.
14
+
15
+ ## The audit in one table
16
+
17
+ Eight synthetic experiments, seven implanted defects. Bold values are flags;
18
+ each lands on the row where its defect was implanted, and the clean row
19
+ carries none.
20
+
21
+ <!-- abkit:demo -->
22
+ | experiment | implanted defect | SRM p | contam. | fragility | look FPR | novelty gap | uncorrected | exagg. | flags |
23
+ |---|---|---|---|---|---|---|---|---|---|
24
+ | clean | nothing | 0.89 | 0 | 20 | - | -0.02 | - | 1.0 | none |
25
+ | traffic-leak | 3% of traffic diverted to treatment | **4.1e-14** | 0 | 20 | - | -0.02 | - | 1.0 | sample ratio mismatch |
26
+ | double-dipper | 25 units assigned to both arms | 0.67 | **25** | 20 | - | -0.03 | - | 1.0 | assignment contamination |
27
+ | whale-driven | 3 extreme units carry the significance | 0.11 | 0 | **1** | - | 0.26 | - | 1.2 | outlier fragility |
28
+ | peeker | null effect, stopped at the first significant look | 0.55 | 0 | 8 | **0.19** | -0.17 | - | 1.2 | peeking |
29
+ | fading-novelty | early-half effect 0.30, late-half effect 0.00 | 0.041 | 0 | 20 | - | **0.31** | - | 1.0 | novelty |
30
+ | metric-fisher | null effect, 40 secondary metrics tested | 0.054 | 0 | 0 | - | -0.11 | **2** | 3.4 | uncorrected winners |
31
+ | underpowered-winner | true effect 0.1, powered for 0.14 of that chance | 0.91 | 0 | 8 | - | -0.38 | - | **2.7** | winner's curse |
32
+ <!-- /abkit:demo -->
33
+
34
+ The row to sit with is `peeker`: a true null effect, analyzed after every
35
+ batch of 250 units and stopped the moment z crossed 1.96. Its final readout
36
+ is indistinguishable from a real win — p < 0.05, decent sample size — but the
37
+ schedule it followed has a 19% false-positive rate, not 5%. The audit flags
38
+ the process, which is the only place that defect lives.
39
+
40
+ ![peeking](results/figures/peeking.png)
41
+
42
+ ![exaggeration](results/figures/exaggeration.png)
43
+
44
+ `make demo` regenerates the table, the [full report](results/report.md) and
45
+ the figures from fixed seeds; CI rebuilds them from pinned dependencies and
46
+ fails if a committed number differs from what the code produces.
47
+
48
+ ## 4,873 real experiments, audited
49
+
50
+ [`examples/upworthy/`](examples/upworthy/) runs abkit's aggregate checks over
51
+ the exploratory split of the [Upworthy Research Archive](https://doi.org/10.1038/s41597-021-00934-7)
52
+ — 4,873 real headline A/B tests from 2013–2015. Committed results:
53
+ [examples/upworthy/results/upworthy.md](examples/upworthy/results/upworthy.md).
54
+
55
+ - **16% of real tests fail the sample-ratio check** at p < 0.001 against the
56
+ platform's equal-allocation design — and not because packages were added
57
+ mid-test; the rate is, if anything, higher in tests whose variants all
58
+ launched together. Those readouts carried unknown exposure bias.
59
+ - **10.9% of tests have a "significant winner"** (best headline beats the
60
+ runner-up at raw p < 0.05), but Benjamini–Hochberg across all 4,873
61
+ winner comparisons keeps only **2.7%** of them.
62
+ - The median significant winning lift (0.85 CTR points, power 0.62) is
63
+ expected to **overstate its true effect by 1.27x** even when the winner is
64
+ real — the winner's curse, measured on real experiments.
65
+
66
+ ## Install
67
+
68
+ ```
69
+ pip install git+https://github.com/mohammadi-hadi/abkit
70
+ ```
71
+
72
+ Python 3.11+. Runtime dependencies: numpy, pydantic, matplotlib.
73
+
74
+ ## Quickstart
75
+
76
+ Log one JSON object per randomized unit, and the design the experiment was
77
+ supposed to follow:
78
+
79
+ ```json
80
+ {"unit_id": "u1", "arm": "treatment", "metrics": {"revenue": 12.4, "conversion": 1.0}, "pre": {"revenue": 9.1}, "t": 1718040000}
81
+ ```
82
+
83
+ ```json
84
+ {
85
+ "split": {"control": 0.5, "treatment": 0.5},
86
+ "control": "control",
87
+ "treatment": "treatment",
88
+ "primary_metric": "revenue",
89
+ "alpha": 0.05,
90
+ "mde": 0.5,
91
+ "looks": [{"n": 20000, "z": 1.31}, {"n": 40000, "z": 2.11}]
92
+ }
93
+ ```
94
+
95
+ ```python
96
+ from abkit import load_units, load_design, run_audit
97
+
98
+ audit = run_audit(load_units("units.jsonl"), load_design("design.json"))
99
+ for flag in audit.flags:
100
+ print(flag.name, "--", flag.detail)
101
+ ```
102
+
103
+ Or from the shell, with an exit code CI can gate on:
104
+
105
+ ```
106
+ abkit report units.jsonl --design design.json --out audit --fail-on-flags
107
+ ```
108
+
109
+ Alongside `report.md` the audit writes `report.json` for pipelines that gate
110
+ on specific numbers. Any check that could not run is listed with the exact
111
+ fields to log to enable it — a skip describes the log file, not the
112
+ experiment.
113
+
114
+ ## What it checks
115
+
116
+ | check | question it answers | needs |
117
+ |---|---|---|
118
+ | sample ratio mismatch | did the split the readout assumes actually happen? | arms + intended split |
119
+ | assignment contamination | is any unit in more than one arm? | unit ids |
120
+ | covariate balance | were the groups equivalent before the treatment? | `pre` covariates |
121
+ | outlier fragility | how many extreme units does significance rest on? | per-unit primary metric |
122
+ | uncorrected winners | do the claimed wins survive multiple-testing control? | 2+ metrics |
123
+ | peeking | what false-positive rate did the stopping rule really have? | `design.looks` |
124
+ | novelty | is the single reported effect averaging over a changing one? | `t` per unit |
125
+ | winner's curse | how exaggerated is a significant estimate at this power? | `design.mde` (or observed) |
126
+ | variance ratio | does the treatment move the distribution, not just the mean? | per-unit primary metric |
127
+ | CUPED headroom | how much tighter could the CI have been? | `pre` covariates |
128
+
129
+ ## How the numbers are defended
130
+
131
+ - **Cross-checked implementations.** The chi-square survival function matches
132
+ scipy to 1e-10 across df 1–10, Welch's statistic matches
133
+ `scipy.stats.ttest_ind`, the two-proportion z matches statsmodels
134
+ `proportions_ztest`, and Benjamini–Hochberg reproduces statsmodels
135
+ `multipletests` decisions exactly. The library itself depends on none of
136
+ them.
137
+ - **Known closed-form results.** The Gelman–Carlin power / Type-S /
138
+ exaggeration triple is checked against numerical integration to 1e-6, and
139
+ the peeking simulator reproduces the classical repeated-significance rates
140
+ (0.083 at 2 looks, 0.142 at 5, 0.193 at 10) within Monte-Carlo tolerance.
141
+ - **Validation by implantation.** The synthetic experiments have defect
142
+ dials, and the tests require each probe to recover its dial — an implanted
143
+ 0.3 covariate shift must read as SMD 0.3 — to stay monotone in it, and to
144
+ stay silent on the clean experiment.
145
+ - **Drift-checked results.** The demo table, report and README numbers are
146
+ regenerated by CI from pinned dependencies and diffed against the
147
+ committed copy.
148
+
149
+ ## Design notes
150
+
151
+ - **Innocent bands, not point nulls.** With enough data an SMD of 0.02
152
+ excludes zero while meaning nothing. Band probes declare the interval a
153
+ healthy experiment could plausibly occupy — SMD and novelty gap within
154
+ ±0.10 — and flag only when the whole 95% bootstrap CI leaves it. Exact
155
+ procedures use the field's conventional thresholds: SRM at p < 0.001,
156
+ contamination at zero tolerance. All constants live in one place
157
+ ([`probes.py`](src/abkit/probes.py)) and are easy to disagree with.
158
+ - **The stopping rule is audited, not the final p.** A peeked experiment's
159
+ final test statistic looks ordinary; what is broken is the procedure that
160
+ produced it. The peeking probe Monte-Carlos the analyst's actual look
161
+ schedule under the null and reports the false-positive rate that schedule
162
+ had.
163
+ - **Fragility is counted in units, not sigmas.** "Removing two customers
164
+ overturns the decision" is a statement a stakeholder can act on; a
165
+ leave-one-out influence statistic is not.
166
+ - **Exaggeration needs a basis effect.** With `design.mde` the winner's-curse
167
+ probe reports power and exaggeration at the effect the experiment was
168
+ designed for; without it, the observed effect is used and labeled an
169
+ optimistic bound, since conditioning on significance inflates it.
170
+ - **Stratified bootstrap.** Units are resampled within their arm, never
171
+ across, so group sizes — which determine every statistic's sampling
172
+ distribution — are preserved.
173
+ - **Deterministic to the digit.** Seeded PCG64 everywhere, no BLAS in any
174
+ statistic's path, and a pinned drift environment: the same data gives the
175
+ same report on any machine.
176
+
177
+ ## Limitations
178
+
179
+ - An audit is bounded by what was logged. No pre-period covariates, no
180
+ balance or CUPED checks; no look history, no peeking check; no per-unit
181
+ data at all, and only the aggregate checks (`abkit.stats`) apply.
182
+ - The innocent bands are judgment calls, not derivations.
183
+ - The peeking probe assumes the recorded looks are all the looks there were.
184
+ - Fragility uses a greedy removal path; it upper-bounds, and in adversarial
185
+ cases may not find, the true minimal flip set.
186
+ - Implantation shows the probes detect the mechanisms simulated; real
187
+ experiments can fail in ways not simulated here.
188
+ - The Upworthy analysis treats the archive's documented equal allocation as
189
+ the intended split; if some tests intentionally used unequal splits, their
190
+ SRM flags are misattributed.
191
+
192
+ ## Related work
193
+
194
+ The failure modes measured here are the standard ones from the online
195
+ experimentation literature: sample-ratio mismatch and its taxonomy
196
+ ([Fabijan et al., 2019](https://doi.org/10.1145/3292500.3330722)), trustworthy
197
+ experimentation practice ([Kohavi, Tang & Xu, 2020](https://experimentguide.com/)),
198
+ peeking and always-valid inference ([Johari et al., 2017](https://doi.org/10.1145/3097983.3097992)),
199
+ variance reduction with pre-experiment data — CUPED
200
+ ([Deng et al., 2013](https://doi.org/10.1145/2433396.2433413)),
201
+ Type-M/Type-S errors ([Gelman & Carlin, 2014](https://doi.org/10.1177/1745691614551642)),
202
+ the winner's curse in A/B testing ([Lee & Shen, 2018](https://doi.org/10.1145/3219819.3219905)),
203
+ and experimentation culture at scale
204
+ ([Kaufman, Pitchforth & Vermeer, 2017](https://arxiv.org/abs/1710.08217)).
205
+ The real-data case study uses the Upworthy Research Archive
206
+ ([Matias, Munger & Watts, 2021](https://doi.org/10.1038/s41597-021-00934-7)).
207
+ abkit's contribution is packaging the checks as one auditable tool with
208
+ uncertainty on every number and validation by implantation.
209
+
210
+ Companion projects: [judgekit](https://github.com/mohammadi-hadi/judgekit)
211
+ applies the same audit-before-you-trust pattern to LLM judges;
212
+ [trajectory-judge](https://github.com/mohammadi-hadi/trajectory-judge)
213
+ measures what outcome-only judges miss on agent trajectories.
214
+
215
+ ## Citation
216
+
217
+ If abkit is useful in your work, please cite it (see
218
+ [CITATION.cff](CITATION.cff)):
219
+
220
+ ```bibtex
221
+ @software{mohammadi_abkit,
222
+ author = {Mohammadi, Hadi},
223
+ title = {abkit: audit an A/B-test readout before you ship the decision},
224
+ url = {https://github.com/mohammadi-hadi/abkit},
225
+ version = {0.1.0},
226
+ year = {2026}
227
+ }
228
+ ```
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "abkit"
7
+ version = "0.1.0"
8
+ description = "Audit an A/B-test readout before you ship the decision"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "Hadi Mohammadi"}]
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "numpy>=1.26",
15
+ "pydantic>=2.5",
16
+ "matplotlib>=3.8",
17
+ ]
18
+ keywords = ["ab-testing", "experimentation", "srm", "statistics", "audit"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Science/Research",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Scientific/Engineering :: Information Analysis",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/mohammadi-hadi/abkit"
31
+ Issues = "https://github.com/mohammadi-hadi/abkit/issues"
32
+
33
+ [project.scripts]
34
+ abkit = "abkit.cli:main"
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["pytest>=8", "ruff>=0.4", "mypy>=1.10", "scipy>=1.12", "statsmodels>=0.14"]
38
+
39
+ [tool.setuptools.package-data]
40
+ abkit = ["py.typed"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py311"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I", "UP", "B"]
48
+
49
+ [tool.mypy]
50
+ disallow_untyped_defs = true
51
+ warn_return_any = true
52
+ warn_unused_ignores = true
53
+ no_implicit_optional = true
54
+
55
+ [[tool.mypy.overrides]]
56
+ module = "matplotlib.*"
57
+ ignore_missing_imports = true
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
abkit-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """abkit: audit an A/B-test readout before you ship the decision."""
2
+
3
+ from .audit import run_audit
4
+ from .io import dump_design, dump_units, load_design, load_units
5
+ from .result import AuditResult, ProbeResult, SkippedProbe
6
+ from .schema import Design, Look, Unit
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "AuditResult",
12
+ "Design",
13
+ "Look",
14
+ "ProbeResult",
15
+ "SkippedProbe",
16
+ "Unit",
17
+ "__version__",
18
+ "dump_design",
19
+ "dump_units",
20
+ "load_design",
21
+ "load_units",
22
+ "run_audit",
23
+ ]
@@ -0,0 +1,58 @@
1
+ """Run every applicable probe against one experiment readout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from . import probes
8
+ from .result import AuditResult, ProbeResult, SkippedProbe
9
+ from .schema import Design, Unit
10
+ from .stats import welch
11
+
12
+
13
+ def run_audit(units: Sequence[Unit], design: Design, name: str = "experiment") -> AuditResult:
14
+ result = AuditResult(experiment=name)
15
+
16
+ treat = [
17
+ u.metrics[design.primary_metric]
18
+ for u in units
19
+ if u.arm == design.treatment and design.primary_metric in u.metrics
20
+ ]
21
+ ctrl = [
22
+ u.metrics[design.primary_metric]
23
+ for u in units
24
+ if u.arm == design.control and design.primary_metric in u.metrics
25
+ ]
26
+ if len(treat) >= 2 and len(ctrl) >= 2:
27
+ effect, se, z, p = welch(treat, ctrl)
28
+ result.summary = {
29
+ "primary_metric": design.primary_metric,
30
+ "effect": effect,
31
+ "se": se,
32
+ "z": z,
33
+ "p": p,
34
+ "n_treatment": len(treat),
35
+ "n_control": len(ctrl),
36
+ "alpha": design.alpha,
37
+ }
38
+
39
+ outcomes: list[ProbeResult | SkippedProbe | list[ProbeResult]] = [
40
+ probes.sample_ratio(units, design),
41
+ probes.contamination(units),
42
+ probes.covariate_balance(units, design),
43
+ probes.outlier_fragility(units, design),
44
+ probes.uncorrected_winners(units, design),
45
+ probes.peeking(design),
46
+ probes.novelty(units, design),
47
+ probes.winners_curse(units, design),
48
+ probes.variance_ratio(units, design),
49
+ probes.cuped_headroom(units, design),
50
+ ]
51
+ for outcome in outcomes:
52
+ if isinstance(outcome, SkippedProbe):
53
+ result.skipped.append(outcome)
54
+ elif isinstance(outcome, list):
55
+ result.probes.extend(outcome)
56
+ else:
57
+ result.probes.append(outcome)
58
+ return result