vanity 1.0.0 → 1.1.0
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.
- data/CHANGELOG +35 -0
- data/README.rdoc +33 -6
- data/lib/vanity.rb +13 -7
- data/lib/vanity/backport.rb +43 -0
- data/lib/vanity/commands/report.rb +13 -3
- data/lib/vanity/experiment/ab_test.rb +98 -66
- data/lib/vanity/experiment/base.rb +51 -5
- data/lib/vanity/metric.rb +213 -0
- data/lib/vanity/mock_redis.rb +76 -0
- data/lib/vanity/playground.rb +78 -61
- data/lib/vanity/rails/dashboard.rb +11 -2
- data/lib/vanity/rails/helpers.rb +3 -3
- data/lib/vanity/templates/_ab_test.erb +3 -4
- data/lib/vanity/templates/_experiment.erb +4 -4
- data/lib/vanity/templates/_experiments.erb +2 -2
- data/lib/vanity/templates/_metric.erb +9 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +14 -3
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +16 -4
- data/lib/vanity/templates/vanity.js +96 -0
- data/test/ab_test_test.rb +159 -96
- data/test/experiment_test.rb +99 -18
- data/test/experiments/age_and_zipcode.rb +1 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +1 -0
- data/test/metric_test.rb +287 -0
- data/test/playground_test.rb +1 -80
- data/test/rails_test.rb +9 -6
- data/test/test_helper.rb +37 -6
- data/vanity.gemspec +1 -1
- data/vendor/{redis-0.1 → redis-rb}/LICENSE +0 -0
- data/vendor/{redis-0.1 → redis-rb}/README.markdown +0 -0
- data/vendor/{redis-0.1 → redis-rb}/Rakefile +0 -0
- data/vendor/redis-rb/bench.rb +44 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/bin/distredis +33 -0
- data/vendor/redis-rb/examples/basic.rb +16 -0
- data/vendor/redis-rb/examples/incr-decr.rb +18 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/dist_redis.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/hash_ring.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/pipeline.rb +0 -2
- data/vendor/{redis-0.1 → redis-rb}/lib/redis.rb +25 -7
- data/vendor/{redis-0.1 → redis-rb}/lib/redis/raketasks.rb +0 -0
- data/vendor/redis-rb/profile.rb +22 -0
- data/vendor/redis-rb/redis-rb.gemspec +30 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/redis_spec.rb +113 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/spec_helper.rb +0 -0
- data/vendor/redis-rb/speed.rb +16 -0
- data/vendor/{redis-0.1 → redis-rb}/tasks/redis.tasks.rb +5 -1
- metadata +37 -14
data/CHANGELOG
CHANGED
@@ -1,3 +1,38 @@
|
|
1
|
+
== 1.1.0 (2009-12-4)
|
2
|
+
This release introduces metrics. Metrics are the gateway drug to better software.
|
3
|
+
|
4
|
+
It’s as simple as defining a metric:
|
5
|
+
|
6
|
+
metric "Cheers" do
|
7
|
+
description "They love us, don't they?"
|
8
|
+
end
|
9
|
+
|
10
|
+
Tracking it from your code:
|
11
|
+
|
12
|
+
track! :cheers
|
13
|
+
|
14
|
+
And watching the graph from the Dashboard.
|
15
|
+
|
16
|
+
You can (should) also use metrics with your A/B tests, for example:
|
17
|
+
|
18
|
+
ab_test "Pricing options" do
|
19
|
+
metrics :signup
|
20
|
+
alternatives 15, 25, 29
|
21
|
+
end
|
22
|
+
|
23
|
+
This new usage may become requirement in a future release.
|
24
|
+
|
25
|
+
Much thanks to Ian Sefferman for fixing issues with Ruby 1.8.7 and Rails support.
|
26
|
+
|
27
|
+
* Added: Metrics.
|
28
|
+
* Added: Use Vanity.playground.mock! when running tests and you'd rather not access a live Redis server.
|
29
|
+
* Changed: A/B tests now using metrics for tracking.
|
30
|
+
* Changed: Now throwing NameError instead of LoadError when failing to load experiment/metric. NameError can be rescued on same line.
|
31
|
+
* Changed: New, easier URL mapping for Dashboard: map.vanity "/vanity", :controller=>:vanity.
|
32
|
+
* Changed: All tests are green on Ruby 1.8.6, 1.8.7 and 1.9.1.
|
33
|
+
* Changed: Switched to redis-rb from http://github.com/ezmobius/redis-rb.
|
34
|
+
* Deprecated: Please call experiment method with experiment identifier (a symbol) and not experiment name.
|
35
|
+
|
1
36
|
== 1.0.0 (2009-11-19)
|
2
37
|
This release changes the way you define a new experiment. You can use a method suitable for the type of experiment you want to define, or call the generic define method (previously: experiment method). For example:
|
3
38
|
|
data/README.rdoc
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
Vanity is an Experiment Driven Development framework for Rails.
|
2
2
|
|
3
|
-
*
|
3
|
+
* All about Vanity: http://vanity.labnotes.org
|
4
4
|
* On github: http://github.com/assaf/vanity
|
5
|
-
* Vanity requires
|
5
|
+
* Vanity requires Redis 1.0 or later.
|
6
6
|
|
7
7
|
http://farm3.static.flickr.com/2540/4099665871_497f274f68_o.jpg
|
8
8
|
|
9
9
|
|
10
|
-
== A/B Testing
|
10
|
+
== A/B Testing With Rails (In 5 Easy Steps)
|
11
11
|
|
12
12
|
<b>Step 1:</b> Start using Vanity in your Rails application:
|
13
13
|
|
@@ -24,6 +24,7 @@ And:
|
|
24
24
|
ab_test "Price options" do
|
25
25
|
description "Mirror, mirror on the wall, who's the better price of all?"
|
26
26
|
alternatives 19, 25, 29
|
27
|
+
metrics :signups
|
27
28
|
end
|
28
29
|
|
29
30
|
<b>Step 3:</b> Present the different options to your users:
|
@@ -36,7 +37,7 @@ And:
|
|
36
37
|
def signup
|
37
38
|
@account = Account.new(params[:account])
|
38
39
|
if @account.save
|
39
|
-
track! :
|
40
|
+
track! :signups
|
40
41
|
redirect_to @acccount
|
41
42
|
else
|
42
43
|
render action: :offer
|
@@ -49,8 +50,34 @@ And:
|
|
49
50
|
vanity --output vanity.html
|
50
51
|
|
51
52
|
|
53
|
+
== Building From Source
|
54
|
+
|
55
|
+
To run the test suite for the first time:
|
56
|
+
|
57
|
+
$ gem install rails mocha timecop
|
58
|
+
$ rake
|
59
|
+
|
60
|
+
You can also +rake test+ if you insist on being explicit.
|
61
|
+
|
62
|
+
To build the documentation:
|
63
|
+
|
64
|
+
$ gem install yardoc jekyll
|
65
|
+
$ rake docs
|
66
|
+
$ open html/index.html
|
67
|
+
|
68
|
+
To clean up after yourself:
|
69
|
+
|
70
|
+
$ rake clobber
|
71
|
+
|
72
|
+
To package Vanity as a gem and install on your machine:
|
73
|
+
|
74
|
+
$ rake install
|
75
|
+
|
76
|
+
|
52
77
|
== Credits/License
|
53
78
|
|
54
|
-
|
79
|
+
Original code, copyright of Assaf Arkin, released under the MIT license.
|
80
|
+
|
81
|
+
Documentation available under the Creative Commons Attribution license.
|
55
82
|
|
56
|
-
|
83
|
+
For full list of credits and licenses: http://vanity.labnotes.org/credits.html.
|
data/lib/vanity.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../vendor/redis-
|
1
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../vendor/redis-rb/lib")
|
2
2
|
require "redis"
|
3
3
|
require "openssl"
|
4
|
+
require "date"
|
5
|
+
require "logger"
|
4
6
|
|
5
|
-
# All the cool stuff happens in other places
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
7
|
+
# All the cool stuff happens in other places.
|
8
|
+
# @see Vanity::Rails
|
9
|
+
# @see Vanity::Playground
|
10
|
+
# @see Vanity::Metric
|
11
|
+
# @see Vanity::Experiment
|
9
12
|
module Vanity
|
10
13
|
# Version number.
|
11
14
|
module Version
|
@@ -17,8 +20,11 @@ module Vanity
|
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
require "vanity/
|
23
|
+
require "vanity/backport" if RUBY_VERSION < "1.9"
|
24
|
+
require "vanity/metric"
|
21
25
|
require "vanity/experiment/base"
|
22
26
|
require "vanity/experiment/ab_test"
|
23
|
-
require "vanity/
|
27
|
+
require "vanity/playground"
|
28
|
+
Vanity.autoload :MockRedis, "vanity/mock_redis"
|
24
29
|
Vanity.autoload :Commands, "vanity/commands"
|
30
|
+
require "vanity/rails" if defined?(Rails)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Time
|
2
|
+
unless method_defined?(:to_date)
|
3
|
+
# Backported from Ruby 1.9.
|
4
|
+
def to_date
|
5
|
+
jd = Date.__send__(:civil_to_jd, year, mon, mday, Date::ITALY)
|
6
|
+
Date.new!(Date.__send__(:jd_to_ajd, jd, 0, 0), 0, Date::ITALY)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Date
|
12
|
+
unless method_defined?(:to_date)
|
13
|
+
# Backported from Ruby 1.9.
|
14
|
+
def to_date
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
unless method_defined?(:to_time)
|
20
|
+
# Backported from Ruby 1.9.
|
21
|
+
def to_time
|
22
|
+
Time.local(year, mon, mday)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Symbol
|
28
|
+
unless method_defined?(:to_proc)
|
29
|
+
# Backported from Ruby 1.9.
|
30
|
+
def to_proc
|
31
|
+
Proc.new { |*args| args.shift.__send__(self, *args) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Array
|
37
|
+
unless method_defined?(:minmax)
|
38
|
+
# Backported from Ruby 1.9.
|
39
|
+
def minmax
|
40
|
+
[min, max]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -15,15 +15,25 @@ module Vanity
|
|
15
15
|
struct.send :include, Render
|
16
16
|
locals = struct.new(*locals.values_at(*keys))
|
17
17
|
dir, base = File.split(path)
|
18
|
-
path = File.
|
19
|
-
ERB.new(path, nil, '
|
18
|
+
path = File.join(dir, "_#{base}")
|
19
|
+
erb = ERB.new(File.read(path), nil, '<>')
|
20
|
+
erb.filename = path
|
21
|
+
erb.result(locals.instance_eval { binding })
|
20
22
|
end
|
21
23
|
|
22
24
|
# Escape HTML.
|
23
25
|
def h(html)
|
24
|
-
CGI.
|
26
|
+
CGI.escapeHTML(html)
|
25
27
|
end
|
26
28
|
|
29
|
+
# Dumbed down from Rails' simple_format.
|
30
|
+
def simple_format(text, options={})
|
31
|
+
open = "<p #{options.map { |k,v| "#{k}=\"#{CGI.escapeHTML v}\"" }.join(" ")}>"
|
32
|
+
text = open + text.gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
|
33
|
+
gsub(/\n\n+/, "</p>\n\n#{open}"). # 2+ newline -> paragraph
|
34
|
+
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') + # 1 newline -> br
|
35
|
+
"</p>"
|
36
|
+
end
|
27
37
|
end
|
28
38
|
|
29
39
|
# Commands available when running Vanity from the command line (see bin/vanity).
|
@@ -44,7 +44,7 @@ module Vanity
|
|
44
44
|
|
45
45
|
# Conversion rate calculated as converted/participants, rounded to 3 places.
|
46
46
|
def conversion_rate
|
47
|
-
@conversion_rate ||= (participants > 0 ? (converted.to_f/participants.to_f).round
|
47
|
+
@conversion_rate ||= (participants > 0 ? (converted.to_f/participants.to_f * 1000).round / 1000.0 : 0.0)
|
48
48
|
end
|
49
49
|
|
50
50
|
# The measure we use to order (sort) alternatives and decide which one is better (by calculating z-score).
|
@@ -94,19 +94,39 @@ module Vanity
|
|
94
94
|
@alternatives = [false, true]
|
95
95
|
end
|
96
96
|
|
97
|
+
|
98
|
+
# -- Metric --
|
99
|
+
|
100
|
+
# Tells A/B test which metric we're measuring, or returns metric in use.
|
101
|
+
#
|
102
|
+
# @example Define A/B test against coolness metric
|
103
|
+
# ab_test "Background color" do
|
104
|
+
# metrics :coolness
|
105
|
+
# alternatives "red", "blue", "orange"
|
106
|
+
# end
|
107
|
+
# @example Find metric for A/B test
|
108
|
+
# puts "Measures: " + experiment(:background_color).metrics.map(&:name)
|
109
|
+
def metrics(*args)
|
110
|
+
@metrics = args.map { |id| @playground.metric(id) } unless args.empty?
|
111
|
+
@metrics
|
112
|
+
end
|
113
|
+
|
114
|
+
|
97
115
|
# -- Alternatives --
|
98
116
|
|
99
|
-
# Call this method once to set alternative values for this experiment
|
100
|
-
#
|
117
|
+
# Call this method once to set alternative values for this experiment
|
118
|
+
# (requires at least two values). Call without arguments to obtain
|
119
|
+
# current list of alternatives.
|
120
|
+
#
|
121
|
+
# @example Define A/B test with three alternatives
|
101
122
|
# ab_test "Background color" do
|
123
|
+
# metrics :coolness
|
102
124
|
# alternatives "red", "blue", "orange"
|
103
125
|
# end
|
104
126
|
#
|
105
|
-
#
|
127
|
+
# @example Find out which alternatives this test uses
|
106
128
|
# alts = experiment(:background_color).alternatives
|
107
129
|
# puts "#{alts.count} alternatives, with the colors: #{alts.map(&:value).join(", ")}"
|
108
|
-
#
|
109
|
-
# If you want to know how well each alternative is doing, use #score.
|
110
130
|
def alternatives(*args)
|
111
131
|
unless args.empty?
|
112
132
|
@alternatives = args.clone
|
@@ -129,24 +149,25 @@ module Vanity
|
|
129
149
|
end
|
130
150
|
private :_alternatives
|
131
151
|
|
132
|
-
# Returns an Alternative with the specified value.
|
133
|
-
#
|
134
|
-
#
|
135
|
-
# end
|
136
|
-
# Then:
|
152
|
+
# Returns an Alternative with the specified value.
|
153
|
+
#
|
154
|
+
# @example
|
137
155
|
# alternative(:red) == alternatives[0]
|
138
156
|
# alternative(:blue) == alternatives[2]
|
139
157
|
def alternative(value)
|
140
158
|
alternatives.find { |alt| alt.value == value }
|
141
159
|
end
|
142
160
|
|
143
|
-
# Defines an A/B test with two alternatives: false and true.
|
161
|
+
# Defines an A/B test with two alternatives: false and true. This is the
|
162
|
+
# default pair of alternatives, so just syntactic sugar for those who love
|
163
|
+
# being explicit.
|
164
|
+
#
|
165
|
+
# @example
|
144
166
|
# ab_test "More bacon" do
|
167
|
+
# metrics :yummyness
|
145
168
|
# false_true
|
146
169
|
# end
|
147
170
|
#
|
148
|
-
# This is the default pair of alternatives, so just syntactic sugar for
|
149
|
-
# those who love being explicit.
|
150
171
|
def false_true
|
151
172
|
alternatives false, true
|
152
173
|
end
|
@@ -160,7 +181,7 @@ module Vanity
|
|
160
181
|
# alternative for the same identity, and randomly split alternatives
|
161
182
|
# between different identities.
|
162
183
|
#
|
163
|
-
#
|
184
|
+
# @example
|
164
185
|
# color = experiment(:which_blue).choose
|
165
186
|
def choose
|
166
187
|
if active?
|
@@ -177,38 +198,22 @@ module Vanity
|
|
177
198
|
@alternatives[index.to_i]
|
178
199
|
end
|
179
200
|
|
180
|
-
# Tracks a conversion. You probably want to use the Rails helper method
|
181
|
-
# track! instead.
|
182
|
-
#
|
183
|
-
# For example:
|
184
|
-
# experiment(:which_blue).track!
|
185
|
-
def track!
|
186
|
-
return unless active?
|
187
|
-
identity = identity()
|
188
|
-
return if redis[key("participants:#{identity}:show")]
|
189
|
-
index = alternative_for(identity)
|
190
|
-
if redis.sismember(key("alts:#{index}:participants"), identity)
|
191
|
-
redis.sadd key("alts:#{index}:converted"), identity
|
192
|
-
redis.incr key("alts:#{index}:conversions")
|
193
|
-
end
|
194
|
-
check_completion!
|
195
|
-
end
|
196
|
-
|
197
201
|
|
198
202
|
# -- Testing --
|
199
203
|
|
200
204
|
# Forces this experiment to use a particular alternative. You'll want to
|
201
205
|
# use this from your test cases to test for the different alternatives.
|
202
|
-
#
|
206
|
+
#
|
207
|
+
# @example Setup test to red button
|
203
208
|
# setup do
|
204
|
-
# experiment(:
|
209
|
+
# experiment(:button_color).select(:red)
|
205
210
|
# end
|
206
211
|
#
|
207
|
-
# def
|
212
|
+
# def test_shows_red_button
|
208
213
|
# . . .
|
209
214
|
# end
|
210
215
|
#
|
211
|
-
# Use nil to clear
|
216
|
+
# @example Use nil to clear selection
|
212
217
|
# teardown do
|
213
218
|
# experiment(:green_button).select(nil)
|
214
219
|
# end
|
@@ -324,7 +329,7 @@ module Vanity
|
|
324
329
|
# -- Completion --
|
325
330
|
|
326
331
|
# Defines how the experiment can choose the optimal outcome on completion.
|
327
|
-
|
332
|
+
#
|
328
333
|
# By default, Vanity will take the best alternative (highest conversion
|
329
334
|
# rate) and use that as the outcome. You experiment may have different
|
330
335
|
# needs, maybe you want the least performing alternative, or factor cost
|
@@ -370,7 +375,7 @@ module Vanity
|
|
370
375
|
# -- Store/validate --
|
371
376
|
|
372
377
|
def destroy
|
373
|
-
@alternatives.
|
378
|
+
@alternatives.size.times do |i|
|
374
379
|
redis.del key("alts:#{i}:participants")
|
375
380
|
redis.del key("alts:#{i}:converted")
|
376
381
|
redis.del key("alts:#{i}:conversions")
|
@@ -380,55 +385,82 @@ module Vanity
|
|
380
385
|
end
|
381
386
|
|
382
387
|
def save
|
383
|
-
fail "Experiment #{name} needs at least two alternatives" unless alternatives.
|
388
|
+
fail "Experiment #{name} needs at least two alternatives" unless alternatives.size >= 2
|
384
389
|
super
|
390
|
+
if @metrics.nil? || @metrics.empty?
|
391
|
+
warn "Please use metrics method to explicitly state which metric you are measuring against."
|
392
|
+
metric = @playground.metrics[id] ||= Vanity::Metric.new(@playground, name)
|
393
|
+
@metrics = [metric]
|
394
|
+
end
|
395
|
+
@metrics.each do |metric|
|
396
|
+
metric.hook &method(:track!)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# Called when tracking associated metric.
|
401
|
+
def track!(metric_id, timestamp, count, *args)
|
402
|
+
return unless active?
|
403
|
+
identity = identity()
|
404
|
+
return if redis[key("participants:#{identity}:show")]
|
405
|
+
index = alternative_for(identity)
|
406
|
+
redis.sadd key("alts:#{index}:converted"), identity if redis.sismember(key("alts:#{index}:participants"), identity)
|
407
|
+
redis.incrby key("alts:#{index}:conversions"), count
|
408
|
+
check_completion!
|
385
409
|
end
|
386
410
|
|
411
|
+
# If you are not embarrassed by the first version of your product, you’ve
|
412
|
+
# launched too late.
|
413
|
+
# -- Reid Hoffman, founder of LinkedIn
|
414
|
+
|
387
415
|
protected
|
388
416
|
|
417
|
+
# Used for testing.
|
418
|
+
def fake(values)
|
419
|
+
values.each do |value, (participants, conversions)|
|
420
|
+
conversions ||= participants
|
421
|
+
participants.times do |identity|
|
422
|
+
index = @alternatives.index(value)
|
423
|
+
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
424
|
+
redis.sadd key("alts:#{index}:participants"), identity
|
425
|
+
end
|
426
|
+
conversions.times do |identity|
|
427
|
+
index = @alternatives.index(value)
|
428
|
+
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
429
|
+
redis.sadd key("alts:#{index}:converted"), identity
|
430
|
+
redis.incr key("alts:#{index}:conversions")
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
389
435
|
# Chooses an alternative for the identity and returns its index. This
|
390
436
|
# method always returns the same alternative for a given experiment and
|
391
437
|
# identity, and randomly distributed alternatives for each identity (in the
|
392
438
|
# same experiment).
|
393
439
|
def alternative_for(identity)
|
394
|
-
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(17) % @alternatives.
|
395
|
-
end
|
396
|
-
|
397
|
-
# Used for testing Vanity.
|
398
|
-
def count_participant(identity, value)
|
399
|
-
index = @alternatives.index(value)
|
400
|
-
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
401
|
-
redis.sadd key("alts:#{index}:participants"), identity
|
402
|
-
self
|
403
|
-
end
|
404
|
-
|
405
|
-
# Used for testing Vanity.
|
406
|
-
def count_conversion(identity, value)
|
407
|
-
index = @alternatives.index(value)
|
408
|
-
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
409
|
-
redis.sadd key("alts:#{index}:converted"), identity
|
410
|
-
redis.incr key("alts:#{index}:conversions")
|
411
|
-
self
|
440
|
+
Digest::MD5.hexdigest("#{name}/#{identity}").to_i(17) % @alternatives.size
|
412
441
|
end
|
413
442
|
|
414
443
|
begin
|
415
444
|
a = 50.0
|
416
445
|
# Returns array of [z-score, percentage]
|
417
|
-
norm_dist =
|
446
|
+
norm_dist = []
|
447
|
+
(0.0..3.1).step(0.01) { |x| norm_dist << [x, a += 1 / Math.sqrt(2 * Math::PI) * Math::E ** (-x ** 2 / 2)] }
|
418
448
|
# We're really only interested in 90%, 95%, 99% and 99.9%.
|
419
449
|
Z_TO_PROBABILITY = [90, 95, 99, 99.9].map { |pct| [norm_dist.find { |x,a| a >= pct }.first, pct] }.reverse
|
420
450
|
end
|
421
451
|
|
422
452
|
end
|
423
|
-
end
|
424
453
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
454
|
+
|
455
|
+
module Definition
|
456
|
+
# Define an A/B test with the given name. For example:
|
457
|
+
# ab_test "New Banner" do
|
458
|
+
# alternatives :red, :green, :blue
|
459
|
+
# end
|
460
|
+
def ab_test(name, &block)
|
461
|
+
define name, :ab_test, &block
|
462
|
+
end
|
432
463
|
end
|
464
|
+
|
433
465
|
end
|
434
466
|
end
|