trailguide 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/config/routes.rb +2 -0
- data/lib/trail_guide/adapters/participants/anonymous.rb +70 -0
- data/lib/trail_guide/adapters/participants/cookie.rb +99 -0
- data/lib/trail_guide/adapters/participants/multi.rb +41 -0
- data/lib/trail_guide/adapters/participants/redis.rb +83 -0
- data/lib/trail_guide/adapters/participants/session.rb +73 -0
- data/lib/trail_guide/adapters/participants/unity.rb +107 -0
- data/lib/trail_guide/adapters/participants.rb +12 -0
- data/lib/trail_guide/adapters.rb +6 -0
- data/lib/trail_guide/algorithms/bandit.rb +48 -0
- data/lib/trail_guide/algorithms/distributed.rb +26 -0
- data/lib/trail_guide/algorithms/random.rb +19 -0
- data/lib/trail_guide/algorithms/weighted.rb +25 -0
- data/lib/trail_guide/algorithms.rb +24 -0
- data/lib/trail_guide/catalog.rb +71 -0
- data/lib/trail_guide/engine.rb +65 -0
- data/lib/trail_guide/experiment.rb +327 -0
- data/lib/trail_guide/helper.rb +185 -0
- data/lib/trail_guide/participant.rb +110 -0
- data/lib/trail_guide/unity.rb +87 -0
- data/lib/trail_guide/variant.rb +103 -0
- data/lib/trail_guide/version.rb +14 -0
- data/lib/trailguide.rb +57 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 91de95da7df9831c3ab11b091d4caa81d566044ca054f450a4ece7b6c74e50eb
|
4
|
+
data.tar.gz: 3c4a00eccbbb4a3983a87462b52f6e7489ba4079f1f74f5e7bb53959b163a9dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fa89b438040e89c75f7ec183f3ded180c95506d205e60f3c7b1c099b13e63162e91b1205147176cb9271c4a7759f59b0ed1dff793a897c6fae253c0df056007d
|
7
|
+
data.tar.gz: e877c3aa6c6abcd9375cfafe45c30cea9e86f1a4ca0215a3fd0581b7337487c48ed2fe9ca05bd5f5768292901f2d4dda184f7a6ebe06d1b4cfdee9dbba014f69
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Mark Rebec
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# TrailGuide
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'trailguide'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install trailguide
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/config/routes.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Participants
|
4
|
+
class Anonymous
|
5
|
+
include Canfig::Instance
|
6
|
+
|
7
|
+
class << self
|
8
|
+
alias_method :configure, :new
|
9
|
+
def new(context, &block)
|
10
|
+
configure(&block).new(context)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(&block)
|
15
|
+
configure do |config|
|
16
|
+
yield(config) if block_given?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# instance method, creates a new adapter and passes through config
|
21
|
+
def new(context)
|
22
|
+
Adapter.new(context, configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
class Adapter
|
26
|
+
attr_reader :context, :config
|
27
|
+
|
28
|
+
def initialize(context, config)
|
29
|
+
@context = context
|
30
|
+
@config = config
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](key)
|
34
|
+
hash[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
def []=(key, value)
|
38
|
+
hash[key] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(key)
|
42
|
+
hash.delete(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy!
|
46
|
+
@hash = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def keys
|
50
|
+
hash.keys
|
51
|
+
end
|
52
|
+
|
53
|
+
def key?(key)
|
54
|
+
hash.key?(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
hash.to_h
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def hash
|
64
|
+
@hash ||= {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Participants
|
4
|
+
class Cookie
|
5
|
+
include Canfig::Instance
|
6
|
+
|
7
|
+
# TODO maybe be a little better about checking for action dispatch, etc.?
|
8
|
+
|
9
|
+
class << self
|
10
|
+
alias_method :configure, :new
|
11
|
+
def new(context, &block)
|
12
|
+
configure(&block).new(context)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(&block)
|
17
|
+
configure do |config|
|
18
|
+
config.cookie = :trailguide
|
19
|
+
config.path = '/'
|
20
|
+
config.expiration = 1.year.to_i
|
21
|
+
# TODO other cookie options (domain, ssl, etc.)
|
22
|
+
|
23
|
+
yield(config) if block_given?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# instance method, creates a new adapter and passes through config
|
28
|
+
def new(context)
|
29
|
+
raise NoMethodError, "Your current context (#{context}) does not support cookies" unless context.respond_to?(:cookies, true)
|
30
|
+
Adapter.new(context, configuration)
|
31
|
+
end
|
32
|
+
|
33
|
+
class Adapter
|
34
|
+
attr_reader :context, :config
|
35
|
+
|
36
|
+
def initialize(context, config)
|
37
|
+
@context = context
|
38
|
+
@config = config
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
cookie[key.to_s]
|
43
|
+
end
|
44
|
+
|
45
|
+
def []=(key, value)
|
46
|
+
cookie.merge!({key.to_s => value})
|
47
|
+
write_cookie
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(key)
|
51
|
+
cookie.tap { |h| h.delete(key.to_s) }
|
52
|
+
write_cookie
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy!
|
56
|
+
cookies.delete(config.cookie.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
def keys
|
60
|
+
cookie.keys
|
61
|
+
end
|
62
|
+
|
63
|
+
def key?(key)
|
64
|
+
cookie.key?(key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_h
|
68
|
+
cookie.to_h
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def cookie
|
74
|
+
@cookie ||= begin
|
75
|
+
JSON.parse(cookies[config.cookie.to_s])
|
76
|
+
rescue
|
77
|
+
{}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def cookies
|
82
|
+
context.send(:cookies)
|
83
|
+
end
|
84
|
+
|
85
|
+
def write_cookie
|
86
|
+
cookies[config.cookie.to_s] = cookie_options.merge({value: cookie.as_json})
|
87
|
+
end
|
88
|
+
|
89
|
+
def cookie_options
|
90
|
+
{
|
91
|
+
expires: Time.now + config.expiration,
|
92
|
+
path: config.path
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Participants
|
4
|
+
class Multi
|
5
|
+
include Canfig::Instance
|
6
|
+
|
7
|
+
class << self
|
8
|
+
alias_method :configure, :new
|
9
|
+
def new(context, &block)
|
10
|
+
configure(&block).new(context)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(&block)
|
15
|
+
configure do |config|
|
16
|
+
config.adapter = -> (context) do
|
17
|
+
if context.respond_to?(:current_user, true) && context.send(:current_user).present?
|
18
|
+
TrailGuide::Adapters::Participants::Redis
|
19
|
+
elsif context.respond_to?(:cookies, true)
|
20
|
+
TrailGuide::Adapters::Participants::Cookie
|
21
|
+
elsif context.respond_to?(:session, true)
|
22
|
+
TrailGuide::Adapters::Participants::Session
|
23
|
+
else
|
24
|
+
TrailGuide::Adapters::Participants::Anonymous
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
yield(config) if block_given?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# instance method, creates a new adapter and passes through config
|
33
|
+
def new(context)
|
34
|
+
adapter = configuration.adapter.call(context)
|
35
|
+
adapter = configuration.send(adapter) if adapter.is_a?(Symbol)
|
36
|
+
adapter.new(context)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Participants
|
4
|
+
class Redis
|
5
|
+
include Canfig::Instance
|
6
|
+
|
7
|
+
class << self
|
8
|
+
alias_method :configure, :new
|
9
|
+
def new(context, **opts, &block)
|
10
|
+
configure(&block).new(context, **opts)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(&block)
|
15
|
+
configure do |config|
|
16
|
+
config.namespace = :participants
|
17
|
+
config.lookup = -> (context) { context.current_user.id }
|
18
|
+
config.expiration = nil
|
19
|
+
|
20
|
+
yield(config) if block_given?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# instance method, creates a new adapter and passes through config
|
25
|
+
def new(context, **opts)
|
26
|
+
Adapter.new(context, configuration, **opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
class Adapter
|
30
|
+
attr_reader :context, :config, :storage_key
|
31
|
+
|
32
|
+
def initialize(context, config, key: nil)
|
33
|
+
@context = context
|
34
|
+
@config = config
|
35
|
+
|
36
|
+
if key
|
37
|
+
@storage_key = "#{config.namespace}:#{key}"
|
38
|
+
elsif config.lookup
|
39
|
+
if config.lookup.respond_to?(:call)
|
40
|
+
key = config.lookup.call(context)
|
41
|
+
else
|
42
|
+
key = context.send(config.lookup)
|
43
|
+
end
|
44
|
+
@storage_key = "#{config.namespace}:#{key}"
|
45
|
+
else
|
46
|
+
# TODO better error
|
47
|
+
raise ArgumentError, "You must configure a `lookup` proc"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def [](field)
|
52
|
+
TrailGuide.redis.hget(storage_key, field.to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
def []=(field, value)
|
56
|
+
TrailGuide.redis.hset(storage_key, field.to_s, value)
|
57
|
+
TrailGuide.redis.expire(storage_key, config.expiration) if config.expiration
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(field)
|
61
|
+
TrailGuide.redis.hdel(storage_key, field.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy!
|
65
|
+
TrailGuide.redis.del(storage_key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def keys
|
69
|
+
TrailGuide.redis.hkeys(storage_key)
|
70
|
+
end
|
71
|
+
|
72
|
+
def key?(field)
|
73
|
+
TrailGuide.redis.hexists(storage_key, field.to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_h
|
77
|
+
TrailGuide.redis.hgetall(storage_key)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Adapters
|
3
|
+
module Participants
|
4
|
+
class Session
|
5
|
+
include Canfig::Instance
|
6
|
+
|
7
|
+
class << self
|
8
|
+
alias_method :configure, :new
|
9
|
+
def new(context, &block)
|
10
|
+
configure(&block).new(context)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(&block)
|
15
|
+
configure do |config|
|
16
|
+
config.key = :trailguide
|
17
|
+
|
18
|
+
yield(config) if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# instance method, creates a new adapter and passes through config
|
23
|
+
def new(context)
|
24
|
+
raise NoMethodError, "Your current context (#{context}) does not support sessions" unless context.respond_to?(:session, true)
|
25
|
+
Adapter.new(context, configuration)
|
26
|
+
end
|
27
|
+
|
28
|
+
class Adapter
|
29
|
+
attr_reader :context, :config
|
30
|
+
|
31
|
+
def initialize(context, config)
|
32
|
+
@context = context
|
33
|
+
@config = config
|
34
|
+
end
|
35
|
+
|
36
|
+
def [](key)
|
37
|
+
session[key]
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(key, value)
|
41
|
+
session[key] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete(key)
|
45
|
+
session.delete(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def destroy!
|
49
|
+
context.send(:session).delete(config.key)
|
50
|
+
end
|
51
|
+
|
52
|
+
def keys
|
53
|
+
session.keys
|
54
|
+
end
|
55
|
+
|
56
|
+
def key?(key)
|
57
|
+
session.key?(key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_h
|
61
|
+
session.to_h
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def session
|
67
|
+
context.send(:session)[config.key] ||= {}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'trail_guide/unity'
|
2
|
+
|
3
|
+
module TrailGuide
|
4
|
+
module Adapters
|
5
|
+
module Participants
|
6
|
+
class Unity < Multi
|
7
|
+
attr_reader :context, :unity
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
configure do |config|
|
11
|
+
config.visitor_cookie = nil
|
12
|
+
config.user_id_key = :id
|
13
|
+
|
14
|
+
config.user_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
15
|
+
config.namespace = 'unity:users'
|
16
|
+
config.lookup = -> (user_id) { user_id }
|
17
|
+
end
|
18
|
+
|
19
|
+
config.visitor_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
20
|
+
config.namespace = 'unity:visitors'
|
21
|
+
config.lookup = -> (visitor_id) { visitor_id }
|
22
|
+
end
|
23
|
+
|
24
|
+
config.anonymous_adapter = TrailGuide::Adapters::Participants::Anonymous
|
25
|
+
|
26
|
+
yield(config) if block_given?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def new(context)
|
31
|
+
@context = context
|
32
|
+
@unity = TrailGuide::Unity.new
|
33
|
+
|
34
|
+
if trailguide_context?
|
35
|
+
unity.user_id ||= context.send(:try, :trailguide_user).try(configuration.user_id_key)
|
36
|
+
unity.visitor_id ||= context.send(:try, :trailguide_visitor)
|
37
|
+
end
|
38
|
+
|
39
|
+
if logged_in_context?
|
40
|
+
unity.user_id ||= context.send(:current_user).send(configuration.user_id_key)
|
41
|
+
end
|
42
|
+
|
43
|
+
if logged_out_context?
|
44
|
+
unity.visitor_id ||= context.send(:cookies)[configuration.visitor_cookie].gsub(/(%22|")/, '')
|
45
|
+
end
|
46
|
+
|
47
|
+
unity.sync!
|
48
|
+
merge! if unity.synced?
|
49
|
+
|
50
|
+
adapter = configuration.send("#{context_type}_adapter".to_sym)
|
51
|
+
if anonymous_context?
|
52
|
+
adapter.new(context)
|
53
|
+
else
|
54
|
+
adapter.new(unity.send("#{context_type}_id".to_sym))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def context_type
|
61
|
+
if visitor_context?
|
62
|
+
return :visitor
|
63
|
+
end
|
64
|
+
|
65
|
+
if user_context?
|
66
|
+
return :user
|
67
|
+
end
|
68
|
+
|
69
|
+
return :anonymous
|
70
|
+
end
|
71
|
+
|
72
|
+
def merge!
|
73
|
+
user_adapter.keys.each do |key|
|
74
|
+
visitor_adapter[key] = user_adapter[key] unless visitor_adapter[key].present?
|
75
|
+
end
|
76
|
+
user_adapter.destroy!
|
77
|
+
end
|
78
|
+
|
79
|
+
def trailguide_context?
|
80
|
+
context.send(:try, :trailguide_user).present? ||
|
81
|
+
context.send(:try, :trailguide_visitor).present?
|
82
|
+
end
|
83
|
+
|
84
|
+
def logged_in_context?
|
85
|
+
context.send(:try, :current_user).present?
|
86
|
+
end
|
87
|
+
|
88
|
+
def logged_out_context?
|
89
|
+
return false unless configuration.visitor_cookie.present?
|
90
|
+
context.respond_to?(:cookies, true) && context.send(:cookies)[configuration.visitor_cookie].present?
|
91
|
+
end
|
92
|
+
|
93
|
+
def user_context?
|
94
|
+
unity.user_id.present?
|
95
|
+
end
|
96
|
+
|
97
|
+
def visitor_context?
|
98
|
+
unity.visitor_id.present?
|
99
|
+
end
|
100
|
+
|
101
|
+
def anonymous_context?
|
102
|
+
!visitor_context? && !user_context?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "trail_guide/adapters/participants/anonymous"
|
2
|
+
require "trail_guide/adapters/participants/session"
|
3
|
+
require "trail_guide/adapters/participants/cookie"
|
4
|
+
require "trail_guide/adapters/participants/redis"
|
5
|
+
require "trail_guide/adapters/participants/multi"
|
6
|
+
|
7
|
+
module TrailGuide
|
8
|
+
module Adapters
|
9
|
+
module Participants
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'simple-random'
|
2
|
+
|
3
|
+
module TrailGuide
|
4
|
+
module Algorithms
|
5
|
+
class Bandit
|
6
|
+
attr_reader :experiment
|
7
|
+
|
8
|
+
def self.choose!(experiment, **opts)
|
9
|
+
new(experiment).choose!(**opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(experiment)
|
13
|
+
@experiment = experiment
|
14
|
+
end
|
15
|
+
|
16
|
+
def choose!(**opts)
|
17
|
+
guess = best_guess
|
18
|
+
experiment.variants.find { |var| var == guess }
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def best_guess
|
24
|
+
@best_guess ||= begin
|
25
|
+
guesses = {}
|
26
|
+
experiment.variants.each do |variant|
|
27
|
+
guesses[variant.name] = arm_guess(variant.participants, variant.converted)
|
28
|
+
end
|
29
|
+
gmax = guesses.values.max
|
30
|
+
best = guesses.keys.select { |name| guesses[name] == gmax }
|
31
|
+
best.sample
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def arm_guess(participants, conversions)
|
36
|
+
a = [participants, 0].max
|
37
|
+
b = [(participants - conversions), 0].max
|
38
|
+
s = SimpleRandom.new
|
39
|
+
s.set_seed
|
40
|
+
s.beta(a+fairness_constant, b+fairness_constant)
|
41
|
+
end
|
42
|
+
|
43
|
+
def fairness_constant
|
44
|
+
7
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Algorithms
|
3
|
+
class Distributed
|
4
|
+
attr_reader :experiment
|
5
|
+
|
6
|
+
def self.choose!(experiment, **opts)
|
7
|
+
new(experiment).choose!(**opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(experiment)
|
11
|
+
@experiment = experiment
|
12
|
+
end
|
13
|
+
|
14
|
+
def choose!(**opts)
|
15
|
+
variants.sample
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def variants
|
21
|
+
groups = experiment.variants.group_by(&:participants)
|
22
|
+
groups.min_by { |c,g| c }.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Algorithms
|
3
|
+
class Random
|
4
|
+
attr_reader :experiment
|
5
|
+
|
6
|
+
def self.choose!(experiment, **opts)
|
7
|
+
new(experiment).choose!(**opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(experiment)
|
11
|
+
@experiment = experiment
|
12
|
+
end
|
13
|
+
|
14
|
+
def choose!(**opts)
|
15
|
+
experiment.variants.sample
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Algorithms
|
3
|
+
class Weighted
|
4
|
+
attr_reader :experiment
|
5
|
+
|
6
|
+
def self.choose!(experiment, **opts)
|
7
|
+
new(experiment).choose!(**opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(experiment)
|
11
|
+
@experiment = experiment
|
12
|
+
end
|
13
|
+
|
14
|
+
def choose!(**opts)
|
15
|
+
weights = experiment.variants.map(&:weight)
|
16
|
+
reference = rand * weights.inject(:+)
|
17
|
+
|
18
|
+
experiment.variants.zip(weights).each do |variant,weight|
|
19
|
+
return variant if weight >= reference
|
20
|
+
reference -= weight
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|