Wielu licytujących – pojedyncze konto

Ikona określania stawek

Optymalizacja stawek za słowa kluczowe to jedno z najważniejszych działań podczas zarządzania kontami Google Ads. Stawki za słowa kluczowe mają bezpośredni wpływ na miejsce docelowe reklamy (jej pozycję w wyszukiwarce Google) oraz na jej koszt. Często musisz opracować własne strategie ustalania stawek, aby osiągnąć swoje cele.

Reguły automatyczne to funkcja Google Ads, która umożliwia modyfikowanie stawek za słowa kluczowe według ustalonych kryteriów i robienie tego zgodnie z harmonogramem. Jeśli masz dużą liczbę reguł automatycznych, może być trudno nimi zarządzać.

System wielu licytujących korzysta z arkusza kalkulacyjnego, który działa podobnie do reguł automatycznych. Każdy wiersz w arkuszu kalkulacyjnym odpowiada całej regule automatycznej. Zarządzanie tymi regułami w arkuszu kalkulacyjnym jest łatwiejsze niż w Google Ads.

Zrzut ekranu arkusza kalkulacyjnego dla wielu licytujących

Powyższy arkusz kalkulacyjny przedstawia jedną regułę, która:

  • Sprawdza statystyki z THIS_WEEK_SUN_TODAY.
  • Wyszukuje wszystkie słowa kluczowe w kampanii 1, które uzyskały więcej niż 1 wyświetlenie i który CTR jest wyższy niż 25%.
  • Zwiększa stawki o 10%, nie przekraczając 1,40 zł.

Jak to działa

Arkusz kalkulacyjny musi zawierać te kolumny:

  • Działanie
  • Argument
  • Zatrzymaj limit

W razie potrzeby możesz dodać lub usunąć inne kolumny. Poniżej znajdziesz prawidłowe przykłady nazw kolumn i odpowiadających im wartości komórek:

KolumnaWartość w komórce
CampaignName STARTS_WITH '?'Indonesia_
Conversions >= ?4
Status IN [?]'ENABLED', 'PAUSED'

Symbol ? w nazwie kolumny zostanie zastąpiony wartością w odpowiednim wierszu. Kompletny zestaw obsługiwanych kolumn znajdziesz w dokumentacji KeywordSelector.

Działanie

Action jest jedną z tych wartości:

  • Mnożenie przez: mnoży stawkę za słowo kluczowe przez argument. 1.1 zwiększa stawkę o 10%. 0.8 zmniejsza je o 20%.
  • Dodaj: dodaje argument do stawki za słowo kluczowe. 0.3 zwiększa stawkę o 0,30 zł (przy założeniu, że konto jest w walucie USD). -0.14 zmniejsza ją o 0,14 zł.
  • Ustaw na CPC za pierwszą stronę: ustawia stawkę CPC za słowo kluczowe na poziomie stawki CPC za pierwszą stronę. Argument jest ignorowany.
  • Ustaw na CPC u góry strony: ustawia stawkę za słowo kluczowe na poziomie CPC za górę strony. Argument jest ignorowany.

Zatrzymaj limit

Limit zatrzymania służy do ograniczania zmian stawek wprowadzanych przez skrypt. Jeśli zmiany stawki będą dodatnie, stawka nie wzrośnie do poziomu Zatrzymaj limit. Jeśli zmiana jest ujemna, stawka nie zmniejszy się nawet o wartość StopLimit. Jeśli np. stawka za słowo kluczowe wynosi obecnie 2 zł i podniesiesz ją o 25% przy limitze zatrzymania wynoszącym 3 zł, stawka wyniesie 2,50 zł. Jeśli jednak Limit zatrzymania jest ustawiony na 2,30 zł, stawka też wzrośnie.

Wyniki

Wyniki są generowane automatycznie przez skrypt. Zawiera on wszelkie błędy napotkane podczas wykonywania lub informację o liczbie słów kluczowych, które skrypt pobrał i próbował zmienić.

Pamiętaj, że „próbowana zmiana” może nie oznaczać rzeczywistej zmiany. Na przykład podanie wykluczającej stawki słowa kluczowego nie zadziała. Sprawdź w dziennikach wykonania, co dokładnie zrobił skrypt.

Kolumna Wyniki jest ostatnią kolumną w arkuszu kalkulacyjnym. Warunki nie będą obowiązywać, jeśli dodasz je po kolumnie Wyniki.

Planuję

Najczęściej używane opcje planowania w przypadku reguł ustalania stawek to Codziennie i Co tydzień.

