柔軟な予算 - MCC アカウント

ツールアイコン

このスクリプトは、1 つの MCC アカウントの配下にある複数のアカウントに対して実行できるように、柔軟な予算を拡張します。柔軟な予算では、カスタムの予算配分スキームを使用して、キャンペーンの予算を日単位で動的に調整できます。

このスクリプトは、指定されたアカウントまたはキャンペーンとそれに対応する予算(開始日と終了日が関連付けられています)のスプレッドシートを読み取り、キャンペーンを検索して当日の予算を計算し、それをキャンペーンの 1 日の予算として設定して、その結果をスプレッドシートに記録します。スプレッドシートで指定されていないキャンペーンには影響しません。

仕組み

このスクリプトの動作は、単一アカウントの柔軟な予算のスクリプトの動作と同じです。 唯一の追加機能は、指定したスプレッドシートを通じて複数のアカウントをサポートすることです。

最初の 2 つの列は予算を計算するキャンペーンを指定し、次の 3 つの列は予算情報を指定し、最後の 3 つの列は実行結果を記録します。

アカウント ID は MCC アカウントではなく、広告主様のアカウントである必要があります。

同じアカウントやキャンペーンに複数の予算を設定できますが、有効な予算は一度に 1 つのみにしてください。有効にしないと、新しい予算の計算によって古い予算が上書きされる可能性があります。

スプレッドシートでアカウントまたはキャンペーンが指定されていない場合、スクリプトは柔軟な予算を設定しません。

予算設定方法のテスト

スクリプトには、数日間にわたって実行した場合の効果をシミュレーションするためのテスト用のコードが含まれています。これにより、一定の期間にわたって毎日スクリプトが実行されるようにスケジュールされている場合、何が起きるかがよくわかります。

デフォルトでは、このスクリプトは 10 日間で 500 ドルの予算を均等に配分するシミュレーションを行います。

main メソッドで setNewBudget ではなく testBudgetStrategy を呼び出すことで、テストコードを実行できます。

function main() {
  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  //  setNewBudget(calculateBudgetWeighted);
}

setNewBudget 関数呼び出しはコメントアウトされ、スクリプトがテストコードを実行していることを示します。この例の出力結果は次のようになります。

Day 1.0 of 10.0, new budget 50.0, cost so far 0.0
Day 2.0 of 10.0, new budget 50.0, cost so far 50.0
Day 3.0 of 10.0, new budget 50.0, cost so far 100.0
Day 4.0 of 10.0, new budget 50.0, cost so far 150.0
Day 5.0 of 10.0, new budget 50.0, cost so far 200.0
Day 6.0 of 10.0, new budget 50.0, cost so far 250.0
Day 7.0 of 10.0, new budget 50.0, cost so far 300.0
Day 8.0 of 10.0, new budget 50.0, cost so far 350.0
Day 9.0 of 10.0, new budget 50.0, cost so far 400.0
Day 10.0 of 10.0, new budget 50.0, cost so far 450.0
Day 11.0 of 10.0, new budget 0.0, cost so far 500.0

毎日、予算が均等に消費されるよう、新しい予算が毎日計算されます。最初の予算割り当てを超えると、予算はゼロに設定されて支出が停止します。

使用する予算戦略を変更するには、使用する関数を変更するか、関数自体を変更します。このスクリプトには、calculateBudgetEvenlycalculateBudgetWeighted という 2 つの事前構築済み戦略があります。前の例では前者をテストしています。後者を使用するように testBudgetStrategy 行を更新します。

testBudgetStrategy(calculateBudgetWeighted, 10, 500);

[プレビュー] をクリックして、ログの出力結果を確認します。この予算戦略では、早い段階で予算の割り当てを減らし、その後数日間は増やします。

このテスト手法により、予算を算定する関数を変更してシミュレーションを行い、独自の予算配分方法を試すことができます。

予算の割り当て

calculateBudgetWeighted 予算戦略を詳しく見てみましょう。

// One calculation logic that distributes remaining budget in a weighted manner
function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1);
  }
}

この関数には、以下の引数が渡されます。

  • costSoFar: startDateから今日までに、このキャンペーンで発生した費用です。
  • totalBudget: startDateから endDateまでの費用。
  • daysSoFar: startDate から今日までの経過日数。
  • totalDays: startDateendDate の合計日数。

