Два скрипта для эффективного управления ставками в Google AdWords

Одно из условий успешного продвижения в Google AdWords — корректировка ставок по ключевым словам, местоположению, устройству, времени суток, демографическим данным и т. д. С их помощью можно значительно повысить релевантность и эффективность рекламы.

В AdWords есть встроенные стратегии назначения ставок, которые автоматически учитывают эти параметры и помогают в управлении аккаунтом. Однако, на мой взгляд, эффективнее использовать некоторые дополнительные инструменты, которые помогают собрать точные данные и провести гибкую настройку.

В своей работе я использую несколько скриптов для автоматизации управления ставками. Они обеспечивают контроль и гибкость настроек уровнем выше, автоматически собирают нужные данные в таблицы, автоматизируют корректировку ставок и освобождают время на другие операции.

В этом материале я расскажу о двух скриптах, которые использую при ведении клиентских кампаний. Они имеют открытый исходный код, поэтому вы можете пользоваться ими бесплатно.

Тепловые карты

Мой любимый инструмент. С его помощью я анализирую данные аккаунта, чтобы понять, как зависит поведение пользователей от дня недели, времени суток и устройства.

Скрипт создает тепловые карты, которые представляют собой визуализированные данные аккаунта AdWords, с распределением кликов, показов, конверсий и других метрик по времени и устройствам.

Тепловая карта выглядит так:

Тепловая карта

На этом примере видно, как меняется среднее количество кликов в зависимости от времени суток и дня недели.

Скрипт «Тепловые карты» отлично визуализирует работу аккаунта и дает специалисту возможность найти варианты для оптимизации ставок.

Как установить и настроить

Первое, что нужно сделать — скопировать шаблон тепловой карты на свой Google Диск. Находится он по ссылке. Нажмите «Файл» в меню, затем — «Создать копию» и определите место хранения файла на вашем Google Диске.

Создание копии документа

Затем перейдите в аккаунт Google AdWords, нажмите на значок ключа и выберите вкладку «Скрипты».

Добавить скрипт

Дальше добавьте скрипт, нажав на синюю кнопку с плюсом:

Кнопка создания скрипта

Назовите скрипт и вставьте этот код в поле:

/**
*
* Heat Map Creation Tool - with Devices
*
* This script calculates the smoothed average performance of each hour of each day
* of the week, and outputs this into a heat map and graph in a Google Sheet. This
* can be done for all data and for device data. It also suggests ad schedules and
* device bid adjustments based on conversion rates.
*
* Version: 2.0
* Google AdWords Script maintained on brainlabsdigital.com
*
**/


//////////////////////////////////////////////////////////////////////////////
// Options 

var spreadsheetUrl = "https://docs.google.com/YOUR-SPREADSHEET-URL-HERE";
// The URL of the Google Doc the results will be put into.
// Copy the template at https://docs.google.com/spreadsheets/d/19OsCHG5JE_TqHHCZK1HNXyHizrJZ0_iT6dpqUOzvRB4/edit#gid=1022438191
// so you have the correct formatting and charts set up.

var dateRanges = ["2016-09-01,2016-10-31"];
// The start and end date of the date range for your data
// You can have multiple ranges, eg ["2016-06-01,2016-07-31","2016-09-01,2016-10-31"]
// would get data from June, July, September and October 2015.
// Format for each range is "yyyy-mm-dd,yyyy-mm-dd" (where the first date is the
// start of the range and the second is the end).

var ignoreDates = [];
// List any single days that are within your date range but whose data you do not
// want to use in calculations, for instance if they had atypical performance or
// there were technical issues with your site.
// eg ["2016-02-14","2016-03-27"] would mean data from Valentine's Day and Easter
// 2016 would be ignored.
// Format for each day is "yyyy-mm-dd"
// Leave as [] if unwanted.

var fields = ["Impressions", "Clicks", "Conversions"];
// Make heat maps of these fields.
// Allowed values: "Impressions", "Clicks", "Cost", "Conversions",
// "ConversionValue"

var calculatedFields = ["Clicks/Impressions","Conversions/Clicks"];
// Make heat maps of a stat calculated by dividing one field by another. 
// For example "Clicks/Impressions" will give the average clicks divided by the
// average impressions (ie the CTR).
// Allowed fields: "Impressions", "Clicks", "Cost", "Conversions",
// "ConversionValue"

var devices = ["Mobile"];
// Make heat maps and bid modifier suggestions for these devices
// Allowed fields: "Mobile", "Tablet", "Desktop"

var suggestAdSchedules = true;
// If true, the script will suggest hourly ad schedules, based on conversion rate.

var suggestDeviceBidModifiers = true;
// If true, the script will suggest bid modifiers for the devices specified above,
// based on the devices' conversion rates.

var baseDeviceModifiersOnBiddingMultiplier = true;
// If true, then the device bid modifiers given will be adjusted to take into
// account the suggested ad schedules.
// For example suppose that at a certain hour device bids should be increased by
// 30%, and the suggested ad schedule for that hour is 10%.
// If this is false, the the device modifier will be given as 30%.
// If this is true, then the device modifier will be given as 18%, because when
// this and the 10% ad schedules are applied this increases the bid by 30%.

var campaignNameDoesNotContain = [];
// Use this if you want to exclude some campaigns.
// For example ["Display"] would ignore any campaigns with 'Display' in the name,
// while ["Display","Competitors"] would ignore any campaigns with 'display' or
// 'competitors' in the name. Case insensitive.
// Leave as [] to not exclude any campaigns.

var campaignNameContains = [];
// Use this if you only want to look at some campaigns.
// For example ["Brand"] would only look at campaigns with 'Brand' in the name,
// while ["Brand","Generic"] would only look at campaigns with 'brand' or 'generic'
// in the name. Case insensitive.
// Leave as [] to include all campaigns.

