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.
- checksums.yaml +7 -0
- data/lib/tiny_outcome.rb +155 -0
- 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
|
data/lib/tiny_outcome.rb
ADDED
@@ -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: []
|