Testowanie stawek

Ikona określania stawek

Przy ustalaniu najlepszej stawki za słowa kluczowe warto wypróbować różne poziomy stawek i sprawdzić, które stawki będą najskuteczniejsze do osiągnięcia założonych celów. Tutaj pokażemy, jak systematycznie dostosowywać stawki, by znaleźć tę optymalną.

Skrypt ten dostosowuje stawki za słowa kluczowe na podstawie serii mnożników i rejestruje wyniki każdej zmiany.

Jak to działa

Skrypt ten korzysta z arkusza kalkulacyjnego Google do przechowywania stanu (np. mnożników stawek i stawek początkowych), jak również skuteczności katalogu (dla każdego przedziału czasu testowania stawek rejestruje stawkę za słowo kluczowe, CTR, kliknięcia i wyświetlenia).

Następnie mnożniki stawek są stosowane kolejno do każdego wystąpienia skryptu. Przy każdym uruchomieniu do określania stawek za słowa kluczowe stosowany jest kolejny niewykorzystany mnożnik stawki. Na przykład przy stawce początkowej 1 zł i mnożnikach 0,8 i 1,2 stawki będą wynosić 0,80 zł i 1,20 zł.

Aktualizacje stawek

Funkcja updateBids() stosuje mnożnik dla tej iteracji do wszystkich słów kluczowych w wybranej kampanii:

const keywordIter = campaign.keywords().get();
for (const keyword of keywordIter) {
  let 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;
  }
  const newBid = oldBid * multiplier;
  keyword.bidding().setCpc(newBid);
}

Ten kod wykorzystuje maks. stawkę CPC słowa kluczowego (lub domyślną maks. stawkę CPC grupy reklam, jeśli słowo kluczowe nie ma określonej stawki) i stosuje mnożnik. Wykrywa też, czy pomiędzy wykonaniem skryptu a konkretnym słowem kluczowym zostało dodane słowo kluczowe, i zapisuje bieżącą maksymalną stawkę CPC, aby móc ją wykorzystać w przyszłości.

Raport skuteczności stawek

Za każdym razem, gdy skrypt jest uruchomiony i stosowane są stawki przez co najmniej 1 okres (tydzień, tydzień itd.), jest wykonywana funkcja outputReport. Data oznaczona na karcie Mnożniki i dzisiejsza data są wykorzystywane jako zakres dat przy uzyskiwaniu danych o każdym słowie kluczowym. Dane wraz ze stawką za słowo kluczowe z okresu wyznaczonego przez zakres dat są przechowywane w osobnym arkuszu na potrzeby późniejszej analizy.

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

const rows = [['Keyword', 'Max CPC', 'Clicks', 'Impressions', 'Ctr']];
const keywordIter = campaign.keywords().get();
for (const keyword of keywordIter) {
  const 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);

Każde słowo kluczowe w kampanii jest powtarzane i dodawane są nowe wiersze zawierające zestaw słów kluczowych, maks. CPC, kliknięć, wyświetleń i CTR. Ta tablica jest następnie zapisywana w nowym arkuszu (którym nazwa jest zgodną z datami rozpoczęcia i zakończenia raportu).

Jeśli używasz innych kluczowych wskaźników wydajności niż te wymienione w skrypcie, możesz zmienić tę zasadę, aby je uwzględnić. Aby to zrobić, dodaj kolejny wpis w pierwszym wierszu (wierszu nagłówka) w następujący sposób:

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

Zmodyfikuj też skrypt tak, aby zawierał również te dane:

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

Planuję

Zalecamy, by wybrać kampanię do przetestowania skryptu i zaplanować jego uruchamianie Co tydzień (lub według innego, odpowiedniego dla Ciebie harmonogramu). Po zaplanowanym interwale skrypt raportuje skuteczność z poprzedniego okresu (jeśli ma to zastosowanie) i stosuje nowy mnożnik stawki.

Główna funkcja obejmuje funkcje logiczne zarządzające poszczególnymi etapami testowania stawek. Po zakończeniu testowania stawek skrypt rejestruje tę informację, a przyszłe uruchomienia nie będą powodować żadnych dodatkowych zmian:

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

Wyniki testów

Gdy skrypt zastosuje tablicę modyfikatorów stawek do słów kluczowych i zarejestruje skuteczność tych wartości dla wszystkich słów kluczowych, trzeba zdecydować, co zrobić z tymi danymi. Pamiętaj, że ten sam modyfikator stawek może nie zapewniać takiej samej poprawy skuteczności w przypadku wszystkich słów kluczowych.

