Permalink
Browse files

first codebase commit

  • Loading branch information...
1 parent 0ae5e53 commit bbded3f31c3c012f8401c1981b74f5a0ed6b1b3a @bmuller committed Aug 5, 2011
View
@@ -2,3 +2,4 @@
.bundle
Gemfile.lock
pkg/*
+docs/*
View
674 LICENSE

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -1,3 +1,18 @@
= bandit - A multi-armed bandit optmization framework for Rails
-TODO: Write library.
+TODO: Write library.
+
+= Storage:
+# store total count for this alternative
+<experiment>:<alternative>:conversions = count
+<experiment>:<alternative>:participants = count
+
+# store total count for this alternative per day and hour
+<experiment>:<alternative>:conversions:<date>:<hour> = count
+<experiment>:<alternative>:participants:<date>:<hour> = count
+
+# every so often store current epsilon
+<experiment>:epsilon = 0.1
+
+= Reference
+http://untyped.com/untyping/2011/02/11/stop-ab-testing-and-make-out-like-a-bandit/
View
@@ -1 +1,11 @@
require 'bundler/gem_tasks'
+require 'rdoc/task'
+
+desc "Create documentation"
+Rake::RDocTask.new("doc") { |rdoc|
+ rdoc.title = "bandit - A multi-armed bandit optmization framework for Rails"
+ rdoc.rdoc_dir = 'docs'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+}
+
View
@@ -1,4 +1,3 @@
-# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "bandit/version"
@@ -7,7 +6,7 @@ Gem::Specification.new do |s|
s.version = Bandit::VERSION
s.authors = ["Brian Muller"]
s.email = ["brian.muller@livingsocial.com"]
- s.homepage = ""
+ s.homepage = "https://github.com/bmuller/bandit"
s.summary = "Multi-armed bandit testing in rails"
s.description = "Bandit provides a way to do multi-armed bandit optimization of alternatives in a rails website"
View
@@ -1,5 +1,29 @@
require "bandit/version"
+require "bandit/exceptions"
+require "bandit/experiments"
+require "bandit/metric"
+require "bandit/pit_boss"
+require "bandit/players/base"
+require "bandit/players/round_robin"
+require "bandit/storage/base"
+require "bandit/storage/memory"
+require "bandit/config"
module Bandit
- # Your code goes here...
+ def self.config
+ @config ||= Config.new
+ end
+
+ def self.setup(&block)
+ yield config
+ config.check!
+ end
+
+ def self.storage
+ @storage ||= BaseStorage.get_storage(Bandit.config.storage, Bandit.config.storage_config)
+ end
+
+ def self.player
+ @player ||= BasePlayer.get_player(Bandit.config.player, Bandit.config.player_config)
+ end
end
View
@@ -0,0 +1,33 @@
+module Bandit
+
+ class Config
+
+ class MissingConfigurationError < ArgumentError; end
+
+ def self.required_fields
+ [:storage, :player]
+ end
+
+ # storage should be name of storage engine
+ attr_accessor :storage
+
+ # storage_config should be hash of storage config values
+ attr_accessor :storage_config
+
+ # player should be name of player
+ attr_accessor :player
+
+ # player_config should be hash of player config values
+ attr_accessor :player_config
+
+ def check!
+ self.class.required_fields.each do |required_field|
+ unless send(required_field)
+ raise MissingConfigurationError, "#{required_field} must be set"
+ end
+ end
+ end
+
+ end
+
+end
View
@@ -0,0 +1,7 @@
+module Bandit
+ class UnknownStorageEngineError < RuntimeError
+ end
+
+ class UnknownPlayerEngineError < RuntimeError
+ end
+end
View
@@ -0,0 +1,15 @@
+module Bandit
+ class Experiment
+ attr_accessor :name, :title, :description, :metric, :alternatives, :default_alternative, :storage
+ @@instances = []
+
+ def initialize(args=nil)
+ args.each { |k,v| send "#{k}=", v } unless args.nil?
+ @@instances << self
+ end
+
+ def self.instances
+ @@instances
+ end
+ end
+end
View
@@ -0,0 +1,15 @@
+module Bandit
+ class Metric
+ attr_accessor :name, :description
+ @@instances = []
+
+ def initialize(args=nil)
+ args.each { |k,v| send "#{k}=", v } unless args.nil?
+ @@instances << self
+ end
+
+ def self.instances
+ @@instances
+ end
+ end
+end
View
@@ -0,0 +1,18 @@
+module Bandit
+ class BasePlayer
+ def self.get_player(name, config)
+ config ||= {}
+
+ case name
+ when :round_robin then RoundRobinPlayer.new(config)
+ else raise UnknownPlayerEngineError, "#{name} not a known player type"
+ end
+
+ end
+
+ def choose_alternattive(experiment)
+ # return the alternative that should be chosen
+ raise NotImplementedError
+ end
+ end
+end
@@ -0,0 +1,7 @@
+module Bandit
+ class RoundRobinPlayer
+ def choose_alternattive(experiment)
+ experiment.alternatives.choice
+ end
+ end
+end
View
@@ -0,0 +1,36 @@
+module Bandit
+ class BaseStorage
+ def self.get_storage(name, config)
+ config ||= {}
+
+ case name
+ when :memory then MemoryStorage.new(config)
+ else raise UnknownStorageEngineError, "#{name} not a known storage method"
+ end
+ end
+
+ def incr_participants(experiment, alternative, count=1)
+ raise NotImplementedError
+ end
+
+ def incr_conversions(experiment, alternative, count=1)
+ raise NotImplementedError
+ end
+
+ def participant_count(experiment, alternative)
+ raise NotImplementedError
+ end
+
+ def conversion_count(experiment, alternative)
+ raise NotImplementedError
+ end
+
+ def epsilon
+ raise NotImplementedError
+ end
+
+ def epsilon=(value)
+ raise NotImplementedError
+ end
+ end
+end
@@ -0,0 +1,37 @@
+module Bandit
+ class MemoryStorage < BaseStorage
+ def initialize(config)
+ @participants = {}
+ @conversions = {}
+ @epsilon = config['epsilon']
+ end
+
+ def incr_participants(experiment, alternative, count=1)
+ key = [experiment.name, alternative].join(":")
+ @participants[key] = @participants.fetch(key, 0) + count
+ end
+
+ def incr_conversions(experiment, alternative, count=1)
+ key = [experiment.name, alternative].join(":")
+ @conversions[key] = @conversions.fetch(key, 0) + count
+ end
+
+ def participant_count(experiment, alternative)
+ key = [experiment.name, alternative].join(":")
+ @participants.fetch(key, 0)
+ end
+
+ def conversion_count(experiment, alternative)
+ key = [experiment.name, alternative].join(":")
+ @conversions.fetch(key, 0)
+ end
+
+ def epsilon
+ @epsilon
+ end
+
+ def epsilon=(value)
+ @epsilon = value
+ end
+ end
+end
View
@@ -1,3 +1,3 @@
module Bandit
- VERSION = "0.0.1"
+ VERSION = "0.0.2"
end
@@ -0,0 +1,3 @@
+To copy bandit config and initilizer:
+
+ rails generate bandit:install
@@ -0,0 +1,18 @@
+module Bandit
+ module Generators
+
+ class InstallGenerator < Rails::Generators::Base
+ desc "Copy Bandit default config/initialization files"
+ source_root File.expand_path('../templates', __FILE__)
+
+ def copy_initializers
+ copy_file 'bandit.rb', 'config/initializers/bandit.rb'
+ end
+
+ def copy_config
+ copy_file 'bandit.yml', 'config/bandit.yml'
+ end
+ end
+
+ end
+end
@@ -0,0 +1,20 @@
+# Use this setup block to configure all options for Bandit.
+Bandit.setup do |config|
+ yml = YAML.load_file("#{Rails.root}/config/bandit.yml")[Rails.env]
+
+ config.player = yml['player']
+
+ config.player_config = yml['player_config'] || nil
+
+ config.storage = yml['storage']
+end
+
+# Create your metrics here
+# m = Metric.new :name => 'clicks', :description => 'number of people who clicked'
+
+# Create your experiments here
+# e = Experiment.new :title => 'awesome experiment', :description => 'test button sizes'
+# e.alternatives = [ 20, 30, 40 ]
+# e.metric = m
+
+# That's all!
@@ -0,0 +1,16 @@
+development:
+ player: round_robin
+ storage: memory
+
+test:
+ player: round_robin
+ storage: memory
+
+production:
+ player: epsilon_greedy
+ player_config:
+ epsilon: 0.8
+ storage: redis
+ storage_config:
+ host: localhost
+ port: 6379

0 comments on commit bbded3f

Please sign in to comment.