Have you ever wanted to have a smart rotator sending traffic to your best path?
That's exactly what we're going to do in this tutorial!
First, let's define what we call the best path for a funnel such as the one below:

The best path, is the one giving us the highest revenue per view. If on average you earn $0.37 for each view of offer 1, while you earn $0.45 for each view of offer 3, you will want to show offer 3 to your visitors more often than offer 1. Granted you have reached statistical significance.
You can see the earning per view (EPV) by enabling the following heatmap:

And you can see how confident you are that an offer will bring more revenue than others by enabling the EPv Winners calculator:

In this example, we are only 49% confident that offer 3 will be more profitable than the other offers.
What we will do is take this EPv winner rate to automatically split the traffic to these offers.
In the example above, since we are about 26% confident that offer 1 and offer 2 will outperform the others, and about 49% confident that offer 3 will be the best performer, we will route 26% of traffic to offer 1, 26% to offer 2 and the rest to offer 3.
The more confident FunnelFlux's Bayes calculator is that an offer is going to outperform others, the more percentage of traffic it will send toward it.
So here are the steps to put this in place.
First, you have to split the traffic via a PHP node instead of a regular rotator. Let's call this PHP Node "EPV Weight Auto-Routing"

Here is the code that you will use in that PHP node:
<?php
require_once __DIR__ . './../admin/api/v2/toolbox.php';
$cacheExpirationInMinutes = 5;
$checkLastHours = '[[epv-auto-routing-lasthours]]';
$checkLastHours = empty($checkLastHours) ? 24 : $checkLastHours;
$cacheKey = '{node-id}-{trafficsource-id}';
$cache = Cache::getInstance( Cache::TYPE_FILE );
$nodeWeights = $cache->get( $cacheKey );
if( $nodeWeights === Cache::NOT_FOUND )
{
$nodeWeights = [];
$nodeConnections = DBTableCampaignFunnelConnections::getNodeExitConnections( '{node-id}' );
if( !empty( $nodeConnections ) )
{
$nodeIds = [];
foreach( $nodeConnections as $connection )
{
$targetNodeId = $connection->getTargetNodeId();
$nodeIds[] = $targetNodeId;
$nodeWeights[ $targetNodeId ] = -1;
}
$to = time();
$from = $to - ($checkLastHours * 60 * 60);
$to = (new DateTime())->setTimestamp( $to );
$from = (new DateTime())->setTimestamp( $from );
$drilldownRequest = [
'timeRange' => [
'start' => [
'date' => [ 'year' => $from->format('Y'), 'month' => $from->format('m'), 'day' => $from->format('d') ],
'time' => [ 'hour' => $from->format('H'), 'minutes' => $from->format('i') ]
],
'end' => [
'date' => [ 'year' => $to->format('Y'), 'month' => $to->format('m'), 'day' => $to->format('d') ],
'time' => [ 'hour' => $to->format('H'), 'minutes' => $to->format('i') ]
],
],
'timeZone' => [ 'name' => 'UTC' ],
'options' => [
'idFunnelFilter' => '{funnel-id}',
'idTrafficSourceFilter' => '{trafficsource-id}',
'computeEPVConfidenceRate' => true,
'confidenceRateIncludeAll' => true
],
'groupings' => [
[ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ]
]
];
$report = \FluxAPI\v2\Toolbox::statsDrilldownRequest( $drilldownRequest );
$nodesInReport = 0;
foreach( $report['rows'] as $row )
{
$nodeId = $row['cells'][0]['raw'];
$epvWinnerRate = $row['epvConfidenceRate']['rate'];
if( $epvWinnerRate != -1 )
{
$nodeWeights[ $nodeId ] = $epvWinnerRate;
$nodesInReport++;
}
}
// If some nodes have no traffic yet, we need to re-scale the weights of the nodes that did get traffic
if( $nodesInReport < count($nodeIds) )
{
$ratio = $nodesInReport / count($nodeIds);
$assignedWeight = 0;
foreach( $nodeWeights as $k => $w )
{
if( $nodeWeights[ $k ] != -1 )
{
$nodeWeights[ $k ] = $w * $ratio;
$assignedWeight += $nodeWeights[ $k ];
}
}
$missingWeight = 1.0 - $assignedWeight;
$missingNodesCount = count($nodeIds) - $nodesInReport;
if( $missingNodesCount > 0 )
{
$missingWeightPerNode = $missingWeight / $missingNodesCount;
foreach( $nodeIds as $nodeId )
{
if( $nodeWeights[ $nodeId ] == -1 )
{
$nodeWeights[ $nodeId ] = $missingWeightPerNode;
}
}
}
}
$nodeWeights = array_values( $nodeWeights );
}
$cache->set( $cacheKey, $nodeWeights, $cacheExpirationInMinutes * 60 );
}
$choice = mt_rand() / mt_getrandmax();
if( empty($nodeWeights) )
{
$iRoute = -1;
}
else
{
foreach( $nodeWeights as $iRoute => $weight )
{
if( $choice < $weight )
{
break;
}
$choice -= $weight;
}
}
return( $iRoute + 1 );
?>

That's pretty neat!
It could have saved me a lot of money when I wasn't around for the weekend and an offers suddenly stopped converting.
awesome - thanks!
You Sir are quick, looking forward to put this into practice. Sending you good energy to Keep Making Badass Developmental Progress!
Isn't it amazing how quickly you can make stuff like this happen with a small, dedicated team lead by an absolute genius developer?
Vitavee you're always full of surprises! How long did this take, like 2 days? My god.
Also, worth noting that there are some cool flexibilities here:
'groupings' => [
[ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ]
]
'groupings' => [
[ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ],
[ 'groupBy' => 'Location: Country Code', 'whitelistFilters' => [ '{location-countrycode}' ] ]
]
Ahh, right. Have updated our documentation to suit. That's still pretty easy to do since its just a basic reporting API call in the code and subsequent change to cacheKey.