Bid to Impression Share - Manager Account

Stay organized with collections Save and categorize content based on your preferences.

Bidding icon

This script extends Bid to Impression Share to run for multiple accounts under a manager account.

Some advertisers want their ads to show up as often as possible in search results, no matter the cost. Others choose a more modest impression share that costs less but provides sufficient visibility for their ads nonetheless.

This script lets you incrementally reach your impression share goals by looking through your keywords, finding the ones most in need of adjustment, and adjusting their bids in order to affect how often they show in search results.

Scheduling

The script is considering the last 7 days of statistics. Schedule it to run Weekly.

How it works

The script does two things:

  • First, it finds keywords whose impression share is too low, and increases their bids.
  • Then, it locates all keywords whose CTR is better than 1% and impression share too high, and decreases their bids.

Note that a single iterator can only fetch 50,000 keywords. Consequently, at most 50,000 bids are increased (and another 50,000 bids decreased) per script execution.

The script uses executeInParallel to process the accounts in parallel. While this allows greater throughput, only 50 accounts can be processed at a time.

To ensure that your script runs successfully, don't label more than 50 accounts for processing.

You should carefully adjust the bid to your account's situation. You might also want to consider factors other than just CTR when deciding to adjust your keyword bids.

Parameters

Update the following parameters in the script:

  • TARGET_IMPRESSION_SHARE specifies the impression share you intend to achieve.
  • Once the keyword's impression share is within TOLERANCE of TARGET_IMPRESSION_SHARE, its bids are no longer updated. This prevents a keyword's bid from fluctuating up and down due to its impression share being 49 as opposed to 51 percent.
  • BID_ADJUSTMENT_COEFFICIENT specifies the multiplier to be used when adjusting keyword bids. The bigger the multiplier, the more aggressive the bid changes.

Setup

  • Create a new script with the source code below.
  • Update TARGET_IMPRESSION_SHARE, TOLERANCE, and BID_ADJUSTMENT_COEFFICIENT in the code.
  • Modify the conditions used to fetch the keywords as necessary.
  • Schedule the script for Weekly.

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 MCC Bid To Impression Share
 *
 * @overview The MCC Bid To Impression Share script adjusts your bids and allows
 *     you to steer ads in a group of advertiser accounts under your MCC account
 *     into a desired impression share in the search results.
 *     See
 * https://developers.google.com/google-ads/scripts/docs/solutions/adsmanagerapp-bid-to-impression-share
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 3.0.0
 *
 * @changelog
 * - version 3.0.0
 *   - Updated to use new Google Ads scripts features.
 * - version 2.0.2
 *   - Fixed old column name, changed Labels to LabelNames.
 * - version 2.0.1
 *   - Fixed logic when determining which keywords to raise and lower.
 * - version 2.0.0
 *   - Refactored to target impression share rather than position.
 * - version 1.0.1
 *   - Refactored to improve readability.
 * - version 1.0
 *   - Released initial version.
 */

// An account-level label that identifies all the accounts you are trying to
// optimize.
const TARGET_ACCOUNT_LABEL = 'High spend account';

// Ad impression share you are trying to achieve, representated as a percentage.
const TARGET_IMPRESSION_SHARE = 0.5;

// Once the keywords fall within TOLERANCE of TARGET_IMPRESSION_SHARE,
// their bids will no longer be adjusted. Represented as a percentage.
const TOLERANCE = 0.05;

// How much to adjust the bids.
const BID_ADJUSTMENT_COEFFICIENT = 1.05;

// Email address to send the summary report. Leave blank to skip sending emails.
const EMAIL_ADDRESS = 'YOUR_EMAIL_ADDRESS';

// List of email addresses to which summary report will be cc'd. Leave the array
// empty to skip sending email copies.
const EMAIL_CC_ADDRESS = ['YOUR_EMAIL_CC_ADDRESS'];

/**
 * The main method.
 */
function main() {
  const accountSelector = AdsManagerApp.accounts();
  if (TARGET_ACCOUNT_LABEL != '') {
    const labelIterator = AdsApp.labels().withCondition(`label.name = '${TARGET_ACCOUNT_LABEL}'`).get();
    if(labelIterator.hasNext()) {
      const label = labelIterator.next();
      accountSelector.withCondition(
        `customer_client.applied_labels CONTAINS ANY ('${label.getResourceName()}')`);
    } else {
        throw `could not find label ${TARGET_ACCOUNT_LABEL}`;
    }
  }
  accountSelector.executeInParallel('adjustBids', 'sendEmail');
}

/**
 * Adjusts the bid for a single account.
 *
 * @return {!Object} a result object that has details about how many keyword
 *      bids were adjusted.
 */
function adjustBids() {
  const raisedKeywordCount = raiseKeywordBids();
  const loweredKeywordCount = lowerKeywordBids();
  const retval = {'raised': raisedKeywordCount, 'lowered': loweredKeywordCount};
  return JSON.stringify(retval);
}

/**
 * Raises the bids for keywords in an account.
 *
 * @return {number} The number of keywords whose bids were raised.
 */
function raiseKeywordBids() {
  const keywordsToRaise = getKeywordsToRaise();

  for (const keyword of keywordsToRaise) {
    keyword.bidding().setCpc(getIncreasedCpc(keyword.bidding().getCpc()));
  }
  return keywordsToRaise.totalNumEntities();
}

/**
 * Lowers the bids for keywords in an account.
 *
 * @return {number} The number of keywords whose bids were lowered.
 */
