显示带有标签的帖子 电子邮件. 显示所有帖子
显示带有标签的帖子 电子邮件. 显示所有帖子

2013年6月22日,星期六

广告创意测试自动化脚本

这是我已经工作了一段时间的脚本,我认为它确实很有用。我希望你也这样做。

每个有价值的SEM经理都在不断进行创造性的测试。而且,由于您正在阅读此博客,因此许多人可能以某种方式使用脚本来简化此过程。但是,您如何确定测试何时开始?对于每个广告组,它们可能会有所不同。

我见过有些人使用标签,有些人使用时间表,但是我从不真正喜欢跟踪那些东西。因此,我创建了此脚本,该脚本将自动跟踪创意测试的开始日期。

我采用的方法是将您的广告ID副本存储在Google电子表格中。然后,它会对照电子表格中的广告检查广告组中的当前广告,并通过查看广告ID找出是否有所更改。如果已经存在,则脚本假定已开始新的测试,并相应地在电子表格中记录了日期。使用此方法,营销人员可以确保脚本正在比较适当时间范围内的统计信息以确定测试获胜者。

但是后来一位读者提到,他们希望能够选择度量标准来衡量广告,并希望使用阈值仅在广告组中获得一定点击次数后才开始进行衡量。因此,我也将该功能添加到了脚本中。您只需要在脚本顶部调整一些参数,即可使用功能齐全的广告素材测试脚本。

每当它通过暂停对广告采取行动时,结果都会通过电子邮件发送给您,以便您可以为该广告组创建新的挑战者广告。

我用这个脚本做了一些不同的事情。我一直很讨厌让我的读者进入Google文档,并为我的脚本创建一个空白电子表格以用作存储。因此,为了解决这个问题,此脚本将检查存储电子表格ID的帐户中是否存在特殊标签。如果找到一个,它将使用它。并且如果缺少它(如第一次运行),它将创建电子表格和标签。这样,脚本中不会出现混乱的电子表格网址。这是一个变通办法,但是直到Google允许我们为脚本存储其他配置数据之前,我认为这是处理此问题的一种好方法。

与往常一样,我们欢迎您反馈此脚本是否对您有用。您还通过哪些其他指标来判断广告效果?该脚本还缺少什么?

谢谢,
拉斯

/************************************
* 广告创意测试自动化脚本
* Version: 1.3
* Changelog v1.3 - Data is written to the 电子表格 a little faster
* Changelog v1.2 - Added 广告 ditional threshold options
* Changelog v1.1 - Fixed issue with 日期 in 电子邮件
* Created By: 拉斯 Savage
* FreeAdWordsScripts.com
************************************/
// You can use any of the same values here as you can for METRIC below
var THRESHOLD = { metric : 'Clicks', value : 100 };
var TO = ['[email protected]'];
  
//Try any of these values for METRIC:
//AverageCpc, AverageCpm, AveragePageviews, AveragePosition, 
//AverageTimeOnSite, BounceRate, Clicks, ConversionRate, 
//Conversions, Cost, Ctr, Impressions
var METRIC = 'Ctr';
var ASC_OR_DESC = 'ASC'; //ASC - 暂停广告 with lowest value, DESC - 暂停广告 with highest value
  
function main() {
  //Start by finding what has changed in the account
  var 广告 _map = buildCurrentAdMap();
  var prev_ad_map = readMapFromSpreadsheet();
  prev_ad_map = 更新PreviousAdMap(prev_ad_map,ad_map);
  writeMapToSpreadsheet(prev_ad_map);
    
  //Now run through the 广告群组 to find creative tests
  var ag_iter = AdWordsApp.adGroups().get();
  var 暂停d_ads = [];
  while(ag_iter.hasNext()) {
    var ag = ag_iter.next();
    if(!prev_ad_map[ag.getId()]) { continue; }
      
    //Here is the date range for the test metrics
    var last_touched_str = _getDateString(prev_ad_map[ag.getId()].last_touched,'yyyyMMdd');
    var get_today_str = _getDateString(new Date(),'yyyyMMdd');
      
    var ag_stats = ag.getStatsFor(last_touched_str, get_today_str);
    if(ag_stats['get'+THRESHOLD.metric]() >= THRESHOLD.value) {
      var 广告 _iter = ag.ads().withCondition('Status = ENABLED')
                            .forDateRange(last_touched_str, get_today_str)
                            .orderBy(METRIC+" "+ASC_OR_DESC).get();
      var 广告  = 广告 _iter.next();
      var metric = 广告 .getStatsFor(last_touched_str, get_today_str)['get'+METRIC]();
      ad.pause();
      paused_ads.push({a : 广告 , m : metric});
    }
  }
  sendEmailForPausedAds(paused_ads);
}
  