var ignorePausedCampaigns = true;
// Set this to true to only look at currently active campaigns.
// Set to false to include campaigns that had impressions but are currently paused.


//////////////////////////////////////////////////////////////////////////////
// Advanced settings.

var smoothingWindow = [-2,   -1,   0,   1,    2   ];
var smoothingWeight = [0.25, 0.75, 1,   0.75, 0.25];
// The weights used for smoothing.
// The smoothingWindow gives the relative hour (eg 0 means the current hour,
// -2 means 2 hours before the current hour) and the smoothingWeight gives the
// weighting for that hour.

var minBidMultiplierSuggestion = -0.35;
var maxBidMultiplierSuggestion = 0.35;
// The minimum and maximum for the suggested bidding multipliers.


//////////////////////////////////////////////////////////////////////////////
function main() {
  
  // Check the spreadsheet works.
  var spreadsheet = checkSpreadsheet(spreadsheetUrl, "the spreadsheet");
  
  
  // Check the field names are correct, and get a list with the correct capitalisation
  var allowedFields = ["Conversions", "ConversionValue", "Impressions", "Clicks", "Cost"];
  var fieldsToCheck = [];
  for (var i=0; i<calculatedFields.length; i++) {
    if (calculatedFields[i].indexOf("/") === -1) {
      throw "Calculated Field " + calculatedFields[i] + " does not contain '/'";
    }
    var components = calculatedFields[i].split("/");
    fieldsToCheck = fieldsToCheck.concat(components);
    calculatedFields[i] = checkFieldNames(allowedFields, components, "calculatedFields", false);
  }
  var fieldsToCheck = fieldsToCheck.concat(fields);
  if (suggestAdSchedules || suggestDeviceBidModifiers) {
    var fieldsToCheck = fieldsToCheck.concat(["Clicks", "Conversions"]);
  }
  var allFields = checkFieldNames(allowedFields, fieldsToCheck, "fields", true);
  
  
  
  // Check there are date ranges and fields
  // - otherwise there'd be no data to put into heat maps
  if (dateRanges.length == 0) {
    throw "No date ranges given.";
  }
  if (allFields.length == 0) {
    throw "No fields were specified.";
  }
  
  
  // Check the device names are correct, and make WHERE statements for them
  var allowedDevices = ["Mobile", "Tablet", "Desktop"];
  devices = checkFieldNames(allowedDevices, devices, "devices", true);
  var whereStatements = [""]; // The blank one is for all devices
  for (var i=0; i<devices.length; i++) {
    if (devices[i] == "Mobile") {
      whereStatements.push("AND Device = HIGH_END_MOBILE ");
    } else {
       whereStatements.push("AND Device = " + devices[i].toUpperCase() + " ");
    }
  }
  
  var dayNames = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"];
  var dailyData = {}
  var numberDays = {};
  var smoothedData = {};
  
  var fieldsIncDevice = allFields.slice();
  for (var i=0; i<devices.length; i++) {
    fieldsIncDevice = fieldsIncDevice.concat(allFields.map(function (a) {return devices[i] + a;}));
  }
  
  
  // Initialise data
  for (var d=0; d<dayNames.length; d++) {
    smoothedData[dayNames[d]] = {};
    numberDays[dayNames[d]] = 0;
    smoothedData[dayNames[d]] = {};
    
    for (var h=0; h<24; h++) {
      smoothedData[dayNames[d]][h+""] = {};
      for (var f=0; f<fieldsIncDevice.length; f++) {
        smoothedData[dayNames[d]][h+""][fieldsIncDevice[f]] = 0;
      }
    }
    
  }
  
  
  // Get all the campaign IDs (based on campaignNameDoesNotContain, campaignNameContains
  // and ignorePausedCampaigns options).
  var campaignIds = getCampaignIds();
  
  
  // Construct the reports
  for (var d=0; d<dateRanges.length; d++) {
    for (var i=0; i<whereStatements.length; i++) {
      
      if (i == 0) {
        var fieldNames = allFields;
      } else {
        var fieldNames = allFields.map(function (a) {return devices[i-1] + a;});
      }
      
      var report = AdWordsApp.report("SELECT DayOfWeek, Date, HourOfDay, " + allFields.join(", ") + " " +
        "FROM CAMPAIGN_PERFORMANCE_REPORT " +
          "WHERE CampaignId IN [" + campaignIds.join(",") + "] " +
            whereStatements[i] +
              "DURING " + dateRanges[d].replace(/-/g,"")
              );
      
      var rows = report.rows();
      while (rows.hasNext()) {
        var row = rows.next();
        if (ignoreDates.indexOf(row["Date"]) > -1) {
          continue;
        }
        if (dailyData[row["Date"]] == undefined) {
          dailyData[row["Date"]] = {};
          dailyData[row["Date"]]["Day"] = row["DayOfWeek"];
          for (var h=0; h<24; h++) {
            dailyData[row["Date"]][h+""] = {};
            for (var f=0; f<fieldsIncDevice.length; f++) {
              dailyData[row["Date"]][h+""][fieldsIncDevice[f]] = 0;
            }
          }
        }
        
        for (var f=0; f<allFields.length; f++) {
          dailyData[row["Date"]][row["HourOfDay"]][fieldNames[f]] += parseInt(row[allFields[f]].replace(/,/g,""),10);
        }
      } // end while
      
    }// end for whereStatements
  }// end for dateRanges
  
  
  // Daily data is smoothed and totalled for each day of week
  for (var date in dailyData) {
    var day = dailyData[date]["Day"];
    numberDays[day]++;
    
    var dateBits = date.split("-");
    var yesterday = new Date(dateBits[0],parseInt(dateBits[1],10)-1,parseInt(dateBits[2],10)-1);
    var tomorrow = new Date(dateBits[0],parseInt(dateBits[1],10)-1,parseInt(dateBits[2],10)+1);
    yesterday = Utilities.formatDate(yesterday, "UTC", "yyyy-MM-dd");
    tomorrow = Utilities.formatDate(tomorrow, "UTC", "yyyy-MM-dd");
    
    for (var h=0; h<24; h++) {
      for (var f=0; f<fieldsIncDevice.length; f++) {
        var totalWeight = 0;
        var smoothedTotal = 0;
        
        for (var w=0; w<smoothingWindow.length; w++) {
          if (h + smoothingWindow[w] < 0) {
            if (dailyData[yesterday] != undefined) {
              totalWeight += smoothingWeight[w];
              smoothedTotal += smoothingWeight[w] * dailyData[yesterday][(h + smoothingWindow[w] + 24)][fieldsIncDevice[f]];
            }
          } else if (h + smoothingWindow[w] > 23) {
            if (dailyData[tomorrow] != undefined) {
              totalWeight += smoothingWeight[w];
              smoothedTotal += smoothingWeight[w] * dailyData[tomorrow][(h + smoothingWindow[w] - 24)][fieldsIncDevice[f]];
            }
          } else {
            totalWeight += smoothingWeight[w];
            smoothedTotal += smoothingWeight[w] * dailyData[date][(h + smoothingWindow[w])][fieldsIncDevice[f]];
          }
        }
        if (totalWeight != 0) {
          smoothedData[day][h][fieldsIncDevice[f]] += smoothedTotal / totalWeight;
        }
      }
    }
  } // end for dailyData
  Logger.log("Collected daily data.");
  
  
  // Calculate the averages from the smoothed data
  var hourlyAvg = {};
  var totalConversions = 0;
  var totalClicks = 0;
  var deviceClicks = {};
  var deviceConversions = {};
  for (var i=0; i<devices.length; i++) {
    deviceClicks[devices[i]] = 0;
    deviceConversions[devices[i]] = 0;
  }
  
  for (var d=0; d<dayNames.length; d++) {
    hourlyAvg[dayNames[d]] = {};
    for (var h=0; h<24; h++) {
      hourlyAvg[dayNames[d]][h+""] = {}
      
      if (numberDays[dayNames[d]] == 0) {
        for (var f=0; f<fieldsIncDevice.length; f++) {
          hourlyAvg[dayNames[d]][h+""][fieldsIncDevice[f]] = "-";
        }
        continue;
      }
      
      for (var f=0; f<fieldsIncDevice.length; f++) {
        hourlyAvg[dayNames[d]][h+""][fieldsIncDevice[f]] = smoothedData[dayNames[d]][h+""][fieldsIncDevice[f]]/numberDays[dayNames[d]];
      }
      
      for (var c=0; c<calculatedFields.length; c++) {
        
        var multiplier = smoothedData[dayNames[d]][h+""][calculatedFields[c][0]];
        var divisor = smoothedData[dayNames[d]][h+""][calculatedFields[c][1]];
        
        if (divisor == 0 || divisor == "-" || multiplier == "-") {
          hourlyAvg[dayNames[d]][h+""][calculatedFields[c].join("/")] = "-";
        } else {
          hourlyAvg[dayNames[d]][h+""][calculatedFields[c].join("/")] = multiplier / divisor;
        }
        
        for (var i=0; i<devices.length; i++) {
          var multiplier = smoothedData[dayNames[d]][h+""][devices[i]+calculatedFields[c][0]];
          var divisor = smoothedData[dayNames[d]][h+""][devices[i]+calculatedFields[c][1]];
          
          if (divisor == 0 || divisor == "-" || multiplier == "-") {
            hourlyAvg[dayNames[d]][h+""][devices[i]+calculatedFields[c].join("/")] = "-";
          } else {
            hourlyAvg[dayNames[d]][h+""][devices[i]+calculatedFields[c].join("/")] = multiplier / divisor;
          }
          
        }
      }
      
      // Add up the clicks and conversions, for generating the suggested ad schedules
      if (suggestAdSchedules || suggestDeviceBidModifiers) {
        totalConversions += smoothedData[dayNames[d]][h+""]["Conversions"];
        totalClicks += smoothedData[dayNames[d]][h+""]["Clicks"];
        if (suggestDeviceBidModifiers) {
          for (var i=0; i<devices.length; i++) {
            deviceClicks[devices[i]] += smoothedData[dayNames[d]][h+""][devices[i]+"Clicks"];
            deviceConversions[devices[i]] += smoothedData[dayNames[d]][h+""][devices[i]+"Conversions"];
          }
        }
      }
      
    }
  }
  
  
  // Calculate suggested ad schedules based on the average conversion rate
  if (suggestAdSchedules || suggestDeviceBidModifiers) {
    if (totalClicks == 0) {
      var meanConvRate = 0;
    } else {
      var meanConvRate = totalConversions / totalClicks;
    }
        
    for (var d=0; d<dayNames.length; d++) {
      for (var h=0; h<24; h++) {
        if (meanConvRate == 0 || smoothedData[dayNames[d]][h+""]["Clicks"] == 0) {
          hourlyAvg[dayNames[d]][h+""]["AdSchedules"] = "-";
        } else {
          var convRate = smoothedData[dayNames[d]][h+""]["Conversions"] / smoothedData[dayNames[d]][h+""]["Clicks"];
          
          // The suggested multiplier is generated from the mean.
          // It is dampened by taking the square root.
          var multiplier = Math.sqrt(convRate/meanConvRate)-1;
          
          if (multiplier > maxBidMultiplierSuggestion) {
            multiplier = maxBidMultiplierSuggestion;
          } else if (multiplier < minBidMultiplierSuggestion) {
            multiplier = minBidMultiplierSuggestion;
          } 
          hourlyAvg[dayNames[d]][h+""]["AdSchedules"] = multiplier;
        }
        
      }
    }
    
    // Device level bid modifiers
    if (suggestDeviceBidModifiers) {
      var deviceConvRate = {};
      for (var i=0; i<devices.length; i++) {
        if (deviceClicks[devices[i]] == 0) {
          deviceConvRate[devices[i]] = 0;
        } else {
          deviceConvRate[devices[i]] = deviceConversions[devices[i]] / deviceClicks[devices[i]];
        }
      }
      
      for (var d=0; d<dayNames.length; d++) {
        for (var i=0; i<devices.length; i++) {
          for (var h=0; h<24; h++) {
            if (hourlyAvg[dayNames[d]][h+""]["AdSchedules"] == "-" || deviceConvRate[i] == 0 || smoothedData[dayNames[d]][h+""][devices[i] + "Clicks"] == 0) {
              hourlyAvg[dayNames[d]][h+""][devices[i] + "BidModifiers"] = "-";
            } else {
              var convRate = smoothedData[dayNames[d]][h+""][devices[i] + "Conversions"] / smoothedData[dayNames[d]][h+""][devices[i] + "Clicks"];
              
              // We calculate the multiplier we want to end up with
              var endMultiplier = Math.sqrt(convRate/deviceConvRate[devices[i]])-1;
              
              if (baseDeviceModifiersOnBiddingMultiplier) {
                // The bid modifier is calculated so that if the bidding multiplier is set up as an
                // ad schedule, this is the correct device bid modifier to get the desired multiplier
                var modifier = ((1+endMultiplier)/(1+hourlyAvg[dayNames[d]][h+""]["AdSchedules"])) - 1;
              } else {
                var modifier = endMultiplier;
              }
              
              if (modifier > maxBidMultiplierSuggestion) {
                modifier = maxBidMultiplierSuggestion;
              } else if (modifier < minBidMultiplierSuggestion) {
                modifier = minBidMultiplierSuggestion;
              }
              hourlyAvg[dayNames[d]][h+""][devices[i] + "BidModifiers"] = modifier;
            }
          } 
        }
      }
    }
    
  } // end if suggestAdSchedules or suggestDeviceBidModifiers
  Logger.log("Averaged and smoothed data.");
  
  
  // Make the heat maps on the spreadsheet
  var sheet0 = spreadsheet.getSheets()[0];
  var calculatedFieldNames = calculatedFields.map(function (arr){return arr.join("/")});
  var baseFields = checkFieldNames(allowedFields, fields, "", true).concat(calculatedFieldNames);
  var allFieldNames = baseFields.slice();
  for (var i=0; i<devices.length; i++) {
    allFieldNames = allFieldNames.concat(baseFields.map(function (a) {return devices[i] + a;}));
  }
  if (suggestAdSchedules) {
    allFieldNames.push("AdSchedules");
  }
  if (suggestDeviceBidModifiers) {
    for (var i=0; i<devices.length; i++) {
      allFieldNames.push(devices[i] + "BidModifiers");
    }
  }
  
  if (sheet0.getName() == "Template") {
    sheet0.setName(allFieldNames[0].replace(/[A-Z\/]/g, function (x){return " " + x;}).trim());
  }
  
  for (var f=0; f<allFieldNames.length; f++) {
    var fieldName = allFieldNames[f].replace(/[A-Z\/]/g, function (x){return " " + x;}).trim();
    var sheet = spreadsheet.getSheetByName(fieldName);
    if (sheet == null) {
      sheet = sheet0.copyTo(spreadsheet);
      sheet.setName(fieldName);
    }
    sheet.getRange(1, 1).setValue(fieldName);
    
    //Post the heat map data
    var sheetData = [];
    sheetData.push([""].concat(dayNames)); // The header
    var totalValue = 0;
    for (var h=0; h<24; h++) {
      var rowData = [h];
      for (var d=0; d<dayNames.length; d++) {
        if (hourlyAvg[dayNames[d]][h+""][allFieldNames[f]] == undefined) {
          rowData.push("-");
        } else {
          rowData.push(hourlyAvg[dayNames[d]][h+""][allFieldNames[f]]);
        }
        totalValue += hourlyAvg[dayNames[d]][h+""][allFieldNames[f]];
      }
      sheetData.push(rowData);
    }
    sheet.getRange(3, 1, sheetData.length, sheetData[0].length).setValues(sheetData);
    
    // Work out which format to use and format the numbers in the heat map
    var averageValue = totalValue / (24*7);
    if (averageValue < 50) {
      var format = "#,##0.0";
    } else {
      var format = "#,###,##0";
    }
    if (allFieldNames[f].indexOf("/") > -1) {
      var components = allFieldNames[f].split("/");
      var multiplierIsMoney = (components[0] == "Cost" || components[0] == "ConversionValue");
      var divisorIsMoney = (components[1] == "Cost" || components[1] == "ConversionValue");
      if ((!multiplierIsMoney && !divisorIsMoney) || (multiplierIsMoney && divisorIsMoney)) {
        // If neither component is monetary, or both components are, then the result is a percentage
        format = "#,##0.00%";
      }
    }
    if (allFieldNames[f] == "AdSchedules" || allFieldNames[f].substr(-12) == "BidModifiers") {
      format = "#,##0.00%";
    }
    sheet.getRange(4, 2, sheetData.length, sheetData[0].length).setNumberFormat(format);
    
    // Update the chart title
    var charts = sheet.getCharts();
    if (sheet.getCharts().length === 0) {
      Logger.log("Warning: chart missing from the " + fieldName + " sheet."); 
    } else {
      var chart = charts[0];
      chart = chart.modify().setOption('title', fieldName).build();
      sheet.updateChart(chart);
    }
  }
  
  Logger.log("Posted data to spreadsheet.");
  Logger.log("Finished.");
}


