best_choice 0.0.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 +7 -0
- data/README.rdoc +3 -0
- data/Rakefile +32 -0
- data/lib/best_choice.rb +26 -0
- data/lib/best_choice/algorithm.rb +65 -0
- data/lib/best_choice/algorithms/epsilon_greedy.rb +31 -0
- data/lib/best_choice/rails_util.rb +76 -0
- data/lib/best_choice/selector.rb +51 -0
- data/lib/best_choice/selectors/greedy_redis.rb +16 -0
- data/lib/best_choice/storage/redis_hash.rb +71 -0
- data/lib/best_choice/version.rb +5 -0
- data/test/algorithm_test.rb +40 -0
- data/test/algorithms/epsilon_greedy_test.rb +152 -0
- data/test/best_choice_test.rb +97 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/main_controller.rb +20 -0
- data/test/dummy/app/views/layouts/application.html.erb +11 -0
- data/test/dummy/app/views/main/landing.html.erb +22 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +12 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +5059 -0
- data/test/dummy/test/controllers/main_controller_test.rb +116 -0
- data/test/fixtures/dummy_algorithm.rb +6 -0
- data/test/fixtures/dummy_storage.rb +6 -0
- data/test/test_helper.rb +20 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2e6e75982850ff066a696cdb4b59c075d3842719
|
4
|
+
data.tar.gz: d85afa3104d948cc738f4e9481d60a6072e86e2f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a7b1352cae375f7465245be07ef977320c048cec1f053cc85f63106470b85b71b141f8c1acb2786992c259cfd5cdfc79bedd3469fe0e93fde72c15b6c6ca398a
|
7
|
+
data.tar.gz: 10dd60b5a74bcca468a1507b33f69b583e719d58a9ae43f094824b589882f61367777f1cfdaf315cd5e3eb185b5271b5598e9abbeb5302777e5c56a6d5802ec3
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'BestChoice'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
data/lib/best_choice.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require './lib/best_choice/algorithm'
|
2
|
+
require './lib/best_choice/selector'
|
3
|
+
|
4
|
+
require './lib/best_choice/algorithms/epsilon_greedy'
|
5
|
+
require './lib/best_choice/storage/redis_hash'
|
6
|
+
|
7
|
+
require './lib/best_choice/selectors/greedy_redis'
|
8
|
+
|
9
|
+
|
10
|
+
module BestChoice
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def for selector_name, selector: default_selector, **opts
|
15
|
+
selector.new(selector_name, opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_selector
|
19
|
+
@default_selector ||= Selectors::GreedyRedis
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_selector= selector
|
23
|
+
@default_selector = selector
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module BestChoice
|
2
|
+
|
3
|
+
class Algorithm
|
4
|
+
|
5
|
+
class NoAvailableOptionsError < StandardError ; end
|
6
|
+
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
raise NotImplementedError.new 'Expected subclass to implement'
|
10
|
+
end
|
11
|
+
|
12
|
+
def pick_option stats
|
13
|
+
unless stats.kind_of? Array
|
14
|
+
raise ArgumentError.new 'Expected an array'
|
15
|
+
end
|
16
|
+
|
17
|
+
case stats.length
|
18
|
+
when 0
|
19
|
+
raise NoAvailableOptionsError
|
20
|
+
when 1
|
21
|
+
stats.first
|
22
|
+
else
|
23
|
+
do_actual_pick stats
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def do_actual_pick stats=[]
|
31
|
+
raise NotImplementedError.new 'Expected subclass to implement'
|
32
|
+
end
|
33
|
+
|
34
|
+
def choice_rate option_data
|
35
|
+
display_count = option_data[:display_count]
|
36
|
+
success_count = option_data[:success_count]
|
37
|
+
|
38
|
+
unless is_non_negative_int?(display_count)
|
39
|
+
raise ArgumentError.new "Invalid display_count: #{display_count.inspect}"
|
40
|
+
end
|
41
|
+
unless is_non_negative_int?(success_count)
|
42
|
+
raise ArgumentError.new "Invalid success_count: #{success_count.inspect}"
|
43
|
+
end
|
44
|
+
if option_data[:success_count] > option_data[:display_count]
|
45
|
+
raise ArgumentError.new "success_count: #{success_count} " \
|
46
|
+
"higher than display_count: #{display_count}"
|
47
|
+
end
|
48
|
+
|
49
|
+
if option_data[:display_count] == 0
|
50
|
+
100
|
51
|
+
else
|
52
|
+
option_data[:success_count].to_f / option_data[:display_count]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def is_non_negative_int? arg
|
60
|
+
arg.kind_of?(Integer) && arg >= 0
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module BestChoice
|
2
|
+
module Algorithms
|
3
|
+
|
4
|
+
class EpsilonGreedy < BestChoice::Algorithm
|
5
|
+
|
6
|
+
DEFAULT_RANDOMNESS_FACTOR = 10
|
7
|
+
|
8
|
+
|
9
|
+
def initialize randomness_factor: DEFAULT_RANDOMNESS_FACTOR
|
10
|
+
unless randomness_factor.between? 0, 100
|
11
|
+
raise ArgumentError.new "Invalid randomness_factor: #{randomness_factor}"
|
12
|
+
end
|
13
|
+
@randomness_factor = randomness_factor
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def do_actual_pick stats
|
20
|
+
options_by_rate = stats.sort_by{ |o| choice_rate(o) }
|
21
|
+
if rand(100)+1 > @randomness_factor
|
22
|
+
options_by_rate.last
|
23
|
+
else
|
24
|
+
options_by_rate[0..-2].sample
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module BestChoice
|
4
|
+
module RailsUtil
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
|
9
|
+
included do
|
10
|
+
before_filter do
|
11
|
+
RailsUtil.instance_variable_set('@session', session)
|
12
|
+
end
|
13
|
+
|
14
|
+
after_filter do
|
15
|
+
if @_best_choice_selectors
|
16
|
+
RailsUtil.best_choice_picks = @_best_choice_selectors.inject(
|
17
|
+
RailsUtil.best_choice_picks) do |picks, (_, selector)|
|
18
|
+
picks.merge!(selector.name => selector.picked_option)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
module SelectorExtension
|
26
|
+
|
27
|
+
def self.extended klass
|
28
|
+
klass.class_eval do
|
29
|
+
define_method :mark_success do
|
30
|
+
if pick = RailsUtil.best_choice_picks[name]
|
31
|
+
super pick
|
32
|
+
RailsUtil.best_choice_picks.delete name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# -------------------------------------------------
|
42
|
+
# Module functions
|
43
|
+
|
44
|
+
def self.best_choice_picks
|
45
|
+
@session[:_best_choice_picks] ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.best_choice_picks= picked_options
|
49
|
+
@session[:_best_choice_picks] = picked_options
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# -------------------------------------------------
|
54
|
+
# Methods visible to Rails controller
|
55
|
+
|
56
|
+
def best_choice_for s_name, selector: BestChoice.default_selector, **opts
|
57
|
+
s = selector.new s_name, opts
|
58
|
+
s.extend SelectorExtension
|
59
|
+
best_choice_selector_register s
|
60
|
+
s.picked_option = RailsUtil.best_choice_picks[s_name]
|
61
|
+
s
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Save the test data in the instance variable, so that
|
68
|
+
# it can be later retrieved in the after_filter.
|
69
|
+
#
|
70
|
+
def best_choice_selector_register selector
|
71
|
+
@_best_choice_selectors ||= {}
|
72
|
+
@_best_choice_selectors[selector.name] ||= selector
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module BestChoice
|
2
|
+
|
3
|
+
class Selector
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
attr_writer :picked_option
|
7
|
+
|
8
|
+
|
9
|
+
def initialize name, storage:, algorithm:, **opts
|
10
|
+
@name = name
|
11
|
+
@storage = storage.new(name)
|
12
|
+
@algorithm = algorithm.new(opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
def option option
|
16
|
+
@storage.add option
|
17
|
+
|
18
|
+
if should_display? option
|
19
|
+
mark_display option
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def mark_success option
|
25
|
+
@marked_success ||= !!@storage.success_count_incr(option)
|
26
|
+
end
|
27
|
+
|
28
|
+
def picked_option
|
29
|
+
@picked_option ||= @algorithm.pick_option(@storage.stats)[:name]
|
30
|
+
end
|
31
|
+
|
32
|
+
def equal? selector
|
33
|
+
self.class == selector.class && name == selector.name
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :== :equal?
|
37
|
+
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def should_display? option
|
42
|
+
picked_option == option
|
43
|
+
end
|
44
|
+
|
45
|
+
def mark_display option
|
46
|
+
@marked_display ||= !!@storage.display_count_incr(option)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module BestChoice
|
2
|
+
module Selectors
|
3
|
+
|
4
|
+
class GreedyRedis < BestChoice::Selector
|
5
|
+
|
6
|
+
def initialize name, opts={}
|
7
|
+
super name, opts.merge({
|
8
|
+
storage: Storage::RedisHash,
|
9
|
+
algorithm: Algorithms::EpsilonGreedy
|
10
|
+
})
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'redis-objects'
|
3
|
+
|
4
|
+
module BestChoice
|
5
|
+
module Storage
|
6
|
+
|
7
|
+
class RedisHash
|
8
|
+
|
9
|
+
def initialize storage_name
|
10
|
+
@hash_key = Redis::HashKey.new "best_choice:#{storage_name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def options
|
14
|
+
if @hash_key[:options]
|
15
|
+
JSON.parse @hash_key[:options]
|
16
|
+
else
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add option
|
22
|
+
unless has_option? option
|
23
|
+
@hash_key[:options] = (options << option).to_json
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def display_count_incr option
|
28
|
+
@hash_key.incr display_count_key(option)
|
29
|
+
end
|
30
|
+
|
31
|
+
def success_count_incr option
|
32
|
+
@hash_key.incr success_count_key(option)
|
33
|
+
end
|
34
|
+
|
35
|
+
def stats
|
36
|
+
options.inject([]) { |memo, option|
|
37
|
+
memo << {
|
38
|
+
name: option,
|
39
|
+
display_count: display_count(option),
|
40
|
+
success_count: success_count(option)
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def has_option? option
|
49
|
+
!!options.index(option)
|
50
|
+
end
|
51
|
+
|
52
|
+
def display_count option
|
53
|
+
(@hash_key[display_count_key(option)] || 0).to_i
|
54
|
+
end
|
55
|
+
|
56
|
+
def success_count option
|
57
|
+
(@hash_key[success_count_key(option)] || 0).to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
def display_count_key option
|
61
|
+
"display_#{option}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def success_count_key option
|
65
|
+
"success_#{option}"
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AlgorithmTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@algorithm = DummyAlgorithm.new
|
7
|
+
end
|
8
|
+
|
9
|
+
test 'pick_option' do
|
10
|
+
# Empty stats
|
11
|
+
assert_raises(BestChoice::Algorithm::NoAvailableOptionsError) do
|
12
|
+
@algorithm.pick_option []
|
13
|
+
end
|
14
|
+
|
15
|
+
# One option
|
16
|
+
assert_equal 'an_option', @algorithm.pick_option(['an_option'])
|
17
|
+
|
18
|
+
# Multiple options
|
19
|
+
options = ['a','b']
|
20
|
+
mock(@algorithm).do_actual_pick options
|
21
|
+
@algorithm.pick_option options
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'the class itself does not implement do_actual_pick method' do
|
25
|
+
assert_raises(NotImplementedError) do
|
26
|
+
@algorithm.send :do_actual_pick
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'is_non_negative_int?' do
|
31
|
+
[ nil, '', 'a', -1, -0.01, 0.1, 12.23 ].each do |arg|
|
32
|
+
refute @algorithm.send :is_non_negative_int?, arg
|
33
|
+
end
|
34
|
+
|
35
|
+
[ 0, 1 ].each do |arg|
|
36
|
+
assert @algorithm.send :is_non_negative_int?, arg
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class EpsilonGreedyTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@algorithm = BestChoice::Algorithms::EpsilonGreedy.new
|
7
|
+
end
|
8
|
+
|
9
|
+
test 'randomness_factor validation' do
|
10
|
+
invalid_values = [ -1, -0.1, 100.5, 101 ]
|
11
|
+
invalid_values.each { |val|
|
12
|
+
assert_raises(ArgumentError) do
|
13
|
+
BestChoice::Algorithms::EpsilonGreedy.new(randomness_factor: val)
|
14
|
+
end
|
15
|
+
}
|
16
|
+
|
17
|
+
valid_values = [ 0.1, 1, 100 ]
|
18
|
+
valid_values.each { |val|
|
19
|
+
assert_nothing_raised do
|
20
|
+
BestChoice::Algorithms::EpsilonGreedy.new(randomness_factor: val)
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'choice_rate' do
|
26
|
+
invalid_data = [
|
27
|
+
{ display_count: nil , success_count: 0 },
|
28
|
+
{ display_count: 1 , success_count: nil },
|
29
|
+
{ display_count: 0 , success_count: -1 },
|
30
|
+
{ display_count: -1 , success_count: -2 },
|
31
|
+
{ display_count: 1.0 , success_count: 0 },
|
32
|
+
{ display_count: 1.1 , success_count: 0 },
|
33
|
+
{ display_count: 1 , success_count: 2 },
|
34
|
+
]
|
35
|
+
|
36
|
+
invalid_data.each { |data|
|
37
|
+
assert_raises(ArgumentError) do
|
38
|
+
@algorithm.send :choice_rate, data
|
39
|
+
end
|
40
|
+
}
|
41
|
+
|
42
|
+
assert_equal 100,
|
43
|
+
@algorithm.send(:choice_rate, { display_count: 0, success_count: 0 })
|
44
|
+
assert_equal 0.5,
|
45
|
+
@algorithm.send(:choice_rate, { display_count: 6, success_count: 3 })
|
46
|
+
assert_equal 2/3.to_f,
|
47
|
+
@algorithm.send(:choice_rate, { display_count: 6, success_count: 4 })
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'actual picking via pick_option' do
|
51
|
+
|
52
|
+
# --------------------------------------------------------
|
53
|
+
# Non-random version
|
54
|
+
# --------------------------------------------------------
|
55
|
+
|
56
|
+
non_random_algorithm = BestChoice::Algorithms::EpsilonGreedy.new(
|
57
|
+
randomness_factor: 0)
|
58
|
+
|
59
|
+
# The first option should be always chosen.
|
60
|
+
stats = [
|
61
|
+
{ display_count: 2 , success_count: 2 },
|
62
|
+
{ display_count: 2 , success_count: 1 },
|
63
|
+
{ display_count: 1 , success_count: 0 }
|
64
|
+
]
|
65
|
+
|
66
|
+
10.times {
|
67
|
+
assert_equal stats[0], non_random_algorithm.pick_option(stats)
|
68
|
+
}
|
69
|
+
|
70
|
+
# If the second option has the same success_count as the first one, but
|
71
|
+
# a higher display_count, the first option should still be chosen.
|
72
|
+
#
|
73
|
+
stats = [
|
74
|
+
{ display_count: 2 , success_count: 2 },
|
75
|
+
{ display_count: 3 , success_count: 2 },
|
76
|
+
{ display_count: 1 , success_count: 0 }
|
77
|
+
]
|
78
|
+
|
79
|
+
10.times {
|
80
|
+
assert_equal stats[0], non_random_algorithm.pick_option(stats)
|
81
|
+
}
|
82
|
+
|
83
|
+
# Even if the ratio of success_count to display_count of the first option
|
84
|
+
# drops to the ratio of the second option, it would still be preferred
|
85
|
+
# if its display_count is higher.
|
86
|
+
#
|
87
|
+
stats = [
|
88
|
+
{ display_count: 6 , success_count: 4 },
|
89
|
+
{ display_count: 3 , success_count: 2 },
|
90
|
+
{ display_count: 1 , success_count: 0 }
|
91
|
+
]
|
92
|
+
|
93
|
+
10.times {
|
94
|
+
assert_equal stats[0], non_random_algorithm.pick_option(stats)
|
95
|
+
}
|
96
|
+
|
97
|
+
# Changing the ratio in favor of the second option, should make it the
|
98
|
+
# new optimal choice.
|
99
|
+
#
|
100
|
+
stats = [
|
101
|
+
{ display_count: 6 , success_count: 3 },
|
102
|
+
{ display_count: 4 , success_count: 3 },
|
103
|
+
{ display_count: 1 , success_count: 0 }
|
104
|
+
]
|
105
|
+
|
106
|
+
10.times {
|
107
|
+
assert_equal stats[1], non_random_algorithm.pick_option(stats)
|
108
|
+
}
|
109
|
+
|
110
|
+
# --------------------------------------------------------
|
111
|
+
# 100% random version
|
112
|
+
# --------------------------------------------------------
|
113
|
+
|
114
|
+
totally_random_algorithm = BestChoice::Algorithms::EpsilonGreedy.new(
|
115
|
+
randomness_factor: 100)
|
116
|
+
|
117
|
+
stats = [
|
118
|
+
{ display_count: 5, success_count: 2 },
|
119
|
+
{ display_count: 5, success_count: 1 },
|
120
|
+
{ display_count: 2, success_count: 0 }
|
121
|
+
]
|
122
|
+
|
123
|
+
# The probability of choosing each non-optimal option should be equal.
|
124
|
+
#
|
125
|
+
chosen_options = []
|
126
|
+
20.times {
|
127
|
+
chosen_options << totally_random_algorithm.pick_option(stats)
|
128
|
+
}
|
129
|
+
assert_equal stats[1..2],
|
130
|
+
chosen_options.uniq.sort_by{ |d| d[:success_count] }.reverse
|
131
|
+
|
132
|
+
# --------------------------------------------------------
|
133
|
+
# Default version
|
134
|
+
# --------------------------------------------------------
|
135
|
+
|
136
|
+
stats = [
|
137
|
+
{ display_count: 5, success_count: 2 },
|
138
|
+
{ display_count: 5, success_count: 1 }
|
139
|
+
]
|
140
|
+
|
141
|
+
# The second option should be chosen only in ~10% of cases.
|
142
|
+
#
|
143
|
+
chosen_options = []
|
144
|
+
1000.times { chosen_options << @algorithm.pick_option(stats) }
|
145
|
+
|
146
|
+
counts = Hash.new 0
|
147
|
+
chosen_options.each { |opt| counts[opt] += 1 }
|
148
|
+
|
149
|
+
assert counts[stats[1]] > 80 # Leave some margin.
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|