ablab 0.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.
@@ -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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ dump.rdb
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in loveos-abtest.gemspec
4
+ gemspec
@@ -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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'ablab/store/memory'
2
+ require 'ablab/store/redis'
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module ABLab
2
+ VERSION = "0.1.0"
3
+ end
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: []