skiftet_statistical 0.1.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +346 -2
  3. data/README.md +1618 -22
  4. data/lib/skiftet_statistical/additive_contrasts.rb +894 -0
  5. data/lib/skiftet_statistical/bandit/allocator.rb +688 -0
  6. data/lib/skiftet_statistical/bandit/checks.rb +128 -0
  7. data/lib/skiftet_statistical/bandit/decay.rb +86 -0
  8. data/lib/skiftet_statistical/bandit/draw.rb +179 -0
  9. data/lib/skiftet_statistical/bandit/normal_argmax.rb +209 -0
  10. data/lib/skiftet_statistical/bandit/pass.rb +94 -0
  11. data/lib/skiftet_statistical/bandit/posteriors.rb +497 -0
  12. data/lib/skiftet_statistical/bandit/prior.rb +264 -0
  13. data/lib/skiftet_statistical/bandit/probabilities.rb +929 -0
  14. data/lib/skiftet_statistical/bandit/ranking.rb +552 -0
  15. data/lib/skiftet_statistical/bandit/summary.rb +152 -0
  16. data/lib/skiftet_statistical/bandit/tallies.rb +297 -0
  17. data/lib/skiftet_statistical/bandit/weighted_posterior.rb +65 -0
  18. data/lib/skiftet_statistical/bandit/win_hold.rb +86 -0
  19. data/lib/skiftet_statistical/bandit.rb +3 -3
  20. data/lib/skiftet_statistical/beta_argmax.rb +526 -0
  21. data/lib/skiftet_statistical/beta_function.rb +153 -0
  22. data/lib/skiftet_statistical/cholesky.rb +244 -0
  23. data/lib/skiftet_statistical/cusum.rb +183 -0
  24. data/lib/skiftet_statistical/descriptive.rb +3 -3
  25. data/lib/skiftet_statistical/gamma.rb +97 -0
  26. data/lib/skiftet_statistical/gauss_legendre.rb +73 -0
  27. data/lib/skiftet_statistical/hierarchical_rates.rb +450 -0
  28. data/lib/skiftet_statistical/inputs.rb +214 -0
  29. data/lib/skiftet_statistical/joint_beta.rb +63 -0
  30. data/lib/skiftet_statistical/linear_value.rb +364 -0
  31. data/lib/skiftet_statistical/numbers.rb +98 -0
  32. data/lib/skiftet_statistical/prequential.rb +206 -0
  33. data/lib/skiftet_statistical/shared_credit.rb +811 -0
  34. data/lib/skiftet_statistical/significance.rb +65 -5
  35. data/lib/skiftet_statistical/version.rb +1 -1
  36. data/lib/skiftet_statistical.rb +57 -5
  37. metadata +43 -11
