ablab 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/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +76 -0
- data/Rakefile +6 -0
- data/ablab.gemspec +26 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/ablab.rb +124 -0
- data/lib/ablab/controller.rb +16 -0
- data/lib/ablab/store.rb +2 -0
- data/lib/ablab/store/memory.rb +30 -0
- data/lib/ablab/store/redis.rb +39 -0
- data/lib/ablab/version.rb +3 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4e0904fd152e122cdfb9d1ffc0df8d53218da409
|
4
|
+
data.tar.gz: 045967d4a48b868743483c63330705eb033b02ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 60ec6daba0bd352ebe1e2ababb95b52a0dab305ed6635bf8bdede39497beb2fb50edaf49b7d9ac1fc92fa8d61afdf4f87cb62b1b19ebdb2b6ea3071a4a4b26b5
|
7
|
+
data.tar.gz: 57e929adb46056e0b1172720552150fb92ab7f998431bb6ad04a8a5d8bac16e350e9a5846e961469f4c1a4dee00505d7dc3cc7a10d8414490127508e1e5d8011
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# ABLab
|
2
|
+
|
3
|
+
A minimal library for performing AB-tests in Rails applications and checking
|
4
|
+
their statistical significance.
|
5
|
+
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'ablab'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install ablab
|
22
|
+
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
# In `initializers/ablab.rb`
|
28
|
+
|
29
|
+
ABLab.setup do
|
30
|
+
store :redis, host: 'localhost', port: 6379
|
31
|
+
|
32
|
+
experiment :product_page do
|
33
|
+
description "Experiments on the product page"
|
34
|
+
|
35
|
+
group :a, control: true, description: "control group"
|
36
|
+
group :b, description: "show more products from the shop at the top"
|
37
|
+
end
|
38
|
+
|
39
|
+
experiment :search do
|
40
|
+
description "Search experiments"
|
41
|
+
|
42
|
+
group :a, control: true, description: "control group"
|
43
|
+
group :b, description: "boost CTR"
|
44
|
+
group :c, description: "boost GMV"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# In application_controller.rb
|
50
|
+
|
51
|
+
require 'ablab'
|
52
|
+
|
53
|
+
class ApplicationController < ActionController::Base
|
54
|
+
include ABLab::Controller
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# In app controllers/views code
|
59
|
+
|
60
|
+
experiment(:product_page).in_group?(:a) # => true or false
|
61
|
+
experiment(:product_page).group # => :a or :b
|
62
|
+
|
63
|
+
experiment(:product_page).track_view!
|
64
|
+
experiment(:product_page).track_conversion!
|
65
|
+
|
66
|
+
|
67
|
+
# Results of the experiment
|
68
|
+
ABTest.experiments.each do |experiment|
|
69
|
+
puts "#{experiment.name}: #{experiment.results.inspect}"
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/lucaong/ablab).
|
data/Rakefile
ADDED
data/ablab.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ablab/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ablab"
|
8
|
+
spec.version = ABLab::VERSION
|
9
|
+
spec.authors = ["Luca Ongaro"]
|
10
|
+
spec.email = ["lukeongaro@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "Minimal library for performing AB tests"
|
13
|
+
spec.description = "Minimal library for performing AB tests, measuring statistical significance."
|
14
|
+
spec.homepage = "http://github.com/lucaong/ablab"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "redis"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "loveos/abtest"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/ablab.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require "ablab/version"
|
2
|
+
require "ablab/controller"
|
3
|
+
require "ablab/store"
|
4
|
+
|
5
|
+
module ABLab
|
6
|
+
module ModuleMethods
|
7
|
+
attr_reader :experiments
|
8
|
+
|
9
|
+
def setup(&block)
|
10
|
+
instance_exec(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def experiment(name, &block)
|
14
|
+
@experiments ||= {}
|
15
|
+
@experiments[name] = Experiment.new(name, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def store(type, *args)
|
19
|
+
if type.is_a? Class
|
20
|
+
@tracker = Class.new(*args)
|
21
|
+
else
|
22
|
+
class_name = type.to_s.split('_').map(&:capitalize).join
|
23
|
+
@tracker = ABLab::Store.const_get(class_name).new(*args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def tracker
|
28
|
+
@tracker ||= ABLab::Store::Memory.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
extend ModuleMethods
|
33
|
+
|
34
|
+
class Experiment
|
35
|
+
attr_reader :name, :groups, :control
|
36
|
+
|
37
|
+
def initialize(name, &block)
|
38
|
+
@name = name
|
39
|
+
@groups = []
|
40
|
+
instance_exec(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def description(desc = nil)
|
44
|
+
@description = desc if desc
|
45
|
+
@description
|
46
|
+
end
|
47
|
+
|
48
|
+
def group(name, options = {})
|
49
|
+
group = Group.new(name, options[:description])
|
50
|
+
@control = group if options[:control]
|
51
|
+
@groups << group
|
52
|
+
end
|
53
|
+
|
54
|
+
def results
|
55
|
+
@result ||= Result.new(self)
|
56
|
+
@result.data
|
57
|
+
end
|
58
|
+
|
59
|
+
def run(uid)
|
60
|
+
draw = Random.new(uid.hash * name.hash).rand(1000)
|
61
|
+
Run.new(self, draw)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Run
|
66
|
+
attr_reader :group, :experiment
|
67
|
+
|
68
|
+
def initialize(experiment, draw)
|
69
|
+
idx = (draw / (1000.0 / experiment.groups.size)).floor
|
70
|
+
@experiment = experiment
|
71
|
+
@group = experiment.groups[idx].name
|
72
|
+
end
|
73
|
+
|
74
|
+
def in_group?(name)
|
75
|
+
group == name
|
76
|
+
end
|
77
|
+
|
78
|
+
def track_view!
|
79
|
+
ABLab.tracker.track_view!(experiment.name, group)
|
80
|
+
end
|
81
|
+
|
82
|
+
def track_conversion!
|
83
|
+
ABLab.tracker.track_conversion!(experiment.name, group)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Group < Struct.new(:name, :description); end
|
88
|
+
|
89
|
+
class Result
|
90
|
+
extend Forwardable
|
91
|
+
def_delegators :@experiment, :name, :control, :groups
|
92
|
+
|
93
|
+
def initialize(experiment)
|
94
|
+
@experiment = experiment
|
95
|
+
end
|
96
|
+
|
97
|
+
def data
|
98
|
+
raise NoControlGroup.new("no control group") if control.nil?
|
99
|
+
c_views, c_conv = views_and_conversions(control)
|
100
|
+
groups.map do |group|
|
101
|
+
if group == control
|
102
|
+
next { views: c_views, conversions: c_conv, control: true }
|
103
|
+
end
|
104
|
+
views, conv = views_and_conversions(group)
|
105
|
+
z = z_score(views, conv, c_views, c_conv)
|
106
|
+
{ views: views, conversions: conv, z_score: z, control: false }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private def views_and_conversions(group)
|
111
|
+
views = ABLab.tracker.views(name, group.name)
|
112
|
+
conversions = ABLab.tracker.conversions(name, group.name)
|
113
|
+
[views, conversions]
|
114
|
+
end
|
115
|
+
|
116
|
+
private def z_score(views, conv, c_views, c_conv)
|
117
|
+
p = conv.to_f / views
|
118
|
+
pc = c_conv.to_f / c_views
|
119
|
+
(p - pc) / Math.sqrt((p*(1 - p) / views) + (pc*(1 - pc) / c_views))
|
120
|
+
end
|
121
|
+
|
122
|
+
class NoControlGroup < StandardError; end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ABLab
|
2
|
+
module Controller
|
3
|
+
private def experiment(name)
|
4
|
+
@experiments ||= {}
|
5
|
+
unless ABLab.experiments.has_key?(name)
|
6
|
+
raise "No experiment with name #{name}"
|
7
|
+
end
|
8
|
+
@experiments[name] ||=
|
9
|
+
ABLab.experiments[name].run(user_id_for_experiments)
|
10
|
+
end
|
11
|
+
|
12
|
+
private def user_id_for_experiments
|
13
|
+
env['rack.session'].id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/ablab/store.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module ABLab
|
2
|
+
module Store
|
3
|
+
class Memory
|
4
|
+
def initialize
|
5
|
+
@views = Hash.new do |hash, key|
|
6
|
+
hash[key] = Hash.new { |hash, key| hash[key] = 0 }
|
7
|
+
end
|
8
|
+
@conversions = Hash.new do |hash, key|
|
9
|
+
hash[key] = Hash.new { |hash, key| hash[key] = 0 }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def track_view!(experiment, bucket)
|
14
|
+
@views[experiment][bucket] += 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def track_conversion!(experiment, bucket)
|
18
|
+
@conversions[experiment][bucket] += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def views(experiment, bucket)
|
22
|
+
@views[experiment][bucket]
|
23
|
+
end
|
24
|
+
|
25
|
+
def conversions(experiment, bucket)
|
26
|
+
@conversions[experiment][bucket]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module ABLab
|
4
|
+
module Store
|
5
|
+
class Redis
|
6
|
+
attr_reader :redis
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
@key_prefix = opts[:key_prefix] || 'ablab'
|
10
|
+
@redis = ::Redis.new(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def track_view!(experiment, bucket)
|
14
|
+
redis.hincrby(key(:views), field(experiment, bucket), 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
def track_conversion!(experiment, bucket)
|
18
|
+
redis.hincrby(key(:conversions), field(experiment, bucket), 1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def views(experiment, bucket)
|
22
|
+
(redis.hget(key(:views), field(experiment, bucket)) || 0).to_i
|
23
|
+
end
|
24
|
+
|
25
|
+
def conversions(experiment, bucket)
|
26
|
+
(redis.hget(key(:conversions), field(experiment, bucket)) || 0).to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
private def key(type)
|
30
|
+
"#{@key_prefix}:#{type}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private def field(experiment, bucket)
|
34
|
+
"#{experiment}:#{bucket}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ablab
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luca Ongaro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Minimal library for performing AB tests, measuring statistical significance.
|
70
|
+
email:
|
71
|
+
- lukeongaro@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".travis.yml"
|
79
|
+
- Gemfile
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- ablab.gemspec
|
83
|
+
- bin/console
|
84
|
+
- bin/setup
|
85
|
+
- lib/ablab.rb
|
86
|
+
- lib/ablab/controller.rb
|
87
|
+
- lib/ablab/store.rb
|
88
|
+
- lib/ablab/store/memory.rb
|
89
|
+
- lib/ablab/store/redis.rb
|
90
|
+
- lib/ablab/version.rb
|
91
|
+
homepage: http://github.com/lucaong/ablab
|
92
|
+
licenses: []
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.2.2
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: Minimal library for performing AB tests
|
114
|
+
test_files: []
|