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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +346 -2
- data/README.md +1618 -22
- data/lib/skiftet_statistical/additive_contrasts.rb +894 -0
- data/lib/skiftet_statistical/bandit/allocator.rb +688 -0
- 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/weighted_posterior.rb +65 -0
- data/lib/skiftet_statistical/bandit/win_hold.rb +86 -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 +153 -0
- 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/gamma.rb +97 -0
- 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/joint_beta.rb +63 -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 +65 -5
- data/lib/skiftet_statistical/version.rb +1 -1
- data/lib/skiftet_statistical.rb +57 -5
- 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
|