tiny_outcome 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/tiny_outcome.rb +155 -0
  3. metadata +44 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4bcfb2b56da2846a690a84f9488326c2ddf16f48d1906ead7da68a900c8cc3ec
4
+ data.tar.gz: 8709cc918943d9dfbc7b02ec9bd5ff666808c123e9f557a9fb04b8659a6d394b
5
+ SHA512:
6
+ metadata.gz: c3789f5b4df3368b9b5e76997176c294de4f0c6ef27f6562b8562c186fa2d8feca4c784598d34fecf8babaa32b52705761a6a0a5d843ea13865f40ff844afe90
7
+ data.tar.gz: 78c4b3f3a69110a43ff58963da0e6371d43a1a84eb6db2190580f23967a8b955b22b2a18429e0ad8ec396f1e4af8de246e3c20d987e4de68e9463aa79db03864
@@ -0,0 +1,155 @@
1
+ # TinyOutcomes are used to track a history of binary outcomes to the specified
2
+ # precision. for example:
3
+ # Outcome.new(128) # tracks 128 historic outcomes
4
+ #
5
+ # when the number of samples added exceeds the number of samples to track, then
6
+ # the oldest sample is dropped automatically. in this way, a TinyOutcome that
7
+ # tracks 128 samples will start discarding its oldest sample as soon as the
8
+ # 129th sample is added.
9
+ #
10
+ # TinyOutcomes can also be cold, or warm. warm TinyOutcomes have received a
11
+ # minimum number of historic samples to be considered useful. see #initialize
12
+ # for more information on how warmth works.
13
+ #
14
+ # Usage:
15
+ # o = TinyOutcome.new(
16
+ # 128, precision of 128 samples
17
+ # TinyOutcome::WARM_TWO_THIRDS warms up at 2/3rs of precision
18
+ # )
19
+ # o.to_hash convenient way to see what's up
20
+ # 87.times { o << rand(2) }
21
+ #
22
+ # to_s reveals how we're doing:
23
+ # L10 1111000110 coinflip 0.49 84/84::128/128
24
+ #
25
+ # this tells us that of the 128 precision capacity, we're currently warmed up
26
+ # because we have the minimum (at least 84 samples) to be considered warmed up.
27
+ # there's also a prediction here: that the outcome is essentially a coinflip.
28
+ # this is because the observed likelihood of an outcome of 1 is 49% in this
29
+ # example, well within the range of random chance. if we had a TinyOutcome with
30
+ # a precision of 10,000 we wouldn't necessarily consider 49% a true coinflip,
31
+ # but because we're trying to predict things within a relatively small sample
32
+ # size, we don't want to go all the way to that level of precision, it's more
33
+ # like just trying to win more than we lose.
34
+ class TinyOutcome
35
+ attr_reader :precision,
36
+ :samples,
37
+ :warmth,
38
+ :warmup,
39
+ :value
40
+
41
+ WARM_FULL = :full
42
+ WARM_TWO_THIRDS = :two_thirds
43
+ WARM_HALF = :half
44
+ WARM_ONE_THIRD = :one_third
45
+ WARM_NONE = :none
46
+
47
+ # precision: the number of historic samples you want to store
48
+ # warmup: defaults to WARM_FULL, lets the user specify how many samples we
49
+ # need in order to consider this Outcome tracker "warm", i.e. it has enough
50
+ # samples that we can trust the probability output
51
+ def initialize(precision, warmup=WARM_FULL)
52
+ @precision = precision
53
+ @samples = 0
54
+ @warmth = 0
55
+ @value = 0
56
+ @warmup = case warmup
57
+ when WARM_FULL then precision
58
+ when WARM_TWO_THIRDS then (precision / 3) * 2
59
+ when WARM_HALF then precision / 2
60
+ when WARM_ONE_THIRD then precision / 3
61
+ when WARM_NONE then 0
62
+ else
63
+ raise "Invalid warmup: #{warmup.inspect}"
64
+ end
65
+ end
66
+
67
+ # add a sample to the historic outcomes. the new sample is added to the
68
+ # low-order bits. the new sample is literally left-shifted into the value. the
69
+ # only reason this is a custom method is because some metadata needs to be
70
+ # updated when a new sample is added
71
+ def <<(sample)
72
+ raise "Invalid sample: #{sample}" unless sample == 0 || sample == 1
73
+
74
+ @value = ((value << 1) | sample) & (2**precision - 1)
75
+ @warmth += 1 unless warmth == warmup
76
+ @samples += 1 unless samples == precision
77
+
78
+ value
79
+ end
80
+
81
+ # true if #probability is >= percentage
82
+ # false otherwise
83
+ def winner_at?(percentage)
84
+ probability >= percentage
85
+ end
86
+
87
+ # float: 0.0-1.0
88
+ # percentage of 1s out of the existing samples
89
+ #
90
+ # number of 1s
91
+ # probabilty = ---------------
92
+ # total samples
93
+ def probability
94
+ return -1 unless warm?
95
+ value.to_s(2).count('1') / samples.to_f
96
+ end
97
+
98
+ # classifies the probability of the next outcome
99
+ #
100
+ # :cold - if this Outcome isn't yet warm
101
+ # :highly_positive - greater than 95% chance that the next outcome will be a 1
102
+ # :positive - greater than 90% chance the next outcome will be a 1
103
+ # :coinflip - 50% chance (+/- 5%) that the next outcome will be a 1
104
+ # :negative - less than 10% chance the next outcome will be a 1
105
+ # :highly_negative - less than 5% chance the next outcome will be a 1
106
+ # :weak - for all other outcomes
107
+ def prediction
108
+ return :cold unless warm?
109
+
110
+ case probability
111
+ when 0...0.05 then :disaster
112
+ when 0.05...0.1 then :strongly_negative
113
+ when 0.1...0.32 then :negative
114
+ when 0.32..0.34 then :one_third
115
+ when 0.34...0.48 then :weakly_negative
116
+ when 0.48..0.52 then :coinflip
117
+ when 0.52...0.65 then :weakly_positive
118
+ when 0.65..0.67 then :two_thirds
119
+ when 0.67..0.9 then :positive
120
+ when 0.9...0.95 then :strongly_positive
121
+ when 0.95..1.0 then :amazing
122
+ end
123
+ end
124
+
125
+ # true if we've received at least warmup number of samples
126
+ # false otherwise
127
+ def warm?
128
+ warmth == warmup
129
+ end
130
+
131
+ # true if we've received at least precision number of samples
132
+ # false otherwise
133
+ def full?
134
+ samples == precision
135
+ end
136
+
137
+ # convenient way to see what's up
138
+ def to_hash
139
+ [:value,
140
+ :samples,
141
+ :warmth,
142
+ :warmup,:warm?,
143
+ :probability,
144
+ :prediction,
145
+ ].each_with_object({}) do |attr, memo|
146
+ memo[attr] = send(attr)
147
+ memo
148
+ end
149
+ end
150
+
151
+ def to_s
152
+ max_backward = [value.to_s(2).length, 10].min
153
+ "L10 #{value.to_s(2)[-max_backward..-1].rjust(10, '?')} #{prediction} #{'%.2f' % probability} #{warmth}/#{warmup}::#{samples}/#{precision}"
154
+ end
155
+ end
metadata ADDED
@@ -0,0 +1,44 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tiny_outcome
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeff Lunt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: a tiny outcome tracker with almost no features
14
+ email: jefflunt@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/tiny_outcome.rb
20
+ homepage: https://github.com/jefflunt/tiny_outcome
21
+ licenses:
22
+ - MIT
23
+ metadata: {}
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ requirements: []
39
+ rubygems_version: 3.3.7
40
+ signing_key:
41
+ specification_version: 4
42
+ summary: want to track the outcome of binary events, and absolutely nothing else?
43
+ then this library is for you.
44
+ test_files: []