trickle-down 0.1.0__py3-none-any.whl

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.
trickle_down/USAGE.md ADDED
@@ -0,0 +1,531 @@
1
+ # trickle-down Usage Guide
2
+
3
+ This guide teaches an AI agent (or human user) how to use `trickle_down`
4
+ to test a family of hypotheses arranged in a tree while controlling
5
+ the familywise error rate. The library's two entry points are
6
+ `interweaved_hierarchy`, which builds the whole hypothesis
7
+ architecture for validating an uplift model, and `Hierarchy`, which
8
+ runs the procedure over a tree you assemble yourself.
9
+
10
+ The library accompanies Wilson (2026), *Interpretable and Validatable
11
+ Uplift Modeling*, arXiv:2610.37966, which proves the procedure
12
+ controls the familywise error rate and studies its power by
13
+ simulation. The paper calls the two kinds of uplift hypothesis
14
+ *effect* and *heterogeneity*; the code calls them *magnitude* and
15
+ *difference*.
16
+
17
+ **For AI agents:** This guide gives recipes and conceptual
18
+ orientation, but every public function and class carries a
19
+ NumPy-style docstring documenting its parameters, return values and
20
+ edge cases. Read the docstring before calling:
21
+
22
+ ```python
23
+ from trickle_down import (
24
+ Hierarchy,
25
+ interweaved_hierarchy,
26
+ select_alternative,
27
+ )
28
+
29
+ help(interweaved_hierarchy)
30
+ help(Hierarchy.test_hypotheses)
31
+ help(select_alternative)
32
+ ```
33
+
34
+ The docstrings are the authoritative reference for parameter names,
35
+ types and defaults.
36
+
37
+ ## Conceptual Foundation
38
+
39
+ Suppose we have fitted an uplift model and distilled it into a shallow
40
+ decision tree. We would like to tell a decision-maker several things
41
+ at once: that the treatment helps men, that it harms women, and that
42
+ the difference between the two is real. Each of those sentences is a
43
+ hypothesis test, and reporting whichever ones came out significant
44
+ without adjustment inflates the error rate.
45
+
46
+ The ordinary remedy is Bonferroni: test each of the $m$ hypotheses
47
+ at $\alpha / m$. This is valid but wasteful. It charges the same
48
+ price for the root of the tree, which we have ample power to test, as
49
+ for a leaf describing two percent of the population, and it cannot
50
+ reuse the level spent on a hypothesis we went on to reject.
51
+
52
+ The Trickle Down Procedure exploits the tree. Each node carries two
53
+ numbers:
54
+
55
+ - its **weight** $w$, the share of its parent's level it receives,
56
+ normalized so that siblings' weights sum to one; and
57
+ - its **stake** $v \in [0, 1]$, the fraction of its own available
58
+ level it wagers on its own test.
59
+
60
+ Write $a$ for the level available at a node. The node tests its
61
+ hypothesis at $v a$. What it passes to its children depends on the
62
+ verdict:
63
+
64
+ - **Rejected.** The node passes its whole available level $a$ down,
65
+ split among the children in proportion to their weights. If the
66
+ hypothesis was true, rejecting it is already a familywise error, and
67
+ nothing that happens below can make that worse; if it was false, no
68
+ error was made. Either way the level has done its job and can be
69
+ reused.
70
+ - **Retained.** The node passes only the unwagered part,
71
+ $(1 - v) a$, again split by weight.
72
+
73
+ A leaf should stake everything, $v = 1$: there is nothing below it
74
+ to hold level in reserve for, and a procedure that leaves level
75
+ unspent is uniformly improvable.
76
+
77
+ The consequence is that evidence trickles downward. A strong effect at
78
+ the root buys the full level for the subgroup analysis beneath it, and
79
+ the deep, narrow claims that Bonferroni could never afford become
80
+ testable. Nothing trickles sideways: a node's level reaches only its
81
+ own descendants, never a sibling's subtree, so the split between
82
+ siblings is zero-sum.
83
+
84
+ ### What this controls
85
+
86
+ The procedure controls the familywise error rate strongly at
87
+ $\alpha$: the probability of rejecting **any** true hypothesis is at
88
+ most $\alpha$, whatever the configuration of true and false
89
+ hypotheses. This holds for any non-negative weights and any stakes in
90
+ $[0, 1]$, so those are tuning choices and not correctness
91
+ conditions. The paper requires $v > 0$; the library also accepts
92
+ $v = 0$, which never rejects and passes the node's whole available
93
+ level to its children, as if the node were not there.
94
+
95
+ Strong control is the guarantee a reader needs when the findings are
96
+ consumed selectively. False discovery rate control permits a
97
+ proportion of false positives, and that guarantee does not survive an
98
+ audience applying its own filters to the results.
99
+
100
+ ### Relationship to the graphical approach
101
+
102
+ The Trickle Down Procedure is a special case of the graphical approach
103
+ of Bretz, Maurer, Brannath and Posch (2009), under the encoding in
104
+ `trickle_down_graph`. The `graphical` module implements their
105
+ Algorithm 1 directly so the equivalence can be checked empirically
106
+ rather than only argued on paper; `test_graphical.py` does exactly
107
+ that over random trees. For ordinary use you do not need the module,
108
+ but it is the thing to reach for when you want to confirm that a
109
+ hierarchy behaves as the theory says.
110
+
111
+ ## Imports
112
+
113
+ ```python
114
+ from trickle_down import (
115
+ ArmSummary,
116
+ Claim,
117
+ ContrastTest,
118
+ FixedPValue,
119
+ FTest,
120
+ GraphicalProcedure,
121
+ Hierarchy,
122
+ Hypothesis,
123
+ TTest,
124
+ UpliftClaims,
125
+ UpliftNode,
126
+ bonferroni_rejections,
127
+ hierarchical_bonferroni_rejections,
128
+ interweaved_hierarchy,
129
+ select_alternative,
130
+ trickle_down_graph,
131
+ )
132
+ ```
133
+
134
+ ## Recipe: validate an uplift model
135
+
136
+ This is the case the library was written for. Fit the uplift model on
137
+ a training split, evaluate it on a held-out split, and certify what
138
+ the evaluation split supports.
139
+
140
+ ```python
141
+ from trickle_down import (
142
+ ArmSummary,
143
+ UpliftNode,
144
+ interweaved_hierarchy,
145
+ )
146
+
147
+ men = UpliftNode(
148
+ name="men",
149
+ treated=ArmSummary(mean=0.42, variance=0.24, size=5_000),
150
+ control=ArmSummary(mean=0.35, variance=0.23, size=5_000),
151
+ tau_train=0.065,
152
+ )
153
+ women = UpliftNode(
154
+ name="women",
155
+ treated=ArmSummary(mean=0.29, variance=0.21, size=5_000),
156
+ control=ArmSummary(mean=0.33, variance=0.22, size=5_000),
157
+ tau_train=-0.038,
158
+ )
159
+ everyone = UpliftNode(
160
+ name="everyone",
161
+ treated=ArmSummary(mean=0.355, variance=0.229, size=10_000),
162
+ control=ArmSummary(mean=0.340, variance=0.224, size=10_000),
163
+ tau_train=0.014,
164
+ children=[women, men],
165
+ )
166
+
167
+ claims = interweaved_hierarchy(everyone, alpha=0.05).test()
168
+ print(claims)
169
+
170
+ for claim in claims.certified():
171
+ print(claim.sentence())
172
+ ```
173
+
174
+ Three things about the inputs matter.
175
+
176
+ **Moments come from the evaluation split.** The `treated` and
177
+ `control` summaries are what the hypotheses are tested against. Pass
178
+ the unbiased sample variance (`ddof=1`), not the squared standard
179
+ error; the tests divide by the sample size themselves.
180
+
181
+ If you are starting from the observations rather than from moments,
182
+ use `ArmSummary.from_samples`, which gets the `ddof` right:
183
+
184
+ ```python
185
+ # y: outcomes; z: treatment indicator; subgroup: boolean mask
186
+ treated = ArmSummary.from_samples(y[subgroup & (z == 1)])
187
+ control = ArmSummary.from_samples(y[subgroup & (z == 0)])
188
+ ```
189
+
190
+ `np.var` defaults to `ddof=0`, which understates the variance by a
191
+ factor of $(n-1)/n$. Feeding that to a Welch test makes it
192
+ anticonservative, and nothing downstream will complain.
193
+
194
+ `from_samples` also refuses a slice containing NaN or infinity.
195
+ Drop or impute missing outcomes before you get here; a single NaN
196
+ gives a NaN variance, a NaN statistic and a NaN p-value, and
197
+ `nan <= level` is false, so the node retains and the report looks
198
+ ordinary. Deciding what a missing outcome means is yours, not the
199
+ library's.
200
+
201
+ **`tau_train` comes from the training split, and only picks a
202
+ direction.** It never enters a p-value. Choosing the side of a
203
+ one-sided test from an independent split is legitimate because,
204
+ conditional on that split, the family of hypotheses is fixed (Cox,
205
+ 1975). A wrong guess costs power, not validity.
206
+
207
+ **Node names must be unique and children come in pairs.** Duplicate
208
+ names raise, because claims are joined back to nodes by name and a
209
+ collision would silently report one node's estimate under another
210
+ node's hypothesis. A node has either zero children or two.
211
+
212
+ ### The hypothesis architecture
213
+
214
+ `interweaved_hierarchy` attaches two kinds of hypothesis to the
215
+ decision tree and interweaves them. Writing $M_S$ for the magnitude
216
+ hypothesis at node $S$ and $D_S$ for the difference hypothesis at
217
+ its split,
218
+
219
+ ```text
220
+ M_root -> D_root -> { M_left , M_right } -> ...
221
+ ```
222
+
223
+ A magnitude hypothesis asks whether the average treatment effect in
224
+ the node's subpopulation clears a threshold `delta`. A difference
225
+ hypothesis asks whether the effect differs between the node's two
226
+ children, which is what it means for the split to have found real
227
+ heterogeneity. Each magnitude hypothesis has exactly one child, the
228
+ difference hypothesis at the same node; each difference hypothesis has
229
+ the two child magnitude hypotheses as its children. Leaves carry a
230
+ magnitude hypothesis and nothing below.
231
+
232
+ The ordering matters: a split is only interesting once we believe
233
+ something about the node it splits, so the magnitude test's verdict
234
+ sets the level available to the difference test, whose verdict in turn
235
+ sets the level available to the children. With a stake below one a
236
+ retention reduces that level rather than ending testing.
237
+
238
+ ### The knobs
239
+
240
+ | Parameter | Default | What it does |
241
+ |--------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
242
+ | `alpha` | required | Familywise error rate. |
243
+ | `delta` | `0.0` | Threshold for the magnitude hypotheses. Zero tests for benefit or harm; positive sets a break-even point an expensive treatment must clear; negative sets a non-inferiority margin. |
244
+ | `magnitude_stake` | `0.5` | Stake for magnitude hypotheses at internal nodes. Leaves always stake one. |
245
+ | `difference_stake` | `0.5` | Stake for difference hypotheses. |
246
+ | `adaptive` | `True` | Pick each test's direction from the training split. `False` makes every test two-sided. |
247
+ | `size_weighted` | `False` | Split a node's level between children in proportion to subpopulation size rather than equally. |
248
+
249
+ A stake below one keeps part of the level in reserve, so testing
250
+ continues past a retention at reduced power. A stake of one is
251
+ all-or-nothing: retain at a node and everything below it is tested at
252
+ level zero.
253
+
254
+ Equal weights treat a small subpopulation as worth as much attention
255
+ as a large one, which is what finding a carve-out requires. Size
256
+ weighting buys power where the most people are. Neither is more
257
+ correct.
258
+
259
+ ## Recipe: one-sided tests and what they buy
260
+
261
+ With `adaptive=True` the procedure commits to a direction before
262
+ seeing the evaluation split, which buys two things.
263
+
264
+ The first is a factor of two in level. The second, and the more
265
+ important, is the strength of the claim. A two-sided rejection of a
266
+ difference hypothesis licenses "the effect differs across this split",
267
+ which leaves the reader to infer the direction from an uncertified
268
+ point estimate. A one-sided rejection licenses "the effect is larger
269
+ for men than for women", which is a sentence someone can act on.
270
+ Going from the first to the second is a Type III error argument this
271
+ library does not make.
272
+
273
+ To choose a direction outside the uplift machinery:
274
+
275
+ ```python
276
+ from trickle_down import (
277
+ TTest,
278
+ select_alternative,
279
+ )
280
+
281
+ alternative = select_alternative(tau_train=0.065, delta=0.02) # "greater"
282
+
283
+ test = TTest(
284
+ mean1=0.42,
285
+ mean2=0.35,
286
+ var1=0.24,
287
+ var2=0.23,
288
+ n1=5_000,
289
+ n2=5_000,
290
+ delta=0.02,
291
+ alternative=alternative,
292
+ name="M[men]",
293
+ )
294
+ ```
295
+
296
+ `alternative="greater"` tests $H_0: \mu_1 - \mu_2 \leq \delta$, so
297
+ rejecting proves the effect exceeds `delta`. `"less"` tests the
298
+ reverse inequality. The naming follows scipy's.
299
+
300
+ ## Recipe: build a hierarchy by hand
301
+
302
+ `Hierarchy` runs the procedure over any tree of `Hypothesis` objects,
303
+ so the uplift architecture is one use and not the only one. A common
304
+ pattern is an omnibus F-test gating the pairwise comparisons beneath
305
+ it: there is often power to show the groups differ without power to
306
+ say which is best, and the omnibus test spends nothing on the pairwise
307
+ tests until that much is established.
308
+
309
+ ```python
310
+ import numpy as np
311
+ from trickle_down import (
312
+ FTest,
313
+ Hierarchy,
314
+ TTest,
315
+ )
316
+
317
+ rng = np.random.default_rng(0)
318
+ a, b, c = (rng.normal(size=400) for _ in range(3))
319
+
320
+ omnibus = FTest(a, b, c, stake=0.5, name="any difference")
321
+ for label, (x, y) in {"a vs b": (a, b), "a vs c": (a, c)}.items():
322
+ omnibus.add_child(
323
+ TTest(
324
+ mean1=x.mean(),
325
+ mean2=y.mean(),
326
+ var1=x.var(ddof=1),
327
+ var2=y.var(ddof=1),
328
+ n1=len(x),
329
+ n2=len(y),
330
+ weight=1.0,
331
+ stake=1.0,
332
+ name=label,
333
+ )
334
+ )
335
+
336
+ hierarchy = Hierarchy(fwer=0.05, root=omnibus)
337
+ hierarchy.test_hypotheses()
338
+
339
+ for hypothesis in hierarchy.hypotheses():
340
+ print(hypothesis.name, hypothesis.rejected, hypothesis.available_level)
341
+ ```
342
+
343
+ Weights are relative; siblings are normalized to sum to one when the
344
+ procedure runs, so passing `1.0` everywhere gives an even split. The
345
+ root's own weight is ignored, since it takes no share from a parent.
346
+
347
+ When the test you want is not one this library implements, a
348
+ randomization test or a bootstrap or a model-based Wald test, wrap its
349
+ p-value:
350
+
351
+ ```python
352
+ from trickle_down import FixedPValue
353
+
354
+ node = FixedPValue(p=0.013, stake=0.5, name="bootstrap test")
355
+ ```
356
+
357
+ `FixedPValue` is also how the simulations drive a hierarchy with
358
+ prescribed p-values.
359
+
360
+ ## Recipe: bounds that survive a retention
361
+
362
+ A retention is not the end of what the data support. The level a
363
+ hypothesis was tested at is the level its inversion is valid at, so
364
+ inverting the test gives a confidence bound simultaneous with every
365
+ other claim the procedure makes.
366
+
367
+ In the running example every claim is rejected at $\alpha = 0.05$.
368
+ At $\alpha = 0.001$ the overall magnitude claim is retained:
369
+
370
+ ```python
371
+ strict = interweaved_hierarchy(everyone, alpha=0.001).test()
372
+ for claim in strict.claims:
373
+ if not claim.rejected:
374
+ print(claim.bound_sentence())
375
+ # the effect for everyone is above -0.00715
376
+ ```
377
+
378
+ This turns "we cannot show the treatment helps everyone" into "we
379
+ cannot show the treatment helps everyone, and it costs at most 0.007
380
+ per person", which is a far more useful sentence.
381
+
382
+ Three properties of these bounds are worth keeping straight.
383
+
384
+ **They are simultaneous, not marginal.** They are not the intervals
385
+ one would report for a single node in isolation, and for a deep node
386
+ they are correspondingly wide. `claim.interval` gives the raw
387
+ inversion at `claim.level`.
388
+
389
+ **A rejecting node may claim only its threshold.** `reportable_bound`
390
+ truncates at `delta`. A node that rejected has already passed its
391
+ level down to its children, so a threshold past the one it cleared is
392
+ a fresh claim it has nothing left to pay for.
393
+
394
+ **A two-sided claim gets no free bound.** Its enlarged family is not
395
+ nested, so the node holds a true member whatever the effect is and
396
+ rejecting never releases the level. `bound_sentence` says so rather
397
+ than reporting a number.
398
+
399
+ `FixedPValue` cannot be inverted: a p-value computed elsewhere carries
400
+ no estimate or standard error, so `confidence_interval` raises
401
+ `NotImplementedError`. Calling `claim.interval` on such a claim will
402
+ surface that.
403
+
404
+ ## Recipe: compare against the comparators
405
+
406
+ ```python
407
+ plain = bonferroni_rejections(claims.claims, alpha=0.05)
408
+ hier = hierarchical_bonferroni_rejections(claims.claims, alpha=0.05)
409
+ trickle = [claim.rejected for claim in claims.claims]
410
+ ```
411
+
412
+ `bonferroni_rejections` tests everything at $\alpha / m$, ignoring
413
+ the tree. `hierarchical_bonferroni_rejections` tests depth $k$ at
414
+ $2^{-k} \alpha / n_k$, where $n_k$ counts the hypotheses at that
415
+ depth; the levels sum to at most $\alpha$, so it is valid, but power
416
+ falls off exponentially with depth. Both are useful as the baseline in
417
+ a simulation.
418
+
419
+ The $k$ there is depth in the **hypothesis** tree, counted from one,
420
+ which interleaves the two kinds: a decision-tree node at depth $d$
421
+ contributes its magnitude hypothesis at $2d + 1$ and its difference
422
+ hypothesis at $2d + 2$. `Claim.depth` is the decision-tree depth,
423
+ counted from zero. They are different numbers and are not
424
+ interchangeable — swapping them leaves a procedure that still looks
425
+ valid and spends quite different levels.
426
+
427
+ ## Recipe: report results by depth
428
+
429
+ ```python
430
+ from collections import defaultdict
431
+
432
+ by_depth = defaultdict(list)
433
+ for claim in claims.claims:
434
+ by_depth[claim.depth].append(claim)
435
+
436
+ for depth in sorted(by_depth):
437
+ at_depth = by_depth[depth]
438
+ certified = sum(claim.rejected for claim in at_depth)
439
+ print(f"depth {depth}: {certified} / {len(at_depth)}")
440
+ ```
441
+
442
+ `Claim.depth` is the depth of the decision-tree node the claim is
443
+ about, with the root at zero; both claims at a node share it. Read it
444
+ off the field rather than parsing the node name — a name is a
445
+ human-readable path like `x2>0.5, x9<=-0.4`, and counting its commas
446
+ breaks the moment a name contains one for another reason.
447
+
448
+ Depth is worth separating out. A hierarchy concentrates level near the
449
+ top and starves the bottom, so two procedures that look similar on a
450
+ total claim count can differ sharply at every individual depth.
451
+
452
+ ## Recipe: check a hierarchy against the graphical approach
453
+
454
+ ```python
455
+ import numpy as np
456
+ from trickle_down import (
457
+ GraphicalProcedure,
458
+ trickle_down_graph,
459
+ )
460
+
461
+ root = claims.hierarchy.root # already tested, from the first recipe
462
+ ordered, allocation, transitions = trickle_down_graph(root, alpha=0.05)
463
+ procedure = GraphicalProcedure(allocation, transitions)
464
+ rejected = procedure.reject([h.pvalue() for h in ordered])
465
+
466
+ assert list(rejected) == [h.rejected for h in ordered]
467
+ ```
468
+
469
+ `procedure.alpha` reports the total level allocated, which is less
470
+ than the nominal $\alpha$ when some root-to-leaf path ends at a node
471
+ with stake below one. That shortfall is real: such a procedure can be
472
+ uniformly improved.
473
+
474
+ The `selection` argument to `reject` exposes which rejectable
475
+ hypothesis is taken first ("argmin", "first" or "random"). Bretz et
476
+ al. note the final rejection set does not depend on that choice; the
477
+ argument exists so the invariance can be tested.
478
+
479
+ ## Pitfalls
480
+
481
+ **Re-running re-tests everything.** `Hierarchy.test_hypotheses`
482
+ re-tests every node on each call, so changing `fwer` and calling it
483
+ again gives verdicts consistent with the new levels. The previous
484
+ verdicts are overwritten, so record any you want to keep first.
485
+
486
+ **A `Hypothesis` is single-use.** Each instance caches its p-value
487
+ on first call. Do not reuse one across datasets.
488
+
489
+ **Weights are normalized in place.** Both `test_hypotheses` and
490
+ `trickle_down_graph` rewrite `child.weight` so that siblings sum to
491
+ one. Read the weights you passed in from your own records, not off the
492
+ objects afterwards.
493
+
494
+ **Level can reach zero.** A node below a chain of retentions with
495
+ stake one is tested at level zero, and `pvalue() <= 0` is false for
496
+ any positive p-value. This is the procedure working as designed, not a
497
+ bug, but it means a printed level of `0.0000` deserves a glance at
498
+ what happened above.
499
+
500
+ **Scoring a selective procedure needs the realized directions.** When
501
+ `select_alternative` picks each test's direction from a training
502
+ split, which nulls are true becomes data-dependent. If a node's effect
503
+ is positive and the split pointed its test at harm, that null is true,
504
+ and rejecting it is a Type I error rather than a claim earned. A
505
+ simulation that scores against a fixed two-sided null set --- where a
506
+ null holds only when the effect sits at the threshold exactly ---
507
+ credits the procedure with power for a wrong guess and hides what
508
+ guessing wrong costs. The selection cannot be precomputed: recompute
509
+ the true-null set per replication, against the alternatives the
510
+ procedure actually chose.
511
+
512
+ **`Hierarchy.visualize_hierarchy` imports its drawing libraries
513
+ lazily.** `matplotlib`, `networkx` and `EoN` are installed with
514
+ `trickle-down`, but they are imported only when you draw, so importing
515
+ the package stays fast and works in environments that lack them.
516
+
517
+ ## References
518
+
519
+ Wilson, B. (2026). Interpretable and validatable uplift modeling.
520
+ arXiv:2610.37966.
521
+
522
+ Bretz, F., Maurer, W., Brannath, W., and Posch, M. (2009). A graphical
523
+ approach to sequentially rejective multiple test procedures.
524
+ *Statistics in Medicine*, 28(4), 586-604.
525
+
526
+ Burman, C.-F., Sonesson, C., and Guilbaud, O. (2009). A recycling
527
+ framework for the construction of Bonferroni-based multiple tests.
528
+ *Statistics in Medicine*, 28(5), 739-761.
529
+
530
+ Cox, D. R. (1975). A note on data-splitting for the evaluation of
531
+ significance levels. *Biometrika*, 62(2), 441-444.
@@ -0,0 +1,62 @@
1
+ """Testing Statistical Hypotheses in a Tree-Like Hierarchy."""
2
+
3
+ import importlib.resources
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from trickle_down.graphical import (
8
+ GraphicalProcedure,
9
+ trickle_down_graph,
10
+ )
11
+ from trickle_down.hierarchy import Hierarchy
12
+ from trickle_down.hypothesis import (
13
+ Alternative,
14
+ contrast_moments,
15
+ ContrastTest,
16
+ FixedPValue,
17
+ FTest,
18
+ Hypothesis,
19
+ select_alternative,
20
+ TTest,
21
+ )
22
+ from trickle_down.uplift import (
23
+ ArmSummary,
24
+ bonferroni_rejections,
25
+ Claim,
26
+ hierarchical_bonferroni_rejections,
27
+ interweaved_hierarchy,
28
+ UpliftClaims,
29
+ UpliftNode,
30
+ )
31
+
32
+ __all__: list[str] = [
33
+ "Alternative",
34
+ "ArmSummary",
35
+ "Claim",
36
+ "ContrastTest",
37
+ "FTest",
38
+ "FixedPValue",
39
+ "GraphicalProcedure",
40
+ "Hierarchy",
41
+ "Hypothesis",
42
+ "TTest",
43
+ "UpliftClaims",
44
+ "UpliftNode",
45
+ "bonferroni_rejections",
46
+ "contrast_moments",
47
+ "hierarchical_bonferroni_rejections",
48
+ "interweaved_hierarchy",
49
+ "select_alternative",
50
+ "trickle_down_graph",
51
+ "usage",
52
+ ]
53
+
54
+
55
+ def usage() -> str:
56
+ """Return the contents of USAGE.md."""
57
+ return importlib.resources.files(__name__).joinpath("USAGE.md").read_text()
58
+
59
+
60
+ def __dir__() -> list[str]:
61
+ """Restrict tab completion to the public API."""
62
+ return __all__