Przy określaniu najważniejszych danych dostępne są zarówno maks. stawka CPC słowa kluczowego, jak i różne wskaźniki skuteczności. Możesz potem sprawdzać każde słowo kluczowe w wielu interwałach i własnie wybrać najlepszą stawkę dla każdego z nich.

Konfiguracja

  • Zrób kopię tego arkusza kalkulacyjnego.
  • Zapisz adres URL kopii.
  • Utwórz nowy skrypt Google Ads na podstawie podanego niżej kodu źródłowego.
  • Zmień wartość zmiennej SPREADSHEET_URL na adres URL swojej kopii arkusza kalkulacyjnego.
  • Wybierz kampanię, w której chcesz testować stawki, i zmień odpowiednio wartość zmiennej CAMPAIGN_NAME.

Kod źródłowy

// 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 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - 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.
 */

const SPREADSHEET_URL = 'YOUR_SPREADSHEET_URL';
const CAMPAIGN_NAME = 'YOUR_CAMPAIGN_NAME';

const FIELDS = ['ad_group_criterion.keyword.text',
                'ad_group.cpc_bid_micros',
                'metrics.impressions',
                'metrics.clicks',
                'metrics.ctr',
                'campaign.id'];

function main() {
  validateCampaignName();
  console.log(`Using spreadsheet - ${SPREADSHEET_URL}`);
  const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());

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

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

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

  let shouldReport = multiplierRow > 1;
  let shouldIncreaseBids = multiplierRow < multipliers.length;
  const 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.
    const lastRun = multipliers[multiplierRow - 1][1];
    if (lastRun == today) {
      console.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.
      const permissions = multipliersSheet.getSheetProtection();
      permissions.setProtected(true);
      multipliersSheet.setSheetProtection(permissions);
      console.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) {
    console.log(`Script complete, all bid modifiers tested and reporting. ` +
      `Please remove this script's schedule.`);
  }
}

function updateBids(spreadsheet, multiplier) {
  console.log(`Applying bid multiplier of ${multiplier}`);

  let startingBids = getStartingBids(spreadsheet);
  if (!startingBids) {
    startingBids = recordStartingBids(spreadsheet);
  }
  const campaign = getCampaign();
  const keywordIter = campaign.keywords().get();
  for (const keyword of keywordIter) {
    let 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;
    }
    const newBid = oldBid * multiplier;
    keyword.bidding().setCpc(newBid);
  }
  saveStartingBids(spreadsheet, startingBids);
}

function outputReport(spreadsheet, start, end) {
  console.log(`Reporting on ${start} -> ${end}`);

  // Create a new sheet to output keywords to.
  const reportSheet = spreadsheet.insertSheet(`${start} - ${end}`);

  const rows = [['Keyword', 'Max CPC', 'Clicks', 'Impressions', 'Ctr']];
  const fields = FIELDS.join(",");
  const keywordIter =
        AdsApp.search(`SELECT ${fields} FROM keyword_view ` +
                      `WHERE campaign.name = '${CAMPAIGN_NAME}' ` +
                      `AND segments.date BETWEEN '${start}' AND '${end}'`);
  for (const keyword of keywordIter) {
    if (keyword.metrics.impressions == 0)
      keyword.metrics.ctr = 0;
    rows.push([keyword.adGroupCriterion.keyword.text,
               keyword.adGroup.cpcBidMicros/1000000,
               keyword.metrics.clicks,
               keyword.metrics.impressions,
               parseFloat(keyword.metrics.ctr).toFixed(5)]);
  }

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

function recordStartingBids(spreadsheet) {
  const startingBids = {};
  const keywords = getCampaign().keywords().get();
  for (const keyword of keywords) {
    const bid = keyword.bidding().getCpc() ||
          keyword.getAdGroup().bidding().getCpc();
    startingBids[keyword.getText()] = bid;
  }
  saveStartingBids(spreadsheet, startingBids);
  return startingBids;
}

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

function saveStartingBids(spreadsheet, startingBids) {
  let sheet = spreadsheet.getSheetByName('Starting Bids');
  if (!sheet) {
    sheet = spreadsheet.insertSheet('Starting Bids');
  }
  const rows = [];
  for (const 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);
}