// A function to send an 电子邮件 with an 在tached report of 广告 it has 暂停d
function sendEmailForPausedAds(ads) {
  if(ads.length == 0) { return; } //No 广告 暂停d, no 电子邮件
  var 电子邮件_body = '"' + ['CampaignName','AdGroupName','Headline','Desc1','Desc2','DisplayUrl',METRIC].join('","') + '"\n';
  for(var i in 广告) {
    var 广告  = 广告[i].a;
    var metric = 广告[i].m;
    email_body += '"' + [ad.getCampaign().getName(),
                         ad.getAdGroup().getName(),
                         ad.getHeadline(),
                         ad.getDescription1(),
                         ad.getDescription2(),
                         ad.getDisplayUrl(),
                         metric
                        ].join('","') +
                  '"\n';
  }
  var date_str = _getDateString(new Date(),'yyyy-MM-dd');
  var options = { 附件: [Utilities.newBlob(email_body, 'text/csv', "FinishedTests_"+date_str+'.csv')] };
  var subject = 'Finished Tests - ' + date_str;
  for(var i in TO) {
    MailApp.sendEmail(TO[i], subject, 'See 在tached.', options);
  }
}
  
//Given two lists of 广告, this checks to make sure they are the same.
function sameAds(ads1,ads2) {
  for(var i in 广告1) {
    if(ads1[i] != 广告2[i]) { return false; }
  }
  return true;
}
  
//This reads the stored data from the 电子表格
function readMapFromSpreadsheet() {
  var 广告 _map = {};
  var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
  var data = sheet.getRange('A:C').getValues();
  for(var i in data) {
    if(data[i][0] == '') { break; }
    var [ag_id,last_touched,ad_ids] = data[i];
    ad_map[ag_id] = { 广告 _ids : (''+ad_ids).split(','), last_touched : new Date(last_touched) };
  }
  return 广告 _map;
}
  
//This will search for a 标签 containing the 电子表格 id
//If one isn't found, it will 创造 a new one and the 标签 as well
function findSpreadsheetId() {
  var 电子表格_id = "";
  var 标签_iter = AdWordsApp.labels().withCondition("Name STARTS_WITH 'history_script:'").get();
  if(label_iter.hasNext()) {
    var 标签 = 标签_iter.next();
    return 标签.getName().split(':')[1]; 
  } else {
    var sheet = SpreadsheetApp.create('AdGroups History');
    var sheet_id = sheet.getId();
    AdWordsApp.createLabel('history_script:'+sheet_id, 'stores sheet id for 广告群组 changes script.');
    return sheet_id;
  }
}
  
//This will store the data from the account into a 电子表格
function writeMapToSpreadsheet(ad_map) {
  var toWrite = [];
  for(var ag_id in 广告 _map) {
    var 广告 _ids = 广告 _map[ag_id].ad_ids;
    var last_touched = 广告 _map[ag_id].last_touched;
    toWrite.push([ag_id,last_touched,ad_ids.join(',')]);
  }
  writeToSpreadsheet(toWrite);
}

// Write the 关键词 data to the 电子表格
function writeToSpreadsheet(toWrite) {
  var sheet = SpreadsheetApp.openById(findSpreadsheetId()).getActiveSheet();
  sheet.clear();
  
  var numRows = sheet.getMaxRows();
  if(numRows < toWrite.length) {
    sheet.insertRows(1,toWrite.length-numRows); 
  }
  var range = sheet.getRange(1,1,toWrite.length,toWrite[0].length);
  range.setValues(toWrite);
}
  
