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,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../numbers"
|
|
4
|
+
require_relative "../inputs"
|
|
5
|
+
require_relative "checks"
|
|
6
|
+
|
|
7
|
+
module SkiftetStatistical
|
|
8
|
+
class Bandit
|
|
9
|
+
# One allocation pass with a hold on its win: probe, then crown on the same draws only when the hold allows. The
|
|
10
|
+
# caller's block is the allocation (Allocator.call or Allocator.from_summary, or any rule answering a Hash); the
|
|
11
|
+
# pass decides how often it runs and with which flag, and hands it its randomness.
|
|
12
|
+
#
|
|
13
|
+
# hold = SkiftetStatistical::Bandit::WinHold.new(leading_arm: "a", leading_since: now - 90_000, now: now,
|
|
14
|
+
# last_pass_at: now - 600)
|
|
15
|
+
# SkiftetStatistical::Bandit::Pass.call(status: "running", hold: hold) do |hold_satisfied, rng|
|
|
16
|
+
# SkiftetStatistical::Bandit::Allocator.call(arms: %i[control a], tallies: tallies, status: "running",
|
|
17
|
+
# winner_arm: nil, previous_weights: weights, rng: rng,
|
|
18
|
+
# draws: 20_000, hold_satisfied: hold_satisfied)
|
|
19
|
+
# end
|
|
20
|
+
# # => the crown's answer (weights, phase: :won, stats, winner: :a, result_changed, leader: :a) and ready: true
|
|
21
|
+
#
|
|
22
|
+
# The rule:
|
|
23
|
+
# - the probe: the block is yielded (false, Random.new(seed)), so a leader gets the step, never the won split;
|
|
24
|
+
# - ready: the status reads "running" (a String by its bytes, anything else by its to_s, as the Allocator reads
|
|
25
|
+
# it, once, before the first yield: Inputs.text, never a String's own ==), the
|
|
26
|
+
# probe's Hash names a leader under :leader (nil for none; read by the bandit's one lookup in a caller's Hash,
|
|
27
|
+
# Checks.lookup, so a default the Hash holds is not read) and `hold.satisfied_for?(leader)` answers true; the
|
|
28
|
+
# hold is asked only then, once, about that leader;
|
|
29
|
+
# - the crown: only when `crown` is true and the pass is ready, the block is yielded again (true, a fresh
|
|
30
|
+
# Random.new(seed)), so the probe and the crown read the same draws and only the decision changes;
|
|
31
|
+
# - the answer: the last yield's Hash merged with `ready:` (true or false) by Hash's own merge, nothing else. A paused or won status
|
|
32
|
+
# takes the block's own path at the probe and is never ready. The caller computes its next hold itself (for a
|
|
33
|
+
# WinHold, `after_pass` with the answer's leader).
|
|
34
|
+
#
|
|
35
|
+
# With `crown: false` a ready leader keeps the probe's answer (its step toward its P(best)), and `ready:` says it
|
|
36
|
+
# would have been crowned, so the caller can crown it by its own hand.
|
|
37
|
+
#
|
|
38
|
+
# `seed` defaults to Random.new_seed, drawn once a pass; a given seed is honoured, so a pass is repeatable. The
|
|
39
|
+
# checks (ArgumentError, before the first yield): a block is given, a status that is no String answers to_s
|
|
40
|
+
# without raising (the library's one call into a caller's object, Inputs.asked), the hold answers
|
|
41
|
+
# `satisfied_for?` and the seed is an
|
|
42
|
+
# Integer within a Float's range (the bandit's one reading of a whole number, Checks.integer, as `top` and the
|
|
43
|
+
# odds' `draws` and `seed` are read); each yield must answer a Hash (ArgumentError otherwise), checked when it is
|
|
44
|
+
# answered. The block and the hold's satisfied_for? are the caller's own rules: what they raise reaches the caller
|
|
45
|
+
# as raised.
|
|
46
|
+
module Pass
|
|
47
|
+
class << self
|
|
48
|
+
# The probe, then the crown when `crown` and ready: the last yield's Hash plus { ready: true | false }.
|
|
49
|
+
def call(status:, hold:, crown: true, seed: Random.new_seed, &allocation)
|
|
50
|
+
running = check(status, hold, seed, allocation)
|
|
51
|
+
probe = run(allocation, false, seed)
|
|
52
|
+
ready = ready?(running, Checks.lookup(probe, :leader, "the block's Hash"), hold)
|
|
53
|
+
answer = crown && ready ? run(allocation, true, seed) : probe
|
|
54
|
+
Inputs.merged(answer, { ready: ready })
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Ready: a running status, a probe leader, and the hold satisfied for that leader (asked only then).
|
|
60
|
+
def ready?(running, leader, hold)
|
|
61
|
+
return false unless running && !nil.equal?(leader)
|
|
62
|
+
|
|
63
|
+
hold.satisfied_for?(leader) ? true : false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# One yield, with a fresh Random.new(seed): the block's Hash.
|
|
67
|
+
def run(allocation, hold_satisfied, seed)
|
|
68
|
+
answer = allocation.call(hold_satisfied, Random.new(seed))
|
|
69
|
+
return answer if Hash === answer
|
|
70
|
+
|
|
71
|
+
raise ArgumentError, "a pass's block must answer a Hash (got #{Inputs.class_name(answer)})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The checks, before the first yield; answers whether the status reads "running": the library's one reading of
|
|
75
|
+
# a value as text (Inputs.text: a String status read by its bytes and encoding alone, by Inputs.string; any
|
|
76
|
+
# other by its to_s, asked by the library's one call into a caller's object, so a to_s that raises is an
|
|
77
|
+
# ArgumentError, its String answer read the same way), whose copy is the String "running" (so a String's own
|
|
78
|
+
# == or any other of its methods is never called).
|
|
79
|
+
def check(status, hold, seed, allocation)
|
|
80
|
+
raise ArgumentError, "a pass needs a block: the allocation it runs" if allocation.nil?
|
|
81
|
+
unless Inputs.responds?(status, :to_s, "a status")
|
|
82
|
+
raise ArgumentError, "a status must answer to_s (got #{Inputs.shown(status)})"
|
|
83
|
+
end
|
|
84
|
+
text = Inputs.text(status, "a status")
|
|
85
|
+
unless Inputs.responds?(hold, :satisfied_for?, "a hold")
|
|
86
|
+
raise ArgumentError, "a hold must answer satisfied_for? (got #{Inputs.class_name(hold)})"
|
|
87
|
+
end
|
|
88
|
+
Checks.integer(seed, "a seed")
|
|
89
|
+
!nil.equal?(text) && text == "running"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../numbers"
|
|
4
|
+
require_relative "../inputs"
|
|
5
|
+
require_relative "../sampler"
|
|
6
|
+
require_relative "../beta_argmax"
|
|
7
|
+
require_relative "checks"
|
|
8
|
+
require_relative "tallies"
|
|
9
|
+
require_relative "prior"
|
|
10
|
+
require_relative "draw"
|
|
11
|
+
require_relative "normal_argmax"
|
|
12
|
+
|
|
13
|
+
module SkiftetStatistical
|
|
14
|
+
class Bandit
|
|
15
|
+
# A posterior set: items (non-blank Strings, in input order, which is the tie order everywhere), each with a
|
|
16
|
+
# posterior of one kind and its views. Yes/no tallies become Beta pairs [alpha, beta] (`yes_no`), amount tallies
|
|
17
|
+
# Normal pairs [mean, standard deviation] for the mean per view (`amounts`), and pairs may be given as they are
|
|
18
|
+
# (`beta`, `normal`). Any number of items, none included; only the exact odds have limits (EXACT_PICK_MAX,
|
|
19
|
+
# EXACT_ORDER_MAX).
|
|
20
|
+
#
|
|
21
|
+
# posteriors = SkiftetStatistical::Bandit::Posteriors
|
|
22
|
+
# set = posteriors.yes_no({ "a" => { views: 40, outcomes: 8 }, "b" => { views: 10, outcomes: 1 } })
|
|
23
|
+
# set.parameters # => { "a" => [9.0, 33.0], "b" => [2.0, 10.0] }
|
|
24
|
+
# set.greedy # => "a", the largest posterior mean
|
|
25
|
+
# set.values(rng: Random.new(1)) # => one posterior draw per item, in order
|
|
26
|
+
#
|
|
27
|
+
# A tally is a Hash with Symbol or String keys, read by the bandit's one tally reader (Tallies.read), each sum by
|
|
28
|
+
# the library's one number reader (SkiftetStatistical::Numbers: a finite real number of magnitude at most
|
|
29
|
+
# Numbers::LARGEST; a String, a numeric one included, is not a number); a nil tally or a missing sum reads 0,
|
|
30
|
+
# whatever default the Hash holds (Checks.named reads a tally by its pairs, never by a default), and a tally that
|
|
31
|
+
# compares its keys by identity reads as a plain one.
|
|
32
|
+
# Yes/no: `views` and `outcomes` (>= 0, fractional allowed, outcomes clamped to the views), optionally
|
|
33
|
+
# `weighted_views` (sum 1/p), `weighted_outcomes` (sum y/p) and `weighted_views_squared` (sum 1/p^2), each >= 0. The
|
|
34
|
+
# tally is weighted when weighted_views is above 0 and weighted_outcomes is given: the weighted outcomes are
|
|
35
|
+
# rescaled to n, the Kish size (sum w)^2 / sum w^2 (never above the views) when the squared sum is above 0, else the
|
|
36
|
+
# views. Amounts: `views`, `outcomes` (the sum of amounts, any sign) and `outcome_squares`, optionally the weighted
|
|
37
|
+
# sums and `weighted_outcome_squares` (sum x^2/p), weighted only when all three weighted sums are given and
|
|
38
|
+
# weighted_views is above 0. A set's `views` are each tally's own views; the Kish size never replaces them.
|
|
39
|
+
#
|
|
40
|
+
# Priors: `yes_no` takes nil (Beta(1, 1)), one pair [alpha, beta] (each a number >= 0) for every item, or a Hash
|
|
41
|
+
# naming each item's pair; `amounts` takes one Prior::Amount or a Hash naming each item's (there is no flat amounts
|
|
42
|
+
# prior, so nil and a pair are refused), each Amount's numbers read again by its own checks (Tallies.amount_prior).
|
|
43
|
+
# A Hash of priors or views naming a name that is not an item is refused; a Hash of priors must name every item; an
|
|
44
|
+
# item missing from `views` reads 0. Such a Hash, and the given uniforms' priority and draw, is read by its pairs,
|
|
45
|
+
# each key mapped to its item (`item_values`), so one that compares its keys by identity reads as a plain one, and
|
|
46
|
+
# two keys naming one item are refused.
|
|
47
|
+
#
|
|
48
|
+
# Limits, checked when the set is built: every pair is read by the library's one reading of a pair (Numbers.pair),
|
|
49
|
+
# so every parameter, given or computed from a tally, is a number in the input domain (Numbers::LARGEST either side
|
|
50
|
+
# of 0); a Beta pair's two parameters are above 0; a Normal's standard deviation is above 0 (its mean any number in
|
|
51
|
+
# the domain). A pair a tally and a prior compute passes the bandit's one check of an answer first
|
|
52
|
+
# (Checks.finite_answer, naming the item's tally and prior). A Beta pair whose total is above LARGEST_TOTAL is
|
|
53
|
+
# scaled down to it, its mean kept, in `parameters`, the one pair every draw and every odds call reads: the
|
|
54
|
+
# quantile draws and the RNG draws (`values` and the RNG source of `rank` and `pick`), every odds call (exact or by
|
|
55
|
+
# its stated Monte Carlo), `summary`'s P(best) and loss, and `greedy`; the pairs as given feed `summary`'s means and
|
|
56
|
+
# intervals alone. A scaled pair never sums above LARGEST_TOTAL, so a set built from another set's `parameters`
|
|
57
|
+
# keeps them bit for bit. A parameter above 0 but below BetaArgmax::SMALLEST_PARAMETER builds: each computation
|
|
58
|
+
# that cannot read it refuses it on the pairs it reads (the quantile draws and the odds, exact or counted, on
|
|
59
|
+
# `parameters`, `summary`'s interval on the pair as given), and the RNG draws take it. An item name that is not a
|
|
60
|
+
# String (a Symbol, say), is blank, is given twice, holds the same bytes as an earlier name (in another encoding)
|
|
61
|
+
# or holds a NUL byte (the byte a keyed draw joins its parts with) is an ArgumentError, as is anything else a
|
|
62
|
+
# builder cannot read: a keyed draw reads a name by its bytes, so every item's bytes are its own.
|
|
63
|
+
#
|
|
64
|
+
# For the files that reopen this class, private: `given_parameters` ({ item => the pair as given }),
|
|
65
|
+
# `mean_as_given(item)` (the posterior mean on the pair as given), `sampler_draw(sampler, item)` (one draw of the
|
|
66
|
+
# item from a Sampler on its scaled pair, as [the key the order rule compares it by, the value it answers]),
|
|
67
|
+
# `normal_draw(mean, deviation, z)` (THE Normal draw at a standard deviate z, [its exact key, its value], for every
|
|
68
|
+
# source), `mean_of(pair)` (the posterior mean of a pair: alpha / (alpha + beta), or the Normal's mean),
|
|
69
|
+
# `contenders(may_lead, free)` (the items that may lead), `listed_items(names, label, free, none)` (the one reading
|
|
70
|
+
# of a list of item names, each list read once: [the items of `free` it names, or `none`; the items it read], which
|
|
71
|
+
# `contenders`, the order rule's `may_lead` and `pinned`, and `summary` share), `indexed_item(index, name, label)`
|
|
72
|
+
# (the one refusal of a name that is not an item, on the one lookup `item_position(index, name, label)`, both mixed
|
|
73
|
+
# into the class and its instances, as is `item_values(given, items, index, label, strangers:)`, the one reading of
|
|
74
|
+
# a caller's Hash keyed by item names into a Hash keyed by the set's own Strings), `eligible?(item, min_views)` (the
|
|
75
|
+
# one eligibility test), `largest_draw(among, maximum:)` (the one exact P(largest), and E[max], by kind on the
|
|
76
|
+
# scaled pairs), `checked_sampler(rng)` and `refuse_below_smallest_parameter(among, pairs:, computing:)` (GB6's one
|
|
77
|
+
# refusal, before anything is computed, over `among` on the pairs the computation reads, `parameters` unless told,
|
|
78
|
+
# its words naming what is computed), and the @item_index ivar ({ item => index }).
|
|
79
|
+
class Posteriors
|
|
80
|
+
# THE reading of item names a caller gives, written once for both readers of them: a Hash keyed by item names
|
|
81
|
+
# (`item_values`: a builder's priors or views, before the set is built, and the given uniforms' priority and
|
|
82
|
+
# draw) and a set's list of names (`listed_items`: the rule's `may_lead` and `pinned`, `summary`'s `among`), each
|
|
83
|
+
# name looked up among the items by one lookup (`item_position`) and refused by one refusal (`indexed_item`).
|
|
84
|
+
# Mixed into the class and its instances alike, so neither reaches into the other's private methods.
|
|
85
|
+
module ItemNames
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# index[name] when the Hash `index`, keyed by the items, holds the name; else an ArgumentError, "<label>:
|
|
89
|
+
# <name> is not an item". The one refusal of a name that is not an item, on the one lookup (`item_position`).
|
|
90
|
+
def indexed_item(index, name, label)
|
|
91
|
+
found = item_position(index, name, label)
|
|
92
|
+
return found unless found.nil?
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "#{label}: #{Inputs.shown(name)} is not an item"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# index[name] when the Hash `index`, keyed by the items, holds the name; else nil. THE lookup of a caller's
|
|
98
|
+
# name among the items, written once. A String name is read by the library's one reading of a caller's String
|
|
99
|
+
# (Inputs.string) and its copy looked up, so none of its own methods is called. Any other name is looked up
|
|
100
|
+
# as given when it can be (the library's one reading, Inputs.name?: it answers hash, eql? and inspect, its hash
|
|
101
|
+
# an Integer; a BasicObject, say, cannot, and the items are Strings, so no such name is one), the lookup calling
|
|
102
|
+
# its hash and eql?, one call into the caller's object (the library's one such call, Inputs.asked): a name whose
|
|
103
|
+
# hash or eql? raises is an ArgumentError naming `label`.
|
|
104
|
+
def item_position(index, name, label)
|
|
105
|
+
copy = Inputs.string(name)
|
|
106
|
+
return index[copy] unless nil.equal?(copy)
|
|
107
|
+
|
|
108
|
+
Inputs.name?(name, label) ? Inputs.asked(name, "#{label}: a name's lookup") { index[name] } : nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# THE reading of a caller's Hash keyed by item names, written once (a builder's priors and views, the given
|
|
112
|
+
# uniforms' priority and draw): { item => value }, a Hash the library builds, keyed by the set's own item
|
|
113
|
+
# Strings (`items`, at the positions `index` holds), from the caller's pairs (Inputs.pairs: Hash's own to_a),
|
|
114
|
+
# each key mapped to its item by the one lookup (`item_position`). Every value is then looked up in this Hash,
|
|
115
|
+
# never in the caller's by a key of the library's, so a Hash that compares its keys by identity
|
|
116
|
+
# (compare_by_identity) reads exactly as a plain one, and a key is read by one reading wherever it is checked
|
|
117
|
+
# and looked up. A key that names no item is refused by the one refusal (`indexed_item`), or, with `strangers:
|
|
118
|
+
# :skip`, not read; two keys that name one item (a Hash that compares by identity, or a key whose own hash
|
|
119
|
+
# differs from its String's, can hold both) are an ArgumentError, "<label> names the item <item> twice".
|
|
120
|
+
def item_values(given, items, index, label, strangers: :refuse)
|
|
121
|
+
read = {}
|
|
122
|
+
Inputs.pairs(given).each do |name, value|
|
|
123
|
+
position = strangers == :skip ? item_position(index, name, label) : indexed_item(index, name, label)
|
|
124
|
+
next if position.nil?
|
|
125
|
+
|
|
126
|
+
item = items[position]
|
|
127
|
+
raise ArgumentError, "#{label} names the item #{item.inspect} twice" if read.key?(item)
|
|
128
|
+
|
|
129
|
+
read[item] = value
|
|
130
|
+
end
|
|
131
|
+
read
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
private_constant :ItemNames
|
|
135
|
+
include ItemNames
|
|
136
|
+
extend ItemNames
|
|
137
|
+
|
|
138
|
+
# The most contenders whose pick odds are computed exactly.
|
|
139
|
+
EXACT_PICK_MAX = 20
|
|
140
|
+
# The most free positions whose order and position odds are computed exactly.
|
|
141
|
+
EXACT_ORDER_MAX = 10
|
|
142
|
+
# A Beta pair whose total is above this is scaled down to it, its mean kept, when the set is built.
|
|
143
|
+
LARGEST_TOTAL = 1_000_000.0
|
|
144
|
+
|
|
145
|
+
YES_NO_SUMS = %i[views outcomes weighted_views weighted_outcomes weighted_views_squared].freeze
|
|
146
|
+
AMOUNT_SUMS = %i[views outcomes outcome_squares weighted_views weighted_outcomes weighted_outcome_squares
|
|
147
|
+
weighted_views_squared].freeze
|
|
148
|
+
# The names of a pair's two numbers, by kind.
|
|
149
|
+
PAIR_NAMES = { beta: %w[alpha beta].freeze, normal: [ "mean", "standard deviation" ].freeze }.freeze
|
|
150
|
+
# Half of LARGEST_TOTAL, against which half a pair's total is judged (half of each parameter, summed).
|
|
151
|
+
HALF_LARGEST_TOTAL = LARGEST_TOTAL * 0.5
|
|
152
|
+
# The smallest Float above 0: a scaled parameter never reads less, so a scaled pair stays above 0.
|
|
153
|
+
SMALLEST_SCALED_PARAMETER = 0.0.next_float
|
|
154
|
+
private_constant :YES_NO_SUMS, :AMOUNT_SUMS, :PAIR_NAMES, :HALF_LARGEST_TOTAL, :SMALLEST_SCALED_PARAMETER
|
|
155
|
+
|
|
156
|
+
# items: [String], in input order; kind: :beta or :normal; parameters: { item => pair }, a Beta pair above
|
|
157
|
+
# LARGEST_TOTAL scaled to it; views: { item => Float }. All frozen.
|
|
158
|
+
attr_reader :items, :kind, :parameters, :views
|
|
159
|
+
|
|
160
|
+
class << self
|
|
161
|
+
# Beta posteriors from yes/no tallies, { item => tally }, under `priors` (nil, one pair, or { item => pair }).
|
|
162
|
+
def yes_no(items, priors: nil)
|
|
163
|
+
entries = named_entries(items, "items")
|
|
164
|
+
prior_of = prior_per_item(nil.equal?(priors) ? Prior.flat : priors, entries, "priors") do |given, label|
|
|
165
|
+
prior_pair(given, label)
|
|
166
|
+
end
|
|
167
|
+
views = {}
|
|
168
|
+
pairs = entries.to_h do |item, tally|
|
|
169
|
+
label = "item #{item.inspect}'s tally"
|
|
170
|
+
sums = Tallies.read(tally, label, YES_NO_SUMS)
|
|
171
|
+
weighted, squared = Tallies.counted(sums[:weighted_views], [ sums[:weighted_outcomes] ],
|
|
172
|
+
sums[:weighted_views_squared])
|
|
173
|
+
views[item] = sums[:views] || 0.0
|
|
174
|
+
pair = Tallies.yes_no(views: views[item], outcomes: sums[:outcomes] || 0.0,
|
|
175
|
+
weighted_views: weighted ? sums[:weighted_views] : nil,
|
|
176
|
+
weighted_outcomes: weighted ? sums[:weighted_outcomes] : nil,
|
|
177
|
+
weighted_views_squared: squared ? sums[:weighted_views_squared] : nil,
|
|
178
|
+
prior: prior_of.fetch(item))
|
|
179
|
+
[ item, Checks.finite_answer(pair, "item #{item.inspect}'s tally and prior") ]
|
|
180
|
+
end
|
|
181
|
+
new(:beta, pairs, views)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Normal posteriors for the mean per view from amount tallies, { item => tally }, under `priors` (one
|
|
185
|
+
# Prior::Amount, or { item => Prior::Amount }).
|
|
186
|
+
def amounts(items, priors:)
|
|
187
|
+
entries = named_entries(items, "items")
|
|
188
|
+
prior_of = prior_per_item(priors, entries, "priors") { |given, label| Tallies.amount_prior(given, label) }
|
|
189
|
+
views = {}
|
|
190
|
+
pairs = entries.to_h do |item, tally|
|
|
191
|
+
sums = Tallies.read(tally, "item #{item.inspect}'s tally", AMOUNT_SUMS, amounts: true)
|
|
192
|
+
weighted, squared = Tallies.counted(sums[:weighted_views],
|
|
193
|
+
[ sums[:weighted_outcomes], sums[:weighted_outcome_squares] ],
|
|
194
|
+
sums[:weighted_views_squared])
|
|
195
|
+
views[item] = sums[:views] || 0.0
|
|
196
|
+
pair = Tallies.amounts(views: views[item], outcomes: sums[:outcomes] || 0.0,
|
|
197
|
+
outcome_squares: sums[:outcome_squares] || 0.0,
|
|
198
|
+
weighted_views: weighted ? sums[:weighted_views] : nil,
|
|
199
|
+
weighted_outcomes: weighted ? sums[:weighted_outcomes] : nil,
|
|
200
|
+
weighted_outcome_squares: weighted ? sums[:weighted_outcome_squares] : nil,
|
|
201
|
+
weighted_views_squared: squared ? sums[:weighted_views_squared] : nil,
|
|
202
|
+
prior: prior_of.fetch(item))
|
|
203
|
+
[ item, Checks.finite_answer(pair, "item #{item.inspect}'s tally and prior") ]
|
|
204
|
+
end
|
|
205
|
+
new(:normal, pairs, views)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Beta posteriors given as pairs, { item => [alpha, beta] }, with views { item => Float } when given.
|
|
209
|
+
def beta(pairs, views: nil)
|
|
210
|
+
from_pairs(:beta, pairs, views)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Normal posteriors given as pairs, { item => [mean, standard_deviation] }, with views when given.
|
|
214
|
+
def normal(pairs, views: nil)
|
|
215
|
+
from_pairs(:normal, pairs, views)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def from_pairs(kind, pairs, views)
|
|
221
|
+
entries = named_entries(pairs, "pairs")
|
|
222
|
+
read = entries.to_h { |item, pair| [ item, pair_numbers(kind, pair, "item #{item.inspect}") ] }
|
|
223
|
+
new(kind, read, given_views(views, entries))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# [[item, value], ...] in input order, each item name checked: a String, holding no NUL byte, not blank, not
|
|
227
|
+
# given twice, and holding bytes no earlier name holds (a keyed draw reads a name by its bytes, so two names of
|
|
228
|
+
# one byte string in different encodings would draw alike). Answers frozen names.
|
|
229
|
+
def named_entries(entries, label)
|
|
230
|
+
unless Hash === entries
|
|
231
|
+
raise ArgumentError, "#{label} must be a Hash of item => value (got #{Inputs.class_name(entries)})"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
by_bytes = {}
|
|
235
|
+
Inputs.pairs(entries).map do |name, value|
|
|
236
|
+
item = checked_item_name(name)
|
|
237
|
+
earlier = by_bytes[item.b]
|
|
238
|
+
refuse_same_bytes(earlier, item) unless earlier.nil?
|
|
239
|
+
|
|
240
|
+
by_bytes[item.b] = item
|
|
241
|
+
[ item, value ]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def refuse_same_bytes(earlier, item)
|
|
246
|
+
raise ArgumentError, "the item #{item.inspect} is given twice" if earlier == item
|
|
247
|
+
|
|
248
|
+
raise ArgumentError, "the items #{earlier.inspect} (#{earlier.encoding}) and #{item.inspect} " \
|
|
249
|
+
"(#{item.encoding}) hold the same bytes, which a keyed draw cannot tell apart"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# The name as a frozen plain String, read by the library's one reading of a caller's String (Inputs.string: a
|
|
253
|
+
# copy by String's own b and encoding, so none of the name's own methods is called, a subclass's or a singleton
|
|
254
|
+
# method's), when it holds no NUL byte and is not blank.
|
|
255
|
+
def checked_item_name(name)
|
|
256
|
+
copy = Inputs.string(name)
|
|
257
|
+
raise ArgumentError, "an item name must be a String (got #{Inputs.shown(name)})" if nil.equal?(copy)
|
|
258
|
+
|
|
259
|
+
if copy.b.include?(Draw::SEPARATOR)
|
|
260
|
+
raise ArgumentError, "an item name must not hold a NUL byte, which a keyed draw joins its parts with " \
|
|
261
|
+
"(got #{copy.inspect})"
|
|
262
|
+
end
|
|
263
|
+
raise ArgumentError, "an item name must not be blank (got #{copy.inspect})" if blank_name?(copy)
|
|
264
|
+
|
|
265
|
+
copy
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Empty or white space only (Unicode white space in a readable text, ASCII white space in any other bytes).
|
|
269
|
+
def blank_name?(name)
|
|
270
|
+
text = name.encoding.ascii_compatible? && name.valid_encoding? ? name : name.b
|
|
271
|
+
text.match?(/\A[[:space:]]*\z/)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# { item => prior }: one prior for every item (read once), or a Hash naming every item's and nothing else, read
|
|
275
|
+
# by the one reading of a caller's Hash keyed by item names (`item_values`).
|
|
276
|
+
def prior_per_item(given, entries, label)
|
|
277
|
+
items = entries.map(&:first)
|
|
278
|
+
unless Hash === given
|
|
279
|
+
shared = yield(given, label)
|
|
280
|
+
return items.to_h { |item| [ item, shared ] }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
named = item_values(given, items, items.each_with_index.to_h, label)
|
|
284
|
+
missing = items.find { |item| !named.key?(item) }
|
|
285
|
+
raise ArgumentError, "#{label} names no prior for the item #{missing.inspect}" unless missing.nil?
|
|
286
|
+
|
|
287
|
+
items.to_h { |item| [ item, yield(named.fetch(item), "#{label}[#{item.inspect}]") ] }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# A yes/no prior, by the library's one reading of a pair (Numbers.pair): two numbers, each >= 0 (a 0 is taken
|
|
291
|
+
# here, as WeightedPosterior.beta takes it; Prior's own pairs are above 0).
|
|
292
|
+
def prior_pair(given, label)
|
|
293
|
+
Numbers.pair(given, label, PAIR_NAMES.fetch(:beta)) { |number, name| Checks.nonnegative(number, name) }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# A pair's two numbers as Floats, by the library's one reading of a pair (Numbers.pair), named by kind.
|
|
297
|
+
def pair_numbers(kind, pair, label)
|
|
298
|
+
Numbers.pair(pair, label, PAIR_NAMES.fetch(kind))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# { item => Float }: nil reads 0 for every item; a Hash names only items, each views >= 0, an item it does not
|
|
302
|
+
# name reading 0 (read by the one reading of a caller's Hash keyed by item names, `item_values`, from its pairs,
|
|
303
|
+
# so a default the Hash holds is never read).
|
|
304
|
+
def given_views(views, entries)
|
|
305
|
+
items = entries.map(&:first)
|
|
306
|
+
return items.to_h { |item| [ item, 0.0 ] } if nil.equal?(views)
|
|
307
|
+
raise ArgumentError, "views must be a Hash of item => views (got #{Inputs.class_name(views)})" unless Hash === views
|
|
308
|
+
|
|
309
|
+
named = item_values(views, items, items.each_with_index.to_h, "views")
|
|
310
|
+
items.to_h do |item|
|
|
311
|
+
given = named.fetch(item, nil)
|
|
312
|
+
next [ item, 0.0 ] if nil.equal?(given)
|
|
313
|
+
|
|
314
|
+
[ item, Checks.nonnegative(given, "item #{item.inspect}'s views") ]
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
private_class_method :new
|
|
319
|
+
|
|
320
|
+
# kind :beta or :normal; given { item => [Float, Float] } with checked names, in input order; views
|
|
321
|
+
# { item => Float }. Each pair is checked here, once, and a Beta pair above LARGEST_TOTAL scaled.
|
|
322
|
+
def initialize(kind, given, views)
|
|
323
|
+
@kind = kind
|
|
324
|
+
@given_parameters = given.to_h { |item, pair| [ item, checked_pair(item, pair).freeze ] }.freeze
|
|
325
|
+
@parameters = @given_parameters.transform_values { |pair| scaled_to_largest_total(pair) }.freeze
|
|
326
|
+
@items = @given_parameters.keys.freeze
|
|
327
|
+
@views = @items.to_h { |item| [ item, views.fetch(item) ] }.freeze
|
|
328
|
+
@item_index = @items.each_with_index.to_h.freeze
|
|
329
|
+
Checks.finite_answer(@given_parameters.values.flatten + @parameters.values.flatten + @views.values,
|
|
330
|
+
"the parameters and views")
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# One posterior draw per item, in item order, on the scaled pairs (`parameters`, the pairs every draw and every
|
|
334
|
+
# odds call read): Sampler#beta, or mean + sd * Sampler#gaussian, from `rng` and nothing else from it. Takes any
|
|
335
|
+
# parameter above 0.
|
|
336
|
+
def values(rng:)
|
|
337
|
+
sampler = checked_sampler(rng)
|
|
338
|
+
drawn = @items.map { |item| sampler_draw(sampler, item).last }
|
|
339
|
+
Checks.finite_answer(drawn, "the parameters and the rng's answers")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The contender with the largest posterior mean (ties to the earlier item); nil for an empty set. `may_lead`
|
|
343
|
+
# names the items that may lead (nil: all; when it names no item, all). The order rule's one choice of a first
|
|
344
|
+
# largest (`rank_first`, ranking.rb) on the means of the scaled pairs (`parameters`, the pairs the keyed draws
|
|
345
|
+
# and every odds call read, so a pick, its odds and the greedy item read one pair), so for Betas it is
|
|
346
|
+
# BetaArgmax.greedy's rule on `parameters`: the first largest alpha / (alpha + beta), by the same quotient and
|
|
347
|
+
# the same strict comparison, for a set of any size. Above LARGEST_TOTAL the scaled mean may differ from the
|
|
348
|
+
# mean as given by a rounding, so two means within a rounding may order differently on the two pairs.
|
|
349
|
+
def greedy(may_lead: nil)
|
|
350
|
+
candidates = contenders(may_lead)
|
|
351
|
+
rank_first(candidates, candidates.to_h { |item| [ item, mean_of(@parameters.fetch(item)) ] }, :>)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
# { item => pair as given }, frozen; `parameters` differs from it only for a Beta pair above LARGEST_TOTAL.
|
|
357
|
+
attr_reader :given_parameters
|
|
358
|
+
|
|
359
|
+
# The posterior mean on the pair as given (`mean_of`).
|
|
360
|
+
def mean_as_given(item)
|
|
361
|
+
mean_of(@given_parameters.fetch(item))
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# THE posterior mean of a pair, written once: alpha / (alpha + beta) (BetaArgmax.greedy's quotient; both
|
|
365
|
+
# parameters lie in the input domain, so the total never overflows), or the Normal's mean.
|
|
366
|
+
def mean_of(pair)
|
|
367
|
+
first, second = pair
|
|
368
|
+
@kind == :beta ? first / (first + second) : first
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# THE draw of an item from `sampler`, written once for every RNG draw (`values`, the order rule's RNG source and
|
|
372
|
+
# the odds' Monte Carlo), on its scaled pair (`parameters`), as [the key the order rule compares it by, the value
|
|
373
|
+
# it answers]: a Beta Sampler#beta, its key its value; a Normal mean + sd * Sampler#gaussian (`normal_draw`).
|
|
374
|
+
def sampler_draw(sampler, item)
|
|
375
|
+
first, second = @parameters.fetch(item)
|
|
376
|
+
return normal_draw(first, second, sampler.gaussian) unless @kind == :beta
|
|
377
|
+
|
|
378
|
+
value = sampler.beta(first, second)
|
|
379
|
+
[ value, value ]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# THE Normal draw at the standard deviate z, written once for every source (a quantile draw's z is the standard
|
|
383
|
+
# quantile of its uniform, an RNG draw's Sampler#gaussian): [its key, its value]. The value is mean + sd * z
|
|
384
|
+
# rounded to a Float, the value a draw answers; the key is that sum exact, formed by the bandit's one exact
|
|
385
|
+
# arithmetic on Floats (Tallies.exact, Tallies.exact_sum: a Float is an exact multiple of a power of 2) and held
|
|
386
|
+
# as one Integer on a common power of 2 (Tallies.exact_key), so two draws compare by the exact sign of (mean_i -
|
|
387
|
+
# mean_j) + (sd_i z_i - sd_j z_j), never by their rounded values: where a standard deviation lies at or below a
|
|
388
|
+
# rounding of its mean, the values of two draws round together or out of order, while their keys order them as
|
|
389
|
+
# the posteriors do.
|
|
390
|
+
def normal_draw(mean, deviation, z)
|
|
391
|
+
exact = Tallies.exact_sum(Tallies.exact(mean), Tallies.exact(deviation, z))
|
|
392
|
+
[ Tallies.exact_key(exact), mean + (deviation * z) ]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# The items of `free` (in their order) that may lead: those `may_lead` names (nil names all); when it names none
|
|
396
|
+
# of them, all of `free`. A `may_lead` name that is not an item is an ArgumentError.
|
|
397
|
+
def contenders(may_lead, free = @items)
|
|
398
|
+
return free if nil.equal?(may_lead)
|
|
399
|
+
|
|
400
|
+
listed_items(may_lead, "may_lead", free, free).first
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# THE reading of a list of item names, written once (`contenders`, the order rule's `may_lead` and `pinned`, and
|
|
404
|
+
# `summary`'s `among` read through it). Answers [chosen, read]: chosen the items of `free` (in their order) that
|
|
405
|
+
# the list names, read as a set (a name given twice counts once), or `none` when it names none of them, the one
|
|
406
|
+
# thing its readers differ in; read the items it read, as the set's own Strings in the list's order (a name given
|
|
407
|
+
# twice listed twice), in a frozen Array of the set's own. The list is read by the bandit's one list reader
|
|
408
|
+
# (Tallies.list: any Enumerable but a Hash, read once, by one pass of its each, so a list that answers
|
|
409
|
+
# differently on a second pass, a lazy list or a list whose to_a answers something else is read by that one
|
|
410
|
+
# pass), and only that Array is read and kept, never the caller's object. A `names` that is not a list, or a
|
|
411
|
+
# name that is not an item (the one refusal, `indexed_item`), is an ArgumentError naming `label`.
|
|
412
|
+
def listed_items(names, label, free, none)
|
|
413
|
+
read = Tallies.list(names, label, "items").map { |name| @items[indexed_item(@item_index, name, label)] }.freeze
|
|
414
|
+
named = read.to_h { |item| [ item, true ] }
|
|
415
|
+
chosen = free.select { |item| named.key?(item) }
|
|
416
|
+
[ chosen.empty? ? none : chosen, read ]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# THE eligibility test, written once for the order rule and the pick odds: the item has at least `min_views` views
|
|
420
|
+
# (it may take the explored head, and a head that has them leads).
|
|
421
|
+
def eligible?(item, min_views)
|
|
422
|
+
@views.fetch(item) >= min_views
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# THE P(largest) by kind, written once for the pick odds, `summary` and the order odds: for the items of `among`
|
|
426
|
+
# (1 to EXACT_PICK_MAX, in its order) on the scaled pairs (`parameters`), [P(the item's draw is the largest) for
|
|
427
|
+
# each, and with `maximum: true` E[the largest draw], else nil], by BetaArgmax or NormalArgmax as the set's kind
|
|
428
|
+
# is :beta or :normal, each by quadrature within its rule's stated bound (BetaArgmax's TAIL a member,
|
|
429
|
+
# NormalArgmax's 2 k (k + 1) TAIL + 2e-12).
|
|
430
|
+
def largest_draw(among, maximum: false)
|
|
431
|
+
pairs = among.map { |item| @parameters.fetch(item) }
|
|
432
|
+
argmax = @kind == :beta ? BetaArgmax : NormalArgmax
|
|
433
|
+
[ argmax.probabilities(pairs), maximum ? argmax.expected_maximum(pairs) : nil ]
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# A Sampler (0.3.0's, as written) on `rng`, which must answer rand (the library's one reading of an rng,
|
|
437
|
+
# Inputs.rng: Kernel#respond_to?, so an object that answers rand only through method_missing is refused), wrapped
|
|
438
|
+
# by Inputs.checked so that each of its answers is checked, as a Float in [0, 1), when it is drawn.
|
|
439
|
+
def checked_sampler(rng)
|
|
440
|
+
Sampler.new(Inputs.checked(rng))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# THE refusal of 01 GB6, written once for every computation that cannot read a Beta parameter below
|
|
444
|
+
# BetaArgmax::SMALLEST_PARAMETER, each refusing on the pairs it reads (`pairs`: `parameters` unless told), its
|
|
445
|
+
# words naming what it computes (`computing`): the keyed and given-uniform draws of `rank` and `pick` (whatever
|
|
446
|
+
# branch the uniforms choose, the shuffled one included) and every exact probability (the pick odds, the order
|
|
447
|
+
# and position odds, `summary`'s P(best) and loss) on the scaled pairs, over the free items (every item but the
|
|
448
|
+
# pinned ones, whichever of them may lead or are named, so one set is refused or taken alike by every such call
|
|
449
|
+
# under one pinning; for `summary`, every item whenever `among` names one); and `summary`'s interval on the
|
|
450
|
+
# pairs as given, over every item, on every path. An ArgumentError, before anything is computed, when an item of
|
|
451
|
+
# `among` holds a parameter below it there; a parameter at it is taken. A Normal set has no such bound, and the
|
|
452
|
+
# RNG draws and `values` never ask.
|
|
453
|
+
def refuse_below_smallest_parameter(among = @items, pairs: @parameters,
|
|
454
|
+
computing: "a quantile draw or an exact probability on the scaled pairs")
|
|
455
|
+
return unless @kind == :beta
|
|
456
|
+
|
|
457
|
+
smallest = BetaArgmax::SMALLEST_PARAMETER
|
|
458
|
+
among.each do |item|
|
|
459
|
+
pair = pairs.fetch(item)
|
|
460
|
+
next if pair.min >= smallest
|
|
461
|
+
|
|
462
|
+
raise ArgumentError, "item #{item.inspect}'s Beta parameters #{pair.inspect} must be at least #{smallest} " \
|
|
463
|
+
"for #{computing}"
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# A pair as given or computed, checked by the library's one reading of a pair (Numbers.pair): numbers in the input
|
|
468
|
+
# domain, as Floats; a Beta's both above 0; a Normal's standard deviation above 0.
|
|
469
|
+
def checked_pair(item, pair)
|
|
470
|
+
Numbers.pair(pair, "item #{item.inspect}", PAIR_NAMES.fetch(@kind)) do |number, name, index|
|
|
471
|
+
@kind == :beta || index == 1 ? Checks.positive(number, name) : number
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# A Beta pair whose total is above LARGEST_TOTAL, scaled down to it with its mean kept; every other pair as it
|
|
476
|
+
# is. Half the total (half of each parameter, summed) is judged against half of LARGEST_TOTAL, and the scale is
|
|
477
|
+
# LARGEST_TOTAL / total to the bit. The
|
|
478
|
+
# scaled pair never sums above LARGEST_TOTAL, so a set built from these parameters keeps them as they are: when
|
|
479
|
+
# the two products round to a sum above it, the larger becomes LARGEST_TOTAL less the smaller (a sum of exactly
|
|
480
|
+
# LARGEST_TOTAL; the mean moves by at most a rounding). A product that rounds to 0 reads the smallest Float above 0.
|
|
481
|
+
def scaled_to_largest_total(pair)
|
|
482
|
+
return pair unless @kind == :beta
|
|
483
|
+
|
|
484
|
+
alpha, beta = pair
|
|
485
|
+
half_total = (alpha * 0.5) + (beta * 0.5)
|
|
486
|
+
return pair unless half_total > HALF_LARGEST_TOTAL
|
|
487
|
+
|
|
488
|
+
scale = HALF_LARGEST_TOTAL / half_total
|
|
489
|
+
alpha = [ alpha * scale, SMALLEST_SCALED_PARAMETER ].max
|
|
490
|
+
beta = [ beta * scale, SMALLEST_SCALED_PARAMETER ].max
|
|
491
|
+
return [ alpha, beta ].freeze unless alpha + beta > LARGEST_TOTAL
|
|
492
|
+
|
|
493
|
+
(alpha < beta ? [ alpha, LARGEST_TOTAL - alpha ] : [ LARGEST_TOTAL - beta, beta ]).freeze
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|