Skip to content

Commit f6baa43

Browse files
author
Peter de Ruijter
committed
Merge pull request #5 from Springest/weighted_scenarios
Weighted scenarios.
2 parents 013833f + 9c2b8c0 commit f6baa43

File tree

10 files changed

+165
-14
lines changed

10 files changed

+165
-14
lines changed

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,35 @@ Or install it yourself as:
1818

1919
$ gem install ab_panel
2020

21+
## Upgrading from 0.2.0 to 0.3.0
22+
23+
In this new version we've added weights to different conditions/scenarios. This
24+
is so that you can rollout certain features slowly. We've also removed the
25+
original (control scenario) that is added standard.
26+
27+
The only thing you need to do to upgrade is update the ``ab_panel.yml``.
28+
29+
Old:
30+
31+
```yaml
32+
33+
foo:
34+
- bar1
35+
- bar2
36+
37+
```
38+
39+
New (if you want to keep original or need original):
40+
41+
```yaml
42+
43+
foo:
44+
bar1: 2
45+
bar2: 2
46+
original: 2
47+
48+
```
49+
2150
## Usage
2251

2352
Create a config file with one or more experiments and conditions.
@@ -26,13 +55,14 @@ In `config/ab_panel.yml`
2655

2756
```yaml
2857
my_experiment:
29-
- condition_b
30-
- condition_c
58+
original: 1
59+
condition_b: 1
60+
condition_c: 1
3161
```
3262
3363
Note that this will create 3 conditions:
3464
35-
1. Original condition (control condition)
65+
1. Original condition
3666
2. Condition B
3767
3. Condition C
3868

ab_panel.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ require 'ab_panel/version'
66
Gem::Specification.new do |spec|
77
spec.name = "ab_panel"
88
spec.version = AbPanel::VERSION
9-
spec.authors = ["Wouter de Vos", "Mark Mulder"]
10-
spec.email = ["wouter@springest.com", "markmulder@gmail.com"]
9+
spec.authors = ["Wouter de Vos", "Mark Mulder", "Peter de Ruijter"]
10+
spec.email = ["wouter@springest.com", "markmulder@gmail.com", "hello@thisiswho.im"]
1111
spec.description = %q{Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.}
1212
spec.summary = %q{Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.}
1313
spec.homepage = "https://github.com/Springest/ab_panel"

lib/ab_panel.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
require 'set'
2+
require_relative './array'
3+
24
Dir[File.expand_path(File.join(
35
File.dirname(__FILE__),'ab_panel','**','*.rb'))]
46
.each {|f| require f}
@@ -37,6 +39,10 @@ def scenarios(experiment)
3739
config.scenarios experiment
3840
end
3941

42+
def weights(experiment)
43+
config.weights experiment
44+
end
45+
4046
def properties
4147
@env[:properties]
4248
end
@@ -83,7 +89,7 @@ def assign_conditions!(already_assigned=nil)
8389
selected = begin
8490
already_assigned.send(experiment).condition
8591
rescue
86-
scenarios(experiment)[rand(scenarios(experiment).size)]
92+
scenarios(experiment).weighted_sample(weights(experiment))
8793
end
8894

8995
cs[experiment]["#{selected}?"] = true

lib/ab_panel/config.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ def experiments
1212

1313
def scenarios(experiment)
1414
raise ArgumentError.new( "Fatal: Experiment config not found for #{experiment}" ) unless experiments.include? experiment.to_sym
15-
( settings[experiment.to_sym].map(&:to_sym) + [:original] ).uniq
15+
( settings[experiment.to_sym].keys.map(&:to_sym)).uniq
1616
end
1717

18+
def weights(experiment)
19+
raise ArgumentError.new( "Fatal: Experiment config not found for #{experiment}" ) unless experiments.include? experiment.to_sym
20+
settings[experiment.to_sym].map { |s| s[1] }
21+
end
1822