//This builds a map of the 广告 in the account so that it is easy to compare
function buildCurrentAdMap() {
  var 广告 _map = {}; // { ag_id : { 广告 _ids : [ 广告 _id, ... ], last_touched : date } }
  var 广告 _iter = AdWordsApp.ads().withCondition('Status = ENABLED').get();
  while(ad_iter.hasNext()) {
    var 广告  = 广告 _iter.next();
    var ag_id = 广告 .getAdGroup().getId();
    if(ad_map[ag_id]) {
      ad_map[ag_id].ad_ids.push(ad.getId());
      ad_map[ag_id].ad_ids.sort();
    } else {
      ad_map[ag_id] = { 广告 _ids : [ad.getId()], last_touched : new Date() };
    }
  }
  return 广告 _map;
}
  
//This takes the old 广告  map and the current 广告  map and returns an
//updated map with all changes.
function 更新PreviousAdMap(prev_ad_map,ad_map) {
  for(var ag_id in 广告 _map) {
    var current_ads = 广告 _map[ag_id].ad_ids;
    var previous_ads = (prev_ad_map[ag_id]) ? prev_ad_map[ag_id].ad_ids : [];
    if(!sameAds(current_ads,previous_ads)) {
      prev_ad_map[ag_id] = 广告 _map[ag_id];
    }
  }
  return prev_ad_map;
}
  
//Helper function to format the date
function _getDateString(date,format) {
  return Utilities.formatDate(date,AdWordsApp.currentAccount().getTimeZone(),format); 
}

2013年6月17日,星期一

查找关键字,广告组和广告中的异常

2013年7月23日更新:添加了向广告添加标签的功能。这可能对创意测试很有帮助。

在一个 最近来自SearchEngineLand的帖子,拉里·金(Larry Kim)希望PPC经理不要再偷懒。除了链接诱饵,他还有一些优点。我认为花费在倾倒客户电子表格上的大部分时间实际上是在试图回答“异常在哪里?”这个简单的问题。

异常是关键字或广告组,其效果似乎与同一个广告组或广告系列中的兄弟姐妹不同。也许您有一个包含15个关键字和一个或两个关键字的广告组,似乎获得了几乎所有的点击。或者,也许您只是拥有一个广告组,似乎正在占用预算的很大一部分,而您想对其进行更多控制。在这两种情况下,您都需要某种方法来快速确定要对哪些关键字或广告组进行操作。

因此,我创建了以下脚本来帮助我做到这一点。它内含一些统计信息,可以计算一组实体(广告组或关键字)的均值和标准差。然后,我将标签应用于似乎与均值相差两个标准偏差以上的任何实体,这表明该实体的性能要比其同级产品更好或更差。这样,我可以在自己的帐户中轻松地对这些实体采取措施。

该脚本还将每天向您发送一封电子邮件,其中包含它认为是异常的实体的摘要。您的目标是通过将这些问题转移到自己的广告系列和广告组中,或者甚至完全消除它们(可能是负面的)来解决这些问题。

目前,脚本将检查AdWordsApp.stats对象中可用的每个指标。可以轻松对其进行修改,以检查诸如每次转化费用或每次展示利润。您将在下面的代码中看到,您可以在其中删除也不感兴趣的统计信息。

需要警告的一点是,我并没有声称自己是统计学家,所以这可能不是一种看待事物的有效方法。希望它可以帮助您快速找到帐户中的问题并加以解决,以便您可以将更多时间花在懒惰上:)

另外,我欢迎您对此脚本提出意见和建议。对其他人有用吗?

谢谢,
拉斯

/**************************************
* Find the Anomalies
* Created By: 拉斯 Savage
* Version: 1.2
* Changelog v1.2
*  - Fixed divide by 0 错误
*  - Changed SIG_FIGS to DECIMAL_PLACES
* Changelog v1.1
*  - Added ability to tag 广告  anomalies as well
* FreeAdWordsScripts.com
**************************************/
var DATE_RANGE = 'LAST_30_DAYS';
var DECIMAL_PLACES = 3;
var STANDARD_DEVIATIONS = 2;
var TO = ['[email protected]_domain.com'];
 