関数は独自に記述できますが、これらの引数は必ず渡す必要があります。これらの値を使用することで、これまでに費やした金額を全体の予算額と比較したり、予算全体の消化日程における現在の達成状況を確認したりできます。

特に、この予算戦略では予算の残高(totalBudget - costSoFar)を計算し、残り日数の 2 倍で割ります。これにより、キャンペーン終了時の予算配分が比較されます。 startDate 以降の費用を使用することで、設定した予算のすべてを消化しない「遅い日」も考慮されます。

本番環境での予算編成

目的の予算戦略が決まったら、スクリプトを毎日実行するようスケジュール設定する前に、いくつか変更を加える必要があります。

まず、スプレッドシートを更新して、アカウント、キャンペーン、予算、開始日、終了日を指定します(キャンペーンの予算ごとに 1 行ずつ)。

  • アカウント ID: 予算戦略を適用するキャンペーンのアカウント ID(形式: xxx-xxx-xxxx)。
  • キャンペーン名: 予算戦略を適用するキャンペーンの名前。
  • 開始日: 予算戦略の開始日です。現在または過去の日付で指定します。
  • 終了日: この予算を使って広告を掲載する最終日。
  • 合計予算: 消化したい予算の総額。この値は、アカウントの通貨単位で指定します。なお、スクリプトの実行がスケジュール設定された時間によっては、値が超過する場合もあります。

次に、テストを無効にして、実際に予算を変更するロジックを有効にします。

function main() {
  //  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  setNewBudget(calculateBudgetWeighted);
}

各キャンペーンの結果が [Execution Result] 列に記録されます。

スケジュール設定

ローカル タイムゾーンの深夜 12 時かその直後に毎日スクリプトが実行されるようスケジュールを設定して、翌日にできるだけ多くの予算が配分されるようにします。ただし、取得されたレポートデータ(費用など)は約 3 時間遅れる可能性があるため、costSoFar パラメータは、午前 0 時以降に実行するようにスケジュール設定されたスクリプトの前日の合計を参照している可能性があります。

設定

ソースコード

// 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 Flexible Budgets
 *
 * @overview The MCC Flexible Budgets script dynamically adjusts campaign budget
 *     daily for accounts under an MCC account with a custom budget distribution
 *     scheme. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/manager-flexible-budgets
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.1
 *
 * @changelog
 * - version 2.1
 *   - Split into info, config, and code.
 *  - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0.4
 *   - Add support for video and shopping campaigns.
 * - version 1.0.3
 *   - Added validation for external spreadsheet setup.
 * - version 1.0.2
 *   - Fix a minor bug in variable naming.
 *   - Use setAmount on the budget instead of campaign.setBudget.
 * - version 1.0.1
 *   - Improvements to time zone handling.
 * - version 1.0
 *   - Released initial version.
 */
/**
 * Configuration to be used for the Flexible Budgets script.
 */
CONFIG = {
  // URL of the default spreadsheet template. This should be a copy of
  // https://docs.google.com/spreadsheets/d/17wocOgrLeRWF1Qi_BjEigCG0qVMebFHrbUS-Vk_kpLg/copy
  // Make sure the sheet is owned by or shared with same Google user executing the script
  'spreadsheet_url': 'YOUR_SPREADSHEET_URL',

  'advanced_options': {
    // Please fix the following variables if you need to reformat the
    // spreadsheet
    // column numbers of each config column. Column A in your spreadsheet has
    // column number of 1, B has number of 2, etc.
    'column': {
      'accountId': 2,
      'campaignName': 3,
      'startDate': 4,
      'endDate': 5,
      'totalBudget': 6,
      'results': 7
    },

    // Actual config (without header and margin) starts from this row
    'config_start_row': 5
  }
};

const SPREADSHEET_URL = CONFIG.spreadsheet_url;
const COLUMN = CONFIG.advanced_options.column;
const CONFIG_START_ROW = CONFIG.advanced_options.config_start_row;

function main() {
  // Uncomment the following function to test your budget strategy function
  // testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  setNewBudget(calculateBudgetWeighted);
}

