Bid Testing

When determining the best bid for your keywords, it can be effective to try different levels of bidding to determine what bids work best to achieve your goals. Here we show how to systematically adjust your bids to find the "sweet spot" for your keyword bids.

The script will adjust your keyword bids based on a series of multipliers and record the results of each change.

How it works

Working with spreadsheets

This script uses a Google Spreadsheet both to store state (for example, the bid multipliers and starting bids) as well as catalog performance (for each interval of bid testing, we record the keyword bid, CTR, clicks, and impressions).

The bid multipliers will be applied successively for each iteration of the script. Each run will apply the next unused bid multiplier to your keyword bids. For example, a starting bid of $1 and multipliers of .8 and 1.2 would see bids of $0.80 and $1.20.

Updating bids

The updateBids() function applies the multiplier for this iteration to all keywords in the campaign you chose:

var keywordIter = campaign.keywords().get();
while (keywordIter.hasNext()) {
  var keyword = keywordIter.next();
  var oldBid = startingBids[keyword.getText()];
  if (!oldBid) {
    // If we don't have a starting bid, keyword has been added since we
    // started testing.
    oldBid = keyword.bidding().getCpc() || keyword.getAdGroup().bidding().getCpc();
    startingBids[keyword.getText()] = oldBid;
  }
  var newBid = oldBid * multiplier;
  keyword.bidding().setCpc(newBid);
}

The code above uses keyword's max CPC bid (or ad group's default max CPC bid if the keyword has no bid) and applies the multiplier. It also detects if a keyword has been added between script executions and stores the current max CPC bid for future reference.

Reporting on bid performance

Each time the script runs and bids have been applied for at least one time period (week, day, etc.), the outputReport function executes. We use the date marked on the Multipliers tab and today's date as the date range in obtaining metrics for each keyword. We store this, along with the keyword bid during the period defined by the date range, in a separate sheet for later analysis.

// Create a new sheet to output keywords to.
var reportSheet = spreadsheet.insertSheet(start + ' - ' + end);
var campaign = getCampaign();

var rows = [['Keyword', 'Max CPC', 'Clicks', 'Impressions', 'Ctr']];
var keywordIter = campaign.keywords().get();
while (keywordIter.hasNext()) {
  var keyword = keywordIter.next();
  var stats = keyword.getStatsFor(start, end);
  rows.push([keyword.getText(), keyword.bidding().getCpc(), stats.getClicks(),
      stats.getImpressions(), stats.getCtr()]);
}

reportSheet.getRange(1, 1, rows.length, 5).setValues(rows);

We're iterating over each keyword in the campaign and adding a row containing Keyword, Max CPC, Clicks, Impressions, and CTR to an array. This array then gets written to the new sheet (which is named according to the start and end dates we're reporting on).

If you use other key performance indicators than those in the script, you can alter this logic to include them. To do this, add another entry in the first row (header row) like this:

var rows = [['Keyword', 'Max CPC', 'Clicks', 'Impressions', 'Ctr']];

and modify the script to also include this metric:

rows.push([keyword.getText(), keyword.bidding().getCpc(), stats.getClicks(),
    stats.getImpressions(), stats.getCtr()]);

Scheduling

We recommend you pick a campaign to test it with and schedule the script to run Weekly (or on any other schedule that suits you). At the scheduled interval, the script will report on the previous period's performance (if applicable) and apply a new bid multiplier.

The main function contains the logic that manages each step of your bid testing. Once bid testing is complete, the script will log this fact and future executions will not make any additional changes:

if (finishedReporting) {
  Logger.log('Script complete, all bid modifiers tested and reporting. ' +
    'Please remove this script\'s schedule.');
}

Analyzing test results

The script has applied an array of bid modifiers to your keywords and logged the performance of these values across all keywords. Now it's time to decide what to do with this data. Keep in mind that the same bid modifier may not produce the same performance benefit for all keywords.

We've recorded both the keyword's max CPC bid as well as various performance indicators. It's up to you to determine which is most important. You should then evaluate each keyword across the multiple intervals and use your KPI to choose the best bid for each keyword.

Setup

  • Make a copy of this spreadsheet.
  • Note the URL of the copy.
  • Create a new Google Ads script with the source code below.
  • Change the value of the SPREADSHEET_URL variable to be the URL of the your copy of the spreadsheet. Next, choose a campaign to perform bid testing on and change the value of the CAMPAIGN_NAME variable to match it.

Source code

// 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 Bid Testing
 *
 * @overview The Bid Testing script allows you to try different levels of
 *     bidding for keywords in your advertiser account to determine what bids
 *     work best to achieve your goals.
 *     See https://developers.google.com/google-ads/scripts/docs/solutions/bid-testing
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.0.3
 *
 * @changelog
 * - version 1.0.3
 *   - Replaced deprecated keyword.setMaxCpc() and keyword.getMaxCpc().
 * - version 1.0.2
 *   - Added validation for user settings.
 * - version 1.0.1
 *   - Improvements to time zone handling.
 * - version 1.0
 *   - Released initial version.
 */

var SPREADSHEET_URL = 'YOUR_SPREADSHEET_URL';
var CAMPAIGN_NAME = 'YOUR_CAMPAIGN_NAME';

