# Code by me@bemmu.com, use as you like.

import random, math

class NeverUseAnyVariationsEngine():
	def serve_page(self, variations, outcome_func):
		outcome = outcome_func([])

	def __repr__(self):
		return "Never use any variations"

class UseHorsesVariationsEngine():
	def __init__(self, assumed_ratio = 0.5):
		self.tests = {}
		self.assumed_ratio = assumed_ratio

	def chance_that_a_is_better(self, a, b):
		# Calc current winning ratios
		#f = lambda s:len([x for x in s if x == "1"])
		a_ratio = a['convs'] / float(a['served'])
		b_ratio = b['convs'] / float(b['served'])
#		print a_ratio, b_ratio

		# Figure which is better and chance other one 
		# is actually better and lost out of pure chance. 
		if b_ratio > a_ratio:
			# Zeroes needed in B for A to catch up
			zeroes_needed = (b['convs'] - a_ratio * float(b['served']))/float(a_ratio)
			chance = math.pow(1 - self.assumed_ratio, zeroes_needed)
			return chance
		else:
			# Zeroes needed in A for B to catch up
			zeroes_needed = (a['convs'] - b_ratio * float(a['served']))/float(b_ratio)

			# hm these might be just wrong. I'm computing chance
			# that by picking the worse one it might still catch
			# up. Well that's the decision you can make...
			chance = math.pow(1 - self.assumed_ratio, zeroes_needed)

			# So with this chance should pick B
			# And inverse is then chance that should pick A
			return 1 - chance

		return True

	def decide_variations_to_use(self):
		variations_to_use = []
		for v in self.tests.keys():
			p = self.chance_that_a_is_better(self.tests[v]['on'], self.tests[v]['off'])
			if random.random() <= p:
				variations_to_use.append(v)
		return variations_to_use

	def serve_page(self, variations, outcome_func):
		for v in variations:
			if v not in self.tests:
				self.tests[v] = {
					'on' : {
						'convs' : 1,
						'served' : 1	
					},
					'off' : {
						'convs' : 1,
						'served' : 1
					}
				}
		
		variations_to_use = self.decide_variations_to_use()
		outcome = outcome_func(variations_to_use)

		test = self.tests[v]
		for v in variations:
			if v in variations_to_use:
				self.tests[v]['on']['served'] += 1
				if outcome:
					self.tests[v]['on']['convs'] += 1
			else:
				self.tests[v]['off']['served'] += 1
				if outcome:
					self.tests[v]['off']['convs'] += 1

	def __repr__(self):
		return "Use horses engine with ratio %.2f" % self.assumed_ratio	

class UseRandomVariationsEngine():
	def __init__(self):
		self.tests = {}

	def serve_page(self, variations, outcome_func):
		variations_to_use = []
		for var in variations:
			if random.random() < 0.5:
				variations_to_use.append(var)
		outcome = outcome_func(variations_to_use)

	def __repr__(self):
		return "Just use totally random variations"	

# For each known test, serve 1000 with half using the variation and half not, then go with better one.
class TryXAndQuitEngine():
	def __init__(self, x):
		self.tests = {}
		self.x = x

	def serve_page(self, variations, outcome_func):
		variations_to_use = []

		# Use a variation evenly until we have served it 1000 times. After that
		# start using the variation which performed better.
		for key in variations:
			if key not in self.tests:
				self.tests[key] = {'on_visits' : 0, 'off_visits' : 0, 'on_convs' : 0, 'off_convs' : 0}

			total_visits = self.tests[key]['on_visits'] + self.tests[key]['off_visits']
			if total_visits >= self.x:
				#print total_visits
				on_conv_ratio = self.tests[key]['on_convs'] / float(self.tests[key]['on_visits'])
				off_conv_ratio = self.tests[key]['off_convs'] / float(self.tests[key]['off_visits'])
				if on_conv_ratio > off_conv_ratio:
					variations_to_use.append(key)
		#			print "Using because on ratio is %.5f and off ratio is %.5f" % (on_conv_ratio, off_conv_ratio)
			else:
				if self.tests[key]['off_visits'] > self.tests[key]['on_visits']:
					variations_to_use.append(key)

		#print variations_to_use
		converted = outcome_func(variations_to_use)

		for key in variations:
			if key in variations_to_use:
				self.tests[key]['on_visits'] += 1
				if converted:
					self.tests[key]['on_convs'] += 1
			else:				
				self.tests[key]['off_visits'] += 1
				if converted:
					self.tests[key]['off_convs'] += 1

	def __repr__(self):
		return "Serve %d impressions then use better variation" % self.x

class SimulatedEnvironment():
	def __init__(self, engine):
		self.variations = {}
		self.value_of_conversion = 39.90
		self.cost_of_ab_test = 0
		self.conversion_ratio = 0.05
		self.engine = engine
		self.money = 0

	def add_variation(self, var):
		self.variations[var] = {
			'improvement' : random.gauss(0, 0.25)
		}
		self.money -= self.cost_of_ab_test
		#print self.variations

	# Converted or not?
	def outcome(self, variations):
		ratio = self.conversion_ratio
		for var in variations:
			# If ratio used to be 0.01 and it improves by 10% then it is 0.01 + 0.01 * 10% = 0.011
			ratio += ratio * self.variations[var]['improvement']

		converted = random.random() < ratio
		if converted:
			self.money += self.value_of_conversion
		return converted

	def step(self):
		self.engine.serve_page(self.variations.keys(), self.outcome)

from collections import defaultdict
engine_total_revenues = defaultdict(int, {})
steps = 10000
iterations = 1000
for iteration in range(0, iterations):
	print iteration
	engines = [
		UseHorsesVariationsEngine(0.05),
		UseHorsesVariationsEngine(0.25),
		UseHorsesVariationsEngine(0.5),
		NeverUseAnyVariationsEngine(), 
		TryXAndQuitEngine(1000), 
		TryXAndQuitEngine(2000), 
		TryXAndQuitEngine(500), 
		UseRandomVariationsEngine()
	]
	for engine in engines:
		sim = SimulatedEnvironment(engine)
		sim.add_variation("test1")
		sim.add_variation("test2")
		sim.add_variation("test3")
		sim.add_variation("test4")
		for i in range(0, steps):
			sim.step()
		engine_total_revenues[str(engine)] += sim.money

for name, total_revenue in engine_total_revenues.items():
	print "Avg. revenue: $%.2f with %s" % (total_revenue / float(iterations), name)