skiftet_statistical 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +308 -8
- data/README.md +1416 -39
- data/lib/skiftet_statistical/additive_contrasts.rb +894 -0
- data/lib/skiftet_statistical/bandit/allocator.rb +477 -63
- data/lib/skiftet_statistical/bandit/checks.rb +128 -0
- data/lib/skiftet_statistical/bandit/decay.rb +86 -0
- data/lib/skiftet_statistical/bandit/draw.rb +179 -0
- data/lib/skiftet_statistical/bandit/normal_argmax.rb +209 -0
- data/lib/skiftet_statistical/bandit/pass.rb +94 -0
- data/lib/skiftet_statistical/bandit/posteriors.rb +497 -0
- data/lib/skiftet_statistical/bandit/prior.rb +264 -0
- data/lib/skiftet_statistical/bandit/probabilities.rb +929 -0
- data/lib/skiftet_statistical/bandit/ranking.rb +552 -0
- data/lib/skiftet_statistical/bandit/summary.rb +152 -0
- data/lib/skiftet_statistical/bandit/tallies.rb +297 -0
- data/lib/skiftet_statistical/bandit.rb +3 -3
- data/lib/skiftet_statistical/beta_argmax.rb +526 -0
- data/lib/skiftet_statistical/beta_function.rb +28 -1
- data/lib/skiftet_statistical/cholesky.rb +244 -0
- data/lib/skiftet_statistical/cusum.rb +183 -0
- data/lib/skiftet_statistical/descriptive.rb +3 -3
- data/lib/skiftet_statistical/gauss_legendre.rb +73 -0
- data/lib/skiftet_statistical/hierarchical_rates.rb +450 -0
- data/lib/skiftet_statistical/inputs.rb +214 -0
- data/lib/skiftet_statistical/linear_value.rb +364 -0
- data/lib/skiftet_statistical/numbers.rb +98 -0
- data/lib/skiftet_statistical/prequential.rb +206 -0
- data/lib/skiftet_statistical/shared_credit.rb +811 -0
- data/lib/skiftet_statistical/significance.rb +5 -6
- data/lib/skiftet_statistical/version.rb +1 -1
- data/lib/skiftet_statistical.rb +43 -4
- metadata +37 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8324b9625b6fd9bc14c83e0012409543d0de68b2dd876187f76c5124afe800c6
|
|
4
|
+
data.tar.gz: a4feb3dd72cce8563ff41c8b3e08aecdbab2c69ee8429a9e1592896b2e9f85cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23fa458b6316cc5baa296ce0e33e556e0eb7b8552c939e3ac9e4dd9ffa27b24ecca44af4458403ca26c054b998a1074f5899a977ee7319c41e8c8c24a8feb00f
|
|
7
|
+
data.tar.gz: 33cd6f0ba9b919182cdf445a41e24a3b3be45ce98356eb7c616c5f7ef6b9f9e73b211ed486e99169427d5a7083fe47b9356c289a058961c7d87cd0119e22d1fe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,311 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - unreleased
|
|
4
|
+
|
|
5
|
+
The generic bandit: one posterior set of items, ordered and picked by one rule
|
|
6
|
+
from a key, given uniforms or an RNG, with the odds of what that rule draws
|
|
7
|
+
(exact within the sizes stated below, otherwise a seeded Monte Carlo estimate
|
|
8
|
+
with its standard error) and the summaries an allocation reads. The caller
|
|
9
|
+
hands it items, views and outcomes (yes/no or amounts) and decides what counts
|
|
10
|
+
as each. Pure Ruby, no new dependency, Ruby >= 3.1 as before. 0.3.0 was never
|
|
11
|
+
released, so its additions (its entry below) ship in this version. Every 0.3.0
|
|
12
|
+
module answers as 0.3.0 did, for every input, and every 0.3.0 spec and pin
|
|
13
|
+
passes unedited.
|
|
14
|
+
|
|
15
|
+
- `Bandit::Posteriors`, the posterior set. `yes_no(items, priors: nil)`: Beta
|
|
16
|
+
posteriors from tallies `{ item => { views:, outcomes: } }`, optionally with
|
|
17
|
+
inverse-propensity sums (`weighted_views`, `weighted_outcomes`,
|
|
18
|
+
`weighted_views_squared`) rescaled to the Kish size; priors nil (Beta(1, 1)),
|
|
19
|
+
one pair or one pair an item. `amounts(items, priors:)`: Normal posteriors
|
|
20
|
+
for the mean per view from `views`, `outcomes` (the sum of amounts) and
|
|
21
|
+
`outcome_squares`, under a `Prior::Amount` (or one an item). `beta(pairs,
|
|
22
|
+
views: nil)` and `normal(pairs, views: nil)` take pairs as they are. A set
|
|
23
|
+
answers `items`, `kind`, `parameters`, `views`, `values(rng:)` (one draw an
|
|
24
|
+
item) and `greedy(may_lead: nil)` (the first largest mean). Items are
|
|
25
|
+
non-blank Strings, any number of them, none included, in input order, which
|
|
26
|
+
is the tie order everywhere; a Symbol, a blank or repeated name, a name
|
|
27
|
+
holding a NUL byte or an earlier name's bytes is refused when the set is
|
|
28
|
+
built. `EXACT_PICK_MAX` (20), `EXACT_ORDER_MAX` (10), `LARGEST_TOTAL` (1e6).
|
|
29
|
+
- One pair everywhere: a Beta pair above `LARGEST_TOTAL` is scaled to it, its
|
|
30
|
+
mean kept, in `parameters`, the one pair every draw (`rank`, `pick` and
|
|
31
|
+
`values`, from any source), every odds call (exact or Monte Carlo),
|
|
32
|
+
`summary`'s P(best) and loss and `greedy` read; the pairs as given feed
|
|
33
|
+
`summary`'s means and intervals alone. A scaled pair never sums above
|
|
34
|
+
`LARGEST_TOTAL`, so a set built from another set's `parameters` keeps them
|
|
35
|
+
bit for bit. A Beta parameter above 0 but below
|
|
36
|
+
`BetaArgmax::SMALLEST_PARAMETER` (1e-6) builds; each computation that cannot
|
|
37
|
+
read it (the quantile draws, the odds, `summary`) refuses it, and the RNG
|
|
38
|
+
draws take it.
|
|
39
|
+
- The order rule, written once: `rank(key:, uniforms:, rng:, shuffle: 0.0,
|
|
40
|
+
explore: 0.0, may_lead: nil, min_views: 0.0, pinned: {}, top: nil)` answers
|
|
41
|
+
an `Order` (`items`, `leader`, `branch`, `draws`), and `pick` (`rank` with
|
|
42
|
+
top 1 and no pins) a `Choice` (`item`, `head`, `branch`, `draws`), both
|
|
43
|
+
frozen keyword Structs. The branch is `:shuffled` (by priority), `:explored`
|
|
44
|
+
(an eligible item of smallest priority leads, the rest by draw) or
|
|
45
|
+
`:thompson` (by draw); pinned items keep their positions and draw nothing;
|
|
46
|
+
the leader is the head when it has at least `min_views` views. Exactly one
|
|
47
|
+
source: a key, whose uniforms are `Draw.uniform_for(*key, "explore")` and,
|
|
48
|
+
per item, `(*key, "priority", item)` and `(*key, "draw", item)`, so a key
|
|
49
|
+
draws the same order in every process and no item's uniforms depend on
|
|
50
|
+
another item;
|
|
51
|
+
given uniforms; or an RNG (one branch value, the priorities, one `Sampler`
|
|
52
|
+
draw a free item, nothing else from the stream). A key or given uniforms
|
|
53
|
+
draw by quantile (`BetaArgmax.position`, or mean + sd times the Normal
|
|
54
|
+
quantile on the open grid), an RNG by `Sampler`; each draws exactly from the
|
|
55
|
+
posteriors, so one set of odds covers all three. Over 1 000 keys with
|
|
56
|
+
`shuffle: 0.1` and no exclusion or pin, an added item never changed the
|
|
57
|
+
others' relative order (with an item pinned at position 1 it did on 352).
|
|
58
|
+
Refused, each an `ArgumentError` before any draw and before anything is
|
|
59
|
+
taken from an RNG: a rule outside its bounds, a pinned position or name that
|
|
60
|
+
is not an item, a name pinned twice, a `may_lead` name that is not an item,
|
|
61
|
+
`top` below 1, no source or more than one, a malformed `uniforms` Hash, and
|
|
62
|
+
under a key or given uniforms a Beta parameter below 1e-6. An RNG's own
|
|
63
|
+
answer is checked as it comes: one that is not a Float in [0, 1), or a raise
|
|
64
|
+
from its `rand`, is an `ArgumentError` at that answer, after the draws taken
|
|
65
|
+
before it.
|
|
66
|
+
- `Order#repin(pinned)`: `rank(pinned:)` again on an order's own quantile draws.
|
|
67
|
+
- Exact Normal keys: the order rule compares two Normal draws, from every
|
|
68
|
+
source, by the exact sign of (mean_i - mean_j) + (sd_i z_i - sd_j z_j), each
|
|
69
|
+
mean + sd * z formed exactly, never rounded, so two draws never tie or
|
|
70
|
+
misorder where a standard deviation lies below a rounding of its mean, and
|
|
71
|
+
the odds are the probability of what the rule draws; `draws` answers the
|
|
72
|
+
rounded values.
|
|
73
|
+
- The odds of what the rule draws: `pick_probabilities`, `order_probability`,
|
|
74
|
+
`positions` and `order_summary`, with `Choice#probability` and
|
|
75
|
+
`Order#probability`. They are exact, by quadrature, for a pick among up to
|
|
76
|
+
`EXACT_PICK_MAX` (20) contenders and for orders and positions over up to
|
|
77
|
+
`EXACT_ORDER_MAX` (10) free items with none barred from leading; the orders
|
|
78
|
+
and positions sum on every item's quantiles at 400 levels an item (an
|
|
79
|
+
internal grid, not a public constant), each sum on points where its own
|
|
80
|
+
item's points keep their digits (Betas share one grid in the position on a
|
|
81
|
+
log scale the rule compares; each Normal sums on its own points in its own
|
|
82
|
+
standardised variable, the other items read there), extrapolated from the
|
|
83
|
+
points and their every other level, within 4e-6 of closed forms. Otherwise
|
|
84
|
+
they are one of two stated Monte Carlo forms on `Random.new(seed:)`: the
|
|
85
|
+
pick's share of draws in which each contender is the largest, and the rule's
|
|
86
|
+
own visits for orders and positions, each drawing on the scaled pairs the
|
|
87
|
+
exact odds read. Both forms refuse a Beta parameter below 1e-6 at every
|
|
88
|
+
size, as a keyed draw does.
|
|
89
|
+
- `Posteriors.mixture(shares:, probabilities:)`: the probability that a
|
|
90
|
+
two-layer draw shows each item, the sum over branches of a branch's share
|
|
91
|
+
times its odds.
|
|
92
|
+
- `summary(credible_mass: 0.95, among: nil)`: per item its views, mean and
|
|
93
|
+
equal-tailed interval on the pair as given (`BetaFunction.quantile`,
|
|
94
|
+
accurate to about 2e-10 up to totals near 1e8; beyond about 3e8 one bound
|
|
95
|
+
can fall on or past the mean), and for the items `among` names (at most 20)
|
|
96
|
+
the exact P(best) and expected loss (never below 0), the shape
|
|
97
|
+
`Allocator.from_summary` takes; `among: []` is a means-only summary for a set
|
|
98
|
+
of any size, and `means` answers those means alone (`{ item => mean }`,
|
|
99
|
+
frozen, bit for bit summary's), with no interval and no quantile call.
|
|
100
|
+
- `Bandit::Prior`: `flat`, `given(mean:, strength:)`, `amount(mean:,
|
|
101
|
+
variance:, strength:)` (a frozen `Prior::Amount`), `borrowed(group:, own:)`
|
|
102
|
+
(the one leave-one-out rule), `pooled`, `pooled_new`, `pooled_mean`,
|
|
103
|
+
`tilts(multipliers, clip:)`, `chain(levels:, root_mean:, tilt:, clip:)` and
|
|
104
|
+
`pooled_amount`, which refuses a group with nothing left out or no variance
|
|
105
|
+
(the caller then states its own scale with `amount`).
|
|
106
|
+
- `Bandit::Decay`: `weight(age, half_life:, cap: nil)`, 2 ** (-age /
|
|
107
|
+
half_life) and 0.0 past the cap, and `fold(rows, half_life:, cap: nil)`,
|
|
108
|
+
every sum of the rows weighted.
|
|
109
|
+
- `Bandit::Draw`: `uniform(text)` (the first 8 bytes of SHA256, on the 2**53
|
|
110
|
+
grid), `uniform_for(*parts)`, `seed_for(*parts)`, `weighted(weights,
|
|
111
|
+
uniform)` (the one draw by weight, `[key, share]`; a weight that is not a
|
|
112
|
+
finite number above 0 counts 0), `from_weights` (its key) and
|
|
113
|
+
`open_uniform(uniform)` (clamped to [2**-53, 1 - 2**-53], the one reading of
|
|
114
|
+
a quantile draw's uniform).
|
|
115
|
+
- `Bandit::NormalArgmax`: `probabilities(params, epsilon: 0.0)` and
|
|
116
|
+
`expected_maximum(params)`, BetaArgmax's rule on Normals. Each member is
|
|
117
|
+
integrated in its own standardised variable, so a narrow member far from
|
|
118
|
+
the others is weighed on its own scale; each answer lies within
|
|
119
|
+
2 k (k + 1) * 1e-10 + 2e-12 of the exact probability for k members.
|
|
120
|
+
- `Bandit::Pass.call(status:, hold:, crown: true, seed: Random.new_seed)`: one
|
|
121
|
+
allocation pass with a hold on its win. The block is yielded (false,
|
|
122
|
+
`Random.new(seed)`), then, only when `crown` and the status is "running" and
|
|
123
|
+
the hold is satisfied for the probe's leader, again with (true, a fresh
|
|
124
|
+
`Random.new(seed)`), so the probe and the crown read the same draws; the
|
|
125
|
+
answer is the last block's Hash plus `ready:`.
|
|
126
|
+
- `BetaArgmax.position(uniform, alpha, beta)`, `distribution(x, alpha, beta)`
|
|
127
|
+
and `expected_maximum(params)`, beside `probabilities`, `pick` and `greedy`,
|
|
128
|
+
which are unchanged (`pick` compares its own 0.3.0 position). `position` is a
|
|
129
|
+
guarded Newton step on the same distribution function. Against 0.3.0's
|
|
130
|
+
bisection, recorded on 30 members at 1 000 uniforms each, the spec bounds
|
|
131
|
+
|position - recorded| / max(1, |recorded|), an absolute error where the
|
|
132
|
+
recorded position lies within 1 of 0 and a relative one beyond: 1e-12
|
|
133
|
+
(measured at most 4.7e-13), 1e-9 above alpha + beta 1e4 (measured 3.0e-13)
|
|
134
|
+
and 1e-8 at the smallest parameter (measured 9.5e-11).
|
|
135
|
+
- `Policies::ThompsonSampling` and `Policies::Softmax` are kept as 0.3.0 wrote
|
|
136
|
+
them, as are `EpsilonGreedy`, `UCB1`, `Sampler` and every other 0.3.0 module:
|
|
137
|
+
each answers as in 0.3.0 for every input, the same value or the same error,
|
|
138
|
+
and the same use of the rng. New callers draw through `Bandit::Posteriors`.
|
|
139
|
+
- Every number the code new in 0.4.0 reads lies in one input domain: a real
|
|
140
|
+
number whose Float is finite and at most `SkiftetStatistical::Numbers::LARGEST`
|
|
141
|
+
(1e100) either side of 0. Anything else, a larger number included, is an
|
|
142
|
+
`ArgumentError` where it is read, and no product the new code forms
|
|
143
|
+
multiplies more than two numbers of that size, so no sum or product of
|
|
144
|
+
inputs leaves a Float; every answer of the new bandit is checked finite
|
|
145
|
+
before it is returned, and a Normal quantile draw reads its uniform on
|
|
146
|
+
[2**-53, 1 - 2**-53].
|
|
147
|
+
- A caller's object whose method raises where the new code calls it (a key
|
|
148
|
+
part's `to_s`, a name's `hash` or `eql?`, a list's `each`, an rng's `rand`),
|
|
149
|
+
with a `StandardError` or a `NotImplementedError`, is an `ArgumentError`
|
|
150
|
+
naming the input; a caller's String, Hash or Array is read by its contents,
|
|
151
|
+
never by its own methods, so a missing key reads as absent whatever default
|
|
152
|
+
the Hash holds, and a refusal names a class as Ruby records it. A list is
|
|
153
|
+
any Enumerable but a Hash, read once by its `each`.
|
|
154
|
+
- The moments of amounts, the amounts posterior's mean and every shrunk mean
|
|
155
|
+
(`Prior.pooled_mean`, `pooled`, `pooled_new`, `chain`) are formed exactly
|
|
156
|
+
from the Floats given and rounded once to the nearest Float, and the
|
|
157
|
+
posterior's standard deviation from those moments, so sums that cancel and
|
|
158
|
+
products that would underflow change no answer. A caller's Hash is read by
|
|
159
|
+
its keys as names (items, or a tally's sums): one that compares its keys by
|
|
160
|
+
identity reads as a plain one, and two keys of one name are refused.
|
|
161
|
+
- Two new simulations, each asserting its bounds, every figure in the README,
|
|
162
|
+
"Bandit simulations: measured results". The order rule at small-site
|
|
163
|
+
traffic (`order_simulation_spec.rb`, 13 cells of 50 runs, 28 days each):
|
|
164
|
+
with five items at close rates (5 down to 3 %) and 300, 1 000 and 3 000
|
|
165
|
+
visits a day, `rank(rng:, shuffle: 0.10)` lost 2.4, 1.6 and 1.1 % of the
|
|
166
|
+
best order's expected outcomes, where a fixed random order lost 8.0, 7.4 and
|
|
167
|
+
7.4 %, and put the best item on top on day 28 in 88, 100 and 100 % of runs;
|
|
168
|
+
on amounts (4 down to 1.5 %) it lost 4.3, 2.6 and 1.9 % (a fixed order 12.6
|
|
169
|
+
to 13.8 %); with ten items at 3 000 a day 2.2 % (19.6 %); every cell's
|
|
170
|
+
random share 0.099 to 0.1003 of its visits. The odds against the rule's own
|
|
171
|
+
visits (`odds_simulation_spec.rb`, 2 * 10^5 visits on five fixtures): every
|
|
172
|
+
head and position within 2.75 standard errors of its exact value, the orders'
|
|
173
|
+
chi-square p-values 0.12 to 0.88; P(best) and expected loss within 1.61 and
|
|
174
|
+
1.44 standard errors of 10^6 joint draws.
|
|
175
|
+
- The simulations draw by weight through `Draw.from_weights` (the four copies
|
|
176
|
+
of that draw are gone), and no count moved: 12,263,200 draws compared, 0
|
|
177
|
+
different. Block (j) of the two-layer simulation draws through the posterior
|
|
178
|
+
set (`pick_probabilities`, `greedy`, `from_weights`, `mixture`), its figures
|
|
179
|
+
unchanged (0.5807 and 0.4193; as they ran, 0.6273 and 0.3727). Every count
|
|
180
|
+
and figure of 0.3.0's entry, measured again on 0.4.0, came out the same, its
|
|
181
|
+
run time aside (below).
|
|
182
|
+
- The ten simulation files took 2,013 seconds with YJIT on one development
|
|
183
|
+
machine while other work kept it busy (a load average of 31 to 46), the
|
|
184
|
+
order file 667 of them (263 to 407 in other runs), each figure the sum of
|
|
185
|
+
the examples' own run times, boot and load left out, as 0.3.0's figures
|
|
186
|
+
are; CI's simulations job's timeout goes from 30 to 45 minutes.
|
|
187
|
+
- Costs, measured as CPU time with YJIT: a keyed `rank` of 8 Beta items 0.21
|
|
188
|
+
ms (totals up to 1e3) and 0.54 ms (up to 1e5), of 8 Normals 0.08 ms; an RNG
|
|
189
|
+
`rank` of 8 items 0.017 ms; `BetaArgmax.position` 0.02 to 0.06 ms from
|
|
190
|
+
Beta(3, 300) to Beta(4 000, 96 000), where 0.3.0's bisection took 0.11 to
|
|
191
|
+
0.92 ms in the same run; exact odds and summaries 2 to 560 ms
|
|
192
|
+
a call (1.2 s for the positions of 10 items with exploration), so a caller
|
|
193
|
+
computes them off the request path, once a pass, never once a visit.
|
|
194
|
+
- Docs in generic words; the gemspec's summary and description cover the
|
|
195
|
+
whole toolkit, and its metadata names the source once (`homepage_uri` gone,
|
|
196
|
+
`spec.homepage` kept), so the gem builds with no warning.
|
|
197
|
+
|
|
198
|
+
## [0.3.0] - not released; its additions ship in 0.4.0
|
|
199
|
+
|
|
200
|
+
Allocating on a value of several parts, pooling across cells and groups,
|
|
201
|
+
hierarchical rates, an exact Beta argmax, a prequential score and a change
|
|
202
|
+
test, with per-arm floors and fixed shares in the allocator and one phase rule
|
|
203
|
+
for any posterior. Pure Ruby, no new dependency, Ruby >= 3.1 as before. Every
|
|
204
|
+
0.2.0 call answers as before, bit for bit (the same signatures, defaults,
|
|
205
|
+
arithmetic order and draw order), and every 0.2.0 spec passes unedited.
|
|
206
|
+
|
|
207
|
+
- `Bandit::Allocator.call` gains `arm_floors:` (`{ arm => floor }`, each in
|
|
208
|
+
(0, 1], in place of `Config#floor`) and `fixed_shares:` (`{ arm => share }`,
|
|
209
|
+
each in (0, 1), held exactly in every phase), both nil by default. One lift
|
|
210
|
+
serves every phase; the free arms share `1 - F` by P(best) taken among
|
|
211
|
+
themselves; a fixed arm's `p_best` and `expected_loss` are nil, and a fixed
|
|
212
|
+
arm is never the won arm. Refused with an `ArgumentError` before any draw: a
|
|
213
|
+
key that is not an arm, an arm in both, floors and shares summing above 1;
|
|
214
|
+
every arm fixed raises `FixedAllocation`.
|
|
215
|
+
- `Bandit::Allocator.from_summary(arms:, summary:, exposures:, status:,
|
|
216
|
+
winner_arm:, previous_weights: nil, hold_satisfied: true, config: Config.new,
|
|
217
|
+
fixed_shares: nil, arm_floors: nil)` — the same paused / won / warm-up /
|
|
218
|
+
automatic-win / shifting rule on statistics computed elsewhere (`summary`
|
|
219
|
+
`{ arm => { p_best:, expected_loss:, mean:, lower:, upper: } }`, or nil to
|
|
220
|
+
hold warm-up), answering `call`'s shape. `call` runs on the same private rule,
|
|
221
|
+
so the two cannot disagree on a phase.
|
|
222
|
+
- `Bandit::Allocator.lift(weights, config: Config.new)` — 0.2.0's private
|
|
223
|
+
`with_floor`, step for step, made public; the private method stays as a
|
|
224
|
+
delegate.
|
|
225
|
+
- `SkiftetStatistical::LinearValue` — `summary(arms:, parts:, means:,
|
|
226
|
+
covariance:, parameters:, weights:, draws:, rng:, credible_mass: 0.95,
|
|
227
|
+
rounding: 0.0)` (per arm `point`, `mean`, `lower`, `upper`, `p_best`,
|
|
228
|
+
`expected_loss`) and `each_draw(...)` (each draw's values, the rows `summary`
|
|
229
|
+
is built on): P(best) of a linear value of several rates, one joint normal
|
|
230
|
+
over every (arm, part) pair, with weights drawn from `fixed`, `uniform` or
|
|
231
|
+
clipped `normal` parameters in every draw. Up to 7 arms and 12 parts
|
|
232
|
+
(`MAX_ARMS`, `MAX_PARTS`). The draw order is part of the API.
|
|
233
|
+
- `SkiftetStatistical::Cholesky` — `factor(rows)`, the lower-triangular factor
|
|
234
|
+
(a pivot with no variance set to 0 with its column), and `root(rows,
|
|
235
|
+
rounding: 0.0)`, the same arithmetic largest pivot first, for a matrix of
|
|
236
|
+
lower rank. No matrix library.
|
|
237
|
+
- `SkiftetStatistical::SharedCredit` — `estimate(rows, arms:, parts:)`: Hájek
|
|
238
|
+
means per (arm, part) and their sandwich covariance when one row may credit
|
|
239
|
+
several arms (an ordinary test credits only the arm drawn, 1 / p), with
|
|
240
|
+
Kish's effective size; `accumulator(arms:, parts:)` (`Accumulator#add`,
|
|
241
|
+
`#merge`, `#to_h`, `.from_h`, `.from_sums`, `#estimate`, `#arms`, `#parts`), the
|
|
242
|
+
sufficient sums folded once and stored as JSON; `from_sums(sums, arms:, parts:)`, the same estimate from
|
|
243
|
+
per-arm sums; `linear(estimate, weights:)` and `contrast(estimate, a, b,
|
|
244
|
+
weights:)` at fixed weights; `contrast_for_fit(estimate, arm, reference,
|
|
245
|
+
weights:, minimum_effective: 20)`, a contrast with one per-visit variance
|
|
246
|
+
pooled over its two arms, nil under the Kish minimum. An estimate
|
|
247
|
+
(`SharedCredit::Estimate`) answers `#parts` and `#rounding`; a covariance
|
|
248
|
+
entry within its cancellation rounding answers exactly 0.0.
|
|
249
|
+
- `SkiftetStatistical::HierarchicalRates` — `leave_one_out_share`, `node`
|
|
250
|
+
(a node's ratio, hot probability and tail under a Gamma-Poisson or two-groups
|
|
251
|
+
prior), `fit_strength` (a level's strength and hot share on `GRID`, 57
|
|
252
|
+
strengths from 2^-2 to 2^12, with its profile interval), `fit_leaves` (the
|
|
253
|
+
leaf level's fit, each parent's null rate estimated jointly),
|
|
254
|
+
`smooth_strength`, `marshall` and `boundary_test`. Counts may be real numbers.
|
|
255
|
+
- `SkiftetStatistical::AdditiveContrasts` — `fit(cells:, arms:, sweeps: 5,
|
|
256
|
+
variances: nil)`: the arms' contrasts against a reference arm pooled along a
|
|
257
|
+
nested tree and crossed groupings, the variances by moments or held fixed,
|
|
258
|
+
answering an `AdditiveContrasts::Fit` whose `#variation_tests` gives each
|
|
259
|
+
variance's Cochran's Q, degrees and p; `baseline(rows:, effects:)`, each
|
|
260
|
+
cell's level from all its visits, adjusted on a covariate; `pair(arm, part)`,
|
|
261
|
+
the name of a pooled (arm, part); `pooled_estimate(own:, effects:, baselines:,
|
|
262
|
+
cell:)`, a cell's pooled estimate in `SharedCredit`'s shape, its level shared
|
|
263
|
+
between the arms and each arm's effect its own.
|
|
264
|
+
- `SkiftetStatistical::BetaArgmax` — `probabilities(params, epsilon: 0.0)`,
|
|
265
|
+
the exact probability that each of up to 20 Betas is the largest (composite
|
|
266
|
+
Gauss-Legendre, exact to 1e-8), `pick(params, explore:, priorities:,
|
|
267
|
+
uniforms:, epsilon:)`, the seedless pick those probabilities describe, and
|
|
268
|
+
`greedy(params)`; `TAIL`, `BREAKS`, `NODES`, `MAX_MEMBERS`,
|
|
269
|
+
`SMALLEST_PARAMETER`, `LARGEST_TOTAL`.
|
|
270
|
+
- `SkiftetStatistical::GaussLegendre.nodes(n)` — the n-point rule on [-1, 1],
|
|
271
|
+
computed once per n.
|
|
272
|
+
- `BetaFunction.log_density(x, alpha, beta)`; `regularized` and `quantile` are
|
|
273
|
+
unchanged.
|
|
274
|
+
- `SkiftetStatistical::Prequential` — `score(rows, depth:)`, `gains(rows,
|
|
275
|
+
depths:)` (the bits per event a deeper predictor saves, with a cluster-robust
|
|
276
|
+
standard error) and `clustered_mean(sums:, counts:, level: 0.95)`.
|
|
277
|
+
- `SkiftetStatistical::Cusum` — `run(z, reference:, threshold:)`,
|
|
278
|
+
`average_run_length(shift:, reference:, threshold:)` (Siegmund's
|
|
279
|
+
approximation), `threshold(reference:, false_alarm_steps:)` and
|
|
280
|
+
`minimum_detectable(reference:, threshold:, horizon_steps:)`.
|
|
281
|
+
- Every new module reads numbers by one check (a finite real number, finite as
|
|
282
|
+
a Float too), takes `draws:` as an Integer >= 0, never converted, and takes
|
|
283
|
+
every random input as an injected `rng:` or given uniforms.
|
|
284
|
+
- `spec/pins/` pins the new outputs (1e-12 relative, 1e-9 through log-gamma, a
|
|
285
|
+
pinned 0.0, 1.0 or infinity exact).
|
|
286
|
+
- Six new simulations in `spec/simulations` (`SIMULATIONS=1`), each asserting
|
|
287
|
+
its bounds, every figure in the README, "Bandit simulations: measured
|
|
288
|
+
results". Among them: no share below its floor or off its fixed share in
|
|
289
|
+
2,800 passes through every phase; nothing crowned in 2,000 passes at
|
|
290
|
+
`hold_satisfied: false`; values arriving up to 23 passes late leaving a final
|
|
291
|
+
bias within 1.08 standard errors; 95 % intervals covering the truth in 94.25
|
|
292
|
+
to 96.25 % of 400 runs; across 40 pooled cells whose levels differ, the
|
|
293
|
+
reference arm's mean share spreading 1.007 times the equal-level noise floor's
|
|
294
|
+
at a rank correlation of -0.073; every level's strength within a factor of 2
|
|
295
|
+
in 20 of 20 runs; the argmax within 2.44 standard deviations of 10^4 picks and
|
|
296
|
+
of 10^6 Beta draws on 30 fixtures; the change test's false alarms 0.983 and
|
|
297
|
+
0.260 per 365 steps against a stated 1 and 0.25. All eight simulation files
|
|
298
|
+
take about 14 minutes with YJIT on one development machine; CI's simulations
|
|
299
|
+
job now sets `RUBY_YJIT_ENABLE`, and its path list holds every new module and
|
|
300
|
+
`spec/pins`.
|
|
301
|
+
- The gemspec no longer lists a contact address (`spec.email` removed).
|
|
302
|
+
|
|
3
303
|
## [0.2.0] - 2026-10-16
|
|
4
304
|
|
|
5
|
-
|
|
6
|
-
place. Pure Ruby, no new dependency; each function keeps
|
|
7
|
-
|
|
8
|
-
did, bit for bit.
|
|
305
|
+
Statistics an app had written more than once, and its A/B-test allocator, in
|
|
306
|
+
one place. Pure Ruby, no new dependency; each function keeps the app's
|
|
307
|
+
arithmetic in the app's order, so on the inputs the app gives it (and the same
|
|
308
|
+
seed) it answers as the app did, bit for bit.
|
|
9
309
|
|
|
10
310
|
- `SkiftetStatistical::Gamma` — `regularized_lower(a, x)` and
|
|
11
311
|
`regularized_upper(a, x)`, the regularized incomplete gamma functions P and Q
|
|
@@ -19,7 +319,7 @@ did, bit for bit.
|
|
|
19
319
|
split equally).
|
|
20
320
|
- `Significance.normal_quantile(p)`, `.chi_squared_survival(statistic, dof)`
|
|
21
321
|
and `.sample_ratio_p_value(observed:, expected:, minimum_total: 200)`.
|
|
22
|
-
- `SkiftetStatistical::Bandit::Allocator` —
|
|
322
|
+
- `SkiftetStatistical::Bandit::Allocator` — the A/B-test allocator:
|
|
23
323
|
`call(arms:, tallies:, status:, winner_arm:, rng:, previous_weights: nil,
|
|
24
324
|
draws:, hold_satisfied: true, allocation: :adaptive, config: Config.new)`
|
|
25
325
|
returns `{ weights:, phase:, stats:, winner:, result_changed:, leader: }`
|
|
@@ -27,7 +327,7 @@ did, bit for bit.
|
|
|
27
327
|
automatic win and the won split); `won_weights(arms, winner, config:)`;
|
|
28
328
|
`Allocator::Config` (warmup_exposures, floor, max_step, win_p_best,
|
|
29
329
|
win_loss, won_share, won_rest, result_changed_below, credible_mass,
|
|
30
|
-
prior_alpha, prior_beta;
|
|
330
|
+
prior_alpha, prior_beta; the app's values by default); `Allocator::
|
|
31
331
|
FixedAllocation` (an `ArgumentError`), raised for a fixed split.
|
|
32
332
|
- `SkiftetStatistical::Bandit::WinHold` — the hold on an automatic win:
|
|
33
333
|
`new(leading_arm:, leading_since:, now:, last_pass_at:, hold_seconds:
|
|
@@ -46,13 +346,13 @@ did, bit for bit.
|
|
|
46
346
|
|
|
47
347
|
## [0.1.0] - 2026-06-23
|
|
48
348
|
|
|
49
|
-
Initial release —
|
|
349
|
+
Initial release — a shared statistics toolkit.
|
|
50
350
|
|
|
51
351
|
- `SkiftetStatistical::Descriptive` — mean, variance, standard deviation,
|
|
52
352
|
percentiles and median.
|
|
53
353
|
- `SkiftetStatistical::Significance` — A/B significance testing: two-proportion
|
|
54
354
|
z-test, Welch's t-test, exact normal CDF and two-tailed p-values (consolidates
|
|
55
|
-
|
|
355
|
+
significance math that two apps had each written).
|
|
56
356
|
- `SkiftetStatistical::Bandit` — arms + a pluggable policy, with `#select`,
|
|
57
357
|
`#record`, `#best_arm`, `#stats`, and Hash (de)serialisation.
|
|
58
358
|
- `SkiftetStatistical::Arm` — online reward statistics (pulls, mean, variance,
|