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 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