function main() {
  validateCampaignName();
  Logger.log('Using spreadsheet - %s.', SPREADSHEET_URL);
  var spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());

  var multipliersSheet = spreadsheet.getSheetByName('Multipliers');

  var multipliers = multipliersSheet.getDataRange().getValues();
  // Find if we have a multiplier left to apply.
  var multiplierRow = 1;
  for (; multiplierRow < multipliers.length; multiplierRow++) {
    // if we haven't marked a multiplier as applied, use it.
    if (!multipliers[multiplierRow][1]) {
      break;
    }
  }

  var today = Utilities.formatDate(new Date(),
      AdsApp.currentAccount().getTimeZone(), 'yyyyMMdd');

  var shouldReport = multiplierRow > 1;
  var shouldIncreaseBids = multiplierRow < multipliers.length;
  var finishedReporting = multipliersSheet.getSheetProtection().isProtected();

  if (shouldReport && !finishedReporting) {
    // If we have at least one multiplier marked as applied,
    // let's record performance since the last time we ran.
    var lastRun = multipliers[multiplierRow - 1][1];
    if (lastRun == today) {
      Logger.log('Already ran today, skipping');
      return;
    }
    outputReport(spreadsheet, lastRun, today);

    if (!shouldIncreaseBids) {
      // We've reported one iteration after we finished bids, so mark the sheet
      // protected.
      var permissions = multipliersSheet.getSheetProtection();
      permissions.setProtected(true);
      multipliersSheet.setSheetProtection(permissions);
      Logger.log('View bid testing results here: ' + SPREADSHEET_URL);
    }
  }

  if (shouldIncreaseBids) {
    // If we have a multiplier left to apply, let's do so.
    updateBids(spreadsheet, multipliers[multiplierRow][0]);
    multipliers[multiplierRow][1] = today;
    // Mark multiplier as applied.
    multipliersSheet.getDataRange().setValues(multipliers);
  }

  if (finishedReporting) {
    Logger.log('Script complete, all bid modifiers tested and reporting. ' +
      'Please remove this script\'s schedule.');
  }
}

function updateBids(spreadsheet, multiplier) {
  Logger.log('Applying bid multiplier of ' + multiplier);

  var startingBids = getStartingBids(spreadsheet);
  if (!startingBids) {
    startingBids = recordStartingBids(spreadsheet);
  }
  var campaign = getCampaign();
  var keywordIter = campaign.keywords().get();
  while (keywordIter.hasNext()) {
    var keyword = keywordIter.next();
    var oldBid = startingBids[keyword.getText()];
    if (!oldBid) {
      // If we don't have a starting bid, keyword has been added since we
      // started testing.
      oldBid = keyword.bidding().getCpc() || keyword.getAdGroup().bidding().getCpc();
      startingBids[keyword.getText()] = oldBid;
    }
    var newBid = oldBid * multiplier;
    keyword.bidding().setCpc(newBid);
  }
  saveStartingBids(spreadsheet, startingBids);
}

function outputReport(spreadsheet, start, end) {
  Logger.log('Reporting on ' + start + ' -> ' + end);

  // Create a new sheet to output keywords to.
  var reportSheet = spreadsheet.insertSheet(start + ' - ' + end);
  var campaign = getCampaign();

  var rows = [['Keyword', 'Max CPC', 'Clicks', 'Impressions', 'Ctr']];
  var keywordIter = campaign.keywords().get();
  while (keywordIter.hasNext()) {
    var keyword = keywordIter.next();
    var stats = keyword.getStatsFor(start, end);
    rows.push([keyword.getText(), keyword.bidding().getCpc(), stats.getClicks(),
        stats.getImpressions(), stats.getCtr()]);
  }

  reportSheet.getRange(1, 1, rows.length, 5).setValues(rows);
}

function recordStartingBids(spreadsheet) {
  var startingBids = {};
  var keywords = getCampaign().keywords().get();
  while (keywords.hasNext()) {
    var keyword = keywords.next();
    var bid = keyword.bidding().getCpc() || keyword.getAdGroup().bidding().getCpc();
    startingBids[keyword.getText()] = bid;
  }
  saveStartingBids(spreadsheet, startingBids);
  return startingBids;
}

function getStartingBids(spreadsheet) {
  var sheet = spreadsheet.getSheetByName('Starting Bids');
  if (!sheet) {
    return;
  }
  var rawData = sheet.getDataRange().getValues();
  var startingBids = {};
  for (var i = 0; i < rawData.length; i++) {
    startingBids[rawData[i][0]] = rawData[i][1];
  }
  return startingBids;
}

function saveStartingBids(spreadsheet, startingBids) {
  var sheet = spreadsheet.getSheetByName('Starting Bids');
  if (!sheet) {
    sheet = spreadsheet.insertSheet('Starting Bids');
  }
  var rows = [];
  for (var keyword in startingBids) {
    rows.push([keyword, startingBids[keyword]]);
  }
  sheet.getRange(1, 1, rows.length, 2).setValues(rows);
}

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 getCampaign() {
  return AdsApp.campaigns().withCondition("Name = '" +
      CAMPAIGN_NAME + "'").get().next();
}

/**
 * Validates the provided campaign name and throws a descriptive error
 * if the user has not changed the email from the default fake name.
 *
 * @throws {Error} If the name is the default fake name.
 */
function validateCampaignName(){
  if (CAMPAIGN_NAME == "YOUR_CAMPAIGN_NAME") {
    throw new Error('Please use a valid campaign name.');
  }
}

/**
 * Validates the provided spreadsheet URL
 * to make sure that it's set up properly. Throws a descriptive error message
 * if validation fails.
 *
 * @param {string} spreadsheeturl The URL of the spreadsheet to open.
 * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
 * @throws {Error} If the spreadsheet URL hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
    throw new Error('Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  return SpreadsheetApp.openByUrl(spreadsheeturl);
}