It’s been a while since I posted an AdWords script here, so this month I’ll share an AdWords automation that manages your budgets.
You can skip right to the end to copy and paste the script and run it in your account; or, for those who are interested in learning some AdWords scripting of their own, I’ll explain why and how I took an existing script from Google and adapted it to do something better.
Why we need a script to manage monthly budgets
One of the quirks of AdWords is that budgets are daily, rather than monthly. I hadn’t yet joined Google when they decided to use daily budgets, so I don’t know exactly how that decision came to be, but I was there for plenty of product meetings that dealt with budgeting issues.
One of the most fruitless projects I recall was one where we tried to give advertisers monthly budgets while still using the underlying daily budgeting method the system was built for.
AdWords uses daily budgets, but many advertisers prefer working with monthly budgets; so we needed an automation to help us manage recurring monthly budgets and make sure the entire budget was spent every month. Image licensed from Fotolia.
Despite months of meetings, the project eventually went nowhere because we could never agree on a reasonable way to translate daily budgets to monthly ones without confusing a large group of advertisers.
The problem was simple: When we asked an advertiser for a monthly budget, some of them would give us a number that was going to be the same every month (a true monthly budget), whereas others who were already used to working with daily budgets would tell us a monthly budget based on multiplying a daily budget by the number of days in the month.
The latter group’s budgets would change monthly depending on the number of days in the month. That may not seem particularly confusing to readers of this site, but believe me, when you have over a million advertisers, even small things can cause a huge amount of confusion. We ended up deciding to avoid confusion and left budgets as daily.
To have a monthly budget in AdWords, you could simply divide your month’s budget by the number of days for the month, but that risks underspending — so an automation that evaluates how much budget is left and how many days are left and then sets a new daily budget is the surest way to get close to spending the full budget every month.
Day-of-week fluctuations impact how well you spend a budget
The reality for many SEM managers, agencies and consultants is that their clients or managers set budgets that are annual, quarterly or monthly.
The math to turn these into daily budgets is easy, but setting the correct budget so that the full amount is spent is a bit harder because the different days of the week can have dramatically different potential.
Let’s take a simple example. Say you have $300 to spend during the 30 days of April, which translates to $10 per day. If you set a budget of $10 every day, you may only deliver $4 on a typical weekend day, when fewer people do searches, and then, during the weekdays, your $10 daily budget could deliver up to $12 worth of clicks (thanks to overdelivery) but that would still leave you short of the target, even if you spent $12 a day for all five weekdays.
Knowing that weekend days garner a fraction of the searches that occur on weekdays, you could increase budgets on weekdays to make up for the shortfall on weekends. Google already has a script that updates budgets daily to help meet an overall budget goal for the entire month, and this might work well because day-of-week fluctuations smooth out over the course of several weeks. But we can do better with a customization of their AdWords Script.
Google’s flexible budget script
Google’s script provides two ways of calculating what the new daily budget should be: evenly distributed or backloaded.
In the even distribution method, it simply figures out how much budget remains for the month and how many days remain, then divides the former by the latter. For example, with $100 of budget remaining and 10 days left in the month, the new budget would be set to $100/10 = $10. It assumes all days are the same, so it spreads the budget evenly.
Even budget distribution:
In an even budget distribution, all days get the same budget. Here, with $100 left to spend in 10 days, every day gets a $10 budget.
In the backloaded method, a larger portion of budget is kept for the end of the month. If you know you’ll be running a big promotion toward the end of the budget period, this can be a good method to preserve budget for a time when you expect your ads to perform better in terms of key metrics like CPA or ROAS.
Backloaded budget distribution:
In the backloaded weighted budget allocation method, most of the budget is preserved for later in the budget period. However, if the last day is a day of the week that normally doesn’t see a lot of traffic, a lot of budget could remain unutilized.
The Google script takes a start and end date as inputs and doesn’t automatically know how to deal with full calendar months. The script doesn’t solve my monthly budget problem out of the box, but it provides a great starting point for some customizations which I’ll describe next.
My improved flexible budget script
I wanted my script to automatically start a new budget period on the first of every month. I also wanted it to roll over any unused budgets from the previous month, assuming that the campaign was active all of last month.
Finally, I wanted a script to distribute the budget in a smarter way, based on historical day of week patterns.
Day-of-week budget distribution:
In my version of the script, we added day-of-the week budget allocation that takes into account historical performance. In this example, Wednesday and Thursday are days with big potential, so they get higher budgets than the other days, and in the end, it will be more likely that the full $100 will be spent in the 10 remaining days.
Setting daily budgets that account for day-of-week fluctuations
Distributing the budget based on what days remain in the month makes sense because when there are only three days left in the budget period, it makes a big difference what those days are.
Here’s an example. We used our Optmyzr Hour-of-Week Data Insight tool to plot the cost for an account for the various weekdays.
Screen shot from Optmyzr’s Hour-Of-Week tool shows that more of this campaign’s budget is spent on Wednesday than on Sunday. This is important to know when we update budgets daily to help us meet a target monthly spend.
You can see Wednesday tends to have a higher cost than Friday, Saturday and Sunday. So it makes a big difference if the three remaining days in the budget period are Friday, Saturday and Sunday, or if they are Wednesday, Thursday and Friday. Since Friday, Saturday and Sunday usually spend around the same amount, the budget can be distributed evenly, and you will most likely hit your target.
However, an even distribution applied in the second example could reserve too much budget for Thursday and Friday, when there isn’t typically that much opportunity. Most of the budget should be spent on Wednesday.
This is where a day-of-week distribution comes in handy. It knows the typical percentage of the weekly budget that is used for every day of the week and can distribute the remaining budget accordingly.
To make this work, I did two things:
- I added a function that calculates the historical day-of-week distributions.
- I added a new budget distribution function that Google’s script can use.
Calculating the portion of budget spent on different days
For the first part I wrote a function called calculateDowFluctuations() that uses the reporting capabilities in AdWords Scripts to pull data for several weeks and builds a campaign cost map segmented by day of the week.
That way the code can easily look up how much each campaign spends for any day of the week. I also store the weekly total for each campaign so that I can easily determine what percentage of the week’s cost that represents.
The script produces something like this in the script logs:
CAMPAIGN: Test Campaign
---------------------------------------
- Monday cost is: 83.31 (15% of the weekly total)
- Tuesday cost is: 86.53 (15% of the weekly total)
- Wednesday cost is: 82.59 (15% of the weekly total)
- Thursday cost is: 84.31 (15% of the weekly total)
- Friday cost is: 83.18 (15% of the weekly total)
- Saturday cost is: 73.19 (13% of the weekly total)
- Sunday cost is: 71.35 (13% of the weekly total)
TOTAL COST: 564.46
This particular campaign spreads its budget pretty evenly over a typical week, but Saturday and Sunday are slightly lower, at 13 percent, than the 15 percent on the other days.
Writing a new budget distribution function for Google’s script
The second big change I made was to add a function calculateDowWeightedBudget which uses the data I calculated previously to set the new budget based on which days remain in the month.
Google wrote their code nicely generically so that adding a new function to calculate budgets a different way is very easy. In fact, it was so easy I also wrote a function that front-loads the budget. It’s basically the opposite of the back-loaded function they already included in their example.
Final tweaks to the script
I also made some tweaks to Google’s code to allow unused budgets to be rolled over. One trick I used here was to say that we should only roll over budgets if the campaign appeared to have been active the entire previous month, and the way I check this is to see if there were any impressions in the first few days of that month.
If there weren’t any impressions, then the campaign may be new, and rolling over the budget wouldn’t necessarily make sense. Users can specify how many days at the start of the last month should be considered to see if the campaign was active. Note to Google: If we had programmatic access to change history, we could do this check a little bit cleaner and more reliably.
Finally, I added some code that sets the budget periods to monthly by default, so that there’s no need to revisit settings every time a new month starts. The script just continues to run with the same monthly budget for every new period until a change is made.
You can enter your own settings for campaign name, budget and so on, on lines 44–48.
// Copyright 2015, Google Inc. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
/** | |
* @name Flexible Budgets | |
* | |
* @overview The Flexible budgets script dynamically adjusts campaign budget for | |
* an advertiser account with a custom budget distribution scheme on a daily | |
* basis. See | |
* https://developers.google.com/adwords/scripts/docs/solutions/flexible-budgets | |
* for more details. | |
* | |
* @author AdWords Scripts Team [adwords-scripts@googlegroups.com] | |
* | |
* @version 1.0 | |
* | |
* @changelog | |
* - version 1.0 | |
* - Released initial version. | |
* | |
* Copyright 2016, Optmyzr Inc. All Rights Reserved. | |
* @version 2.0 - Changes by Optmyzr.com | |
* | |
* @changelog | |
* - version 2.0 | |
* - calculate budgets by day of week history | |
* - calculate front-loaded budgets | |
* - automatically recurring monthly budgets | |
* - roll-over unused budget from last month | |
* | |
*/ | |
var currentSetting = new Object(); | |
currentSetting.totalBudget = 500; // Enter the target monthly budget here, e.g. 500 | |
currentSetting.rollOverBudgets = 1; // set to 1 to roll over budget that was not used last month; set to 0 to NOT roll over budgets | |
currentSetting.changeBudgets = 0; // set to 1 to update budgets; set to 0 to leave budgets unchanged (useful while testing) | |
currentSetting.budgetAllocationMode = "calculateDowWeightedBudget"; // budget allocation mode - see Google documentation | |
currentSetting.campaignNameIs = "AdWords Tools - Generic"; // name of the campaign for which this budget applies; leave blank for all campaigns | |
// Advanced Settings | |
currentSetting.budgetPeriod = "Monthly"; | |
currentSetting.numWeeksForAverages = 8; | |
currentSetting.numDaysToTestIfLastPeriodWasActive = 7; | |
currentSetting.verbose = 1; | |
currentSetting.debug = 0; | |
currentSetting.campaignLabel = ""; | |
if(typeof currentSetting.campaignNameIs == "undefined") currentSetting.campaignNameIs = ""; | |
if(typeof currentSetting.campaignLabel == "undefined") currentSetting.campaignLabel = ""; | |
VERBOSE = currentSetting.verbose; | |
DEBUG = currentSetting.debug; | |
currentSetting.changes = new Array(); | |
function main() { | |
var numCampaignsEvaluated = 0; | |
// Get campaigns with right name | |
var campaignNameCaseSensitive = 0; | |
var campaignNameExactMatch = 1; | |
var campaignNameSelectorString = getCampaignsByName(currentSetting.campaignNameIs, campaignNameCaseSensitive, campaignNameExactMatch); | |
switch(currentSetting.budgetPeriod) { | |
case "Weekly Sun-Sat": | |
//Logger.log("weekly sun"); | |
currentSetting.dateRange = "THIS_WEEK_SUN_TODAY"; | |
break; | |
case "Weekly Mon-Sun": | |
//Logger.log("weekly mon"); | |
currentSetting.dateRange = "THIS_WEEK_MON_TODAY"; | |
break; | |
case "Monthly": | |
if(VERBOSE) Logger.log("Using monthly budgets..."); | |
currentSetting.thisDateRange = "THIS_MONTH"; | |
currentSetting.previousDateRange = "LAST_MONTH"; | |
// first few days of last month -- used to check if campaign was active last month | |
var today = new Date(); | |
var activeTestStartDate = new Date(today.getFullYear(), today.getMonth()-1, 1); | |
var activeTestEndDate = new Date(today.getFullYear(), today.getMonth()-1, currentSetting.numDaysToTestIfLastPeriodWasActive); | |
currentSetting.dateRangeToTestIfCampaignWasActiveLastPeriod = dateToString(activeTestStartDate) + "," + dateToString(activeTestEndDate); | |
// | |
var reportDates = getReportDates("THIS_MONTH"); | |
var start = reportDates.startDate; | |
var end = reportDates.lastPossibleDate; | |
//Logger.log(start + " - " + end); | |
// | |
var reportDates = getReportDates("LAST_MONTH"); | |
var previousStart = reportDates.startDate; | |
var previousEnd = reportDates.endDate; | |
//Logger.log(previousStart + " - " + previousEnd); | |
break; | |
case "Custom": | |
var tempDate = new Date(); | |
var startDate = new Date(start); | |
var endDate = new Date(end); | |
var numDays = datediff(startDate, endDate); | |
Logger.log("numDays: " + numDays); | |
currentSetting.thisDateRange = dateToString(startDate) + "," + dateToString(endDate); | |
var previousStartDate = new Date(tempDate.setDate(startDate.getDate() - numDays)); | |
var previousEndDate = new Date(tempDate.setDate(endDate.getDate() - numDays)); | |
currentSetting.previousDateRange = dateToString(previousStartDate) + "," + dateToString(previousEndDate); | |
Logger.log(currentSetting.thisDateRange); | |
Logger.log(currentSetting.previousDateRange); | |
break; | |
} | |
if(currentSetting.budgetAllocationMode.toLowerCase().indexOf("calculatedowweightedbudget") != -1) { | |
calculateDowFluctuations(); | |
} | |
// SEARCH AND DISPLAY CAMPAIGNS | |
var iterator = AdWordsApp.campaigns() | |
.withCondition("Status = ENABLED") | |
.withCondition(campaignNameSelectorString.forScripts) | |
.get(); | |
if(VERBOSE) Logger.log(""); | |
if(VERBOSE) Logger.log("------------------------------------"); | |
if(VERBOSE) Logger.log("| Calculating New Budgets... |"); | |
if(VERBOSE) Logger.log("------------------------------------"); | |
if(VERBOSE) Logger.log(""); | |
while(iterator.hasNext()){ | |
numCampaignsEvaluated++; | |
var item = iterator.next(); | |
var name = item.getName(); | |
var id = item.getId(); | |
currentSetting.campaignId = id; | |
var currentPeriodCost = item.getStatsFor(currentSetting.thisDateRange).getCost(); | |
var lastPeriodCost = item.getStatsFor(currentSetting.previousDateRange).getCost(); | |
if(VERBOSE) Logger.log(""); | |
if(VERBOSE) Logger.log("CAMPAIGN: " + name); | |
if(VERBOSE) Logger.log("------------------------------------"); | |
if(currentSetting.rollOverBudgets) { | |
if(VERBOSE) Logger.log(" This campaign rolls over unused budgets from the previous cycle to the current cycle."); | |
var impressionsToCheckIfCampaignWasActive = item.getStatsFor(dateToString(activeTestStartDate), dateToString(activeTestEndDate)).getImpressions(); | |
if(impressionsToCheckIfCampaignWasActive) { | |
var rolledOverBudget = currentSetting.totalBudget - lastPeriodCost; | |
currentSetting.fullTotalBudget = currentSetting.totalBudget + rolledOverBudget; | |
if(VERBOSE) Logger.log(" Budget to roll over: " + rolledOverBudget); | |
if(VERBOSE) Logger.log(" Full total budget: " + currentSetting.fullTotalBudget); | |
} else { | |
if(VERBOSE) Logger.log(" Because this campaign did not appear to have been active at the start of the previous cycle, no budgets will be rolled over during this cycle."); | |
if(VERBOSE) Logger.log(" We saw no impressions for this campaign in the date range " + dateToString(activeTestStartDate) + " - " + dateToString(activeTestEndDate) + " and that is how we determined this campaign does not qualify to roll over a budget in this cycle."); | |
var rolledOverBudget = 0; | |
currentSetting.fullTotalBudget = currentSetting.totalBudget + rolledOverBudget; | |
} | |
} | |
if(currentSetting.budgetAllocationMode.toLowerCase().indexOf("dow") != -1) { | |
setNewBudget(calculateDowWeightedBudget, id, currentSetting.fullTotalBudget, start, end); | |
} else if(currentSetting.budgetAllocationMode.toLowerCase().indexOf("evenly") != -1) { | |
setNewBudget(calculateBudgetEvenly, id, currentSetting.fullTotalBudget, start, end); | |
} else if(currentSetting.budgetAllocationMode.toLowerCase().indexOf("backloaded") != -1) { | |
setNewBudget(calculateBackLoadedBudget, id, currentSetting.fullTotalBudget, start, end); | |
} else if(currentSetting.budgetAllocationMode.toLowerCase().indexOf("frontloaded") != -1) { | |
setNewBudget(calculateFrontLoadedBudget, id, currentSetting.fullTotalBudget, start, end); | |
} | |
} | |
var logNotes = ""; | |
var emailNotes = ""; | |
var numChanges = currentSetting.changes.length; | |
var logNotes = logNotes + numCampaignsEvaluated + " campaigns evaluated. " + numChanges + " with new budgets."; | |
var emailNotes = emailNotes + numCampaignsEvaluated + " campaigns evaluated. " + numChanges + " with new budgets:<br/><br/>"; | |
if(numChanges) var emailNotes = emailNotes + "<ul>"; | |
for(var changeCounter = 0; changeCounter < currentSetting.changes.length; changeCounter++){ | |
var change = currentSetting.changes[changeCounter]; | |
var campaignName = change.campaignName; | |
var oldBudget = change.oldBudget; | |
var newBudget = change.newBudget; | |
var emailNotes = emailNotes + "<li>Campaign: " + campaignName + " old budget: " + oldBudget + " new budget: " + newBudget + "</li>"; | |
} | |
if(numChanges) { | |
var emailNotes = emailNotes + "</ul>"; | |
sendEmailNotifications("frederick@optmyzr.com", "budgets", emailNotes, "notification" ); | |
} | |
} | |
function calculateDowFluctuations() { | |
if(VERBOSE) Logger.log("------------------------------------"); | |
if(VERBOSE) Logger.log("| DAY OF WEEK DATA |"); | |
if(VERBOSE) Logger.log("------------------------------------"); | |
if(VERBOSE) Logger.log("Here are the fluctuations in cost for each day of the week for each campaign."); | |
if(VERBOSE) Logger.log("This was calculated using cost data from the past " + currentSetting.numWeeksForAverages + " weeks."); | |
if(VERBOSE) Logger.log("This data is only used if you choose the budget method called 'allocate based on typical day of week performance'"); | |
var daysOfWeek = new Object(); | |
daysOfWeek.Monday = 1; | |
daysOfWeek.Tuesday = 2; | |
daysOfWeek.Wednesday = 3; | |
daysOfWeek.Thursday = 4; | |
daysOfWeek.Friday = 5; | |
daysOfWeek.Saturday = 6; | |
daysOfWeek.Sunday = 7; | |
var weekdayNames = new Array(); | |
weekdayNames[1] = "Monday"; | |
weekdayNames[2] = "Tuesday"; | |
weekdayNames[3] = "Wednesday"; | |
weekdayNames[4] = "Thursday"; | |
weekdayNames[5] = "Friday"; | |
weekdayNames[6] = "Saturday"; | |
weekdayNames[7] = "Sunday"; | |
var tempDate = new Date(); | |
var today = new Date(); | |
var numDays = currentSetting.numWeeksForAverages * 7; | |
//Logger.log("numDays: " + numDays); | |
var startDate = new Date(tempDate.setDate(today.getDate() - numDays)); | |
var tempDate = new Date(); | |
var endDate = new Date(tempDate.setDate(today.getDate() - 1)); | |
var dateRangeToUse = dateToString(startDate) + "," + dateToString(endDate); | |
//Logger.log(dateRangeToUse); | |
var report = AdWordsApp.report( | |
'SELECT CampaignId, CampaignName, Cost, DayOfWeek ' + | |
'FROM CAMPAIGN_PERFORMANCE_REPORT ' + | |
'WHERE CampaignStatus = ENABLED ' + | |
'DURING ' + dateRangeToUse + ' '); | |
currentSetting.campaignMap = new Array(); | |
var rows = report.rows(); | |
while (rows.hasNext()) { | |
var row = rows.next(); | |
var cost = parseFloat(row['Cost']); | |
var dow = row['DayOfWeek']; | |
var dowAsNumber = daysOfWeek[dow]; | |
var id = row['CampaignId']; | |
var campaignName = row['CampaignName']; | |
//Logger.log(dow + ": " + cost); | |
if(!currentSetting.campaignMap[id]) { | |
currentSetting.campaignMap[id] = new Object(); | |
currentSetting.campaignMap[id].name = campaignName; | |
currentSetting.campaignMap[id].averageDailyCost = 0; | |
currentSetting.campaignMap[id].averageWeeklyCost = 0; | |
currentSetting.campaignMap[id].dowCosts = new Array(); | |
} | |
currentSetting.campaignMap[id].dowCosts[dowAsNumber] = cost; | |
} | |
// iterate to get total cost and average daily | |
for(var campaignId in currentSetting.campaignMap) { | |
var campaign = currentSetting.campaignMap[campaignId]; | |
var campaignName = campaign.name; | |
//Logger.log(campaignName); | |
var totalCost = 0; | |
for(var dow in campaign.dowCosts) { | |
var cost = campaign.dowCosts[dow]; | |
totalCost += cost; | |
//Logger.log(dow + ": " + cost); | |
} | |
campaign.averageDailyCost = totalCost / 7; | |
campaign.averageWeeklyCost = totalCost; | |
//Logger.log(" avg: " + campaign.averageDailyCost); | |
} | |
// iterate to get percentage fluctuation | |
for(var campaignId in currentSetting.campaignMap) { | |
var campaign = currentSetting.campaignMap[campaignId]; | |
var campaignName = campaign.name; | |
var dailyCost = campaign.averageDailyCost; | |
var weeklyCost = campaign.averageWeeklyCost; | |
if(weeklyCost) { | |
if(VERBOSE) Logger.log(""); | |
if(VERBOSE) Logger.log("CAMPAIGN: " + campaignName + " (id: " + campaignId + ")"); | |
if(VERBOSE) Logger.log("------------------------------------------"); | |
for(var dow in campaign.dowCosts) { | |
var cost = campaign.dowCosts[dow]; | |
var percentOfNormal = cost / dailyCost; | |
var percentCostForWeek = cost / weeklyCost; | |
if(VERBOSE) Logger.log(" - " + weekdayNames[dow] + " cost is: " + cost + " (" + 100*percentCostForWeek.toFixed(2) + "% of the weekly total)"); | |
} | |
if(VERBOSE) Logger.log(" TOTAL COST: " + weeklyCost.toFixed(2)); | |
} | |
} | |
} | |
function setNewBudget(budgetFunction, campaignId, totalBudget, start, end) { | |
var thisChange = new Object(); | |
var today = new Date(); | |
if (today < start) { | |
if(VERBOSE) Logger.log("Today's date is before the start date of your budget so we are not setting a budget yet."); | |
return; | |
} | |
var campaign = AdWordsApp.campaigns(). | |
withCondition('CampaignId = "' + campaignId + '"'). | |
get(). | |
next(); | |
var campaignName = campaign.getName(); | |
var oldBudget = campaign.getBudget(); | |
var costSoFar = campaign.getStatsFor(dateToString(start), dateToString(end)). | |
getCost(); | |
var daysSoFar = datediff(start, today) - 1; | |
var totalDays = datediff(start, end) + 1; | |
//Logger.log(dateToString(start) + " - " + dateToString(end)); | |
if(VERBOSE) Logger.log(" Cost so far: " + costSoFar + " after " + daysSoFar + " days."); | |
if(VERBOSE) Logger.log(" Target cost: " + totalBudget + " in " + totalDays + " days."); | |
var newBudget = budgetFunction(costSoFar, totalBudget, daysSoFar, totalDays); | |
if(VERBOSE) Logger.log(" Old budget: " + oldBudget.toFixed(2)); | |
if(VERBOSE) Logger.log(" --> New budget: " + newBudget.toFixed(2)); | |
thisChange.campaignName = campaignName; | |
thisChange.oldBudget = oldBudget; | |
thisChange.newBudget = newBudget; | |
if(oldBudget != newBudget) { | |
currentSetting.changes.push(thisChange); | |
} | |
if(currentSetting.changeBudgets) { | |
campaign.setBudget(newBudget); | |
if(VERBOSE) Logger.log(" The new budget has been applied."); | |
} else { | |
if(VERBOSE) Logger.log(" No changes were made. Update the script settings if you want to apply new budgets."); | |
} | |
} | |
function calculateDowWeightedBudget(costSoFar, totalBudget, daysSoFar, totalDays) { | |
if(VERBOSE) Logger.log(" New budget is calculated by how much is typically spent on each day of the week. This helps prevent saving too much budget for a day of the week when you normally don't accrue a high cost."); | |
var daysRemaining = totalDays - daysSoFar; | |
//Logger.log("days remaining: " + daysRemaining); | |
var budgetRemaining = totalBudget - costSoFar; | |
//Logger.log("budgetRemaining: " + budgetRemaining); | |
var weeklyCost = currentSetting.campaignMap[currentSetting.campaignId].averageWeeklyCost; | |
if (daysRemaining <= 0) { | |
return budgetRemaining; | |
} else if(weeklyCost) { | |
//Logger.log("weeklyCost: " + weeklyCost); | |
var timeZone = AdWordsApp.currentAccount().getTimeZone(); | |
var thisDow = parseInt(Utilities.formatDate(new Date(), timeZone, "u")); | |
if(DEBUG) Logger.log("thisDow: " + thisDow); | |
var extraDaysRemaining = daysRemaining % 7; | |
var numWeeksRemaining = (daysRemaining - extraDaysRemaining) / 7; | |
//Logger.log("numWeeksRemaining: " + numWeeksRemaining); | |
//Logger.log("extraDaysRemaining: " + extraDaysRemaining); | |
var totalBudgetUnitsRemaining = parseFloat(0); | |
for(var dayCounter = 0; dayCounter < extraDaysRemaining; dayCounter++) { | |
var dow = (thisDow + dayCounter)%7; | |
//Logger.log("dow: " + dow); | |
var possibleBudgetForDow = parseFloat(currentSetting.campaignMap[currentSetting.campaignId].dowCosts[dow] / weeklyCost); | |
totalBudgetUnitsRemaining += possibleBudgetForDow; | |
if(DEBUG) Logger.log(" " + dayCounter + ". possibleBudgetForDow: " + possibleBudgetForDow); | |
if(DEBUG) Logger.log(" " + dayCounter + ". totalBudgetUnitsRemaining: " + totalBudgetUnitsRemaining); | |
} | |
totalBudgetUnitsRemaining += numWeeksRemaining; | |
if(DEBUG) Logger.log(" Budget Units Remaining In Cycle: " + totalBudgetUnitsRemaining.toFixed(2)); | |
var todaysAllocationOfRemainingBudgetUnits = currentSetting.campaignMap[currentSetting.campaignId].dowCosts[thisDow] / weeklyCost / totalBudgetUnitsRemaining; | |
if(DEBUG) Logger.log(" Percentage Of Budget Units To Spend Today: " + 100*todaysAllocationOfRemainingBudgetUnits.toFixed(2) + "%"); | |
if(todaysAllocationOfRemainingBudgetUnits) { | |
// this DOW has data that is not 0 | |
return budgetRemaining * todaysAllocationOfRemainingBudgetUnits; | |
} else { | |
if(VERBOSE) Logger.log(" This date of the week has no historical data so we are allocating budget evenly. If you do not want to run ads this day of the week, please use dayparting."); | |
return budgetRemaining / daysRemaining; | |
} | |
} else { | |
if(VERBOSE) Logger.log(" Because this campaign is too new and we don't have day of week performance data yet, we are allocating budget evenly for now. We will automatically start using day of week data when it is available for this campaign."); | |
return budgetRemaining / daysRemaining; | |
} | |
} | |
function calculateBudgetEvenly(costSoFar, totalBudget, daysSoFar, totalDays) { | |
if(VERBOSE) Logger.log(" New budget is calculated by taking the remaining budget and dividing it evenly across all remaining days."); | |
var daysRemaining = totalDays - daysSoFar; | |
var budgetRemaining = totalBudget - costSoFar; | |
if (daysRemaining <= 0) { | |
return budgetRemaining; | |
} else { | |
return budgetRemaining / daysRemaining; | |
} | |
} | |
function calculateBackLoadedBudget(costSoFar, totalBudget, daysSoFar, totalDays) { | |
if(VERBOSE) Logger.log(" New budget is calculated by reserving most of the budget for use towards the end of the cycle."); | |
var daysRemaining = totalDays - daysSoFar; | |
var budgetRemaining = totalBudget - costSoFar; | |
if (daysRemaining <= 0) { | |
return budgetRemaining; | |
} else { | |
return budgetRemaining / (2 * daysRemaining - 1); | |
} | |
} | |
function calculateFrontLoadedBudget(costSoFar, totalBudget, daysSoFar, totalDays) { | |
if(VERBOSE) Logger.log(" New budget is calculated by using up most of the budget towards the start of the cycle."); | |
var daysRemaining = totalDays - daysSoFar; | |
var budgetRemaining = totalBudget - costSoFar; | |
if (daysRemaining <= 0) { | |
return budgetRemaining; | |
} else { | |
return budgetRemaining / (0.5 * daysRemaining); | |
} | |
} | |
/** | |
* Returns number of days between two dates, rounded up to nearest whole day. | |
*/ | |
function datediff(from, to) { | |
var millisPerDay = 1000 * 60 * 60 * 24; | |
return Math.ceil((to - from) / millisPerDay); | |
} | |
function dateToString(date) { | |
return date.getFullYear() + zeroPad(date.getMonth() + 1) + | |
zeroPad(date.getDate()); | |
} | |
function zeroPad(n) { | |
if (n < 10) { | |
return '0' + n; | |
} else { | |
return '' + n; | |
} | |
} | |
function getReportDates(time) { | |
var timeZone = AdWordsApp.currentAccount().getTimeZone(); | |
var today = new Date() | |
var reportDates = new Object(); | |
switch(time) { | |
case "LAST_30_DAYS": | |
var startDate = new Date().setDate(today.getDate()-30); | |
var endDate = new Date().setDate(today.getDate()-1); | |
break; | |
case "LAST_14_DAYS": | |
var startDate = new Date().setDate(today.getDate()-14); | |
var endDate = new Date().setDate(today.getDate()-1); | |
break; | |
case "LAST_7_DAYS": | |
var startDate = new Date().setDate(today.getDate()-7); | |
var endDate = new Date().setDate(today.getDate()-1); | |
break; | |
case "TODAY": | |
var startDate = new Date().setDate(today); | |
var endDate = new Date().setDate(today); | |
break; | |
case "YESTERDAY": | |
var startDate = new Date().setDate(today.getDate()-1); | |
var endDate = new Date().setDate(today.getDate()-1); | |
break; | |
case "THIS_MONTH": | |
var startDate = new Date(today.getFullYear(), today.getMonth(), 1); | |
var endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); | |
var lastPossibleDate = new Date(today.getFullYear(), today.getMonth()+1, 0); | |
var reportLastPossibleDate = Utilities.formatDate(lastPossibleDate, timeZone, "yyyyMMdd"); | |
reportDates.reportLastPossibleDate = reportLastPossibleDate; | |
reportDates.lastPossibleDate = lastPossibleDate; | |
break; | |
case "LAST_MONTH": | |
var startDate = new Date(today.getFullYear(), today.getMonth()-1, 1); | |
var endDate = new Date(today.getFullYear(), today.getMonth(), 0); | |
//var niceStartDate = Utilities.formatDate(new Date(startDate), timeZone, "yyyy-MM-dd"); | |
//var niceEndDate = Utilities.formatDate(new Date(endDate), timeZone, "yyyy-MM-dd"); | |
break; | |
case "THIS_WEEK_SUN_TODAY": | |
var tempDate = new Date(); | |
var startDate = new Date(tempDate.setDate(today.getDate() - today.getDay())); | |
var endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); | |
break; | |
case "THIS_WEEK_MON_TODAY": | |
var tempDate = new Date(); | |
var startDate = new Date(tempDate.setDate(today.getDate() - today.getDay() + 1)); | |
var endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); | |
break; | |
case "LAST_WEEK": // mon-sun | |
var tempDate = new Date(); | |
var startDate = new Date(tempDate.setDate(today.getDate() - today.getDay() - 6)); | |
var tempDate = new Date(); | |
var endDate = new Date(tempDate.setDate(today.getDate() - today.getDay())); | |
break; | |
case "LAST_WEEK_SUN_SAT": // mon-sun | |
var tempDate = new Date(); | |
var startDate = new Date(tempDate.setDate(today.getDate() - today.getDay() - 7)); | |
var tempDate = new Date(); | |
var endDate = new Date(tempDate.setDate(today.getDate() - today.getDay() -1)); | |
break; | |
case "LAST_BUSINESS_WEEK": // mon-fri | |
var tempDate = new Date(); | |
var startDate = new Date(tempDate.setDate(today.getDate() - today.getDay() - 6)); | |
var tempDate = new Date(); | |
var endDate = new Date(tempDate.setDate(today.getDate() - today.getDay() - 2)); | |
break; | |
} | |
var niceStartDate = Utilities.formatDate(new Date(startDate), timeZone, "yyyy-MM-dd"); | |
var niceEndDate = Utilities.formatDate(new Date(endDate), timeZone, "yyyy-MM-dd"); | |
if(DEBUG == 1) Logger.log("start date: " + niceStartDate); | |
if(DEBUG == 1) Logger.log("end date: " + niceEndDate); | |
var reportStartDate = Utilities.formatDate(new Date(startDate), timeZone, "yyyyMMdd"); | |
var reportEndDate = Utilities.formatDate(new Date(endDate), timeZone, "yyyyMMdd"); | |
reportDates.startDate = startDate; | |
reportDates.endDate = endDate; | |
reportDates.niceStartDate = niceStartDate; | |
reportDates.niceEndDate = niceEndDate; | |
reportDates.reportStartDate = reportStartDate; | |
reportDates.reportEndDate = reportEndDate; | |
return(reportDates); | |
} | |
/* | |
// Function sendEmailNotifications (emailAddresses, subject, body, emailType ) | |
// ------------------------------------------------- | |
// emailType can be: notification or warning | |
// Adds the correct text to both the subject and body to indicate if this is a warning or notification | |
// Adds text to indicate if the script ran in preview mode | |
*/ | |
function sendEmailNotifications(emailAddresses, subject, body, emailType ) { | |
if(emailType.toLowerCase().indexOf("warning") != -1) { | |
var finalSubject = "[Warning] " + subject + " - " + AdWordsApp.currentAccount().getName() + " (" + AdWordsApp.currentAccount().getCustomerId() + ")" | |
} else if(emailType.toLowerCase().indexOf("notification") != -1) { | |
var finalSubject = "[Notification] " + subject + " - " + AdWordsApp.currentAccount().getName() + " (" + AdWordsApp.currentAccount().getCustomerId() + ")" | |
} | |
if(AdWordsApp.getExecutionInfo().isPreview()) { | |
var finalBody = "<b>This script ran in preview mode. No changes were made to your account.</b><br/>" + body; | |
} else { | |
var finalBody = body; | |
} | |
MailApp.sendEmail({ | |
to:emailAddresses, | |
subject: finalSubject, | |
htmlBody: finalBody | |
}); | |
if(DEBUG == 1) Logger.log("email sent to " + emailAddresses + ": " + finalSubject); | |
} | |
/* | |
// function getCampaignsByName(campaignName, caseSensitive, exactMatch) | |
// -------------------------------------------- | |
// | |
// builds the query string to be used with AWQL for fetching a report where the campaignName matches | |
// | |
// campaignNameSelector.forReports | |
// campaignNameSelector.forScripts | |
// | |
// usage: if(campaignNameIncludes) var campaignNameSelectorString = getCampaignsByName(campaignNameIncludes, campaignNameCaseSensitive, campaignNameExactMatch); | |
// | |
*/ | |
function getCampaignsByName(campaignName, caseSensitive, exactMatch) { | |
var campaignNameSelector = new Object(); | |
campaignNameSelector.forReports = "Name != ''" ; | |
campaignNameSelector.forScripts = "Name != ''"; | |
if(campaignName) { | |
if(exactMatch) { | |
var operator = "="; | |
} else { | |
if (caseSensitive) { | |
var operator = "CONTAINS"; | |
} else { | |
var operator = "CONTAINS_IGNORE_CASE"; | |
} | |
} | |
if(campaignName.indexOf("'") != -1) { | |
var needle = '"' + campaignName + '"'; | |
} else { | |
var needle = "'" + campaignName + "'"; | |
} | |
campaignNameSelector.forReports = "CampaignName " + operator + " " + needle; | |
campaignNameSelector.forScripts = "CampaignName " + operator + " " + needle; | |
//Logger.log("campaignNameSelectorString: " + campaignNameSelectorString); | |
} | |
return(campaignNameSelector); | |
} |
Conclusion
We took a piece of code from Google and used it as the basis for building an automation that reduces the manual workload of updating budgets daily to help us hit a monthly target, and we solved our need for a recurring monthly budget rather than a daily budget.
I hope you find this script useful for managing your own campaigns. As always, we’ve added this code to our patent-pending Enhanced Scripts on Optmyzr (my company) where we have a nice UI for managing it across many accounts and many campaigns.
The post A script to set recurring monthly budgets in AdWords appeared first on Search Engine Land.
from SEO Rank Video Blog http://ift.tt/1VoV9XS
via IFTTT
No comments:
Post a Comment