lib/field_test/experiment.rb in field_test-0.1.0 vs lib/field_test/experiment.rb in field_test-0.1.1
- old
+ new
@@ -1,21 +1,24 @@
module FieldTest
class Experiment
- attr_reader :id, :variants, :winner
+ attr_reader :id, :name, :variants, :winner, :started_at, :ended_at
def initialize(attributes)
attributes = attributes.symbolize_keys
@id = attributes[:id]
+ @name = attributes[:name] || @id.to_s.titleize
@variants = attributes[:variants]
@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]
end
def variant(participants, options = {})
return winner if winner
return variants.first if options[:exclude]
- participants = standardize_participants(participants)
+ participants = FieldTest::Participant.standardize(participants)
membership = membership_for(participants) || FieldTest::Membership.new(experiment: id)
if options[:variant] && variants.include?(options[:variant])
membership.variant = options[:variant]
else
@@ -35,11 +38,11 @@
membership.try(:variant) || variants.first
end
def convert(participants)
- participants = standardize_participants(participants)
+ participants = FieldTest::Participant.standardize(participants)
membership = membership_for(participants)
if membership
membership.converted = true
membership.save! if membership.changed?
@@ -52,43 +55,100 @@
def memberships
FieldTest::Membership.where(experiment: id)
end
def results
- data = memberships.group(:variant).group(:converted).count
+ data = memberships.group(:variant).group(:converted)
+ data = data.where("created_at >= ?", started_at) if started_at
+ data = data.where("created_at <= ?", ended_at) if ended_at
+ data = data.count
results = {}
variants.each do |variant|
converted = data[[variant, true]].to_i
participated = converted + data[[variant, false]].to_i
results[variant] = {
participated: participated,
converted: converted,
- conversion_rate: converted.to_f / participated
+ 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|
+ c = results.values[i]
+ b = results.values[(i + 1) % variants.size]
+ a = results.values[(i + 2) % variants.size]
+
+ alpha_a = 1 + a[:converted]
+ beta_a = 1 + a[:participated] - a[:converted]
+ 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] =
+ if variants.size == 2
+ prob_b_beats_a(alpha_b, beta_b, alpha_c, beta_c)
+ else
+ prob_c_beats_a_and_b(alpha_a, beta_a, alpha_b, beta_b, alpha_c, beta_c)
+ end
+ end
+ end
results
end
+ def active?
+ !winner
+ end
+
def self.find(id)
- # reload in dev
- @config = nil if Rails.env.development?
+ experiment = all.index_by(&:id)[id.to_s]
+ raise FieldTest::ExperimentNotFound unless experiment
- @config ||= YAML.load(ERB.new(File.read("config/field_test.yml")).result)
+ experiment
+ end
- settings = @config["experiments"][id.to_s]
- raise FieldTest::ExperimentNotFound unless settings
-
- FieldTest::Experiment.new(settings.merge(id: id.to_s))
+ def self.all
+ FieldTest.config["experiments"].map do |id, settings|
+ FieldTest::Experiment.new(settings.merge(id: id.to_s))
+ end
end
private
def membership_for(participants)
memberships = self.memberships.where(participant: participants).index_by(&:participant)
participants.map { |part| memberships[part] }.compact.first
end
- def standardize_participants(participants)
- Array(participants).map { |v| v.respond_to?(:model_name) ? "#{v.model_name.name}:#{v.id}" : v.to_s }
+ # 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))
+ 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