@@ -0,0 +1,552 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../numbers"
4
+ require_relative "../inputs"
5
+ require_relative "../significance"
6
+ require_relative "../beta_argmax"
7
+ require_relative "checks"
8
+ require_relative "draw"
9
+ require_relative "posteriors"
10
+
11
+ module SkiftetStatistical
12
+ class Bandit
13
+ # A pick: `item` (the leader: the head when it has at least `min_views` views, else nil), `head` (the first free
14
+ # item, nil when there is none), `branch` (:thompson, :explored or :shuffled) and `draws` ({ item => posterior
15
+ # value } for every free item the branch drew; none on the shuffled branch). Frozen. It carries, in instance
16
+ # variables set before the freeze, the posterior set that made it (@posteriors), the rule it was made under
17
+ # (@rule: { shuffle:, explore:, may_lead:, min_views: }, the keywords of `pick` and of `pick_probabilities`) and an
18
+ # empty Hash made before the freeze (@cache), so its odds can be computed under that rule once and kept. `==`
19
+ # compares the four members only.
20
+ Choice = Struct.new(:item, :head, :branch, :draws, keyword_init: true)
21
+
22
+ # An order: `items` (the first `top` items of the whole order, pinned items at their positions), `leader` (the
23
+ # first free item when it has at least `min_views` views, else nil), `branch` and `draws` (as a Choice's). Frozen,
24
+ # carrying @posteriors, @rule ({ shuffle:, explore:, may_lead:, min_views:, pinned:, top: }, the keywords of `rank`
25
+ # and of `order_probability`) and @cache as a Choice does, and, drawn from a key or given uniforms, what `repin`
26
+ # reads: @rank_source (the source as `rank` read it, library data that holds no caller's object: [:key, the key's
27
+ # parts as the one reading of a keyed draw's parts read them, each its bytes, a frozen String], or [:uniforms,
28
+ # { item => the given priority }, { item => the given draw uniform }], two Hashes the set built, keyed by its own
29
+ # item Strings, each holding for every item the number `rank` read, or, for an item it pinned whose entry is
30
+ # missing or malformed, the words of that refusal; all frozen) and @rank_keys ({ item => the key the rule compared
31
+ # its draw by }, frozen, for every item it drew). `==` compares the four members only.
32
+ Order = Struct.new(:items, :leader, :branch, :draws, keyword_init: true)
33
+
34
+ # THE ORDER RULE, written once: `rank` orders a posterior set's items and `pick` is its head (`rank` with top 1
35
+ # and no pins), so the head, its exploration share and the order by draw are one rule under every source.
36
+ #
37
+ # set = SkiftetStatistical::Bandit::Posteriors.beta({ "a" => [ 2.0, 8.0 ], "b" => [ 8.0, 2.0 ] })
38
+ # set.rank(key: [ "visit", "k1" ], explore: 0.1).items # => an order of "a" and "b", the same for that key
39
+ # set.pick(rng: Random.new(1)).item # => the head of one Thompson draw
40
+ #
41
+ # The rule: `shuffle` and `explore` (each in [0, 1], summing to at most 1), `may_lead` (the items that may lead;
42
+ # nil, or a list naming none of the free items, lets every free item lead), `min_views` (0), `pinned`
43
+ # ({ position => item }, 0-based) and `top` (nil: every item). Pinned items keep their positions and draw nothing;
44
+ # the free items fill the other positions in order. The contenders are the free items `may_lead` names, the
45
+ # eligible the contenders with at least `min_views` views. With the branch value c: c < shuffle is :shuffled (the
46
+ # contender of smallest priority leads, the other free items follow by priority); c < shuffle + explore with an
47
+ # eligible item is :explored (the eligible item of smallest priority leads, the others follow by draw); else
48
+ # :thompson (the contender of largest draw leads, the others follow by draw). Ties go to the earlier item. The
49
+ # leader is the free head when it has at least `min_views` views, else nil.
50
+ #
51
+ # Exactly one source of randomness:
52
+ # - `key:` (a String, or an Array of parts): the branch value is Draw.uniform_for(*key, "explore"), an item's
53
+ # priority uniform_for(*key, "priority", item) and its draw uniform uniform_for(*key, "draw", item), so no
54
+ # item's uniforms depend on another item, on the set or on its order;
55
+ # - `uniforms:` ({ explore: Float, priority: { item => Float }, draw: { item => Float } }): the same three given,
56
+ # explore and every free item's draw uniform in [0, 1] and every free item's priority a finite number (entries
57
+ # for other names are not read);
58
+ # - `rng:` (anything that answers rand, each answer a Float in [0, 1)): one branch value when shuffle + explore is
59
+ # above 0, then one priority per free item, in item order, on the shuffled or explored branch, then one Sampler
60
+ # draw per free item, in item order, where the branch draws (Sampler#beta, or mean + sd * Sampler#gaussian), and
61
+ # nothing else from the stream.
62
+ # Every source draws on the scaled pairs (`parameters`), the one pair the odds read too. A key or given uniforms
63
+ # draw by quantile: a Beta item at BetaArgmax.position(uniform, alpha, beta), compared by that position so that two
64
+ # draws never tie by rounding, its draw the value there (exp(p + JOINT) for a position p below 0, else 1 -
65
+ # exp(JOINT - p), the position's two halves, `rank_beta_half`); a Normal item at mean + sd * z, z the standard
66
+ # quantile Significance.normal_quantile(u) of its uniform u on the open grid (Draw.open_uniform, the one reading of
67
+ # a quantile draw's uniform: clamped to [2**-53, 1 - 2**-53], so the quantile is never infinite). An RNG draws a
68
+ # Beta item by Sampler#beta, compared by its value, and a Normal item at z = Sampler#gaussian. Every Normal draw,
69
+ # whatever its source, is compared by the exact sign of (mean_i - mean_j) + (sd_i z_i - sd_j z_j), its mean + sd
70
+ # * z formed exactly (the set's one Normal draw, `normal_draw`), so two draws never tie or misorder by rounding,
71
+ # even where a standard deviation lies below a rounding of its mean; the value it answers is that sum rounded to a
72
+ # Float. Every source draws exactly from the posteriors, so one set of odds covers all three. `draws` answers
73
+ # posterior values, never positions or exact keys, and every one passes the bandit's one check of an answer
74
+ # (Checks.finite_answer).
75
+ #
76
+ # Refused, as ArgumentErrors before any draw and before anything is taken from an RNG: a rule outside the bounds
77
+ # above, a pinned position that is not an Integer in 0...items.size, a pinned name that is not an item, one name
78
+ # pinned twice, a `may_lead` name that is not an item, `top` that is not an Integer of at least 1, no source or
79
+ # more than one, a malformed `uniforms` Hash, and under a key or given uniforms, whatever branch they choose, a
80
+ # Beta parameter of a free item below BetaArgmax::SMALLEST_PARAMETER (the set's one refusal; a parameter at it is
81
+ # taken, and the RNG source takes any parameter above 0). An empty set, or one whose items are all pinned, ranks
82
+ # with no head and no leader.
83
+ #
84
+ # THE RE-PIN (`Order#repin(pinned)`): an order drawn from a key or given uniforms answers the order its set's rule
85
+ # answers on the same source with `pinned` under the held rule (shuffle 0, explore 0, no `may_lead`, `min_views`
86
+ # 0, `top` as the order's own), so the Thompson order of the items `pinned` leaves free, with the pinned items at
87
+ # their positions, the very Order `rank` answers there, bit for bit, `==` and every draw. It is the order rule's
88
+ # own code fed the order's own draws: a free item the order drew keeps its draw (the key it was compared by and its
89
+ # value), and only an item it did not draw (one it pinned, or every item of a shuffled order) is drawn now, by
90
+ # quantile from the kept source: only what `rank` read (each key part's bytes, each given number as read), never a
91
+ # caller's object, so a key part or a given number the caller changes after `rank` moves no re-pin, which answers
92
+ # `rank` on the source as it was. So a caller holding a head asks once more and pays no second draw:
93
+ #
94
+ # order = set.rank(key: [ "visit", "k1" ], explore: 0.1)
95
+ # order.repin({ 0 => "b" }) == set.rank(key: [ "visit", "k1" ], pinned: { 0 => "b" }) # => true
96
+ #
97
+ # Refused as `rank` refuses on that source (a malformed `pinned`, a Beta parameter below the smallest of an item
98
+ # now free, a given uniform of an item now free missing or malformed at rank time, in the words `rank` gives), and,
99
+ # as an ArgumentError, an order drawn from an RNG (a re-pin needs a key or given uniforms) or one no posterior set
100
+ # made.
101
+ #
102
+ # For the files that reopen this class, private: `rank_rule(...)` (the rule's keywords read and checked once,
103
+ # answered as the frozen Hash an Order carries; the pick odds read their rule through it, with no pins and no top),
104
+ # `rank_free(rule)` (the free items under a rule, which the order rule and the order odds both read),
105
+ # `rank_free_slots(pinned)` (the free-slot rule: the slots the free ranks take, in order, which the order rule's
106
+ # whole order and the order and position odds read), `rank_beta_half(position)` (a Beta position's two halves,
107
+ # which the quantile draws' values and the order odds' distribution functions read) and `rank_ordered(rule, key,
108
+ # uniforms, rng)` (the order rule itself, its first answer the whole order; the odds' Monte Carlo visits it).
109
+ class Posteriors
110
+ # What a key or given uniforms compute, in the words of GB6's refusal.
111
+ RANK_QUANTILE_DRAW = "a quantile draw on the scaled pairs (an RNG draw takes any above 0)"
112
+ private_constant :RANK_QUANTILE_DRAW
113
+
114
+ # The posterior set's items in the order the rule draws: an Order.
115
+ def rank(key: nil, uniforms: nil, rng: nil, shuffle: 0.0, explore: 0.0, may_lead: nil, min_views: 0.0,
116
+ pinned: {}, top: nil)
117
+ rule = rank_rule(shuffle: shuffle, explore: explore, may_lead: may_lead, min_views: min_views, pinned: pinned,
118
+ top: top)
119
+ rank_answered(rule, *rank_ordered(rule, key, uniforms, rng))
120
+ end
121
+
122
+ # The head of the order rule (`rank` with top 1 and no pins): a Choice.
123
+ def pick(key: nil, uniforms: nil, rng: nil, shuffle: 0.0, explore: 0.0, may_lead: nil, min_views: 0.0)
124
+ rule = rank_rule(shuffle: shuffle, explore: explore, may_lead: may_lead, min_views: min_views, pinned: {},
125
+ top: nil)
126
+ _full, head, branch, draws = rank_ordered(rule, key, uniforms, rng)
127
+ Checks.finite_answer(draws.values, "the parameters and the draw's source")
128
+ choice = Choice.new(item: rank_leader(head, rule), head: head, branch: branch, draws: draws)
129
+ rank_carrying(choice, rule.slice(:shuffle, :explore, :may_lead, :min_views).freeze)
130
+ end
131
+
132
+ private
133
+
134
+ # The rule's keywords, each read and checked once: { shuffle:, explore:, may_lead:, min_views:, pinned:, top: },
135
+ # frozen, its names the set's own item Strings. shuffle and explore by the bandit's one [0, 1] bound
136
+ # (Checks.unit), summing to at most 1; may_lead nil or a list of items; min_views any finite number (views are
137
+ # >= 0, so one below 0 asks for none); pinned nil or a Hash of distinct Integer positions in 0...items.size to
138
+ # distinct items; top nil or an Integer of at least 1 within a Float's range (the bandit's one reading of a whole
139
+ # number, Checks.integer). The pick odds read their rule here too.
140
+ def rank_rule(shuffle:, explore:, may_lead:, min_views:, pinned:, top:)
141
+ shuffle = Checks.unit(shuffle, "shuffle")
142
+ explore = Checks.unit(explore, "explore")
143
+ if shuffle + explore > 1.0
144
+ raise ArgumentError, "shuffle and explore must sum to at most 1 (got #{shuffle} and #{explore})"
145
+ end
146
+ Checks.integer(top, "top", minimum: 1) unless nil.equal?(top)
147
+
148
+ { shuffle: shuffle, explore: explore, may_lead: rank_may_lead(may_lead),
149
+ min_views: Numbers.read(min_views, "min_views"), pinned: rank_pinned(pinned), top: top }.freeze
150
+ end
151
+
152
+ # nil, or the names `may_lead` gives as the set's own Strings, in its order, frozen: the items the set's one list
153
+ # reading (`listed_items`) read, `may_lead` read once, a name that is not an item and a `may_lead` that is not a
154
+ # list refused there.
155
+ def rank_may_lead(may_lead)
156
+ return nil if nil.equal?(may_lead)
157
+
158
+ listed_items(may_lead, "may_lead", @items, @items).last
159
+ end
160
+
161
+ # { position => item }, frozen: distinct Integer positions inside the items (each read by the bandit's one reading
162
+ # of a whole number, Checks.integer, in 0...items.size), each naming a distinct item. `pinned` is read once; its
163
+ # names are the items the set's one list reading (`listed_items`) read, a name that is not an item refused there.
164
+ def rank_pinned(pinned)
165
+ return {}.freeze if nil.equal?(pinned)
166
+ raise ArgumentError, "pinned must be a Hash of position => item (got #{Inputs.shown(pinned)})" unless Hash === pinned
167
+
168
+ entries = Inputs.pairs(pinned)
169
+ entries.each { |position, _name| Checks.integer(position, "pinned: a position", minimum: 0, below: @items.size) }
170
+ _chosen, items = listed_items(entries.map(&:last), "pinned", @items, [])
171
+ seen = {}
172
+ items.each do |item|
173
+ raise ArgumentError, "pinned: #{Inputs.shown(item)} is pinned twice" if seen.key?(item)
174
+
175
+ seen[item] = true
176
+ end
177
+ entries.map(&:first).zip(items).to_h.freeze
178
+ end
179
+
180
+ # THE free items under the rule, written once (the order rule's draws and the order and position odds read
181
+ # them here): every item but the pinned ones, in item order.
182
+ def rank_free(rule)
183
+ pinned = rule[:pinned]
184
+ pinned.empty? ? @items : @items - pinned.values
185
+ end
186
+
187
+ # THE Order `rank` and the re-pin answer, written once: from the rule's whole order, free head, branch and draws
188
+ # (`rank_ordered`), the first `top` items and the leader, its draws through the bandit's one check of an answer,
189
+ # carrying the set, the rule and what a re-pin keeps (`rank_carrying`), frozen.
190
+ def rank_answered(rule, full, head, branch, draws, kept)
191
+ Checks.finite_answer(draws.values, "the parameters and the draw's source")
192
+ top = rule[:top]
193
+ order = Order.new(items: (nil.equal?(top) ? full : full.first([ top, full.size ].min)).freeze,
194
+ leader: rank_leader(head, rule),
195
+ branch: branch, draws: draws)
196
+ rank_carrying(order, rule, kept)
197
+ end
198
+
199
+ # [the whole order, the free head, the branch, the draws, what a re-pin keeps] under the rule, from exactly one
200
+ # source, every draw on the scaled pairs (`parameters`): a key, given uniforms or an RNG, or, for a re-pin
201
+ # (`rank_repinned`), `kept`, an order's own [source, keys, draws]. What a re-pin keeps is [the source as read,
202
+ # { item => the key its draw was compared by }] from a key or given uniforms, nil from an RNG.
203
+ def rank_ordered(rule, key, uniforms, rng, kept = nil)
204
+ source = nil.equal?(kept) ? rank_source(key, uniforms, rng) : :kept
205
+ free = rank_free(rule)
206
+ leading = contenders(rule[:may_lead], free)
207
+ eligible = leading.select { |item| eligible?(item, rule[:min_views]) }
208
+ head, rest, branch, draws, keys, read = case source
209
+ when :rng then rank_by_rng(rng, rule, free, leading, eligible)
210
+ when :key then rank_by_key(key, rule, free, leading, eligible)
211
+ when :uniforms then rank_by_uniforms(uniforms, rule, free, leading, eligible)
212
+ else rank_by_kept(kept, rule, free, leading, eligible)
213
+ end
214
+ free_order = head.nil? ? [] : [ head, *rest ]
215
+ [ rank_slotted(rule[:pinned], free_order), head, branch, draws,
216
+ nil.equal?(read) ? nil : [ read, (keys || {}).freeze ].freeze ]
217
+ end
218
+
219
+ # :key, :uniforms or :rng, the one source given (nil is not given).
220
+ def rank_source(key, uniforms, rng)
221
+ given = { key: key, uniforms: uniforms, rng: rng }.reject { |_, value| nil.equal?(value) }
222
+ return given.keys.first if given.size == 1
223
+
224
+ raise ArgumentError, "give exactly one source of randomness, key:, uniforms: or rng: (got " \
225
+ "#{given.empty? ? 'none' : given.keys.join(' and ')})"
226
+ end
227
+
228
+ # The RNG source: the branch value (when shuffle + explore is above 0), a priority per free item where the
229
+ # branch reads priorities, a Sampler draw per free item where it draws (`sampler_draw`, on the scaled pairs), in
230
+ # that order, each compared by its key and answered as its value.
231
+ def rank_by_rng(rng, rule, free, leading, eligible)
232
+ sampler = checked_sampler(rng)
233
+ value = (rule[:shuffle] + rule[:explore]).positive? ? Inputs.uniform(rng) : nil
234
+ branch = rank_branch(value, rule, eligible)
235
+ priorities = branch == :thompson ? nil : free.to_h { |item| [ item, Inputs.uniform(rng) ] }
236
+ return rank_decided(branch, free, leading, eligible, priorities, nil, nil) if branch == :shuffled
237
+
238
+ drawn = free.to_h { |item| [ item, sampler_draw(sampler, item) ] }
239
+ rank_decided(branch, free, leading, eligible, priorities, drawn.transform_values(&:first),
240
+ drawn.transform_values(&:last))
241
+ end
242
+
243
+ # The key source: the key's parts read once, by the bandit's one reading of a keyed draw's parts (KeyParts.read:
244
+ # each part's bytes, a frozen String, a part that cannot be read refused in its turn, as Draw.uniform_for refuses
245
+ # it), and every uniform drawn from what it read, the branch value first. A part read so reads again as its own
246
+ # bytes, so each uniform is the one Draw.uniform_for answers on the caller's parts, bit for bit. Kept for a
247
+ # re-pin: [:key, the parts as read], library Strings, never a caller's part, so a part changed after `rank` (a
248
+ # String changed in place, an object whose to_s answers otherwise) moves no re-pin.
249
+ def rank_by_key(key, rule, free, leading, eligible)
250
+ unless String === key || Array === key
251
+ raise ArgumentError, "key must be a String or an Array of parts (got #{Inputs.shown(key)})"
252
+ end
253
+
254
+ refuse_below_smallest_parameter(free, computing: RANK_QUANTILE_DRAW)
255
+ parts = KeyParts.read(*(Array === key ? Inputs.elements(key) : [ key ]))
256
+ branch = rank_branch(Draw.uniform_for(*parts, "explore"), rule, eligible)
257
+ priorities = branch == :thompson ? nil : rank_keyed(parts, "priority", free)
258
+ uniforms = branch == :shuffled ? nil : rank_keyed(parts, "draw", free)
259
+ [ *rank_by_quantile(branch, free, leading, eligible, priorities, uniforms), [ :key, parts ].freeze ]
260
+ end
261
+
262
+ # THE keyed uniform of each item, written once (the key source's priorities and draw uniforms, and a re-pin's
263
+ # draw uniforms): { item => Draw.uniform_for(*parts, part, item) }, in the items' order.
264
+ def rank_keyed(parts, part, items)
265
+ items.to_h { |item| [ item, Draw.uniform_for(*parts, part, item) ] }
266
+ end
267
+
268
+ # The given-uniforms source: explore, and every free item's priority and draw uniform, read and checked before
269
+ # anything is drawn. Kept for a re-pin: [:uniforms, the priorities, the draw uniforms], each as `rank` read it
270
+ # (`rank_kept_given`), never a caller's number.
271
+ def rank_by_uniforms(uniforms, rule, free, leading, eligible)
272
+ unless Hash === uniforms
273
+ raise ArgumentError, "uniforms must be a Hash of explore:, priority: and draw: (got #{Inputs.shown(uniforms)})"
274
+ end
275
+
276
+ refuse_below_smallest_parameter(free, computing: RANK_QUANTILE_DRAW)
277
+ explore = Checks.unit(Checks.fetch(uniforms, :explore, "uniforms"), "uniforms' explore")
278
+ named_priorities = rank_named(uniforms, :priority)
279
+ priorities = rank_read(named_priorities, :priority, free)
280
+ named_draws = rank_named(uniforms, :draw)
281
+ draw_uniforms = rank_read(named_draws, :draw, free)
282
+ branch = rank_branch(explore, rule, eligible)
283
+ [ *rank_by_quantile(branch, free, leading, eligible, branch == :thompson ? nil : priorities,
284
+ branch == :shuffled ? nil : draw_uniforms),
285
+ [ :uniforms, rank_kept_given(named_priorities, :priority, priorities),
286
+ rank_kept_given(named_draws, :draw, draw_uniforms) ].freeze ]
287
+ end
288
+
289
+ # The uniforms' entry `part` (a Hash naming items) read by the set's one reading of a caller's Hash keyed by item
290
+ # names (`item_values`; a key that names no item is not read): { item => the number as given }, the Hash it
291
+ # builds, keyed by the set's own Strings, frozen.
292
+ def rank_named(uniforms, part)
293
+ entries = Checks.fetch(uniforms, part, "uniforms")
294
+ unless Hash === entries
295
+ raise ArgumentError, "uniforms' #{part} must be a Hash of item => number (got #{Inputs.shown(entries)})"
296
+ end
297
+
298
+ item_values(entries, @items, @item_index, "uniforms' #{part}", strangers: :skip).freeze
299
+ end
300
+
301
+ # The given uniforms' entry `part` read for each of `items` (`rank_given`): { item => number }; the first one
302
+ # refused is an ArgumentError.
303
+ def rank_read(named, part, items)
304
+ items.to_h { |item| [ item, rank_given(named, part, item) ] }
305
+ end
306
+
307
+ # THE reading of a given uniform, written once (the given-uniforms source and what it keeps for a re-pin): the
308
+ # item's number in `named` (`rank_named`'s Hash), a priority by the library's one number reader (Numbers.read), a
309
+ # draw uniform by the bandit's one [0, 1] bound (Checks.unit); one that is not given is an ArgumentError.
310
+ def rank_given(named, part, item)
311
+ label = "uniforms' #{part} of the item #{Inputs.shown(item)}"
312
+ raise ArgumentError, "#{label} is not given" unless named.key?(item)
313
+
314
+ given = named.fetch(item)
315
+ part == :priority ? Numbers.read(given, label) : Checks.unit(given, label)
316
+ end
317
+
318
+ # What a re-pin keeps of the given uniforms' entry `part`: { item => the number `rank` read, or the words of a
319
+ # refusal }, for every item, keyed by the set's own Strings, frozen: library data, never a caller's number, so a
320
+ # number changed after `rank` (a caller's Numeric whose to_f answers otherwise, a Hash it changes) moves no
321
+ # re-pin. A free item's is the number `rank` read (`read`). An item `rank` did not read (one the order pinned)
322
+ # keeps a frozen copy that reads exactly as its entry read at rank time, not its number alone. Why: `rank` reads
323
+ # no pinned item's entry, so reading it at rank time and refusing it there would make `rank` refuse a source it
324
+ # takes. So its entry is read at rank time by the one reading (`rank_given`) and its number kept, or, when that
325
+ # reading refuses, the refusal's words in place of the raise; a re-pin answers that number or raises those words
326
+ # (`rank_kept_read`), as `rank` on the source as it was answers or refuses.
327
+ def rank_kept_given(named, part, read)
328
+ @items.to_h { |item| [ item, read.key?(item) ? read.fetch(item) : rank_given_or_words(named, part, item) ] }
329
+ .freeze
330
+ end
331
+
332
+ # The item's given number (`rank_given`), or, refused, the refusal's words, frozen.
333
+ def rank_given_or_words(named, part, item)
334
+ rank_given(named, part, item)
335
+ rescue ArgumentError => e
336
+ e.message.dup.freeze
337
+ end
338
+
339
+ # A re-pin's reading of what the given-uniforms source kept (`rank_kept_given`'s Hash) for each of `items`:
340
+ # { item => the number `rank` read }; one kept as a refusal's words is that refusal, the first an ArgumentError
341
+ # in the same words.
342
+ def rank_kept_read(kept, items)
343
+ items.to_h do |item|
344
+ given = kept.fetch(item)
345
+ raise ArgumentError, given if String === given
346
+
347
+ [ item, given ]
348
+ end
349
+ end
350
+
351
+ # A re-pin's draws (`rank_repinned`): the order rule fed an order's own kept [source, keys, draws] under the
352
+ # re-pin's rule, which takes no branch value (shuffle and explore are 0), so the branch is Thompson. A free item
353
+ # the order drew keeps its draw (the key it was compared by and its value); only the others are drawn now, by
354
+ # quantile (`rank_quantile_draw`), from the kept source, which holds only what `rank` read, never a caller's
355
+ # object: the keyed uniforms of the parts as read (`rank_keyed`), or the given priority and draw uniform as read
356
+ # (`rank_kept_read`). Refused as `rank` refuses on that source, before anything is drawn: a Beta parameter below
357
+ # the smallest over the free items, and a given uniform of an item now free that was missing or malformed at rank
358
+ # time, in the words `rank` gave. Kept for the next re-pin: the same source.
359
+ def rank_by_kept(kept, rule, free, leading, eligible)
360
+ source, keys, values = kept
361
+ refuse_below_smallest_parameter(free, computing: RANK_QUANTILE_DRAW)
362
+ uniforms = rank_kept_uniforms(source, free.reject { |item| keys.key?(item) })
363
+ drawn_keys = {}
364
+ drawn_values = {}
365
+ free.each do |item|
366
+ if keys.key?(item)
367
+ drawn_keys[item] = keys.fetch(item)
368
+ drawn_values[item] = values.fetch(item)
369
+ else
370
+ drawn_keys[item], drawn_values[item] = rank_quantile_draw(item, uniforms.fetch(item))
371
+ end
372
+ end
373
+ [ *rank_decided(rank_branch(nil, rule, eligible), free, leading, eligible, nil, drawn_keys, drawn_values),
374
+ source ]
375
+ end
376
+
377
+ # { item => draw uniform } for the items a re-pin draws, from the kept source: keyed on the parts as read
378
+ # (`rank_keyed`), or given (`rank_kept_read`: each item's priority, then each item's draw uniform, in the order
379
+ # the given-uniforms source reads them).
380
+ def rank_kept_uniforms(source, items)
381
+ origin, first, second = source
382
+ return rank_keyed(first, "draw", items) if origin == :key
383
+
384
+ rank_kept_read(first, items)
385
+ rank_kept_read(second, items)
386
+ end
387
+
388
+ # The quantile draws of a key or given uniforms, on the scaled pairs (`rank_quantile_draw`), each compared by its
389
+ # key and answered as its value.
390
+ def rank_by_quantile(branch, free, leading, eligible, priorities, uniforms)
391
+ return rank_decided(branch, free, leading, eligible, priorities, nil, nil) if uniforms.nil?
392
+
393
+ keys = {}
394
+ values = {}
395
+ uniforms.each { |item, uniform| keys[item], values[item] = rank_quantile_draw(item, uniform) }
396
+ rank_decided(branch, free, leading, eligible, priorities, keys, values)
397
+ end
398
+
399
+ # THE quantile draw of an item at its uniform, written once (a key's, given uniforms' and a re-pin's), on its
400
+ # scaled pair: [the key the rule compares it by, the value it answers]. A Beta item at its position at the uniform
401
+ # (BetaArgmax.position), answered as the value there (the position's half, `rank_beta_half`); a Normal item the
402
+ # set's one Normal draw (`normal_draw`) at the normal quantile of its uniform on the open grid
403
+ # (Draw.open_uniform), compared by its exact key.
404
+ def rank_quantile_draw(item, uniform)
405
+ first, second = @parameters.fetch(item)
406
+ unless @kind == :beta
407
+ return normal_draw(first, second, Significance.normal_quantile(Draw.open_uniform(uniform)))
408
+ end
409
+
410
+ position = BetaArgmax.position(uniform, first, second)
411
+ lower, log_end = rank_beta_half(position)
412
+ [ position, lower ? Math.exp(log_end) : 1.0 - Math.exp(log_end) ]
413
+ end
414
+
415
+ # THE two halves of a Beta draw's position p (BetaArgmax.position's log scale), written once (the quantile
416
+ # draws' values and the order odds' distribution functions read them): [true, log x] below 0, where x = exp(p +
417
+ # JOINT), else [false, log (1 - x)], where 1 - x = exp(JOINT - p), so each half is read from its own end and no
418
+ # draw near either end rounds into the end itself.
419
+ def rank_beta_half(position)
420
+ position.negative? ? [ true, position + BetaArgmax::JOINT ] : [ false, BetaArgmax::JOINT - position ]
421
+ end
422
+
423
+ # The branch at value c (nil when the rule takes none): c < shuffle shuffles; c < shuffle + explore explores
424
+ # when an item is eligible; else Thompson.
425
+ def rank_branch(value, rule, eligible)
426
+ return :thompson if nil.equal?(value)
427
+ return :shuffled if value < rule[:shuffle]
428
+ return :explored if value < rule[:shuffle] + rule[:explore] && !eligible.empty?
429
+
430
+ :thompson
431
+ end
432
+
433
+ # [head, the other free items in order, branch, draws, keys]: the head by the branch, the others by priority on
434
+ # the shuffled branch and by draw (`keys`, largest first) on the others; draws the values, frozen, none on the
435
+ # shuffled branch; keys as given (nil on the shuffled branch).
436
+ def rank_decided(branch, free, leading, eligible, priorities, keys, values)
437
+ head = case branch
438
+ when :shuffled then rank_first(leading, priorities, :<)
439
+ when :explored then rank_first(eligible, priorities, :<)
440
+ else rank_first(leading, keys, :>)
441
+ end
442
+ rest = head.nil? ? [] : free - [ head ]
443
+ rest = branch == :shuffled ? rank_sorted(rest, priorities, 1) : rank_sorted(rest, keys, -1)
444
+ [ head, rest, branch, (values || {}).freeze, keys ]
445
+ end
446
+
447
+ # The first item of `list` (in item order) whose number in `numbers` no later item beats by `beats`.
448
+ def rank_first(list, numbers, beats)
449
+ best = nil
450
+ best_number = nil
451
+ list.each do |item|
452
+ number = numbers.fetch(item)
453
+ next unless best.nil? || number.public_send(beats, best_number)
454
+
455
+ best = item
456
+ best_number = number
457
+ end
458
+ best
459
+ end
460
+
461
+ # `list` by its numbers, smallest first (direction 1) or largest first (-1), ties to the earlier item.
462
+ def rank_sorted(list, numbers, direction)
463
+ list.sort do |left, right|
464
+ by_number = (numbers.fetch(left) <=> numbers.fetch(right)).to_i * direction
465
+ by_number.zero? ? @item_index.fetch(left) <=> @item_index.fetch(right) : by_number
466
+ end
467
+ end
468
+
469
+ # The whole order: pinned items at their positions, the free order filling the free slots in order
470
+ # (`rank_free_slots`).
471
+ def rank_slotted(pinned, free_order)
472
+ return free_order if pinned.empty?
473
+
474
+ full = Array.new(@items.size)
475
+ pinned.each { |position, item| full[position] = item }
476
+ rank_free_slots(pinned).zip(free_order) { |position, item| full[position] = item }
477
+ full
478
+ end
479
+
480
+ # THE free-slot rule, written once (the order rule's whole order, the order odds' reading of an order and the
481
+ # position odds' table read it): the slots the free ranks take, in order, every slot no item is pinned to, so
482
+ # pinned items keep their positions and each free rank takes the next free slot.
483
+ def rank_free_slots(pinned)
484
+ (0...@items.size).reject { |slot| pinned.key?(slot) }
485
+ end
486
+
487
+ # The free head when it has at least min_views views, else nil.
488
+ def rank_leader(head, rule)
489
+ return nil if head.nil?
490
+
491
+ eligible?(head, rule[:min_views]) ? head : nil
492
+ end
493
+
494
+ # The answer, carrying this set, its rule and an empty cache in instance variables, and, for an Order drawn from
495
+ # a key or given uniforms, what a re-pin reads (`kept`: @rank_source and @rank_keys), then frozen.
496
+ def rank_carrying(answer, rule, kept = nil)
497
+ answer.instance_variable_set(:@posteriors, self)
498
+ answer.instance_variable_set(:@rule, rule)
499
+ answer.instance_variable_set(:@cache, {})
500
+ unless nil.equal?(kept)
501
+ answer.instance_variable_set(:@rank_source, kept.first)
502
+ answer.instance_variable_set(:@rank_keys, kept.last)
503
+ end
504
+ answer.freeze
505
+ end
506
+ end
507
+
508
+ # The re-pin's one hand-off from an Order to the set that made it. The set and the Order both include this module,
509
+ # so an Order may call its one protected method on its set (Ruby's protected: a caller whose self includes the
510
+ # module that defines the method), and the set's own private order rule answers: no visibility bypass, and nothing
511
+ # but the set's code reads its private methods.
512
+ module RankRepin
513
+ protected
514
+
515
+ # On the set: the Order its rule answers under the held rule (shuffle 0, explore 0, no may_lead, min_views 0,
516
+ # `top` as the order's own rule's) with `pinned`, fed the order's own kept source, keys and draws
517
+ # (`rank_by_kept`); `pinned` read and refused as `rank` reads it (`rank_rule`).
518
+ def rank_repinned(rule, source, keys, draws, pinned)
519
+ held = rank_rule(shuffle: 0.0, explore: 0.0, may_lead: nil, min_views: 0.0, pinned: pinned, top: rule[:top])
520
+ rank_answered(held, *rank_ordered(held, nil, nil, nil, [ source, keys, draws ]))
521
+ end
522
+ end
523
+ private_constant :RankRepin
524
+
525
+ class Posteriors
526
+ include RankRepin
527
+ end
528
+
529
+ # An order re-pinned on its own draws (THE RE-PIN, above).
530
+ class Order
531
+ include RankRepin
532
+
533
+ # The Order this order's set answers on this order's source with `pinned` ({ position => item }, 0-based)
534
+ # under the held rule (shuffle 0, explore 0, no may_lead, min_views 0, top as this order's): `rank(key: <its
535
+ # key>, pinned: pinned, top: <its top>)`, or the same with its given uniforms, bit for bit, computed on the draws
536
+ # this order made (an item it did not draw is drawn now). An ArgumentError for an order drawn from an RNG, one no
537
+ # posterior set made, and whatever `rank` refuses there.
538
+ def repin(pinned)
539
+ unless Posteriors === @posteriors && Hash === @rule
540
+ raise ArgumentError, "a #{Inputs.class_name(self)} is re-pinned only when a posterior set made it (it " \
541
+ "carries the set, the rule and the draws)"
542
+ end
543
+ unless Array === @rank_source && Hash === @rank_keys
544
+ raise ArgumentError, "a re-pin needs an order drawn from a key or given uniforms (this one was drawn " \
545
+ "from an RNG, whose draws cannot be read again)"
546
+ end
547
+
548
+ @posteriors.rank_repinned(@rule, @rank_source, @rank_keys, draws, pinned)
549
+ end
550
+ end
551
+ end
552
+ end