function main() {
  // This will 广告 d 标签 to and send 电子邮件s about 广告群组, 关键字 and 广告. Remove any if you like.
  var levels_to_tag = ['adgroup','keyword','ad'];
  for(var x in levels_to_tag) {
    var report = getContentRows(levels_to_tag[x]);
    var 实体_map = buildEntityMap(levels_to_tag[x]);
    for(var parent_id in 实体_map) {
      var child_list = 实体_map[parent_id];
      var 统计资料_list = Object.keys(child_list[0].stats);
      for(var i in 统计资料_list) {
        var 意思 = getMean(child_list,stats_list[i]);
        var stand_dev = getStandardDev(child_list,mean,stats_list[i]);
        var 标签_name = 统计资料_list[i]+"_anomaly";
        report += 广告 dLabelToAnomalies(child_list,mean,stand_dev,stats_list[i],label_name,levels_to_tag[x]);
      }
    }
    sendResultsViaEmail(report,levels_to_tag[x]);
  }
}
  
//Takes a report and the level of 报告 and sends and 电子邮件
//with the report as an 在tachment.
function sendResultsViaEmail(report,level) {
  var rows = report.match(/\n/g).length - 1;
  if(rows == 0) { return; }
  var options = { 附件: [Utilities.newBlob(report, 'text/csv', level+"_anomalies_"+_getDateString()+'.csv')] };
  var 电子邮件_body = "There are " + rows + " " + level + "s that have abnormal performance. See 在tachment for details.";
  var subject = 'Abnormal ' + _initCap(level) + ' Entities Report - ' + _getDateString();
  for(var i in TO) {
    MailApp.sendEmail(TO[i], subject, 电子邮件_body, options);
  }
}
  
//Helper function to return a single row of the report formatted correctly
function toReportRow(entity,level,label_name) {
  var ret_val = [AdWordsApp.currentAccount().getCustomerId(),
                 entity.getCampaign().getName()];
  ret_val.push( (level == 'adgroup') ? 实体.getName() : 实体.getAdGroup().getName() );
  if(level == 'keyword') {
    ret_val = ret_val.concat([entity.getText(),entity.getMatchType()]); 
  } else if(level == 'ad') {
    ret_val = ret_val.concat([entity.getHeadline(),entity.getDescription1(),entity.getDescription2(),entity.getDisplayUrl()]); 
  }
  ret_val.push(label_name);
  return '"' + ret_val.join('","') + '"\n';
}
  
//Helper function to return the column headings for the report
function getContentRows(level) {
  var ret_val = ['AccountId','CampaignName','AdGroupName'];
  if(level == 'keyword') {
    ret_val = ret_val.concat(['KeywordText','MatchType']); 
  } else if(level == 'ad') {
    ret_val = ret_val.concat(['Headline','Description1','Description2','DisplayUrl']);
  }
  ret_val.push('LabelName');
  return '"' + ret_val.join('","') + '"\n';
}
  
//Function to 广告 d the 标签 to the entities based on the 标准偏差 and 意思.
//It returns a CSV formatted string for 报告
function 广告 dLabelToAnomalies(entites,mean,sd,stat_key,label_name,level) {
  createLabelIfNeeded(label_name);
  var report = '';
  for(var i in entites) {
    var 实体 = entites[i]['entity'];
    var deviation = Math.abs(entites[i]['stats'][stat_key] - 意思);
    if(sd != 0 && deviation/sd >= STANDARD_DEVIATIONS) {
      entity.applyLabel(label_name);
      report += toReportRow(entity,level,label_name);
    } else {
      entity.removeLabel(label_name);
    }
  }
  return report;
}
  
//This is a helper function to 创造 the 标签 if it does not already exist
function 创造LabelIfNeeded(name) {
  if(!AdWordsApp.labels().withCondition("Name = '"+name+"'").get().hasNext()) {
    AdWordsApp.createLabel(name);
  }
}
  
//This function returns the 标准偏差 for a set of entities
//The stat key determines which stat to calculate it for
function getStandardDev(entites,mean,stat_key) {
  var total = 0;
  for(var i in entites) {
    total += Math.pow(entites[i]['stats'][stat_key] - 意思,2);
  }
  if(Math.sqrt(entites.length-1) == 0) {
    return 0;
  }
  return round(Math.sqrt(total)/Math.sqrt(entites.length-1));
}
  