// Check the spreadsheet URL has been entered, and that it works
function checkSpreadsheet(spreadsheetUrl, spreadsheetName) {
  if (spreadsheetUrl.replace(/[AEIOU]/g,"X") == "https://docs.google.com/YXXR-SPRXXDSHXXT-XRL-HXRX") {
    throw("Problem with " + spreadsheetName + " URL: make sure you've replaced the default with a valid spreadsheet URL.");
  }
  try {
    var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
    
    // Checks if you can edit the spreadsheet
    var sheet = spreadsheet.getSheets()[0];
    var sheetName = sheet.getName();
    sheet.setName(sheetName);
    
    return spreadsheet;
  } catch (e) {
    throw("Problem with " + spreadsheetName + " URL: '" + e + "'");
  }
}


// Get the IDs of campaigns which match the given options
function getCampaignIds() {
  var whereStatement = "WHERE ";
  var whereStatementsArray = [];
  var campaignIds = [];
  
  if (ignorePausedCampaigns) {
    whereStatement += "CampaignStatus = ENABLED ";
  } else {
    whereStatement += "CampaignStatus IN ['ENABLED','PAUSED'] ";
  }
  
  for (var i=0; i<campaignNameDoesNotContain.length; i++) {
    whereStatement += "AND CampaignName DOES_NOT_CONTAIN_IGNORE_CASE '" + campaignNameDoesNotContain[i].replace(/"/g,'\\\"') + "' ";
  }
  
  if (campaignNameContains.length == 0) {
    whereStatementsArray = [whereStatement];
  } else {
    for (var i=0; i<campaignNameContains.length; i++) {
      whereStatementsArray.push(whereStatement + 'AND CampaignName CONTAINS_IGNORE_CASE "' + campaignNameContains[i].replace(/"/g,'\\\"') + '" ');
    }
  }
  
  for (var i=0; i<whereStatementsArray.length; i++) {
    var report = AdWordsApp.report(
      "SELECT CampaignId " +
      "FROM   CAMPAIGN_PERFORMANCE_REPORT " +
      whereStatementsArray[i] +
      "DURING LAST_30_DAYS");
    
    var rows = report.rows();
    while (rows.hasNext()) {
      var row = rows.next();
      campaignIds.push(row['CampaignId']);
    }
  }
  
  if (campaignIds.length == 0) {
    throw("No campaigns found with the given settings.");
  }
  
  return campaignIds;
}


// Verify that all field names are valid, and return a list of them with the
// correct capitalisation. If deduplicate is true, the list is deduplicated
function checkFieldNames(allowedFields, givenFields, souceName, deduplicate) {
  var allowedFieldsLowerCase = allowedFields.map(function (str){return str.toLowerCase()});
  var wantedFields = [];
  var unrecognisedFields = [];
  for (var i=0; i<givenFields.length; i++) {
    var fieldIndex = allowedFieldsLowerCase.indexOf(givenFields[i].toLowerCase().replace(" ","").trim());
    if(fieldIndex === -1){
      unrecognisedFields.push(givenFields[i]);
    } else if(!deduplicate || wantedFields.indexOf(allowedFields[fieldIndex]) < 0) {
      wantedFields.push(allowedFields[fieldIndex]);
    }
  }
  
  if (unrecognisedFields.length > 0) {
    throw unrecognisedFields.length + " field(s) not recognised in '" + souceName + "': '" + unrecognisedFields.join("', '") + 
      "'. Please choose from '" + allowedFields.join("', '") + "'.";
  }
  
  return wantedFields;
}

Затем настройте скрипт, внеся несколько изменений:

  • spreadsheetUrl — это адрес вашего документа с шаблоном тепловой карты;

  • dateRanges отображает период, за который вы хотите получить данные. Каждый диапазон дат использует формат «гггг-мм-дд,гггг-мм-дд», например [«2018-05-01,2018-07-31»]. Также можно указать несколько диапазонов дат, достаточно разделить их запятыми, например [«2018-05-01,2018-06-30», «2018-08-01,2018-09-30»];

  • ignoreDates используется, когда необходимо исключить данные определенных дней. Это список дат, разделенных запятыми, в формате «гггг-мм-дд». Например, с помощью ["2018-03-08","2018-05-01«] исключены праздники 8 марта и 1 мая;

  • fields — в этом поле нужно добавить показатели, для которых будут созданы тепловые карты. Поместите их в кавычки, разделенные запятыми. Я обычно использую показы, клики, стоимость, конверсии;

  • calculateFields — это метрики, которые должны рассчитываться из показателей fields. Например [«Clicks/Impressions», «Conversions/Clicks»]. Первое — CTR, второе — коэффициент конверсии;

  • devices — позволяет разделить тепловые карты по типам устройств, можно указать desktop, mobile, tablet. Если нужен только общий трафик, оставьте это поле пустым;

  • campaignNameContains и campaignNameDoesNotContain — фильтр для определенных кампаний. Если хотите проанализировать определенные кампании, добавьте их название в первое поле. Во втором отмечайте те кампании, данные которых вы хотите исключить из тепловой карты. Оставьте поле пустым, чтобы данные всех кампаний были внесены в таблицу;

  • ignorePausedCampaigns — игнорирует приостановленные кампании. Установите значение «true», если вы хотите посмотреть данные только активных кампаний, или «false», чтобы включить в карту приостановленные кампании. Важно! Удаленные кампании всегда игнорируются и не учитываются в картах.

Далее авторизируемся, активируем скрипт и нажимаем «Выполнить».

Интерфейс работы со скриптами

Важно! Не пугайтесь, если увидите небольшие расхождения показателей в AdWords и таблице. Скрипт показывает среднее значение в час, а не общее. Это позволяет исключить небольшие всплески и спады активности. Чтобы картина была точнее, рекомендуется использовать скрипт после шести недель стабильной работы рекламного аккаунта.

Ставки 24/7

ROI и коэффициент конверсии меняются в зависимости от времени суток, дней недели и устройств.

Для примера рассмотрим службу доставки пиццы. Не так много людей заказывают пиццу в 10 утра, однако многие оформляют заказ примерно в 18:00-19:00. Значит, время суток и день недели для этого бизнеса имеют огромное значение. Также важно устройство, с которого пользователь делает заказ. Поведение потенциального клиента существенно меняется в зависимости от того, ищет ли он товар в рабочее время за рабочим столом или вводит запрос на мобильном устройстве во время отдыха.

Именно поэтому ставки всегда должны быть правильно установлены по отношению к изменениям времени и устройствам. Для этого идеально подходит скрипт «Ставки 24/7». Он позволяет менять ставки каждый час для любого устройства.

Например, зачем тратить большие деньги за первую-вторую позиции в понедельник, когда много трафика, но коэффициент конверсии низкий? Или почему объявление с 20:00 до 21:00 в воскресенье находится на средней позиции 4,2? Ведь в это время наши пользователи конвертируются с большой вероятностью. Скрипт «Ставки 24/7» помогает вступать в торги на полную именно тогда, когда пользователи с большой вероятностью будут становиться клиентами.

Я постоянно использую этот инструмент и с его помощью увеличиваю количество конверсий на 14-30%, сохраняя CPA на прежнем уровне.

Как установить и настроить

В поле для кода на странице скриптов AdWords добавляем этот шаблон:

/*
*
* Advanced ad scheduling
*
* This script will apply ad schedules to campaigns or shopping campaigns and set
* the ad schedule bid modifier and mobile bid modifier at each hour according to
* multiplier timetables in a Google sheet.
*
* This version creates schedules with modifiers for 4 hours, then fills the rest
* of the day and the other days of the week with schedules with no modifier as a
* fail safe.
*
* Version: 3.1
* Updated to allow -100% bids, change mobile adjustments and create fail safes.
* brainlabsdigital.com
*
*/
 
function main() {
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //Options
 
  //The Google sheet to use
  //The default value is the example sheet
  var spreadsheetUrl = "https://docs.google.com/a/brainlabsdigital.com/spreadsheets/d/1JDGBPs2qyGdHd94BRZw9lE9JFtoTaB2AmlL7xcmLx2g/edit#gid=0";
 
  //Shopping or regular campaigns
  //Use true if you want to run script on shopping campaigns (not regular campaigns).
  //Use false for regular campaigns.
  var shoppingCampaigns = false;
 
  //Use true if you want to set mobile bid adjustments as well as ad schedules.
  //Use false to just set ad schedules.
  var runMobileBids = false;
 
  //Optional parameters for filtering campaign names. The matching is case insensitive.
  //Select which campaigns to exclude e.g ["foo", "bar"] will ignore all campaigns
  //whose name contains 'foo' or 'bar'. Leave blank [] to not exclude any campaigns.
  var excludeCampaignNameContains = [];
 
  //Select which campaigns to include e.g ["foo", "bar"] will include only campaigns
  //whose name contains 'foo' or 'bar'. Leave blank [] to include all campaigns.
  var includeCampaignNameContains = [];
 
  //When you want to stop running the ad scheduling for good, set the lastRun
  //variable to true to remove all ad schedules.
  var lastRun = false;
 
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
  //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
 
  //Initialise for use later.
  var weekDays = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"];
  var adScheduleCodes = [];
  var campaignIds = [];
 
  //Retrieving up hourly data
  var scheduleRange = "B2:H25";
  var accountName = AdWordsApp.currentAccount().getName();
  var spreadsheet = SpreadsheetApp.openByUrl(spreadsheetUrl);
  var sheets = spreadsheet.getSheets();
 
  var timeZone = AdWordsApp.currentAccount().getTimeZone();
  var date = new Date();
  var dayOfWeek = parseInt(Utilities.formatDate(date, timeZone, "uu"), 10) - 1;
  var hour = parseInt(Utilities.formatDate(date, timeZone, "HH"), 10);
 
  var sheet = sheets[0];
  var data = sheet.getRange(scheduleRange).getValues();
 
  //This hour's bid multiplier.
  var thisHourMultiplier = data[hour][dayOfWeek];
  var lastHourCell = "I2";
  sheet.getRange(lastHourCell).setValue(thisHourMultiplier);
 
  //The next few hours' multipliers
  var timesAndModifiers = [];
  var otherDays = weekDays.slice(0);
  for (var h=0; h<5; h++) {
    var newHour = (hour + h)%24;
    if (hour + h > 23) {
      var newDay = (dayOfWeek + 1)%7;
    } else {
      var newDay = dayOfWeek;
    }
    otherDays[newDay] = "-";
 
    if (h<4) {
      // Use the specified bids for the next 4 hours
      var bidModifier = data[newHour][newDay];
      if (isNaN(bidModifier) || (bidModifier < -0.9 && bidModifier > -1) || bidModifier > 9) {
        Logger.log("Bid modifier '" + bidModifier + "' for " + weekDays[newDay] + " " + newHour + " is not valid.");
        timesAndModifiers.push([newHour, newHour+1, weekDays[newDay], 0]);
      } else if (bidModifier != -1 && bidModifier.length != 0) {
        timesAndModifiers.push([newHour, newHour+1, weekDays[newDay], bidModifier]);
      }
    } else {
      // Fill in the rest of the day with no adjustment (as a back-up incase the script breaks)
      timesAndModifiers.push([newHour, 24, weekDays[newDay], 0]);
    }
  }
 
  if (hour>0) {
    timesAndModifiers.push([0, hour, weekDays[dayOfWeek], 0]);
  }
 
  for (var d=0; d<otherDays.length; d++) {
    if (otherDays[d] != "-") {
      timesAndModifiers.push([0, 24, otherDays[d], 0]);
    }
  }
 
  //Pull a list of all relevant campaign IDs in the account.
  var campaignSelector = ConstructIterator(shoppingCampaigns);
  for(var i = 0; i < excludeCampaignNameContains.length; i++){
    campaignSelector = campaignSelector.withCondition('Name DOES_NOT_CONTAIN_IGNORE_CASE "' + excludeCampaignNameContains[i] + '"');
  }
  campaignSelector = campaignSelector.withCondition("Status IN [ENABLED,PAUSED]");
  var campaignIterator = campaignSelector.get();
  while(campaignIterator.hasNext()){
    var campaign = campaignIterator.next();
    var campaignName = campaign.getName();
    var includeCampaign = false;
    if(includeCampaignNameContains.length === 0){
      includeCampaign = true;
    }
    for(var i = 0; i < includeCampaignNameContains.length; i++){
      var index = campaignName.toLowerCase().indexOf(includeCampaignNameContains[i].toLowerCase());
      if(index !== -1){
        includeCampaign = true;
        break;
      }
    }
    if(includeCampaign){
      var campaignId = campaign.getId();
      campaignIds.push(campaignId);
    }
  }
 
  //Return if there are no campaigns.
  if(campaignIds.length === 0){
    Logger.log("There are no campaigns matching your criteria.");
    return;
  }
 
  //Remove all ad scheduling for the last run.
  if(lastRun){
    checkAndRemoveAdSchedules(campaignIds, []);
    return;
  }
 
  // Change the mobile bid adjustment
  if(runMobileBids){
    if (sheets.length < 2) {
      Logger.log("Mobile ad schedule sheet was not found in the Google spreadsheet.");
    } else {
      var sheet = sheets[1];
      var data = sheet.getRange(scheduleRange).getValues();
      var thisHourMultiplier_Mobile = data[hour][dayOfWeek];
 
      if (thisHourMultiplier_Mobile.length === 0) {
        thisHourMultiplier_Mobile = -1;
      }
 
      if (isNaN(thisHourMultiplier_Mobile) || (thisHourMultiplier_Mobile < -0.9 && thisHourMultiplier_Mobile > -1) || thisHourMultiplier_Mobile > 3) {
        Logger.log("Mobile bid modifier '" + thisHourMultiplier_Mobile + "' for " + weekDays[dayOfWeek] + " " + hour + " is not valid.");
        thisHourMultiplier_Mobile = 0;
      }
 
      var totalMultiplier = ((1+thisHourMultiplier_Mobile)*(1+thisHourMultiplier))-1;
      sheet.getRange("I2").setValue(thisHourMultiplier_Mobile);
      sheet.getRange("T2").setValue(totalMultiplier);
      ModifyMobileBidAdjustment(campaignIds, thisHourMultiplier_Mobile);
    }
  }
 
  // Check the existing ad schedules, removing those no longer necessary
  var existingSchedules = checkAndRemoveAdSchedules(campaignIds, timesAndModifiers);
 
  // Add in the new ad schedules
  AddHourlyAdSchedules(campaignIds, timesAndModifiers, existingSchedules, shoppingCampaigns);
 
}
 
/**
* Function to add ad schedules for the campaigns with the given IDs, unless the schedules are
* referenced in the existingSchedules array. The scheduling will be added as a hour long periods
* as specified in the passed parameter array and will be given the specified bid modifier.
*
* @param array campaignIds array of campaign IDs to add ad schedules to
* @param array timesAndModifiers the array of [hour, day, bid modifier] for which to add ad scheduling
* @param array existingSchedules array of strings identifying already existing schedules.
* @param bool shoppingCampaigns using shopping campaigns?
* @return void
*/
function AddHourlyAdSchedules(campaignIds, timesAndModifiers, existingSchedules, shoppingCampaigns){
  // times = [[hour,day],[hour,day]]
  var campaignIterator = ConstructIterator(shoppingCampaigns)
  .withIds(campaignIds)
  .get();
  while(campaignIterator.hasNext()){
    var campaign = campaignIterator.next();
    for(var i = 0; i < timesAndModifiers.length; i++){
      if (existingSchedules.indexOf(
        timesAndModifiers[i][0] + "|" + (timesAndModifiers[i][1]) + "|" + timesAndModifiers[i][2]
          + "|" + Utilities.formatString("%.2f",(timesAndModifiers[i][3]+1)) + "|" + campaign.getId())
      > -1) {
 
        continue;
      }
 
      campaign.addAdSchedule({
        dayOfWeek: timesAndModifiers[i][2],
        startHour: timesAndModifiers[i][0],
        startMinute: 0,
        endHour: timesAndModifiers[i][1],
        endMinute: 0,
        bidModifier: Math.round(100*(1+timesAndModifiers[i][3]))/100
      });
    }
  }
}
 
/**
* Function to remove ad schedules from all campaigns referenced in the passed array
* which do not correspond to schedules specified in the passed timesAndModifiers array.
*
* @param array campaignIds array of campaign IDs to remove ad scheduling from
* @param array timesAndModifiers array of [hour, day, bid modifier] of the wanted schedules
* @return array existingWantedSchedules array of strings identifying the existing undeleted schedules
*/
function checkAndRemoveAdSchedules(campaignIds, timesAndModifiers) {
 
  var adScheduleIds = [];
 
  var report = AdWordsApp.report(
    'SELECT CampaignId, Id ' +
    'FROM CAMPAIGN_AD_SCHEDULE_TARGET_REPORT ' +
    'WHERE CampaignId IN ["' + campaignIds.join('","')  + '"]'
  );
 
  var rows = report.rows();
  while(rows.hasNext()){
    var row = rows.next();
    var adScheduleId = row['Id'];
    var campaignId = row['CampaignId'];
    if (adScheduleId == "--") {
      continue;
    }
    adScheduleIds.push([campaignId,adScheduleId]);
  }
 
  var chunkedArray = [];
  var chunkSize = 10000;
 
  for(var i = 0; i < adScheduleIds.length; i += chunkSize){
    chunkedArray.push(adScheduleIds.slice(i, i + chunkSize));
  }
 
  var wantedSchedules = [];
  var existingWantedSchedules = [];
 
  for (var j=0; j<timesAndModifiers.length; j++) {
    wantedSchedules.push(timesAndModifiers[j][0] + "|" + (timesAndModifiers[j][1]) + "|" + timesAndModifiers[j][2] + "|" + Utilities.formatString("%.2f",timesAndModifiers[j][3]+1));
  }
 
  for(var i = 0; i < chunkedArray.length; i++){
    var unwantedSchedules = [];
 
    var adScheduleIterator = AdWordsApp.targeting()
    .adSchedules()
    .withIds(chunkedArray[i])
    .get();
    while (adScheduleIterator.hasNext()) {
      var adSchedule = adScheduleIterator.next();
      var key = adSchedule.getStartHour() + "|" + adSchedule.getEndHour() + "|" + adSchedule.getDayOfWeek() + "|" + Utilities.formatString("%.2f",adSchedule.getBidModifier());
 
      if (wantedSchedules.indexOf(key) > -1) {
        existingWantedSchedules.push(key + "|" + adSchedule.getCampaign().getId());
      } else {
        unwantedSchedules.push(adSchedule);
      }
    }
 
    for(var j = 0; j < unwantedSchedules.length; j++){
      unwantedSchedules[j].remove();
    }
  }
 
  return existingWantedSchedules;
}
 
/**
* Function to construct an iterator for shopping campaigns or regular campaigns.
*
* @param bool shoppingCampaigns Using shopping campaigns?
* @return AdWords iterator Returns the corresponding AdWords iterator
*/
function ConstructIterator(shoppingCampaigns){
  if(shoppingCampaigns === true){
    return AdWordsApp.shoppingCampaigns();
  }
  else{
    return AdWordsApp.campaigns();
  }
}
 
/**
* Function to set a mobile bid modifier for a set of campaigns
*
* @param array campaignIds An array of the campaign IDs to be affected
* @param Float bidModifier The multiplicative mobile bid modifier
* @return void
*/
function ModifyMobileBidAdjustment(campaignIds, bidModifier){
 
  var platformIds = [];
  var newBidModifier = Math.round(100*(1+bidModifier))/100;
 
  for(var i = 0; i < campaignIds.length; i++){
    platformIds.push([campaignIds[i],30001]);
  }
 
  var platformIterator = AdWordsApp.targeting()
  .platforms()
  .withIds(platformIds)
  .get();
  while (platformIterator.hasNext()) {
    var platform = platformIterator.next();
    platform.setBidModifier(newBidModifier);
  }
}

Корректировки ставок устанавливаются в Google Таблицах. Сохраните себе копию этого шаблона и выставьте в нем свои значения. Документ должен выглядеть примерно так:

Stavki-24-na-7

В ячейках от B2 до H25 вводим корректировки ставок: ставки 0% не изменяют ничего, а показатели 25% и — 25% соответственно увеличивают и уменьшают ставки в кампаниях на 25%.

Если вы также хотите использовать корректировки для мобильных устройств, перейдите на второй лист документа и заполните таблицу.

корректировки для мобильных

Затем нужно настроить этот скрипт:

  • spreadsheetUrl — это адрес вашего документа с коэффициентами ставок (добавьте URL);

  • установите значение true для переменной shoppingCampaigns, если нужно использовать скрипт для корректировки торговых кампаний, или false, чтобы использовать только в поисковых;

  • установите для runMobileBids значение true, если хотите изменить корректировки ставок для мобильных устройств;

  • чтобы использовать скрипт для определенных кампаний, используйте excludeCampaignNameContains и includeCampaignNameContains.

Затем авторизуйтесь (активируйте скрипт) и нажмите «Выполнить». Когда скрипт будет полностью настроен, укажите частоту изменений — «Каждый час».

каждый час частота

А теперь сидим сложа руки и наблюдаем за ростом коэффициента конверсии.

Заключение

Каждый день появляются новые скрипты, инструменты и нововведения AdWords. Рынок контекстной рекламы находится в постоянном потоке изменений. Принципы управления рекламными аккаунтами даже на микроуровне меняются ежедневно. Для успеха в таких условиях требуются инновации, адаптивность и терпение, поэтому важно ежедневно анализировать данные и корректировать стратегию продвижения.

Находите способы автоматизации. Это не только ускорит процесс, но и позволит вам делать то, что большинство PPC-специалистов не могут себе позволить из-за нехватки времени. Используйте скрипты AdWords с умом и получайте действительно хорошие результаты.

А какими скриптами пользуетесь вы? Напишите в комментариях.

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: