tiny_outcome 1.0.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 (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: []