유연한 예산 - 관리자 계정

도구 아이콘을 클릭합니다.

이 스크립트는 단일 관리자 계정의 여러 계정에 대해 실행되도록 가변형 예산을 확장합니다. 유연한 예산은 맞춤 예산 분배 체계를 통해 캠페인 예산을 매일 동적으로 조정할 수 있습니다.

스크립트는 지정된 각 계정/캠페인 및 해당 예산 (시작일 및 종료일과 연결됨)의 스프레드시트를 읽고, 캠페인을 찾고, 당일 예산을 계산하고, 이를 캠페인의 일일 예산으로 설정하고, 결과를 스프레드시트에 기록합니다. 스프레드시트에 지정되지 않은 캠페인은 건드리지 않습니다.

사용 방법

스크립트는 단일 계정의 유연한 예산 스크립트와 동일한 방식으로 작동합니다. 유일한 추가 기능은 지정된 스프레드시트를 통해 여러 계정을 지원한다는 것입니다.

처음 2개 열은 예산을 계산할 캠페인을 지정하고, 다음 3개 열은 예산 정보를 지정하며, 마지막 열은 실행 결과를 기록합니다.

계정 ID는 관리자 계정이 아닌 광고주 계정이어야 합니다.

동일한 계정/캠페인에 여러 예산을 사용할 수 있지만 한 번에 하나의 예산만 사용할 수 있습니다. 그러지 않으면 최신 예산 계산 시 이전 예산을 덮어쓸 수 있습니다.

계정/캠페인이 스프레드시트에 지정되지 않은 경우 스크립트는 유연한 예산을 설정하지 않습니다.

예산 전략 테스트

스크립트에는 며칠 동안의 실행 효과를 시뮬레이션하는 테스트 코드가 포함되어 있습니다. 이를 통해 스크립트가 일정 기간 동안 매일 실행되도록 예약되면 어떤 일이 발생하는지 더 잘 파악할 수 있습니다.

기본적으로 이 스크립트는 10일 동안 500달러가 지출되는 균등한 예산 분배를 시뮬레이션합니다.

기본 메서드에서 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

예산이 매일 고르게 지출되도록 매일 새로운 예산이 계산됩니다. 초기 예산 할당을 초과하면 예산이 0으로 설정되어 지출이 중단됩니다.

사용되는 함수를 변경하거나 함수 자체를 수정하여 사용된 예산 전략을 변경할 수 있습니다. 이 스크립트는 두 가지 사전 빌드된 전략인 calculateBudgetEvenlycalculateBudgetWeighted와 함께 제공됩니다. 이전 예에서는 전자를 테스트했습니다. 후자를 사용하도록 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: startDate에서 endDate 사이의 총 일 수입니다.

이러한 인수를 받는 한 자체 함수를 작성할 수 있습니다. 이러한 값을 사용하면 지금까지 지출한 금액과 전체적으로 지출할 금액을 비교하고 전체 예산의 타임라인 내에서 현재 어느 단계에 있는지 파악할 수 있습니다.

특히 이 예산 전략은 남은 예산의 양(totalBudget - costSoFar)을 파악하고 이를 남은 일수의 두 배로 나눕니다. 이렇게 하면 캠페인 후반의 예산 배분이 반영됩니다. startDate 이후의 비용을 사용하면 설정한 전체 예산을 지출하지 않는 '느린 날'도 고려합니다.

제작 시 예산 설정

예산 전략에 만족하면 이 스크립트를 매일 실행하도록 예약하기 전에 몇 가지 사항을 변경해야 합니다.

먼저 스프레드시트를 업데이트하여 계정, 캠페인, 예산, 시작일, 종료일을 지정합니다(캠페인 예산당 한 행씩).

  • 계정 ID: 예산 전략을 적용할 캠페인의 계정 ID (xxx-xxx-xxxx 형식)입니다.
  • 캠페인 이름: 예산 전략을 적용할 캠페인의 이름입니다.
  • 시작일: 예산 전략의 시작일입니다. 현재 날짜 또는 과거의 날짜여야 합니다.
  • 종료일: 이 예산으로 광고하려는 마지막 날입니다.
  • 총 예산: 지출하려는 총 금액입니다. 이 값은 계정 통화로 표시되며 스크립트를 실행하도록 예약한 시기에 따라 초과될 수 있습니다.

다음으로, 테스트를 중지하고 실제로 예산을 변경하는 로직을 사용 설정합니다.

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

각 캠페인의 결과는 실행 결과 열에 기록됩니다.

예약

다음 날의 예산을 가능한 한 많이 보내도록 이 스크립트를 매일 현지 시간대의 자정 또는 그 직후에 실행하도록 예약하세요. 단, 비용과 같은 가져온 보고서 데이터는 약 3시간 지연될 수 있으므로 costSoFar 매개변수는 자정 이후에 실행되도록 예약된 스크립트의 어제 총계를 참조할 수 있습니다.

설정

  • 아래 버튼을 클릭하여 Google Ads 계정에서 스크립트를 만드세요.

    스크립트 템플릿 설치

  • 템플릿 스프레드시트의 사본을 만들려면 아래 버튼을 클릭하세요.

    템플릿 스프레드시트 복사하기

  • 스크립트에서 spreadsheet_url를 업데이트합니다.

  • 스크립트가 매일 실행되도록 예약합니다.

소스 코드

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