vanity 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|