//Returns the 意思 (average) for the set of entities
//Again, stat key determines which stat to calculate this for
function getMean(entites,stat_key) {
  var total = 0;
  for(var i in entites) {
    total += entites[i]['stats'][stat_key];
  }
  if(entites.length == 0) {
    return 0;
  }
  return round(total/entites.length);
}
  
//This function returns a map of the entities that I am processing.
//The format for the map can be found on the first line.
//It is 意思t to work on AdGroups and Keywords
function buildEntityMap(entity_type) {
  var map = {}; // { parent_id : [ { 实体 : 实体, 统计资料 : 实体_stats } ], ... }
  var iter = getIterator(entity_type);
  while(iter.hasNext()) {
    var 实体 = iter.next();
    var 统计资料 = 实体.getStatsFor(DATE_RANGE);
    var 统计资料_map = getStatsMap(stats);
    var parent_id = getParentId(entity_type,entity);
    if(map[parent_id]) { 
      map[parent_id].push({entity : 实体, 统计资料 : 统计资料_map});
    } else {
      map[parent_id] = [{entity : 实体, 统计资料 : 统计资料_map}];
    }
  }
  return map;
}
  
//Given an 实体 type (adgroup or 关键词) this will return the parent id
function getParentId(entity_type,entity) {
  switch(entity_type) {
    case 'adgroup' :
      return 实体.getCampaign().getId();
    case 'keyword':
      return 实体.getAdGroup().getId();
    case 'ad':
      return 实体.getAdGroup().getId();
  }
}
  
//Given an 实体 type this will return the iterator for that.
function getIterator(entity_type) {
  switch(entity_type) {
    case 'adgroup' :
      return AdWordsApp.adGroups().forDateRange(DATE_RANGE).withCondition("Impressions > 0").get();
    case 'keyword' :
      return AdWordsApp.keywords().forDateRange(DATE_RANGE).withCondition("Impressions > 0").get();
    case 'ad' :
      return AdWordsApp.ads().forDateRange(DATE_RANGE).withCondition("Impressions > 0").get();
  }
}
  
//This returns a map of all the 统计资料 for a given 实体.
//You can 评论 out the things you don't really care about.
function getStatsMap(stats) {
  return { // You can 评论 these out as needed.
          avg_cpc : 统计资料.getAverageCpc(),
          avg_cpm : 统计资料.getAverageCpm(),
          avg_pv : 统计资料.getAveragePageviews(),
          avg_pos : 统计资料.getAveragePosition(),
          avg_tos : 统计资料.getAverageTimeOnSite(),
          bounce : 统计资料.getBounceRate(),
          clicks : 统计资料.getClicks(),
          cv : 统计资料.getConversionRate(),
          conv : 统计资料.getConversions(),
          cost : 统计资料.getCost(),
          ctr : 统计资料.getCtr(),
          imps : 统计资料.getImpressions()
         };
}
  