Zwróć uwagę na to, jak częstotliwość harmonogramu może wpływać na używane zakresy dat statystyk. Statystyki Google Ads mogą być opóźnione o do 3 godzin, więc nie planuj skryptu Co godzinę.

Nie da się też w ogóle zaplanować scenariusza, jeśli uznasz to za stosowne.

Konfiguracja

  • Skonfiguruj skrypt oparty na arkuszu kalkulacyjnym za pomocą poniższego kodu źródłowego. Użyj szablonu arkusza kalkulacyjnego dla wielu licytujących.
  • Nie zapomnij zaktualizować kodu SPREADSHEET_URL w kodzie.
  • Pamiętaj, aby przed wykonaniem skryptu wyświetlić jego podgląd.
  • Zastanów się, czy zaplanować harmonogram skryptu.

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 Multi Bidder
 *
 * @overview The Multi Bidder script offers functionality similar to that of
 *     Automated Rules based on a spreadsheet. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/multi-bidder
 *     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.getMaxCpc() and keyword.setMaxCpc().
 * - version 1.0.2
 *   - Added validation of user settings.
 * - version 1.0.1
 *   - Improvements to time zone handling.
 * - version 1.0
 *   - Released initial version.
 */

const SPREADSHEET_URL = 'YOUR_SPREADSHEET_URL';

const spreadsheetAccess = new SpreadsheetAccess(SPREADSHEET_URL, 'Rules');

let totalColumns;

/**
 * The bids of keywords are modified based on the criterion mentioned
 * in the spreadsheet.
 */
