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