function lowerKeywordBids() {
  const keywordsToLower = getKeywordsToLower();

  for (const keyword of keywordsToLower) {
    keyword.bidding().setCpc(getDecreasedCpc(keyword.bidding().getCpc()));
  }
  return keywordsToLower.totalNumEntities();
}

/**
 * Increases a given CPC using the bid adjustment coefficient.
 * @param {number} cpc - the CPC to increase
 * @return {number} the new CPC
 */
function getIncreasedCpc(cpc) {
  return cpc * BID_ADJUSTMENT_COEFFICIENT;
}

/**
 * Decreases a given CPC using the bid adjustment coefficient.
 * @param {number} cpc - the CPC to decrease
 * @return {number} the new CPC
 */
function getDecreasedCpc(cpc) {
  return cpc / BID_ADJUSTMENT_COEFFICIENT;
}

/**
 * Gets an iterator of the keywords that need to have their CPC raised.
 * @return {Iterator} an iterator of the keywords
 */
function getKeywordsToRaise() {
  // Condition to raise bid: Average impression share is worse (less) than
  // target - tolerance
  return AdsApp.keywords()
      .withCondition('ad_group_criterion.status = ENABLED')
      .withCondition(
          `metrics.search_impression_share <
          ${(TARGET_IMPRESSION_SHARE - TOLERANCE)}`)
      .orderBy('metrics.search_impression_share ASC')
      .forDateRange('LAST_7_DAYS')
      .get();
}

/**
 * Gets an iterator of the keywords that need to have their CPC lowered.
 * @return {Iterator} an iterator of the keywords
 */
function getKeywordsToLower() {
  // Conditions to lower bid: Ctr greater than 1% AND
  // average impression share better (greater) than target + tolerance
  return AdsApp.keywords()
      .withCondition('metrics.ctr > 0.01')
      .withCondition(
          `metrics.search_impression_share >
          ${(TARGET_IMPRESSION_SHARE + TOLERANCE)}`)
      .withCondition('ad_group_criterion.status = ENABLED')
      .orderBy('metrics.search_impression_share DESC')
      .forDateRange('LAST_7_DAYS')
      .get();
}

/**
 * Send summary report email to users.
 *
 * @param {Array.<ExecutionResult>} results the ExecutionResult array returned
 *      by executeInParallel method.
 */
function sendEmail(results) {
  const emailBody = [];

  emailBody.push(
      '<html>', '<body>',
      '<table width=800 cellpadding=0 border=0 cellspacing=0>', '<tr>',
      '<td colspan=2 align=right>',
      '<div style=\'font: italic normal 10pt Times New Roman, serif; ' +
          'margin: 0; color: #666; padding-right: 5px;\'>' +
          'Powered by Google Ads Scripts</div>',
      '</td>', '</tr>', '<tr bgcolor=\'#3c78d8\'>', '<td width=500>',
      '<div style=\'font: normal 18pt verdana, sans-serif; ' +
          'padding: 3px 10px; color: white\'>' +
          'BidToImpressionShare summary report</div>',
      '</td>', '<td align=right>',
      '<div style=\'font: normal 18pt verdana, sans-serif; ' +
          'padding: 3px 10px; color: white\'>',
      AdsApp.currentAccount().getCustomerId(), '</h1>', '</td>', '</tr>',
      '</table>', '<table width=800 cellpadding=0 border=0 cellspacing=0>',
      '<tr bgcolor=\'#ddd\'>', '<td style=\'font: 12pt verdana, sans-serif; ' +
          'padding: 5px 0px 5px 5px; background-color: #ddd; ' +
          'text-align: left\'>Customer ID</td>',
      '<td style=\'font: 12pt verdana, sans-serif; ' +
          'padding: 5px 0px 5px 5px; background-color: #ddd; ' +
          'text-align: left\'>Results</td>',
      '</tr>', emailRows(results), '</table>', '</body>', '</html>');


  if (EMAIL_ADDRESS != '') {
    MailApp.sendEmail(
        EMAIL_ADDRESS, 'BidToImpressionShare summary report', '',
        {htmlBody: emailBody.join('\n'), cc: EMAIL_CC_ADDRESS.join(',')});
  }
}

/**
 * Constructs email rows for each customer id processed by the script.
 *
 * @param {Array.<ExecutionResult>} results the ExecutionResult array returned
 *      by executeInParallel method.
 * @return {string} the html body corresponding to the customer ids processed
 *      by the script and its results.
 */
function emailRows(results) {
  const rows = [];
  for (const result of results) {

    rows.push(
        '<tr>',
        `<td style='padding: 5px 10px'> ${result.getCustomerId()} </td>`,
        `<td style='padding: 0px 10px'> ${getResultDescription(result)}` +
            `</td>`,
        '</tr>');
  }
  return rows.join('\n');
}

/**
 * Gets a description text for execution results on a customer id.
 *
 * @param {ExecutionResult} result the ExecutionResult object returned
 *      by executeInParallel method for the customer id.
 * @return {string} a processed text that summarizes the execution result.
 */
function getResultDescription(result) {
  if (result.getStatus() == 'OK') {
    const retval = JSON.parse(result.getReturnValue());
    return `Raised : ${retval.raised} < br/ > ` +
        `Lowered : ${retval.lowered}`;
  } else if (result.getStatus() == 'ERROR') {
    return result.getError();
  } else {
    return 'Script timed out.';
  }
}