pick_me_too 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52db253007776ea2ede6caa43361e3e260660accdd4a0b1eb1439532f1ef92d3
4
- data.tar.gz: 7f370bb747440c33062b8193a3edffb2fe0ac752493735366ea4d7161ded0c4a
3
+ metadata.gz: 0b52966a03f64a0e479d224c5c93abed4ca6e3316ae05b88a86f1c9742da799c
4
+ data.tar.gz: 912417eb4f4c70b4d5211ed0e706f6fbd5be43b518ae4f3be8b198e52d9072b5
5
5
  SHA512:
6
- metadata.gz: 4f70d7864e25ca65ea5ca660c1964f0e79229da429e2414539ffaf3a65da50b2a30f70f186266532adbf6115b4e7e7a9bbd7b0b8727994c690876e9648ff0ab7
7
- data.tar.gz: bcf37dedb54b3304367799a16c3521541fa57c0db4dd4e819220dcb9ca85ff804c37e1b026e7867cf90a8990575d10e2b7ac0b452433d74760a358301b1eec8f
6
+ metadata.gz: f9391158418a645aa5886a86934d695251e22168870c7df05946dfbc9d4a2a64253f8476574dbe44e3bf40fc6f9f9b4e6c9e1d8a466586baef29c4c729c41a03
7
+ data.tar.gz: 3e77cab8f0ce92f2ef30434ad5d45eb99003eae157aad21be287636b71ec16a1f373e3018941c8a8094882053806a1d5749667fec9ae9734a137392806a9075d
data/CHANGES.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.1.1 *2022-10-30*
4
+ * simplified, and perhaps improved, the code to compile the frequency list into a nested ternary expression; ***NOTE***: I am not considering this a breaking change, though it may change the sequence of items picked by a fixed random number sequence
3
5
  ## 1.1.0 *2022-8-21*
4
6
  * added the `randomize!` method
5
7
  ## 1.0.0 *2022-8-14*
data/README.md CHANGED
@@ -29,7 +29,7 @@ picker = PickMeToo.new({foo: 1, bar: 2, baz: 0.5}, -> { rng.rand })
29
29
  counter = Hash.new 0
30
30
  32.times { counter[picker.pick] += 1 }
31
31
  counter
32
- # => {:foo=>13, :bar=>12, :baz=>7}
32
+ # => {:bar=>15, :foo=>12, :baz=>5}
33
33
 
34
34
  # you don't need to provide your own random number sequence
35
35
  picker = PickMeToo({a: 1, b: 2, c: 3})
data/lib/pick_me_too.rb CHANGED
@@ -17,7 +17,7 @@
17
17
  # probability the next thing you pick is also a cat, and the urn will never be picked empty. (And of course
18
18
  # this is all a metaphor.)
19
19
  class PickMeToo
20
- VERSION = '1.1.0'
20
+ VERSION = '1.1.1'
21
21
 
22
22
  class Error < StandardError; end
23
23
 
@@ -42,11 +42,9 @@ class PickMeToo
42
42
  if @objects.length == 1
43
43
  @picker = ->(_p) { 0 }
44
44
  else
45
- frequencies = frequencies.map(&:last)
46
- balanced_binary_tree = bifurcate(frequencies.dup)
47
- probability_tree = probabilities(frequencies, balanced_binary_tree)
45
+ root = balanced_binary_tree(frequencies)
48
46
  # compile everything into a nested ternary expression
49
- @picker = eval "->(p) { #{ternarize(probability_tree)} }"
47
+ @picker = eval "->(p) { #{ternarize(root, 0)} }"
50
48
  end
51
49
  end
52
50
 
@@ -82,94 +80,49 @@ class PickMeToo
82
80
  raise Error, "the following have non-positive frequencies: #{bad.inspect}" if bad.any?
83
81
 
84
82
  total = good.map(&:last).sum.to_f
85
- good.map { |o, n| [o, n / total] }
83
+ # sort by size of probability interval -- optimization step
84
+ good.sort_by(&:last).reverse.map { |o, n| [o, n / total] }
86
85
  end
87
86
 
