a_b_split 0.1.1 → 1.0.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.
- checksums.yaml +4 -4
- data/lib/a_b_split.rb +10 -5
- data/lib/a_b_split/configuration.rb +6 -1
- data/lib/a_b_split/functions.rb +2 -1
- data/lib/a_b_split/functions/heaviside_weighted_split.rb +14 -5
- data/lib/a_b_split/functions/md5_weighted_split.rb +7 -2
- data/lib/a_b_split/functions/weighted_split.rb +19 -16
- data/lib/a_b_split/test.rb +31 -25
- data/lib/a_b_split/version.rb +2 -1
- metadata +27 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28166486a6f870f7b67355794008e620971f416b
|
4
|
+
data.tar.gz: 457b3a539da94205f8533849721d323f114e7b2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e615da6bbd04d68255074bca677c818933444b7ecabf966cd7fee20f3b339d465939f53f9f7ab7d6880ff912290df54eb832e9f4df600cbe78c5ab6458deaf99
|
7
|
+
data.tar.gz: ba65a2075286ef08e830e6eaa238c9af6b1d8c1c5cb5d8a984251300a54673bd22217d762ac8f25c0c4da124f498c597ee73526b35affe193366ec1a6ca7b97a
|
data/lib/a_b_split.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
$LOAD_PATH.unshift File.expand_path('..', __FILE__)
|
2
3
|
|
3
4
|
require 'a_b_split/functions'
|
4
5
|
require 'a_b_split/configuration'
|
@@ -7,16 +8,20 @@ require 'yaml'
|
|
7
8
|
|
8
9
|
module ABSplit
|
9
10
|
class NoValidExperiment < RuntimeError; end
|
10
|
-
end
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
def configuration
|
13
|
+
@configuration
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
+
def configuration=(config)
|
17
|
+
@configuration = config
|
18
|
+
end
|
16
19
|
|
17
20
|
def configure
|
18
21
|
yield(configuration) if block_given?
|
19
22
|
end
|
23
|
+
|
24
|
+
module_function :configure, :configuration, :configuration=
|
20
25
|
end
|
21
26
|
|
22
27
|
ABSplit.configuration = ABSplit::Configuration.new
|
@@ -1,9 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ABSplit
|
4
|
+
# This class represents the configurations used in ABSplit. Every time
|
5
|
+
# that is instanciated contains the following:
|
6
|
+
# - An empty list of experiments to use
|
2
7
|
class Configuration
|
3
8
|
attr_accessor :experiments
|
4
9
|
|
5
10
|
def initialize
|
6
|
-
@experiments =
|
11
|
+
@experiments = {}
|
7
12
|
end
|
8
13
|
end
|
9
14
|
end
|
data/lib/a_b_split/functions.rb
CHANGED
@@ -1,13 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'bigdecimal'
|
2
3
|
require_relative 'weighted_split'
|
3
4
|
|
4
5
|
module ABSplit
|
5
6
|
module Functions
|
7
|
+
# Weighted split based on a modified Heaviside function. In case a numeric value
|
8
|
+
# is passed, it uses a sigmoid function applying a modified Heaviside to choose
|
9
|
+
# the experiment. In case a none numeric value is passed it uses SHA2 algorithm
|
10
|
+
# to get a value from the object, and after decide based on weights.
|
11
|
+
#
|
12
|
+
# Persistent - Supports 256 bits as input
|
13
|
+
#
|
14
|
+
# No collisions are possible
|
6
15
|
class HeavisideWeightedSplit < WeightedSplit
|
7
16
|
MAX_HASH_VALUE = ('f' * 64).to_i(16)
|
8
|
-
|
17
|
+
|
9
18
|
class << self
|
10
|
-
protected
|
19
|
+
protected
|
11
20
|
|
12
21
|
def select_experiment_for(value, experiments)
|
13
22
|
x = value.is_a?(Numeric) ? value : hash(value)
|
@@ -32,16 +41,16 @@ module ABSplit
|
|
32
41
|
experiments.each do |experiment|
|
33
42
|
accumulated_percentages += percentage(experiment)
|
34
43
|
|
35
|
-
return experiment['name'] if x
|
44
|
+
return experiment['name'] if x <= accumulated_percentages
|
36
45
|
end
|
37
46
|
end
|
38
47
|
|
39
48
|
def percentage(experiment)
|
40
49
|
experiment['weight'].to_f / 100
|
41
50
|
end
|
42
|
-
|
51
|
+
|
43
52
|
def sigmoid(x)
|
44
|
-
BigDecimal.new(
|
53
|
+
BigDecimal.new((1.0 / (1 + Math.exp(-2 * x))).to_s)
|
45
54
|
end
|
46
55
|
end
|
47
56
|
end
|
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'digest'
|
2
3
|
require_relative 'weighted_split'
|
3
4
|
|
4
5
|
module ABSplit
|
5
6
|
module Functions
|
7
|
+
# Weighted split based on MD5 digest of value.
|
8
|
+
#
|
9
|
+
# Persistent - Collisions are possible
|
10
|
+
#
|
11
|
+
# Supports 128 bits as input
|
6
12
|
class Md5WeightedSplit < WeightedSplit
|
7
|
-
|
8
13
|
MAX_HASH_VALUE = ('f' * 32).to_i(16)
|
9
14
|
|
10
15
|
class << self
|
11
16
|
protected
|
17
|
+
|
12
18
|
def select_experiment_for(value, experiments)
|
13
19
|
weight = weight(value)
|
14
20
|
experiments = calculate_markers experiments
|
@@ -33,7 +39,6 @@ module ABSplit
|
|
33
39
|
{ marker: cumulative }.merge(experiment)
|
34
40
|
end
|
35
41
|
end
|
36
|
-
|
37
42
|
end
|
38
43
|
end
|
39
44
|
end
|
@@ -1,10 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module ABSplit
|
2
3
|
module Functions
|
4
|
+
# Weighted split based on hash value.
|
5
|
+
#
|
6
|
+
# Non persistent - No collisions are possible
|
7
|
+
#
|
8
|
+
# Based on memory position
|
3
9
|
class WeightedSplit
|
4
|
-
MAX_POSITIONS = (
|
5
|
-
|
6
|
-
class << self
|
10
|
+
MAX_POSITIONS = (9_999_999_999_999_999_999 * 2) + 1 # capacity of Fixnum
|
7
11
|
|
12
|
+
class << self
|
8
13
|
def value_for(x, *params)
|
9
14
|
given_weights = validate(params)
|
10
15
|
|
@@ -16,8 +21,8 @@ module ABSplit
|
|
16
21
|
protected
|
17
22
|
|
18
23
|
def validate(experiments)
|
19
|
-
given_weights = experiments.each_with_object([]) do |param, memo|
|
20
|
-
memo << param['weight'] if param.
|
24
|
+
given_weights = experiments.each_with_object([]) do |param, memo|
|
25
|
+
memo << param['weight'] if param.key?('weight')
|
21
26
|
end
|
22
27
|
|
23
28
|
unless experiments.any? && experiments.size > 1 && given_weights.reduce(0, &:+) <= 100
|
@@ -33,24 +38,22 @@ module ABSplit
|
|
33
38
|
missing_weights = parts - given_percentage.size
|
34
39
|
missing_percentage = 100 - given_percentage.reduce(0, &:+)
|
35
40
|
|
36
|
-
experiments.map do |experiment|
|
37
|
-
if experiment['weight']
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
experiments.map do |experiment|
|
42
|
+
experiment['weight'] = if experiment['weight']
|
43
|
+
experiment['weight'].to_f
|
44
|
+
else
|
45
|
+
missing_percentage.to_f / missing_weights.to_f
|
46
|
+
end
|
42
47
|
|
43
48
|
experiment
|
44
49
|
end
|
45
50
|
end
|
46
|
-
|
51
|
+
|
47
52
|
def select_experiment_for(x, experiments)
|
48
53
|
x_position = x.hash
|
49
54
|
|
50
55
|
markers(experiments).each_with_index do |limit, i|
|
51
|
-
if x_position <= limit
|
52
|
-
return experiments[i]['name']
|
53
|
-
end
|
56
|
+
return experiments[i]['name'] if x_position <= limit
|
54
57
|
end
|
55
58
|
|
56
59
|
experiments.last['name']
|
@@ -59,7 +62,7 @@ module ABSplit
|
|
59
62
|
private
|
60
63
|
|
61
64
|
def markers(experiments)
|
62
|
-
experiments.map do |experiment|
|
65
|
+
experiments.map do |experiment|
|
63
66
|
(self::MAX_POSITIONS * (experiment['weight'] / 100)) - (self::MAX_POSITIONS / 2)
|
64
67
|
end
|
65
68
|
end
|
data/lib/a_b_split/test.rb
CHANGED
@@ -1,41 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module ABSplit
|
2
|
-
|
3
|
-
|
3
|
+
# This class is responsible for spliting the population using the experiment
|
4
|
+
# name passed as a parameter. It will always return an experiment name or a
|
5
|
+
# NoValidExperiment error in case is chosen any unknown experiment.
|
6
|
+
class Test
|
7
|
+
include ABSplit
|
4
8
|
|
5
|
-
|
6
|
-
|
9
|
+
class << self
|
10
|
+
def split(name, x)
|
11
|
+
self.experiment = find(name)
|
7
12
|
|
8
|
-
|
13
|
+
raise ABSplit::NoValidExperiment unless experiment
|
9
14
|
|
10
|
-
|
11
|
-
|
15
|
+
function.value_for(x, *options)
|
16
|
+
end
|
12
17
|
|
13
|
-
|
18
|
+
private
|
14
19
|
|
15
|
-
|
20
|
+
attr_accessor :experiment
|
16
21
|
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
def find(experiment)
|
23
|
+
ABSplit.configuration.experiments[experiment]
|
24
|
+
end
|
20
25
|
|
21
|
-
|
22
|
-
|
26
|
+
def function
|
27
|
+
function = 'WeightedSplit'
|
23
28
|
|
24
|
-
|
25
|
-
|
29
|
+
unless experiment.is_a?(Array)
|
30
|
+
function = experiment['algorithm'] if experiment['algorithm']
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
begin
|
33
|
+
ABSplit::Functions.const_get(function)
|
34
|
+
rescue NameError
|
35
|
+
raise ABSplit::NoValidExperiment
|
36
|
+
end
|
31
37
|
end
|
32
|
-
end
|
33
38
|
|
34
|
-
|
35
|
-
|
39
|
+
ABSplit::Functions.const_get(function)
|
40
|
+
end
|
36
41
|
|
37
|
-
|
38
|
-
|
42
|
+
def options
|
43
|
+
experiment.is_a?(Array) ? experiment : experiment['options']
|
44
|
+
end
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
data/lib/a_b_split/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: a_b_split
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Enrique Figuerola
|
7
|
+
- Enrique M Figuerola Gomez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-03-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -16,44 +16,58 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '3.
|
19
|
+
version: '3.4'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '3.
|
26
|
+
version: '3.4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: pry
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0.
|
33
|
+
version: '0.10'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0.
|
40
|
+
version: '0.10'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '12.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '12.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.47'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.47'
|
55
69
|
description:
|
56
|
-
email:
|
70
|
+
email: me@emfigo.com
|
57
71
|
executables: []
|
58
72
|
extensions: []
|
59
73
|
extra_rdoc_files: []
|
@@ -66,7 +80,7 @@ files:
|
|
66
80
|
- lib/a_b_split/functions/weighted_split.rb
|
67
81
|
- lib/a_b_split/test.rb
|
68
82
|
- lib/a_b_split/version.rb
|
69
|
-
homepage: https://
|
83
|
+
homepage: https://emfigo.com/portfolio.html
|
70
84
|
licenses:
|
71
85
|
- MIT
|
72
86
|
metadata: {}
|
@@ -78,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
78
92
|
requirements:
|
79
93
|
- - ">="
|
80
94
|
- !ruby/object:Gem::Version
|
81
|
-
version:
|
95
|
+
version: 2.0.0
|
82
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
97
|
requirements:
|
84
98
|
- - ">="
|
@@ -86,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
100
|
version: '0'
|
87
101
|
requirements: []
|
88
102
|
rubyforge_project:
|
89
|
-
rubygems_version: 2.
|
103
|
+
rubygems_version: 2.6.8
|
90
104
|
signing_key:
|
91
105
|
specification_version: 4
|
92
106
|
summary: Splits experiment cohorts for A/B testing
|