function main() {
  // Make sure the spreadsheet is using the account's timezone.
  spreadsheetAccess.spreadsheet.setSpreadsheetTimeZone(
      AdsApp.currentAccount().getTimeZone());
  spreadsheetAccess.spreadsheet.getRangeByName('account_id').setValue(
      AdsApp.currentAccount().getCustomerId());
  const columns = spreadsheetAccess.sheet.getRange(5, 2, 5, 100).getValues()[0];
  for (let i = 0; i < columns.length; i++) {
    if (columns[i].length == 0 || columns[i] == 'Results') {
      totalColumns = i;
      break;
    }
  }
  if (columns[totalColumns] != 'Results') {
    spreadsheetAccess.sheet.getRange(5, totalColumns + 2, 1, 1).
        setValue('Results');
  }
  // clear the results column
  spreadsheetAccess.sheet.getRange(6, totalColumns + 2, 1000, 1).clear();

  let row = spreadsheetAccess.nextRow();

  while (row != null) {
    let argument;
    let stopLimit;
    try {
      argument = parseArgument(row);
      stopLimit = parseStopLimit(row);
    } catch (ex) {
      logError(ex);
      row = spreadsheetAccess.nextRow();
      continue;
    }
    let selector = AdsApp.keywords();
    for (let i = 3; i < totalColumns; i++) {
      let header = columns[i];
      let value = row[i];
      if (!isNaN(parseFloat(value)) || value.length > 0) {
        if (header.indexOf("'") > 0) {
          value = value.replace(/\'/g, "\\'");
        } else if (header.indexOf('\"') > 0) {
          value = value.replace(/"/g, '\\\"');
        }
        const condition = header.replace('?', value);
        selector.withCondition(condition);
      }
    }
    selector.forDateRange(spreadsheetAccess.spreadsheet.
        getRangeByName('date_range').getValue());

    const keywords = selector.get();

    try {
      keywords.hasNext();
    } catch (ex) {
      logError(ex);
      row = spreadsheetAccess.nextRow();
      continue;
    }

    let fetched = 0;
    let changed = 0;

    for (const keyword of keywords) {
      let oldBid = keyword.bidding().getCpc();
      let action = row[0];
      let newBid;

      fetched++;
      if (action == 'Add') {
        newBid = addToBid(oldBid, argument, stopLimit);
      } else if (action == 'Multiply by') {
        newBid = multiplyBid(oldBid, argument, stopLimit);
      } else if (action == 'Set to First Page Cpc' ||
        action == 'Set to Top of Page Cpc') {
        let newBid = action == 'Set to First Page Cpc' ?
            keyword.getFirstPageCpc() : keyword.getTopOfPageCpc();
        let isPositive = newBid > oldBid;
        newBid = applyStopLimit(newBid, stopLimit, isPositive);
      }
      if (newBid < 0) {
        newBid = 0.01;
      }
      newBid = newBid.toFixed(2);
      if (newBid != oldBid) {
        changed++;
      }
      keyword.bidding().setCpc(parseFloat(newBid));
    }
    logResult('Fetched ' + fetched + '\nChanged ' + changed);

    row = spreadsheetAccess.nextRow();
  }

  spreadsheetAccess.spreadsheet.getRangeByName('last_execution')
      .setValue(new Date());
}

/**
 * Performs addition on oldbid and argument.
 *
 * @param {string} oldBid The old bid value.
 * @param {string} argument The arugument value in the spreadsheet.
 * @param {string} stopLimit The changes made to the bids.
 * @return {string} Applies stop limit to the bid.
 */
function addToBid(oldBid, argument, stopLimit) {
  return applyStopLimit(oldBid + argument, stopLimit, argument > 0);
}

/**
 * Performs multiplication on oldbid and argument.
 *
 * @param {string} oldBid The old bid value.
 * @param {string} argument The arugument value in the spreadsheet.
 * @param {string} stopLimit The changes made to the bids.
 * @return {string} Applies the stop limit to the bid.
 */
function multiplyBid(oldBid, argument, stopLimit) {
  return applyStopLimit(oldBid * argument, stopLimit, argument > 1);
}

/**
 * Applies stop limit to the bid depending on the conditions.
 *
 * @param {string} newBid The calculated bid value as per the action
 *   in the spreadsheet.
 * @param {string} stopLimit The changes made to the bids.
 * @param {boolean} isPositive checks for the value sign.
 * @return {string} assigns stop limit to the bids and returns.
 */
function applyStopLimit(newBid, stopLimit, isPositive) {
  if (stopLimit) {
    if (isPositive && newBid > stopLimit) {
      newBid = stopLimit;
    } else if (!isPositive && newBid < stopLimit) {
      newBid = stopLimit;
    }
  }
  return newBid;
}

/**
 * If the argument is not specified or bad arguments are passed, an error is
 * returned in the result field.
 *
 * @param {!Object} row The row in the spreadsheet.
 * @return {string} Returns error message in the result column.
 */
function parseArgument(row) {
  if (row[1].length == 0 && (row[0] == 'Add' || row[0] == 'Multiply by')) {
    throw ('\"Argument\" must be specified.');
  }
  let argument = parseFloat(row[1]);
  if (isNaN(argument)) {
    throw 'Bad Argument: must be a number.';
  }
  return argument;
}

/**
 * Parses stop limit from the spreadsheet.
 *
 * @param {!Object} row The row in the spreadsheet.
 * @return {string} Returns error message to the Result field in the row
 */
function parseStopLimit(row) {
  if (row[2].length == 0) {
    return null;
  }
  let limit = parseFloat(row[2]);
  if (isNaN(limit)) {
    throw 'Bad Argument: must be a number.';
  }
  return limit;
}

/**
 * Format the error messages in the spreadsheet
 *
 * @param {string} error The error messages.
 */
function logError(error) {
  spreadsheetAccess.sheet.getRange(spreadsheetAccess.currentRow(),
      totalColumns + 2, 1, 1)
  .setValue(error)
  .setFontColor('#c00')
  .setFontSize(8)
  .setFontWeight('bold');
}

/**
 * Formats the result messages in the spreadsheet
 *
 * @param {string} result The result values.
 */
function logResult(result) {
  spreadsheetAccess.sheet.getRange(spreadsheetAccess.currentRow(),
      totalColumns + 2, 1, 1)
  .setValue(result)
  .setFontColor('#444')
  .setFontSize(8)
  .setFontWeight('normal');
}

/**
 * Provides access to the spreadsheet using spreadsheetUrl, sheetName
 * and validate the spreadsheet.
 *
 * @param {string} spreadsheetUrl The spreadsheet url.
 * @param {string} sheetName The spreadsheet name.
 * @return {string} Returns spreadsheet along with rows.
 */
function SpreadsheetAccess(spreadsheetUrl, sheetName) {
  Logger.log('Using spreadsheet - %s.', spreadsheetUrl);
  this.spreadsheet = validateAndGetSpreadsheet(spreadsheetUrl);

  this.sheet = this.spreadsheet.getSheetByName(sheetName);
  this.cells = this.sheet.getRange(6, 2, this.sheet.getMaxRows(),
      this.sheet.getMaxColumns()).getValues();
  this.rowIndex = 0;

  this.nextRow = function() {
    for (; this.rowIndex < this.cells.length; this.rowIndex++) {
      if (this.cells[this.rowIndex][0]) {
        return this.cells[this.rowIndex++];
      }
    }
    return null;
  };
  this.currentRow = function() {
    return this.rowIndex + 5;
  };
}

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