88
- # reduce the probability tree to nested ternary expressions
89
- def ternarize(ptree)
90
- p, left, right = ptree.values_at :p, :left, :right
91
- left = left.is_a?(Numeric) ? left : ternarize(left)
92
- right = right.is_a?(Numeric) ? right : ternarize(right)
93
- "(p > #{p} ? #{right} : #{left})"
94
- end
95
-
96
- def probabilities(frequencies, tree)
97
- tree = sum_probabilities(tree, 0)
98
- replace_frequencies_with_indices(tree, frequencies.each_with_index.to_a)
99
- tree
100
- end
101
-
102
- def replace_frequencies_with_indices(tree, frequencies)
103
- left, right = tree.values_at :left, :right
104
- if left.is_a?(Numeric)
105
- i = frequencies.index { |v,| v == left }
106
- *, i = frequencies.slice!(i)
107
- tree[:left] = i
108
- else
109
- replace_frequencies_with_indices(left, frequencies)
110
- end
111
- if right.is_a?(Numeric)
112
- i = frequencies.index { |v,| v == right }
113
- *, i = frequencies.slice!(i)
114
- tree[:right] = i
115
- else
116
- replace_frequencies_with_indices(right, frequencies)
87
+ # treat the frequencies as a heap
88
+ # returns the root of this binary tree
89
+ def balanced_binary_tree(frequencies)
90
+ frequencies = frequencies.each_with_index.map { |(*, i), idx| { interval: i, index: idx } }
91
+ frequencies.each do |obj|
92
+ left_idx = obj[:index] * 2 + 1
93
+ next unless (left = frequencies[left_idx])
94
+
95
+ obj[:left] = left
96
+ right_idx = left_idx + 1
97
+ if (right = frequencies[right_idx])
98
+ obj[:right] = right
99
+ end
117
100
  end
101
+ frequencies[0]
118
102
  end
119
103
 
120
- # convert the frequency numbers to probabilities
121
- def sum_probabilities(tree, base)
122
- left, right = tree
123
- p = left.flatten.sum + base
124
- left = left.length == 1 ? left.first : sum_probabilities(left, base)
125
- right = right.length == 1 ? right.first : sum_probabilities(right, p)
126
- { p: p, left: left, right: right }
127
- end
128
-
129
- # distribute the frequencies so their as balanced as possible
130
- # the better to reduce expected length of the binary search
131
- def bifurcate(nums)
132
- return nums if nums.length < 2
104
+ # what is the sum of all intervals under this node?
105
+ def sum(obj)
106
+ return 0 unless obj
133
107
 
134
- max = total = 0
135
- max_index = -1
136
- # make one loop find all these things
137
- nums.each_with_index do |n, i|
138
- total += n
139
- if n > max
140
- max = n
141
- max_index = i
142
- end
143
- end
144
- half = total / 2.0
145
- right = [nums.slice!(max_index)]
146
- if max >= half
147
- [bifurcate(nums), right]
148
- else
149
- gap = half - max
150
- while rv = fit_gap(gap, nums)
151
- removed, remaining_gap = rv
152
- right << removed
153
- break unless gap = remaining_gap
154
- end
155
- [bifurcate(nums), bifurcate(right)]
108
+ obj[:sum] ||= begin
109
+ left = sum(obj[:left])
110
+ right = sum(obj[:right])
111
+ left + right + obj[:interval]
156
112
  end
157
113
  end
158
114
 
159
- # look for the frequency best suited to balance the two branches
160
- def fit_gap(gap, nums)
161
- best_index = 0
162
- best_fit = (gap - nums[0]).abs
163
- nums.each_with_index.drop(1).each do |n, i|
164
- fit = (gap - n).abs
165
- if fit < best_fit
166
- best_index = i
167
- best_fit = fit
168
- end
169
- end
170
- if nums[best_index] < gap * 2
171
- n = nums.slice!(best_index)
172
- [n, n < gap ? gap - n : nil]
115
+ # reduce the probability tree to nested ternary expressions
116
+ def ternarize(node, acc)
117
+ left = sum(node[:left])
118
+ return node[:index] if left == 0 # this is a leaf
119
+
120
+ right = if (r = node[:right])
121
+ increment = acc + left + node[:interval]
122
+ "(p < #{increment} ? #{node[:index]} : #{ternarize(r, increment)})"
123
+ else
124
+ node[:index]
173
125
  end
126
+ "(p < #{left + acc} ? #{ternarize(node[:left], acc)} : #{right})"
174
127
  end
175
128
  end
@@ -21,6 +21,6 @@ class BasicTest < Minitest::Test
21
21
  picker = PickMeToo.new({ foo: 1, bar: 2, baz: 0.5 }, -> { rng.rand })
22
22
  counter = Hash.new 0
23
23
  32.times { counter[picker.pick] += 1 }
24
- assert_equal({ foo: 13, bar: 12, baz: 7 }, counter)
24
+ assert_equal({:bar=>15, :foo=>12, :baz=>5}, counter)
25
25
  end
26
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pick_me_too
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David F. Houghton
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-21 00:00:00.000000000 Z
11
+ date: 2022-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -107,7 +107,7 @@ homepage: https://github.com/dfhoughton/pick_me_too
107
107
  licenses:
108
108
  - MIT
109
109
  metadata: {}
110
- post_install_message:
110
+ post_install_message:
111
111
  rdoc_options: []
112
112
  require_paths:
113
113
  - lib
@@ -122,9 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  requirements: []
125
- rubyforge_project:
126
- rubygems_version: 2.7.6
127
- signing_key:
125
+ rubygems_version: 3.3.7
126
+ signing_key:
128
127
  specification_version: 4
129
128
  summary: Randomly select items from a list with specified frequencies
130
129
  test_files: