Monday, March 18, 2013

Dynamically Adjust Campaign Budgets

UPDATE 2013-04-05: There is an updated version of this script. Check it out. Dynamically Adjust Campaign Budgets v2.0.

UPDATE 2013-04-07: A big thank you to FoxSUP for helping me track down an issue with updating the budgets. Fixed line 78 to multiply the current budget by 1+to_change instead of just to_change. Also fixed a bug in calculating the change (line 76).

Here is a request from a reader:
I manage many small business PPC accounts, and some of these accounts have multiple campaigns, and they usually have a relatively small monthly click budget. I'm looking for a way to pause ALL campaigns if the entire account has spent over a certain amount month-to-date.

This is actually a pretty easy script to put together. Below is a script that will do just that. You can set the MONTHLY_BUDGET at the beginning of the script and run this script daily on your account. Once the campaigns have a total cost greater than the budget specified, it will pause all the campaigns in the account.

Then, on the first of the next month, it will enable those campaigns once again. If you make no changes to the script below, it should do just that. But let's go one step further.

You actually have the power to get and set your campaign budgets using scripts. So let's say your monthly budget is $100, but you want to make sure your ads are spaced out through the month. I have added a function below called _adjust_campaign_budget() which can be enabled through the flag ADJUST_BUDGETS at the top of the script.

The script will then attempt to calculate a run rate for your campaign to figure out if you are going to meet your budget or not. If you are going to go over, it will lower the budget of each campaign (weighted by campaign cost) so that you come in at your target. If you are going to under-spend, it will also attempt to increase your campaign budget to try to allow you to hit your goal. I have also added a _reset_budgets() function to the end that gets called on the first of the month. If you run this script more frequently, you should enable the code for checking if it is the first hour of the first day of the month.

Now understandably, this script comes with a few cautions. THIS SCRIPT MAY CAUSE YOU TO SPEND A LOT OF MONEY. I'm sure the campaigns I was testing this on were quite a bit smaller than your campaigns, with budgets to match.

Thanks,
Russ

/******************************************
* Keep Your Campaigns In Budget
* Version 1.1
* ChangeLog v1.1 
*   - cleaned up code
*   - added ability for any dates
* Created By: Russ Savage
* FreeAdWordsScripts.com
******************************************/
// Let's set some constants
var MONTHLY_BUDGET = 5000.00;

//If you want to work with a monthly budget, leave START_DATE and END_DATE blank.
var TIMEFRAME = "THIS_MONTH";
//But if you want to work with a specific timeframe, fill these in.
//Use the format yyyyMMdd, so for Jan 12th, 2014, you would put 20140112.
var START_DATE = '';
var END_DATE = '';
 
//Set this to true if you want to adjust budgets or
//keep set to false if you want to just pause all the campaigns
//when you hit your budget
var ADJUST_BUDGETS = false;
var DECIMAL_PLACES = 3;
 
function main() {
  var totalCostMTD = getTotalCost();
  var isFirstOfTheMonth = ((new Date()).getDate() == 1);
  if(START_DATE && END_DATE) {
    var today = new Date();
    today.setHours(0,0,0,0);
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,6))-1,
                             START_DATE.substring(6,8));
    isFirstOfTheMonth = (startDate.getTime() == today.getTime());
  }
  //if you run this script more than once per day, uncomment the next line
  //isFirstOfTheMonth = (isFirstOfTheMonth && ((new Date()).getHour() == 0));
  Logger.log("Total cost: " + totalCostMTD + 
           ", Monthly budget:" + MONTHLY_BUDGET +
           ", isFirstOfTheMonth: "+isFirstOfTheMonth);
   
  if(ADJUST_BUDGETS) {
    if(isFirstOfTheMonth) {
      resetBudgets();
    } else {
      adjustCampaignBudget(totalCostMTD);
    }
  } else {
    if(totalCostMTD >= MONTHLY_BUDGET) {
      //If we have hit the limit, pause all ads
      enableOrDisableCampaigns(true);
    } else {
      // let's check if it's the first day of the month
      if((new Date()).getDate() == 1) {
        //enable all the campaigns
        enableOrDisableCampaigns(false);
      }
    }
  }
}
 
// Returns the total cost for the set TIMEFRAME
function getTotalCost() {
  var campIter = AdWordsApp.campaigns().get();
   
  var totalCost = 0;
  while(campIter.hasNext()) {
    if(START_DATE && END_DATE) {
      totalCost += campIter.next().getStatsFor(START_DATE,END_DATE).getCost();
    } else {
      totalCost += campIter.next().getStatsFor(TIMEFRAME).getCost();
    }
  }
  return totalCost;
}
 
// Enables or Disables All Campaigns In Account
function enableOrDisableCampaigns(shouldDisable) {
  var campIter = AdWordsApp.campaigns().get();
  while(campIter.hasNext()) { 
    if(shouldDisable) { 
      campIter.next().pause(); 
    } else { 
      campIter.next().enable(); 
    }
  }
}
 
// Calculates run rate and adjusts campaign bids as needed.
function adjustCampaignBudget(myTotalCost) {
  var today = new Date();
  // Accounting for December
  var eom;
  if(START_DATE && END_DATE) {
    eom = new Date(END_DATE.substring(0,4),
                   parseFloat(END_DATE.substring(4,2))-1,
                   END_DATE.substring(6,2));
  } else {
    eom = (today.getMonth() == 11) ? new Date(today.getFullYear()+1,0,1) : 
                                     new Date(today.getFullYear(),today.getMonth()+1,1);
  }
  var daysLeft = Math.round((eom-today)/1000/60/60/24);
  var daysSpent;
  if(START_DATE && END_DATE) {
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,2))-1,
                             START_DATE.substring(6,2));
    daysSpent = Math.round((today-startDate)/1000/60/60/24);
  } else {
    daysSpent = today.getDate();
  }
  var runRate = round(myTotalCost/daysSpent);
  var projectedTotal = myTotalCost + (runRate * daysLeft);
  var percOver = round((MONTHLY_BUDGET-projectedTotal)/projectedTotal);
   
  changeSpend(percOver,myTotalCost);
}
 
//Adjusts the budget for a given campaign based on percentage of total spend
//Note: if the cost of a campaign is $0 mtd, the budget is not changed.
function changeSpend(percToChange,myTotalCost) {
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
   
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var campCost = (START_DATE && END_DATE) ? camp.getStatsFor(START_DATE,END_DATE).getCost()
                                            : camp.getStatsFor(TIMEFRAME).getCost();
    var percOfTotal = round(campCost/myTotalCost);
    //If there is no cost for the campaign, let's not change it.
    var toChange = (percOfTotal) ? (percOfTotal*percToChange) : 0;
    camp.setBudget(camp.getBudget()*(1+toChange));
  }
}
 
// Resets the budget evenly across all campaigns
function resetBudgets() {
  Logger.log('Resetting budgets at the first of the period.');
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  var campCount = 0;
  while(campIter.hasNext()) {
    campCount++;
    campIter.next();
  }
  campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  while(campIter.hasNext()) {
    campIter.next().setBudget(MONTHLY_BUDGET/campCount);
  }
}

// A helper function to make rounding a little easier
function round(value) {
  var decimals = Math.pow(10,DECIMAL_PLACES);
  return Math.round(value*decimals)/decimals;
}

15 comments:

  1. Excellent script... I was about half way done with something very similar, and this post helped to resolve few obstacles...Thanks!

    ReplyDelete
  2. Thanks again for the script. In regards to the campaigns reactivating on the first of the month, would this script enable every campaign that is currently in a "paused" state in the account, whether or not a campaign was enabled the prior month?

    If so, which lines of the code should be removed? Personally, I'm fine with going back into an acct on the first of the month and manually reactivating specific campaigns.

    Thanks!

    ReplyDelete
    Replies
    1. Hi Mark, You are correct, this script will enable all paused campaigns in the account. Probably the easiest way to only run this script on certain campaigns would be to use labels. So let's assume you create a label called 'script' and apply it to the campaigns you want to manage the budget for.

      Then, anywhere in the script where you see a camp_iter (lines 45, 56, 84, and 98), add the following .withCondition("LabelNames CONTAINS_ANY ['script']") just before the .get();

      var camp_iter = AdWordsApp.campaigns()
      .withCondition("Status = ENABLED")
      .withCondition("LabelNames CONTAINS_ANY ['script']")
      .get();

      That way the script will only operate on certain campaigns.

      Thanks,
      Russ

      Delete
  3. I love this script, I set it up with the labels to control individual campaign budgets and its working great.

    I have also tried it with all campaigns, but I like to spend different amounts on different campaigns and it does not keep those values. Any ideas on how best to achieve this?

    Thanks for your time and effort posting useful AdWords Scripts.

    ReplyDelete
    Replies
    1. Hi FoxSUP, Yeah, right now, the script just simply divides your monthly budget equally across all your campaigns (line 106).

      There are a few different ways you could solve the budget thing. I think the easiest way might be to store the campaign information in an external spreadsheet and read those values at the start of each month. That way, you could adjust budgets without changing the script.

      The change to this was pretty substantial so I put it in a separate post: Dynamically Adjust Campaign Budgets v2.0.

      Thanks,
      Russ

      Delete
    2. This comment has been removed by a blog administrator.

      Delete
    3. Hi FoxSUP, I made a small change to multiple the budgets by 30.5 to handle the daily to monthly issue.

      Thanks,
      Russ

      Delete
  4. For anyone adjusting budgets using this script, I fixed a bug that should adjust the campaign budgets correctly. A big thanks to FoxSUP for helping me track down some issues.

    Thanks,
    Russ

    ReplyDelete
  5. Hi Russel,

    I enjoyed all the scripts on your blog and i found them very help full for my MCC and i have to thank you for this.

    I have a question witch i think it apply to this script: It's there a way to see in a spreadsheet the total budget and the cost of all the campaigns in an account?
    The goal is to correlate the dates between total cost and the budget for last 30 days or last 7 days (weekly).

    Thanks.

    ReplyDelete
    Replies
    1. Hi Dany, I think you might be better off using this script to store that data independently: Store Account Performance Report in a Google Doc. Let me know if that solves your problem.

      Thanks,
      Russ

      Delete
    2. Hi Russel,

      I looked up for that script but it seems i don't have the possibility to extract the budget from the ACCOUNT_PERFORMANCE_REPORT when i put 'Budget' in the var columns.

      "Column 'Budget' is not valid for report type ACCOUNT_PERFORMANCE_REPORT. Double-check your SELECT clause. (line 33) "

      My goal is to see if the Cost is reaching he's Budget.

      Thanks.

      Delete
    3. Hi Dany, You're correct, for some reason, that report doesn't work. The one you want is BUDGET_PERFORMANCE_REPORT. I tried to make the other script generic enough to use with most of the reports available from AdWords. To update the other script, all you need to do it replace ACCOUNT_PERFORMANCE_REPORT with BUDGET_PERFORMANCE_REPORT, update the column list using the values here: BUDGET_PERFORMANCE_REPORT columns, and you should be good to go.

      Let me know if that works for you.
      Thanks,
      Russ

      Delete
    4. Hi Russel,

      Thanks man, the BUDGET_PERFORMANCE_REPORT works ok, and it is that one was i looking for but the thing is (problem) it imports in docs all the campaigns even if it is deleted or paused.
      Any tips to import only the enable campaings?

      My script looks like this:

      //-----------------------------------
      // Store Budget Performance Report In A Google Doc
      // Created By: Russ Savage
      // FreeAdWordsScripts.com
      //-----------------------------------
      function main() {
      var spreadsheet_url = "spreadsheet_url";
      var date_range = 'LAST_WEEK';
      var columns = ['AssociatedCampaignName',
      'AssociatedCampaignStatus',
      'Amount',
      'Cost'];
      var columns_str = columns.join(',') + " ";

      var sheet = getSpreadsheet(spreadsheet_url).getActiveSheet();
      if(sheet.getRange('A1:A1').getValues()[0][0] == "") {
      sheet.clear();
      sheet.appendRow(columns);
      }

      var report_iter = AdWordsApp.report(
      'SELECT ' + columns_str +
      'FROM BUDGET_PERFORMANCE_REPORT ' +
      'DURING ' +date_range, {
      apiVersion: 'v201302'
      }).rows();

      while(report_iter.hasNext()) {
      var row = report_iter.next();
      var row_array = [];
      for(var i in columns) {
      row_array.push(row[columns[i]]);
      }
      sheet.appendRow(row_array);
      }
      }

      function getSpreadsheet(spreadsheetUrl) {
      var matches = new RegExp('key=([^&#]*)').exec(spreadsheetUrl);
      if (!matches || !matches[1]) {
      throw 'Invalid spreadsheet URL: ' + spreadsheetUrl;
      }
      var spreadsheetId = matches[1];
      return SpreadsheetApp.openById(spreadsheetId);
      }

      Delete
    5. Hi Dany, In order to only get active campaigns, you should add a WHERE clause to your AWQL statement. So right before the DURING clause, add "WHERE AssociatedCampaignStatus = 'ACTIVE'".

      Thanks,
      Russ

      Delete
  6. Thanks a lot for useful information. This is exactly what I need!

    ReplyDelete