pick_me_too 1.1.0 → 1.1.1

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 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: