Home > Tracking Campaigns > FunnelFlux

Auto-Optimization Based on Offers' EPV (7)


04-25-2018 07:50 PM #1 vitavee ()
Auto-Optimization Based on Offers' EPV

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:

Code:
<?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 );

?>
By default, it checks the stats of the past 24 hours. You can change this range by adding a custom token called epv-auto-routing-lasthours in the funnels where you place this PHP node:



Notes:


04-25-2018 08:46 PM #2 bbrock32 (Administrator)

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.


04-26-2018 10:15 AM #3 mindfume (AMC Alumnus)

awesome - thanks!


04-26-2018 10:26 AM #4 mihalis09 (Member)

You Sir are quick, looking forward to put this into practice. Sending you good energy to Keep Making Badass Developmental Progress!


04-26-2018 11:25 AM #5 zeno (Administrator)

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:



Pretty cool stuff!


04-27-2018 01:37 PM #6 vitavee ()

Quote Originally Posted by zeno View Post
Vitavee you're always full of surprises! How long did this take, like 2 days?
2 days? Nah... was closer to 1 hour.

About the 4 points you mentioned, all good except the last one. I mistakenly told you that we could do this, but we can't. It's only granular to the node+traffic source level.

To optimize based on other data, we have to change the call to the reporting API to add those new groupings.

This part from the JSON payload:

Code:
      'groupings' => [
        [ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ]
      ]
would need to be changed to add other groupings one want to optimize for. Like this for example:

Code:
      'groupings' => [
        [ 'groupBy' => 'Element: Node ID', 'whitelistFilters' => $nodeIds ],
        [ 'groupBy' => 'Location: Country Code', 'whitelistFilters' => [ '{location-countrycode}' ] ]
      ]
And the cache key should be updated accordingly, as you mentioned already.

However, note that the call to the reporting API in my initial code above takes advantage of cached reports, so it's always fast. If some other groupings are added, it may have to load the reports from raw data, which would be slower. Best to check within the FunnelFlux drilldown reports page if the specific grouping combination one wants to optimize for is going to be pulled from raw data or not before changing the JSON payload.


04-28-2018 06:20 AM #7 zeno (Administrator)

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.


Home > Tracking Campaigns > FunnelFlux