lib/field_test/experiment.rb in field_test-0.1.2 vs lib/field_test/experiment.rb in field_test-0.2.0

- old
+ new

@@ -1,13 +1,14 @@ module FieldTest class Experiment - attr_reader :id, :name, :variants, :weights, :winner, :started_at, :ended_at + attr_reader :id, :name, :description, :variants, :weights, :winner, :started_at, :ended_at def initialize(attributes) attributes = attributes.symbolize_keys @id = attributes[:id] @name = attributes[:name] || @id.to_s.titleize + @description = attributes[:description] @variants = attributes[:variants] @weights = @variants.size.times.map { |i| attributes[:weights].to_a[i] || 1 } @winner = attributes[:winner] @started_at = Time.zone.parse(attributes[:started_at].to_s) if attributes[:started_at] @ended_at = Time.zone.parse(attributes[:ended_at].to_s) if attributes[:ended_at] @@ -31,10 +32,21 @@ membership.participant = participants.first if membership.changed? begin membership.save! + + # log it! + info = { + experiment: id, + variant: membership.variant, + participant: membership.participant + }.merge(options.slice(:ip, :user_agent)) + + # sorta logfmt :) + info = info.map { |k, v| v = "\"#{v}\"" if k == :user_agent; "#{k}=#{v}" }.join(" ") + Rails.logger.info "[field test] #{info}" rescue ActiveRecord::RecordNotUnique membership = memberships.find_by(participant: participants.first) end end @@ -73,14 +85,14 @@ converted: converted, conversion_rate: participated > 0 ? converted.to_f / participated : nil } end case variants.size - when 1 - results[variants[0]][:prob_winning] = 1 - when 2, 3 - variants.size.times do |i| + when 1, 2, 3 + total = 0.0 + + (variants.size - 1).times do |i| c = results.values[i] b = results.values[(i + 1) % variants.size] a = results.values[(i + 2) % variants.size] alpha_a = 1 + a[:converted] @@ -88,17 +100,27 @@ alpha_b = 1 + b[:converted] beta_b = 1 + b[:participated] - b[:converted] alpha_c = 1 + c[:converted] beta_c = 1 + c[:participated] - c[:converted] - results[variants[i]][:prob_winning] = + # TODO calculate this incrementally by caching intermediate results + prob_winning = if variants.size == 2 - prob_b_beats_a(alpha_b, beta_b, alpha_c, beta_c) + cache_fetch ["field_test", "prob_b_beats_a", alpha_b, beta_b, alpha_c, beta_c] do + Calculations.prob_b_beats_a(alpha_b, beta_b, alpha_c, beta_c) + end else - prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c) + cache_fetch ["field_test", "prob_c_beats_a_and_b", alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c] do + Calculations.prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c) + end end + + results[variants[i]][:prob_winning] = prob_winning + total += prob_winning end + + results[variants.last][:prob_winning] = 1 - total end results end def active? @@ -138,35 +160,14 @@ return variants[i] if n >= pick end variants.last end - # formula from - # http://www.evanmiller.org/bayesian-ab-testing.html - def prob_b_beats_a(alpha_a, beta_a, alpha_b, beta_b) - total = 0.0 - - 0.upto(alpha_b - 1) do |i| - total += Math.exp(Math.logbeta(alpha_a + i, beta_b + beta_a) - - Math.log(beta_b + i) - Math.logbeta(1 + i, beta_b) - - Math.logbeta(alpha_a, beta_a)) + def cache_fetch(key) + if FieldTest.cache + Rails.cache.fetch(key.join("/")) { yield } + else + yield end - - total - end - - def prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c) - total = 0.0 - 0.upto(alpha_a - 1) do |i| - 0.upto(alpha_b - 1) do |j| - total += Math.exp(Math.logbeta(alpha_c + i + j, beta_a + beta_b + beta_c) - - Math.log(beta_a + i) - Math.log(beta_b + j) - - Math.logbeta(1 + i, beta_a) - Math.logbeta(1 + j, beta_b) - - Math.logbeta(alpha_c, beta_c)) - end - end - - 1 - prob_b_beats_a(alpha_c, beta_c, alpha_a, beta_a) - - prob_b_beats_a(alpha_c, beta_c, alpha_b, beta_b) + total end end end