Skip to content

Instantly share code, notes, and snippets.

@staylor
Created July 10, 2012 19:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save staylor/3085598 to your computer and use it in GitHub Desktop.
Save staylor/3085598 to your computer and use it in GitHub Desktop.
Multi-Armed Bandit
<?php
class HouseAd extends ModuleType {
const TYPE_QUERY_STRING = 0;
const TYPE_URL_PATH = 1;
const TYPE_COOKIE = 2;
const OPERATOR_EQUALS = 100;
const OPERATOR_NOT_EQUALS = 101;
const OPERATOR_GTE = 102;
const OPERATOR_LTE = 103;
const OPERATOR_LESS_THAN = 104;
const OPERATOR_GREATER_THAN = 105;
const CONDITION_AND = 200;
const CONDITION_OR = 201;
const STRATEGY_AB = 300;
const STRATEGY_EPSILON_GREEDY = 301;
const STRATEGY_EPSILON_FIRST = 302;
const STRATEGY_EPSILON_DECR = 303;
protected function __construct() {
$this->post_type = 'emusic_house_ad';
$this->meta_name = 'house_ad_meta';
parent::__construct();
}
function init() {
parent::init();
add_action( 'wp_ajax_new-house-ad-ruleset', array( $this, 'new_ruleset' ) );
add_action( 'wp_ajax_new-house-ad-rule', array( $this, 'new_rule' ) );
add_action( "add_meta_boxes_{$this->post_type}", array( $this, 'actions' ) );
}
function actions() {
add_action( 'admin_head', array( $this, 'inline_js' ), 20 );
}
function inline_js() {
?>
<style type="text/css">
#rulesets .ruleset {padding: 10px; margin: 10px 0; background: #fff}
</style>
<script type="text/javascript">
(function ($) {
var rulesets;
function new_ruleset() {
$.ajax({
url : ajaxurl,
data : {
action : 'new-house-ad-ruleset',
set : $('.ruleset').length
},
success : function (data) {
rulesets.append($(data));
}
});
return false;
}
function new_rule() {
var set = $(this).parents('.ruleset');
$.ajax({
url : ajaxurl,
data : {
action : 'new-house-ad-rule',
set : set.attr('data-set'),
rule : set.find('.rule').length
},
success : function (data) {
set.find('table').append($(data));
}
});
return false;
}
function add_creative() {
}
$(document).ready(function ($) {
rulesets = $('#rulesets');
$('.add-new-ruleset').click(new_ruleset);
$('.add-new-rule').live('click', new_rule);
window.add_media_callback = add_creative;
});
}(jQuery));
</script>
<?php
}
function type() {
$this->supports = array( 'title', 'thumbnail' );
parent::type( array(
'show_in_menu' => 'emusic-miscellaneous',
'labels' => emusic_inflection( 'House Ad' ),
'taxonomies' => array( 'region' ),
'callback' => array( $this, 'box' )
) );
}
function box() {
add_meta_box( $this->post_type . '_meta_id',
__( 'Configuration', 'emusic' ),
array( $this, 'box_callback' ),
$this->post_type,
'normal',
'low'
);
}
function new_ruleset() {
extract( $_REQUEST );
$this->ruleset( $set );
exit();
}
function new_rule() {
extract( $_REQUEST );
$this->rule( $set, $rule );
exit();
}
function ruleset( $set, $data = null ) {
?>
<div class="ruleset" data-set="<?php echo $set ?>">
<?php if ( 0 < (int) $set ): ?>
<p>Condition: <select name="ruleset[<?php echo $set ?>][condition]">
<option value="<?php echo self::CONDITION_AND ?>" <?php
selected( $data && self::CONDITION_AND === $data->condition ) ?>>AND</option>
<option value="<?php echo self::CONDITION_OR ?>" <?php
selected( $data && self::CONDITION_OR === $data->condition ) ?>>OR</option>
</select></p>
<?php endif ?>
<table>
<tr>
<th>Type</th>
<th>Cookie (optional)</th>
<th>Object</th>
<th>Operator</th>
<th>Value</th>
</tr>
<?php
if ( empty( $data ) ) {
$this->rule( $set );
} else {
$rules = $this->get_rules( $data->id );
if ( empty( $rules ) ) {
$this->rule( $set );
} else {
foreach ( $rules as $index => $the_rule )
$this->rule( $set, $index, $the_rule );
}
}
?>
</table>
<p><a class="add-new-rule button">Add a New Rule</a></p>
</div>
<?php
}
function rule( $set = 0, $rule = 0, $data = null ) {
?>
<tr class="rule">
<?php
if ( $data )
$data->type = (int) $data->type;
?>
<td>
<select name="ruleset[<?php echo $set ?>][rule][<?php echo $rule ?>][type]">
<option value="">-- SELECT MATCH TYPE --</option>
<option value="<?php echo self::TYPE_QUERY_STRING ?>" <?php
selected( $data && self::TYPE_QUERY_STRING === $data->type ) ?>>Query String</option>
<option value="<?php echo self::TYPE_URL_PATH ?>" <?php
selected( $data && self::TYPE_URL_PATH === $data->type ) ?>>URL Path</option>
<option value="<?php echo self::TYPE_COOKIE ?>" <?php
selected( $data && self::TYPE_COOKIE === $data->type ) ?>>Cookie</option>
</select>
</td>
<td>
<input type="text" size="25" class="widefat" name="ruleset[<?php echo $set ?>][rule][<?php echo $rule ?>][parent]" value="<?php
esc_attr_e( $data && !empty( $data->parent ) ? $data->parent : '' ) ?>"/>
</td>
<td>
<input type="text" size="25" class="widefat" name="ruleset[<?php echo $set ?>][rule][<?php echo $rule ?>][name]" value="<?php
esc_attr_e( $data ? $data->name : '' ) ?>"/>
</td>
<?php
if ( $data )
$data->operator = (int) $data->operator;
?>
<td>
<select name="ruleset[<?php echo $set ?>][rule][<?php echo $rule ?>][operator]">
<option value="<?php echo self::OPERATOR_EQUALS ?>" <?php
selected( $data && self::OPERATOR_EQUALS === $data->operator ) ?>> = </option>
<option value="<?php echo self::OPERATOR_NOT_EQUALS ?>" <?php
selected( $data && self::OPERATOR_NOT_EQUALS === $data->operator ) ?>> != </option>
<option value="<?php echo self::OPERATOR_GTE ?>=" <?php
selected( $data && self::OPERATOR_GTE === $data->operator ) ?>> >= </option>
<option value="<?php echo self::OPERATOR_LTE ?>=" <?php
selected( $data && self::OPERATOR_LTE === $data->operator ) ?>> <= </option>
<option value="<?php echo self::OPERATOR_LESS_THAN ?>" <?php
selected( $data && self::OPERATOR_LESS_THAN === $data->operator ) ?>> < </option>
<option value="<?php echo self::OPERATOR_GREATER_THAN ?>" <?php
selected( $data && self::OPERATOR_GREATER_THAN === $data->operator ) ?>> > </option>
</select>
</td>
<td>
<input type="text" size="25" class="widefat" name="ruleset[<?php echo $set ?>][rule][<?php echo $rule ?>][value]" value="<?php
esc_attr_e( $data ? $data->value : '' ) ?>"/>
</td>
</tr>
<?php
}
function box_callback( $post ) {
$code = '';
if ( !empty( $post->ID ) )
$code = get_post_meta( $post->ID, 'omniture_code', true );
?>
<div class="emusic-admin">
<p>
<label><strong>Description of this Ad</strong> (optional)</label>
<input class="widefat" type="text" name="house_ad_desc" value="<?php esc_attr_e( $post ? $post->post_excerpt : '' ) ?>"/>
</p>
<?php
$guid = '';
if ( $post && !strstr( $post->guid, '?post_type=emusic_house_ad' ) )
$guid = $post->guid;
?>
<p>
<label><strong>Click URL</strong></label>
<input class="widefat" type="text" name="house_ad_url" value="<?php esc_attr_e( $guid ) ?>"/>
</p>
<p>
<label><strong>Click Map</strong> (optional)</label>
<input class="widefat" type="text" name="house_ad_map" value="<?php esc_attr_e( $post ? $post->post_content_filtered : '' ) ?>"/>
</p>
<p>
<label><strong>Tracking Code</strong></label>
<input class="widefat" type="text" name="house_ad_code" value="<?php esc_attr_e( $code ) ?>"/>
</p>
<div class="creative">
<?php
if ( $post && $post->ID ) {
$images = get_images( $post->ID );
if ( !empty( $images ) ) {
echo '<ul>';
foreach ( $images as $image )
printf( '<li><a class="ad-creative">%s</a></li>', get_the_post_thumbnail( $image->ID ) );
echo '</ul>';
}
}
?>
<div id="creative-trigger">
<?php $this->_image_id_input( __( 'Add More Creative' ), 'new-creative', 'new-creative', false, true, false ) ?>
</div>
</div>
<div id="strategy">
<p>
<select name="house_ad_strategy">
<option value=""> --- SELECT A STRATEGY --- </option>
<option value="<?php echo self::STRATEGY_AB ?>" <?php
selected( $post && self::STRATEGY_AB === $post->menu_order ) ?>> A / B </option>
<option value="<?php echo self::STRATEGY_EPSILON_GREEDY ?>" <?php
selected( $post && self::STRATEGY_EPSILON_GREEDY === $post->menu_order ) ?>> Epsilon-greedy </option>
<option value="<?php echo self::STRATEGY_EPSILON_FIRST ?>" <?php
selected( $post && self::STRATEGY_EPSILON_GREEDY === $post->menu_order ) ?>> Epsilon-first </option>
<option value="<?php echo self::STRATEGY_EPSILON_DECR ?>" <?php
selected( $post && self::STRATEGY_EPSILON_DECR === $post->menu_order ) ?>> Epsilon-decreasing </option>
</select>
</p>
<p>
<strong>Number of Impressions Per Test (if applicable)</strong>
<input type="text" name="house_ad_impressions"/>
</p>
</div>
<div id="rulesets">
<p><a class="add-new-ruleset button">Add a New Ruleset</a></p>
<?php
if ( !empty( $post->ID ) ):
$rulesets = $this->get_rulesets( $post->ID );
if ( !empty( $rulesets ) ):
foreach ( $rulesets as $index => $ruleset )
$this->ruleset( $index, $ruleset );
endif;
endif;
?>
</div>
</div>
<?php
}
function callback( $id, $post ) {
global $wpdb;
if ( $this->post_type === $post->post_type ) {
$params = array(
'post_content' => '',
'post_excerpt' => '',
'guid' => '',
'post_content_filtered' => '',
'menu_order' => ''
);
if ( !empty( $_POST['house_ad_code'] ) )
$params['post_excerpt'] = stripslashes( $_POST['house_ad_code'] );
if ( !empty( $_POST['house_ad_desc'] ) )
$params['post_content'] = stripslashes( $_POST['house_ad_desc'] );
if ( !empty( $_POST['house_ad_url'] ) )
$params['guid'] = stripslashes( $_POST['house_ad_url'] );
if ( !empty( $_POST['house_ad_map'] ) )
$params['post_content_filtered'] = stripslashes( $_POST['house_ad_map'] );
if ( !empty( $_POST['house_ad_strategy'] ) )
$params['menu_order'] = stripslashes( $_POST['house_ad_strategy'] );
if ( !empty( $params ) ) {
$wpdb->update( $wpdb->posts, $params, array( 'ID' => $id ) );
clean_post_cache( $id );
}
if ( !empty( $_POST['ruleset'] ) ) {
$rulesets = stripslashes_deep( $_POST['ruleset'] );
$rows = $this->get_rulesets( $id );
$sets = count( $rows );
$ruleset_count = 0;
foreach ( $rulesets as $ruleset ) {
if ( empty( $ruleset['rule'] ) )
continue;
$params = array(
'object_id' => $id,
'order' => $ruleset_count,
'condition' => empty( $set['condition'] ) ? '' : $set['condition']
);
if ( $ruleset_count >= $sets || !array_key_exists( $ruleset_count, $rows ) ) {
$wpdb->insert( 'wp_rulesets', $params );
$ruleset_id = $wpdb->insert_id;
} else {
$wpdb->update( 'wp_rulesets', $params, array( 'object_id' => $id, 'order' => $ruleset_count ) );
$ruleset_id = $rows[$ruleset_count]->id;
unset( $rows[$ruleset_count] );
}
$rule_count = 0;
$rules = $this->get_rules( $ruleset_id );
foreach ( $rules as $rule )
$rules[$rule->order] = $rule;
$num_rules = count( $rules );
foreach ( $ruleset['rule'] as $rule ) {
if ( '' === $rule['value'] || '' === $rule['type'] )
continue;
if ( self::TYPE_URL_PATH !== (int) $rule['type'] && empty( $rule['name'] ) )
continue;
$params = array(
'ruleset_id'=> $ruleset_id,
'operator' => $rule['operator'],
'type' => $rule['type'],
'parent' => $rule['parent'],
'name' => $rule['name'],
'value' => $rule['value'],
'order' => $rule_count
);
if ( $rule_count >= $num_rules || !array_key_exists( $rule_count, $ruleset['rule'] ) ) {
$wpdb->insert( 'wp_rules', $params );
} else {
$wpdb->update( 'wp_rules', $params, array( 'id' => $rules[$rule_count]->id ) );
unset( $rules[$rule_count] );
}
$rule_count++;
}
if ( !empty( $rules ) ) {
$ids = join( ',', wp_list_pluck( $rules, 'id' ) );
$wpdb->query( "DELETE FROM wp_rules WHERE ruleset_id IN (" . $ids . ")" );
}
$ruleset_count++;
}
if ( !empty( $rows ) ) {
$ids = join( ',', wp_list_pluck( $rows, 'id' ) );
$wpdb->query( "DELETE FROM wp_rules WHERE ruleset_id IN (" . $ids . ")" );
$wpdb->query( "DELETE FROM wp_rulesets WHERE id IN (" . $ids . ")" );
}
wp_cache_flush_group( 'post-type-ids' );
}
}
}
function do_operator( $operator, $value1, $value2 ) {
$matches = false;
switch ( $operator ) {
case self::OPERATOR_EQUALS:
$matches = $value1 == $value2;
break;
case self::OPERATOR_NOT_EQUALS:
$matches = $value1 != $value2;
break;
case self::OPERATOR_GTE:
$matches = $value1 >= $value2;
break;
case self::OPERATOR_LTE:
$matches = $value1 <= $value2;
break;
case self::OPERATOR_LESS_THAN:
$matches = $value1 < $value2;
break;
case self::OPERATOR_GREATER_THAN:
$matches = $value1 > $value2;
break;
}
return $matches;
}
function parse_query_string( $rule ) {
$qs = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY );
parse_str( $qs, $pieces );
if ( !empty( $pieces ) )
return $this->do_operator( $rule->operator, $pieces[$rule->name], $rule->value );
}
function parse_url_path( $rule ) {
$path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
return strstr( $path, $rule->value );
}
function parse_cookie( $rule ) {
if ( empty( $rule->parent ) ) {
return $this->do_operator( $rule->operator, $_COOKIE[$rule->name], $rule->value );
} else {
parse_str( $_COOKIE[$rule->parent], $pieces );
return $this->do_operator( $rule->operator, $pieces[$rule->name], $rule->value );
}
}
function parse_rules( $post_id ) {
$rulesets = $this->get_rulesets( $post_id );
if ( empty( $rulesets ) )
return;
$matched = false;
foreach ( $rulesets as $index => $ruleset ) {
$rules = $this->get_rules( $ruleset->id );
if ( empty( $rules ) )
continue;
$truthy = 0;
foreach ( $rules as $rule ) {
switch ( $rule->type ) {
case self::TYPE_QUERY_STRING:
if ( $this->parse_query_string( $rule ) )
$truthy++;
break;
case self::TYPE_URL_PATH:
if ( $this->parse_url_path( $rule ) )
$truthy++;
break;
case self::TYPE_COOKIE:
if ( $this->parse_cookie( $rule ) )
$truthy++;
break;
}
}
$matched = $truthy === count( $rules );
if ( isset( $rulesets[$index + 1] ) ) {
switch ( $rulesets[$index + 1]->condition ) {
case self::CONDITION_AND:
if ( !$matched )
return $matched;
break;
case self::CONDITION_OR:
if ( $matched )
return $matched;
break;
}
}
}
return $matched;
}
function pick_creative( $post_id ) {
$choices = get_images( $post_id );
if ( empty( $choices ) )
return;
$ad = get_post( $post_id );
if ( !empty( $ad->menu_order ) ) {
$bandit = new MultiArmedBandit( $post_id, $choices );
switch ( $ad->menu_order ) {
case self::STRATEGY_AB:
$winner = $bandit->epsilon_ab();
break;
case self::STRATEGY_EPSILON_GREEDY:
$winner = $bandit->epsilon_greedy();
break;
case self::STRATEGY_EPSILON_FIRST:
$winner = $bandit->epsilon_first();
break;
case self::STRATEGY_EPSILON_DECR:
$winner = $bandit->epsilon_decreasing();
break;
}
} else {
$winner = $choices[0]->ID;
}
return $winner;
}
function get_rulesets( $post_id ) {
global $wpdb;
return $wpdb->get_results( "SELECT r.* FROM wp_rulesets r WHERE r.object_id = {$post_id} ORDER BY r.order ASC" );
}
function get_rules( $ruleset_id ) {
global $wpdb;
return $wpdb->get_results( "SELECT r.* FROM wp_rules r WHERE r.ruleset_id = {$ruleset_id} ORDER BY r.order ASC" );
}
}
function house_ad_parse_rules( $post_id ) {
return get_emusic()->house_ad->parse_rules( $post_id );
}
function house_ad_pick_creative( $post_id ) {
return get_emusic()->house_ad->pick_creative( $post_id );
}
get_emusic()->house_ad = HouseAd::get_instance();
<?php
add_action( 'wp_ajax_nopriv_bandit-add-reward', array( 'MultiArmedBandit', 'add_reward' ) );
add_action( 'wp_ajax_bandit-add-reward', array( 'MultiArmedBandit', 'add_reward' ) );
class MultiArmedBandit {
const KEY_REWARDS = 'r';
const KEY_PULLS = 'p';
const KEY_EXPECTATION = 'x';
const cache_group = 'bandit';
var $cache_key;
var $trial_key;
var $choices;
var $history;
var $trials;
var $trial_limit;
function __construct( $id, $choices, $trial_limit = 0 ) {
$this->cache_key = $this->create_key( $id );
$this->choices = $choices;
$this->retrieve_history();
if ( !empty( $trial_limit ) ) {
$this->trial_limit = $trial_limit;
$this->trial_key = $this->create_trial_key( $id );
$this->increment_trials();
}
}
function create_key( $id ) {
return 'history-' . $id;
}
function create_trial_key( $id ) {
return 'trials-' . $id;
}
function retrieve_history() {
$base_value = array(
self::KEY_REWARDS => 1,
self::KEY_PULLS => 1,
self::KEY_EXPECTATION => 1
);
$this->history = wp_cache_get( $this->cache_key, self::cache_group );
if ( empty( $this->history ) ) {
$this->history = array();
foreach ( $this->choices as $choice )
$this->history[$choice] = $base_value;
} else {
foreach ( $this->choices as $choice )
if ( !isset( $this->history[$choice] ) )
$this->history[$choice] = $base_value;
}
}
function save_history() {
wp_cache_set( $this->cache_key, $this->history, self::cache_group );
}
function increment_trials() {
$this->trials = wp_cache_get( $this->trial_key, self::cache_group );
if ( empty( $this->trials ) )
$this->trials = 0;
$this->trials++;
wp_cache_set( $this->trial_key, $this->trials, self::cache_group );
}
/**
* Implement Epsilon-greedy
*
*/
/**
* Reward is added via AJAX call fired by click
* User does NOT need to be logged in.
*
*/
function add_reward() {
extract( $_REQUEST );
if ( empty( $id ) || empty( $choice ) )
exit( 'Badly-formed request.' );
$key = $this->create_key( $id );
$history = wp_cache_get( $key, self::cache_group );
$lever =& $history[$choice];
self::increment_lever( $lever );
wp_cache_set( $key, $history, self::cache_group );
exit( 1 );
}
/**
* This is the equivalent of an impression
*
*/
function increment_lever( &$lever ) {
$lever[self::KEY_PULLS] += 1;
$lever[self::KEY_EXPECTATION] = $lever[self::KEY_REWARDS] / $lever[self::KEY_PULLS];
}
/**
* Implements the Mersenne Twister random number generator
*
*/
function mt_random() {
$min = 0;
$max = 1;
return ( $min + mt_rand() / mt_getrandmax() * ( $max - $min ) );
}
function pick_random() {
/**
* Copy so we don't alter with by-reference functions
*
*/
$choices = $this->choices;
/**
* Shuffle the array() to offset PHP's supposed bias
*
*/
shuffle( $choices );
/**
* Get a random element from the array
*
*/
$winner = array_rand( $choices );
$this->increment_lever( $this->history[$winner] );
$this->save_history();
return $winner;
}
function pick_winner() {
$winner = $greatest_expectation = 0;
foreach ( $this->choices as $choice ) {
if ( empty( $winner ) || $choice[self::KEY_EXPECTATION] > $greatest_expectation ) {
$greatest_expectation = $choice[self::KEY_EXPECTATION];
$winner = $choice;
}
}
return $winner;
}
function epsilon_ab() {
if ( $this->trials < $this->trial_limit )
$winner = $this->pick_random();
return $this->pick_winner();
}
function epsilon_greedy() {
if ( $this->mt_random() < 0.1 )
return $this->pick_random();
return $this->pick_winner();
}
function epsilon_first() {
// TODO: implement this
return $this->pick_winner();
}
function epsilon_decreasing() {
// TODO: implement this
return $this->pick_winner();
}
}
<?php
function house_ad( $target = '' ) {
if ( empty( $target ) )
return;
$ids = get_post_type_ids( get_emusic()->house_ad->post_type, $target, 'house_ad_target' );
if ( empty( $ids ) )
return;
foreach ( $ids as $id ) {
if ( house_ad_parse_rules( $id ) ) {
$data = array();
$ad = get_post( $id );
$map = $ad->post_content_filtered;
$data['omniture_code'] = $ad->post_excerpt;
$image_id = house_ad_pick_creative( $id );
?>
<a class="house-ad" data-code="<?php echo esc_attr( $ad->post_excerpt ) ?>" href="<?php echo $ad->guid ?>"><?php
if ( !empty( $image_id ) ) {
$css_id = 'image-' . $image_id;
list( $src, $width, $height ) = wp_get_attachment_image_src( $image_id, 'full' );
printf( '<img src="%s" %s/>', $src, empty( $map ) ? '' : 'usemap="#' . $css_id . '"' );
if ( !empty( $map ) )
printf( '<map name="%1$s" id="%1$s">%2$s</map>', $css_id, $map );
}
?></a>
<?php
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment