best_choice 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|