a_b_split 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b16414fe4467a730169a19275bc259e77ab4c17
4
- data.tar.gz: 4397d8407e13272f1558322192a92cd96229185e
3
+ metadata.gz: 28166486a6f870f7b67355794008e620971f416b
4
+ data.tar.gz: 457b3a539da94205f8533849721d323f114e7b2a
5
5
  SHA512:
6
- metadata.gz: 946b6bd531d8e4e1575a63fbe2a3b7dd7abf88ed2f4b41c02e477ea922e90b08513f356be24ba9115114b891de5a92ee06e1b471177b0ead4c53a6710510ff88
7
- data.tar.gz: 730f33d4df3c5f0abb27e3c0b2e05f7ddc6f314d6b760e822210ebe4f23dcd9412a9ffa04527c3412f2bdb4680958f0bab9cbc9918a7ceb1e556e54d7854a845
6
+ metadata.gz: e615da6bbd04d68255074bca677c818933444b7ecabf966cd7fee20f3b339d465939f53f9f7ab7d6880ff912290df54eb832e9f4df600cbe78c5ab6458deaf99
7
+ data.tar.gz: ba65a2075286ef08e830e6eaa238c9af6b1d8c1c5cb5d8a984251300a54673bd22217d762ac8f25c0c4da124f498c597ee73526b35affe193366ec1a6ca7b97a
data/lib/a_b_split.rb CHANGED
@@ -1,4 +1,5 @@
1
- $:.unshift File.expand_path('..', __FILE__)
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
- module ABSplit
13
- extend self
12
+ def configuration
13
+ @configuration
14
+ end
14
15
 
15
- attr_accessor :configuration
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 = Hash.new
11
+ @experiments = {}
7
12
  end
8
13
  end
9
14
  end
@@ -1,3 +1,4 @@
1
- Dir[File.join(File.dirname(__FILE__), *%w(functions *))].each do |function|
1
+ # frozen_string_literal: true
2
+ Dir[File.join(File.dirname(__FILE__), 'functions', '*')].each do |function|
2
3
  require function
3
4
  end
@@ -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 < accumulated_percentages
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("#{1.0/(1 + Math.exp(-2 * x))}")
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 = (9999999999999999999 * 2) + 1 #capacity of Fixnum
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.has_key?('weight')
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
- experiment['weight'] = experiment['weight'].to_f
39
- else
40
- experiment['weight'] = missing_percentage.to_f / missing_weights.to_f
41
- end
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
@@ -1,41 +1,47 @@
1
+ # frozen_string_literal: true
1
2
  module ABSplit
2
- module Test
3
- extend self
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
- def split(name, x)
6
- self.experiment = find(name)
9
+ class << self
10
+ def split(name, x)
11
+ self.experiment = find(name)
7
12
 
8
- raise ABSplit::NoValidExperiment unless experiment
13
+ raise ABSplit::NoValidExperiment unless experiment
9
14
 
10
- function.value_for(x,*options)
11
- end
15
+ function.value_for(x, *options)
16
+ end
12
17
 
13
- private
18
+ private
14
19
 
15
- attr_accessor :experiment
20
+ attr_accessor :experiment
16
21
 
17
- def find(experiment)
18
- ABSplit.configuration.experiments[experiment]
19
- end
22
+ def find(experiment)
23
+ ABSplit.configuration.experiments[experiment]
24
+ end
20
25
 
21
- def function
22
- function = 'WeightedSplit'
26
+ def function
27
+ function = 'WeightedSplit'
23
28
 
24
- unless experiment.is_a?(Array)
25
- function = experiment['algorithm'] if experiment['algorithm']
29
+ unless experiment.is_a?(Array)
30
+ function = experiment['algorithm'] if experiment['algorithm']
26
31
 
27
- begin
28
- ABSplit::Functions.const_get(function)
29
- rescue NameError
30
- raise ABSplit::NoValidExperiment
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
- ABSplit::Functions.const_get(function)
35
- end
39
+ ABSplit::Functions.const_get(function)
40
+ end
36
41
 
37
- def options
38
- experiment.is_a?(Array) ? experiment : experiment['options']
42
+ def options
43
+ experiment.is_a?(Array) ? experiment : experiment['options']
44
+ end
39
45
  end
40
46
  end
41
47
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ABSplit
2
- VERSION = '0.1.1'
3
+ VERSION = '1.0.0'
3
4
  end
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.1.1
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: 2015-09-12 00:00:00.000000000 Z
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.0'
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.0'
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.9'
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.9'
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: '10.3'
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: '10.3'
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: hard_rock15@msn.com
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://github.com/emfigo/absplit
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: 1.9.3
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.4.5.1
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