//Helper function to format todays date
function _getDateString() {
  return Utilities.formatDate((new Date()), AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
}
  
//Helper function to capitalize the first letter of a string.
function _initCap(str) {
  return str.replace(/(?:^|\s)\S/g, function(a) { return a.toUpperCase(); });
}

// A helper function to make rounding a little easier
function round(value) {
  var decimals = Math.pow(10,DECIMAL_PLACES);
  return Math.round(value*decimals)/decimals;
}

2013年4月10日,星期三

报告帐户中的破损网址

注意:如果您正在寻找此脚本的版本以在MCC级别上运行,请签出 使用MCC级别脚本监视断开的链接.

更新时间:2013-05-20:基于读者的评论,该脚本现在仅检查活动的广告系列和广告组,并且仅检查每个网址一次。

更新时间:2013-04-28:基于读者的评论,我对该脚本进行了一些更新,包括将响应代码添加到电子邮件中并将结果格式化为附件。

它发生在我们最好的人身上。有时,我们会删除网站上的页面或更新链接,而忘记对我们的SEM帐户进行相应的更改。因此,今晚我整理了一个快速脚本来遍历您的所有广告和关键字,并创建一个包含以下内容的电子邮件报告: 找不到404 或一个 500服务器错误 响应码。您可以通过在脚本开头将它们添加到BAD_CODES数组中来轻松添加更多错误代码以进行检查。

谢谢,
拉斯

/****************************
* Find Broken Urls In Your Account
* Version 1.1
* ChangeLog v1.1
*  - Updated to only see Text Ads
* Created By: 拉斯 Savage
* FreeAdWordsScripts.com
****************************/
function main() {
  // You can 广告 d more if you want: http://goo.gl/VhIX
  var BAD_CODES = [404,500];
  var TO = ['[email protected]'/*,'[email protected]'*/];
  var SUBJECT = 'Broken Url Report - ' + _getDateString();
  var HTTP_OPTIONS = {
    muteHttpExceptions:true
  };
   
  //Let's look 在 广告 and 关键字 for 网址
  var iters = [
    //For Ad Level Urls
    AdWordsApp.ads()
      .withCondition("Status = 'ENABLED'")
      .withCondition("AdGroupStatus = 'ENABLED'")
      .withCondition("CampaignStatus = 'ENABLED'")
      .withCondition("Type = 'TEXT_AD'")
      .get(),
    //For Keyword Level Urls
    AdWordsApp.keywords()
      .withCondition("Status = 'ENABLED'")
      .withCondition("DestinationUrl != ''")
      .withCondition("AdGroupStatus = 'ENABLED'")
      .withCondition("CampaignStatus = 'ENABLED'")
      .get()
    ];
  
  var already_checked = {}; 
  var bad_entities = [];
  for(var x in iters) {
    var iter = iters[x];
    while(iter.hasNext()) {
      var 实体 = iter.next();
      if(entity.getDestinationUrl() == null) { continue; }
      var url = 实体.getDestinationUrl();
      if(url.indexOf('{') >= 0) {
        //Let's 去掉 the value track parameters
        url = url.replace(/\{[0-9a-zA-Z]+\}/g,'');
      }
      if(already_checked[url]) { continue; }
      var response_code;
      try {
        Logger.log("Testing url: "+url);
        response_code = UrlFetchApp.fetch(url, HTTP_OPTIONS).getResponseCode();
      } catch(e) {
        //Something is wrong here, we should know about it.
        bad_entities.push({e : 实体, code : -1});
      }
      if(BAD_CODES.indexOf(response_code) >= 0) {
        //This 实体 has an issue.  Save it for later. 
        bad_entities.push({e : 实体, code : response_code});
      }
      already_checked[url] = true;
    }
  }
  var column_names = ['Type','CampaignName','AdGroupName','Id','Headline/KeywordText','ResponseCode','DestUrl'];
  var 在tachment = column_names.join(",")+"\n";
  for(var i in bad_entities) {
    在tachment += _formatResults(bad_entities[i],",");
  }
  if(bad_entities.length > 0) {
    var options = { 附件: [Utilities.newBlob(attachment, 'text/csv', 'bad_urls_'+_getDateString()+'.csv')] };
    var 电子邮件_body = "There are " + bad_entities.length + " 网址 that are 破碎. See 在tachment for details.";
     
    for(var i in TO) {
      MailApp.sendEmail(TO[i], SUBJECT, 电子邮件_body, options);
    }
  }  
}
 
//Formats a row of results separated by SEP
function _formatResults(entity,SEP) {
  var e = 实体.e;
  if(typeof(e['getHeadline']) != "undefined") {
    //this is an 广告  实体
    return ["Ad",
            e.getCampaign().getName(),
            e.getAdGroup().getName(),
            e.getId(),
            e.getHeadline(),
            实体.code,
            e.getDestinationUrl()
           ].join(SEP)+"\n";
  } else {
    // and this is a 关键词
    return ["Keyword",
            e.getCampaign().getName(),
            e.getAdGroup().getName(),
            e.getId(),
            e.getText(),
            实体.code,
            e.getDestinationUrl()
           ].join(SEP)+"\n";
  }
}
 
//Helper function to format todays date
function _getDateString() {
  return Utilities.formatDate((new Date()), AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
}