// Core logic for calculating and setting campaign daily budget
function setNewBudget(budgetFunc) {
  console.log(`Using spreadsheet - ${SPREADSHEET_URL}.`);
  const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());
  const sheet = spreadsheet.getSheets()[0];

  const endRow = sheet.getLastRow();

  const mccAccount = AdsApp.currentAccount();
  sheet.getRange(2, 6, 1, 2).setValue(mccAccount.getCustomerId());

  const today = new Date();

  for (let i = CONFIG_START_ROW; i <= endRow; i++) {
    console.log(`Processing row ${i}`);

    const accountId = sheet.getRange(i, COLUMN.accountId).getValue();
    const campaignName = sheet.getRange(i, COLUMN.campaignName).getValue();
    const startDate = new Date(sheet.getRange(i, COLUMN.startDate).getValue());
    const endDate = new Date(sheet.getRange(i, COLUMN.endDate).getValue());
    const totalBudget = sheet.getRange(i, COLUMN.totalBudget).getValue();
    const resultCell = sheet.getRange(i, COLUMN.results);

    const accountIter = AdsManagerApp.accounts().withIds([accountId]).get();
    if (!accountIter.hasNext()) {
      resultCell.setValue('Unknown account');
      continue;
    }
    const account = accountIter.next();
    AdsManagerApp.select(account);

    const campaign = getCampaign(campaignName);
    if (!campaign) {
      resultCell.setValue('Unknown campaign');
      continue;
    }

    if (today < startDate) {
      resultCell.setValue('Budget not started yet');
      continue;
    }
    if (today > endDate) {
      resultCell.setValue('Budget already finished');
      continue;
    }

    const costSoFar = campaign
                          .getStatsFor(
                              getDateStringInTimeZone('yyyyMMdd', startDate),
                              getDateStringInTimeZone('yyyyMMdd', endDate))
                          .getCost();
    const daysSoFar = datediff(startDate, today);
    const totalDays = datediff(startDate, endDate);
    const newBudget = budgetFunc(costSoFar, totalBudget, daysSoFar, totalDays);
    campaign.getBudget().setAmount(newBudget);
    console.log(
        `AccountId=${accountId}, CampaignName=${campaignName}, ` +
        `StartDate=${startDate}, EndDate=${endDate}, ` +
        `CostSoFar=${costSoFar}, DaysSoFar=${daysSoFar}, ` +
        `TotalDays=${totalDays}, NewBudget=${newBudget}'`);
    resultCell.setValue(`Set today's budget to ${newBudget}`);
  }

  // update "Last execution" timestamp
  sheet.getRange(1, 3).setValue(today);
  AdsManagerApp.select(mccAccount);
}

// One calculation logic that distributes remaining budget evenly
function calculateBudgetEvenly(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / daysRemaining;
  }
}

// One calculation logic that distributes remaining budget in a weighted manner
function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1);
  }
}

// Test function to verify budget calculation logic
function testBudgetStrategy(budgetFunc, totalDays, totalBudget) {
  let daysSoFar = 0;
  let costSoFar = 0;
  while (daysSoFar <= totalDays + 2) {
    const newBudget = budgetFunc(costSoFar, totalBudget, daysSoFar, totalDays);
    console.log(
        `Day ${daysSoFar + 1} of ${totalDays}, ` +
        `new budget ${newBudget}, cost so far ${costSoFar}`);
    costSoFar += newBudget;
    daysSoFar += 1;
  }
}

// Return number of days between two dates, rounded up to nearest whole day.
function datediff(from, to) {
  const millisPerDay = 1000 * 60 * 60 * 24;
  return Math.ceil((to - from) / millisPerDay);
}

// Produces a formatted string representing a given date in a given time zone.
function getDateStringInTimeZone(format, date, timeZone) {
  date = date || new Date();
  timeZone = timeZone || AdsApp.currentAccount().getTimeZone();
  return Utilities.formatDate(date, timeZone, format);
}

/**
 * Finds a campaign by name, whether it is a regular, video, or shopping
 * campaign, by trying all in sequence until it finds one.
 *
 * @param {string} campaignName The campaign name to find.
 * @return {Object} The campaign found, or null if none was found.
 */
function getCampaign(campaignName) {
  const selectors =
      [AdsApp.campaigns(), AdsApp.videoCampaigns(), AdsApp.shoppingCampaigns()];
  for (const selector of selectors) {
    const campaignIter =
        selector.withCondition(`CampaignName = "${campaignName}"`).get();
    if (campaignIter.hasNext()) {
      return campaignIter.next();
    }
  }
  return null;
}

/**
 * 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);
}