From f21b9f55fd21fbf9d09782fbc321ceb123fdd37c Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 19 Sep 2017 18:57:31 +0200 Subject: [PATCH 1/7] Ability to allocate traffic for variant by weights --- src/alephbet.coffee | 12 ++++++++++++ src/utils.coffee | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index 58227dc..03188bf 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -67,6 +67,18 @@ class AlephBet @storage().get("#{@options.name}:variant") pick_variant: -> + variants_has_weight = utils.checkWeights(@variants).every (contains_weight) -> contains_weight + utils.log("all variants has weight: #{variants_has_weight}") + if variants_has_weight then @pick_unweighted_variant() else @pick_weighted_variant() + + pick_weighted_variant: -> + utils.log("picking weighted variant") + weightedIndex = Math.floor(Math.random() * 100) + for key, value of @variants + weightedIndex -= value.weight + return key if weightedIndex < 0 + + pick_unweighted_variant: -> partitions = 1.0 / @variant_names.length chosen_partition = Math.floor(@_random('variant') / partitions) variant = @variant_names[chosen_partition] diff --git a/src/utils.coffee b/src/utils.coffee index 3ea4fa1..b0cfda9 100644 --- a/src/utils.coffee +++ b/src/utils.coffee @@ -18,4 +18,9 @@ class Utils return Math.random() unless seed # a MUCH simplified version inspired by PlanOut.js parseInt(@sha1(seed).substr(0, 13), 16) / 0xFFFFFFFFFFFFF + @checkWeights: (variants) -> + @weightExists value for key, value of variants + @weightExists: (variant) -> + @log("variant.weight: #{variant.weight}") + variant.weight module.exports = Utils From 6191bb33abc96baf5d8458c5d4be978d87e6fb02 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 20 Sep 2017 22:30:33 +0200 Subject: [PATCH 2/7] Fix issue with picking unweighted variant --- src/alephbet.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index 03188bf..d097a3e 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -69,7 +69,7 @@ class AlephBet pick_variant: -> variants_has_weight = utils.checkWeights(@variants).every (contains_weight) -> contains_weight utils.log("all variants has weight: #{variants_has_weight}") - if variants_has_weight then @pick_unweighted_variant() else @pick_weighted_variant() + if variants_has_weight then @pick_weighted_variant() else @pick_unweighted_variant() pick_weighted_variant: -> utils.log("picking weighted variant") From d872f3153738ef66da5086fd1ef17812d6858634 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 26 Sep 2017 11:55:48 +0200 Subject: [PATCH 3/7] Additional cooments in code --- src/alephbet.coffee | 20 ++++++++++++++++++-- src/utils.coffee | 1 - 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index d097a3e..12a5bf4 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -67,16 +67,32 @@ class AlephBet @storage().get("#{@options.name}:variant") pick_variant: -> + # we are checking that all variants of experiment has weights variants_has_weight = utils.checkWeights(@variants).every (contains_weight) -> contains_weight utils.log("all variants has weight: #{variants_has_weight}") + # if all variants has weights than we should fire version for variants with weight if variants_has_weight then @pick_weighted_variant() else @pick_unweighted_variant() pick_weighted_variant: -> - utils.log("picking weighted variant") + + # Choosing a weighted variant: + # For A, B, C with weights 10, 30, 60 + # variants = A, B, C + # weights = 10, 30, 60 + # weightSum = 100 + # weightedIndex = 21 (random number between 0 and weight sum) + # ABBBCCCCCC - (every letter occurence should by multiplied by 10) + # =======^ + # Select C + + # I'm assuming that all weights will sum up to 100 + # then I pick random number from 0 - 100 weightedIndex = Math.floor(Math.random() * 100) for key, value of @variants + # then we are substracting variant weight from selected number + # and it it reaches 0 (or below) we are selecting this variant weightedIndex -= value.weight - return key if weightedIndex < 0 + return key if weightedIndex <= 0 pick_unweighted_variant: -> partitions = 1.0 / @variant_names.length diff --git a/src/utils.coffee b/src/utils.coffee index b0cfda9..52cfe24 100644 --- a/src/utils.coffee +++ b/src/utils.coffee @@ -21,6 +21,5 @@ class Utils @checkWeights: (variants) -> @weightExists value for key, value of variants @weightExists: (variant) -> - @log("variant.weight: #{variant.weight}") variant.weight module.exports = Utils From ccb25aa225b3d15938f8ea4dbf0103b5d9f9b935 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 7 Oct 2017 11:10:45 +0200 Subject: [PATCH 4/7] Amend logic for picking weighted variant --- src/alephbet.coffee | 23 +++++++++++------------ src/utils.coffee | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index 12a5bf4..a28579a 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -68,10 +68,10 @@ class AlephBet pick_variant: -> # we are checking that all variants of experiment has weights - variants_has_weight = utils.checkWeights(@variants).every (contains_weight) -> contains_weight - utils.log("all variants has weight: #{variants_has_weight}") + all_variants_have_weights = utils.checkWeights(@variants) + utils.log("all variants has weight: #{all_variants_have_weights}") # if all variants has weights than we should fire version for variants with weight - if variants_has_weight then @pick_weighted_variant() else @pick_unweighted_variant() + if all_variants_have_weights then @pick_weighted_variant() else @pick_unweighted_variant() pick_weighted_variant: -> @@ -79,20 +79,18 @@ class AlephBet # For A, B, C with weights 10, 30, 60 # variants = A, B, C # weights = 10, 30, 60 - # weightSum = 100 - # weightedIndex = 21 (random number between 0 and weight sum) + # weights_sum = 100 (sum of weights) + # weighted_index = 21 (random number between 0 and weight sum) # ABBBCCCCCC - (every letter occurence should by multiplied by 10) # =======^ # Select C - - # I'm assuming that all weights will sum up to 100 - # then I pick random number from 0 - 100 - weightedIndex = Math.floor(Math.random() * 100) + weights_sum = utils.sumWeights(@variants) + weighted_index = Math.floor(@_random('variant') * 100) for key, value of @variants # then we are substracting variant weight from selected number # and it it reaches 0 (or below) we are selecting this variant - weightedIndex -= value.weight - return key if weightedIndex <= 0 + weighted_index -= value.weight + return key if weighted_index <= 0 pick_unweighted_variant: -> partitions = 1.0 / @variant_names.length @@ -127,7 +125,8 @@ class AlephBet throw 'an experiment name must be specified' if @options.name is null throw 'variants must be provided' if @options.variants is null throw 'trigger must be a function' if typeof @options.trigger isnt 'function' - + every_variant_has_weight = utils.validateWeights @options.variants + throw 'not all variants contains weight' if !every_variant_has_weight class @Goal constructor: (@name, @props={}) -> diff --git a/src/utils.coffee b/src/utils.coffee index 52cfe24..e4e0c98 100644 --- a/src/utils.coffee +++ b/src/utils.coffee @@ -19,7 +19,16 @@ class Utils # a MUCH simplified version inspired by PlanOut.js parseInt(@sha1(seed).substr(0, 13), 16) / 0xFFFFFFFFFFFFF @checkWeights: (variants) -> - @weightExists value for key, value of variants - @weightExists: (variant) -> - variant.weight + contains_weight = [] + contains_weight.push(value.weight?) for key, value of variants + contains_weight.every (has_weight) -> has_weight + @sumWeights: (variants) -> + sum = 0 + for key, value of variants + sum += value.weight if value.weight? + sum + @validateWeights: (variants) -> + contains_weight = [] + contains_weight.push(value.weight?) for key, value of variants + contains_weight.every (has_weight) -> has_weight or contains_weight.every (has_weight) -> !has_weight module.exports = Utils From 3a829e040c17f72b2c8831fe42ee0951cbdfd786 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 7 Oct 2017 11:10:58 +0200 Subject: [PATCH 5/7] Additional tests --- test/alephbet_test.coffee | 28 +++++++++++++++ test/utils_test.coffee | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 test/utils_test.coffee diff --git a/test/alephbet_test.coffee b/test/alephbet_test.coffee index 13389b2..b44f709 100644 --- a/test/alephbet_test.coffee +++ b/test/alephbet_test.coffee @@ -104,3 +104,31 @@ describe 'tracks non unique goals', (t) -> t.assert(tracking.goal_complete.callCount == 2, 'goal_complete was called twice') t.notOk(storage.get('with-goals:my goal'), 'goal not stored') +describe 'when all variants has weights', (t) -> + ex = experiment({ + name: 'with-weights', + variants: + blue: + weight: 0 + activate: activate + green: + weight: 100 + activate: activate + }) + t.plan(2) + t.assert(ex.pick_variant() == 'green', 'always picks green variant') + t.assert(ex.pick_variant() != 'blue', 'never picks blue variant') + +describe 'when only some variants has weights', (t) -> + try ex = experiment({ + name: 'not-all-weights', + variants: + blue: + activate: activate + green: + weight: 100 + activate: activate + }) + catch e then t.assert(true, 'creating experiment should throw error') + t.plan(2) + t.assert(activate.callCount == 0, 'activate function shouldn\'t be called') diff --git a/test/utils_test.coffee b/test/utils_test.coffee new file mode 100644 index 0000000..5563567 --- /dev/null +++ b/test/utils_test.coffee @@ -0,0 +1,71 @@ +test = require('tape') +sinon = require('sinon') +_ = require('lodash') +Utils = require('../src/utils') + +setup = -> + utils = new Utils(); + +describe = (description, fn) -> + test description, (t) -> + setup() + fn(t) + +describe 'utils - checkWeights', (t) -> + variants = { + a: + weight: 50 + b: + weight: 50 + } + all_has_weight = Utils.checkWeights(variants); + variants = { + a: {} + b: {} + } + none_has_weight = Utils.checkWeights(variants); + variants = { + a: + weight: 20 + b: {} + } + some_has_weight = Utils.checkWeights(variants); + t.plan(3) + t.assert(all_has_weight == true, 'all variants contains weight') + t.assert(none_has_weight == false, 'variants does not contain weights') + t.assert(some_has_weight == false, 'only some variants does not have weight') + +describe 'utils - sumWeights', (t) -> + variants = { + a: + weight: 55 + b: + weight: 35 + } + sum = Utils.sumWeights(variants); + t.plan(1) + t.assert(sum == 90, 'sum should be equal 90') + +describe 'utils - validateWeights', (t) -> + variants = { + a: + weight: 55 + b: + weight: 35 + } + all_valid = Utils.validateWeights(variants); + variants = { + a: + weight: 55 + b: {} + } + some_valid = Utils.validateWeights(variants); + variants = { + a: {} + b: {} + } + all_not_have_weight = Utils.validateWeights(variants); + t.plan(3) + t.assert(all_valid == true, 'all should be valid') + t.assert(some_valid == false, 'only some are valid') + t.assert(all_not_have_weight == true, 'all does not contain weight but are valid') From 4227b757bd1cf5632bd93a772da571c5ace8c7c1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 7 Oct 2017 11:22:25 +0200 Subject: [PATCH 6/7] Variant with 0 should never be choosen --- src/alephbet.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index a28579a..ab2e3d7 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -85,7 +85,7 @@ class AlephBet # =======^ # Select C weights_sum = utils.sumWeights(@variants) - weighted_index = Math.floor(@_random('variant') * 100) + weighted_index = Math.floor((@_random('variant') * 100) + 1 ) for key, value of @variants # then we are substracting variant weight from selected number # and it it reaches 0 (or below) we are selecting this variant From ac29ff2c4504f6a985c091a3199a777a7b7b2a35 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 7 Oct 2017 11:47:27 +0200 Subject: [PATCH 7/7] Should use wieghts sum instead of 100 --- src/alephbet.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alephbet.coffee b/src/alephbet.coffee index ab2e3d7..63d9cb7 100644 --- a/src/alephbet.coffee +++ b/src/alephbet.coffee @@ -85,7 +85,7 @@ class AlephBet # =======^ # Select C weights_sum = utils.sumWeights(@variants) - weighted_index = Math.floor((@_random('variant') * 100) + 1 ) + weighted_index = Math.floor((@_random('variant') * weights_sum) + 1 ) for key, value of @variants # then we are substracting variant weight from selected number # and it it reaches 0 (or below) we are selecting this variant