1923
def settings
2024
@settings ||= YAML.load(

lib/ab_panel/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module AbPanel
2-
VERSION = "0.2.0"
2+
VERSION = "0.3.0"
33
end

lib/array.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class Array
2+
def weighted_sample(weights=nil)
3+
weights = Array.new(length, 1.0) if weights.nil? || weights.sum == 0
4+
total = weights.sum
5+
6+
# The total sum of weights is multiplied by a random number
7+
trigger = Kernel::rand * total
8+
9+
subtotal = 0
10+
result = nil
11+
12+
# The subtotal is checked agains the trigger. The higher the sum, the higher
13+
# the probability of triggering a result.
14+
weights.each_with_index do |weight, index|
15+
subtotal += weight
16+
17+
if subtotal > trigger
18+
result = self[index]
19+
break
20+
end
21+
end
22+
# Returns self.last from current array if result is nil
23+
result || last
24+
end
25+
end

spec/ab_panel/config_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'spec_helper'
2+
3+
describe AbPanel::Config do
4+
let(:config) { AbPanel::Config.new }
5+
before do
6+
AbPanel::Config.any_instance.stub(:settings) { { exp1: { scenario1: 25, scenario2: 75 } } }
7+
end
8+
9+
describe '.experiments' do
10+
subject { config.experiments }
11+
it { should =~ [:exp1] }
12+
end
13+
14+
describe '.weights' do
15+
subject { config.weights('exp1') }
16+
17+
it { should =~ [75.0, 25.0] }
18+
end
19+
end

spec/ab_panel_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,29 @@
77
it { should =~ %w(experiment1 experiment2).map(&:to_sym) }
88
end
99

10+
describe ".weights" do
11+
let(:experiment) { AbPanel.experiments.first }
12+
subject { AbPanel.weights(experiment) }
13+
14+
it { should == [25, 25, 25, 25] }
15+
16+
describe "With a nonexistent experiment" do
17+
let(:experiment) { :does_not_exist }
18+
19+
it 'should throw an ArgumentError' do
20+
expect { subject }.to raise_exception ArgumentError
21+
end
22+
end
23+
end
24+
1025
describe ".scenarios" do
1126
subject { AbPanel.scenarios(experiment) }
1227

1328
let(:experiment) { AbPanel.experiments.first }
1429

1530
it { should =~ %w( scenario1 scenario2 scenario3 original ).map(&:to_sym) }
1631

17-
describe "With an unexisting experiment" do
32+
describe "With an nonexistent experiment" do
1833
let(:experiment) { :does_not_exist }
1934

2035
it 'should throw an ArgumentError' do

spec/array_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
require 'spec_helper'
2+
3+
describe Array do
4+
describe '.weighted_sample' do
5+
before do
6+
Kernel.stub(:rand) { 0.5 }
7+
end
8+
9+
context "Stub test" do
10+
subject { Kernel.rand }
11+
it { should eq 0.5 }
12+
end
13+
14+
let(:array) { [1, 2, 3, 4] }
15+
subject { array.weighted_sample }
16+
17+
it { should eq 3 }
18+
19+
context "different random" do
20+
before do
21+
Kernel.stub(:rand) { 0 }
22+
end
23+
24+
it { should eq 1 }
25+
end
26+
27+
context "different random" do
28+
before do
29+
Kernel.stub(:rand) { 1 }
30+
end
31+
32+
it { should eq 4 }
33+
end
34+
35+
context "with weights" do
36+
subject { array.weighted_sample([1, 0, 0, 0]) }
37+
it { should eq 1 }
38+
end
39+
40+
context "all the same weights" do
41+
before { Kernel.stub(:rand) { 1 } }
42+
subject { array.weighted_sample([0, 0, 0, 0]) }
43+
it { should eq 4 }
44+
context "random 0" do
45+
before { Kernel.stub(:rand) { 0 } }
46+
it { should eq 1 }
47+
end
48+
end
49+
end
50+
end
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
experiment1:
2-
- scenario1
3-
- scenario2
4-
- scenario3
2+
scenario1: 25
3+
scenario2: 25
4+
scenario3: 25
5+
original: 25
56

67
experiment2:
7-
- scenario4
8-
- scenario5
8+
scenario4: 33.4
9+
scenario5: 33.3
10+
original: 33.3
911

0 commit comments

Comments
 (0)