Showing posts with label keyword. Show all posts
Showing posts with label keyword. Show all posts

Tuesday, July 30, 2013

Figuring Out When Your Ad, AdGroup, Keyword, or Campaign Was Created

Knowing when an Ad (or entity) was created is impossible using scripts. That information is simply not tracked in AdWords. The next best thing is to find out when your Ad first started receiving impressions and assume that is when it was created (If an Ad is created but no one sees it, does it really exist?).

So in order to help me keep track of when my Ads entities were created, I put together the following script to apply labels to my Ads entities with the date of the first impression. That way, I can find out what ads entities I created and make sure I don't take action on anything that is too young. I can also make changes to all the ads entities built on a given day relatively easily in the AdWords UI by just selecting the right label.

Thanks,
Russ



/**************************************
* Track Entity Creation Date
* Version 1.4
* Changelog v1.4
*  - Removed apiVersion from reporting call
* Changelog v1.3
*  - Updated script to handle all entities
* Changelog v1.2
*  - Fixed an issue with comparing dates
* ChangeLog v1.1
*  - Updated logic to work with larger accounts
* Created By: Russ Savage
* http://www.FreeAdWordsScripts.com
**************************************/
//All my labels will start with this. For example: Created:2013-05-01
var LABEL_PREFIX = 'Created:';
var DAYS_IN_REPORT = 30;
var ENTITY = 'ad'; //or adgroup or keyword or campaign
 
function main() {
  //First we get the impression history of our entity
  var ret_map = getImpressionHistory();
  //Then we apply our labels
  applyLabels(ret_map);
}
 
//Function to apply labels to the ads in an account
function applyLabels(ret_map) {
  var iter;
  if(ENTITY === 'campaign') { iter = AdWordsApp.campaigns().get(); }
  if(ENTITY === 'adgroup') { iter = AdWordsApp.adGroups().get(); }
  if(ENTITY === 'ad') { iter = AdWordsApp.ads().get(); }
  if(ENTITY === 'keyword') { iter = AdWordsApp.keywords().get(); }
  
  while(iter.hasNext()) {
    var entity = iter.next();
    var id = entity.getId();
    if(ret_map[id]) {
      var label_name = LABEL_PREFIX+Utilities.formatDate(ret_map[id], AdWordsApp.currentAccount().getTimeZone(), "yyyy-MM-dd");
      createLabelIfNeeded(label_name);
      entity.applyLabel(label_name);
    }
  }
}
 
//This is a helper function to create the label if it does not already exist
function createLabelIfNeeded(name) {
  if(!AdWordsApp.labels().withCondition("Name = '"+name+"'").get().hasNext()) {
    AdWordsApp.createLabel(name);
  }
}
 
//A helper function to find the date days ago
function getDateDaysAgo(days) {
  var the_past = new Date();
  the_past.setDate(the_past.getDate() - days);
  return Utilities.formatDate(the_past,AdWordsApp.currentAccount().getTimeZone(),"yyyyMMdd");
}
 
//A helper function to compare dates.
//Copied from: http://goo.gl/uW48a
function diffDays(firstDate,secondDate) {
  var oneDay = 24*60*60*1000; // hours*minutes*seconds*milliseconds
  return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime())/(oneDay))); 
}
 
function getImpressionHistory() {
  var API_VERSION = { includeZeroImpressions : false };
  var first_date = new Date('10/23/2000');
  var max_days_ago = diffDays(first_date,new Date());
  var cols = ['Date','Id','Impressions'];
  var report = { 
    'campaign' : 'CAMPAIGN_PERFORMANCE_REPORT',
    'adgroup' : 'ADGROUP_PERFORMANCE_REPORT',
    'ad' : 'AD_PERFORMANCE_REPORT',
    'keyword' : 'KEYWORDS_PERFORMANCE_REPORT'}[ENTITY];
  var ret_map = {};
  var prev_days_ago = 0;
  for(var i = DAYS_IN_REPORT; i < max_days_ago; i+=DAYS_IN_REPORT) {
    var start_date = getDateDaysAgo(i);
    var end_date = getDateDaysAgo(prev_days_ago);
    var date_range = start_date+','+end_date;
    Logger.log('Getting data for ' + date_range);
    var query = ['select',cols.join(','),'from',report,'during',date_range].join(' ');
    var report_iter = AdWordsApp.report(query, API_VERSION).rows();
    if(!report_iter.hasNext()) { Logger.log('No more impressions found. Breaking.'); break; } // no more entries
    while(report_iter.hasNext()) { 
      var row = report_iter.next();
      if(ret_map[row['Id']]) {
        var [year,month,day] = (row['Date']).split('-');
        var from_row = new Date(year, parseFloat(month)-1, day);
        var from_map = ret_map[row['Id']];
         
        if(from_row < from_map) {
          ret_map[row['Id']] = from_row;
        }
      } else {
        var [year,month,day] = (row['Date']).split('-');
        ret_map[row['Id']] = new Date(year, parseFloat(month)-1, day);
      }
    }
    prev_days_ago = i;
  }
  return ret_map;
}

Monday, June 17, 2013

Finding Anomalies In Your Keywords, Adgroups, and Ads

UPDATE 2013-07-23: Added the ability to add labels to your ads as well. This might be helpful for creative testing.

In a recent post from SearchEngineLand, Larry Kim wants PPC Managers to stop being lazy. Link baiting aside, he has some good points. I think much of the time spent pouring over client spreadsheets is really trying to answer the simple question of "Where Are The Anomalies?"

Anomalies are keywords or AdGroups that seem to be performing very differently than their brothers and sisters in the same AdGroup or Campaign. Maybe you have an AdGroup with 15 keywords and one or two keywords seem to be getting nearly all of the clicks. Or maybe you just have an AdGroup that seems to be eating up a good chunk of your budget and you want to have more control over it. In either of these cases, you need some way to quickly identify which Keywords or AdGroups you want to take action on.

So I created the following script to help me do just that. It has a little bit of statistics embedded in it to calculate the mean and standard deviation of a set of entities (AdGroups or Keywords). Then I apply a label to any entity that seems to be more than two standard deviations away from the mean, which would indicate it is performing much better or much worse than its siblings. That way, I can take action on those entities pretty easily in my account.

This script will also send you an email each day with a summary of the entities that it deems to be anomalies. Your goal is to fix these issues by moving them into their own Campaigns and AdGroups or maybe even getting rid of them altogether (possibly negatives).

Right now, the script will check every metric that is available in the AdWordsApp.stats object. It could be easily modified to check for things like cost per conversion or profit per impression. You will see in the code below where you can remove stats that don't interest you as well.

One note of warning is that I do not claim to be a statistics guy, so this may or may not be a valid way to look at things. Hopefully, it will help you locate the issues in an account quickly and fix them so that you can spend more time being lazy :)

Also, I am open to your comments and suggestions for this script. Is it useful to anyone else?

Thanks,
Russ

/**************************************
* Find the Anomalies
* Created By: Russ Savage
* Version: 1.2
* Changelog v1.2
*  - Fixed divide by 0 errors
*  - Changed SIG_FIGS to DECIMAL_PLACES
* Changelog v1.1
*  - Added ability to tag ad anomalies as well
* FreeAdWordsScripts.com
**************************************/
var DATE_RANGE = 'LAST_30_DAYS';
var DECIMAL_PLACES = 3;
var STANDARD_DEVIATIONS = 2;
var TO = ['you@your_domain.com'];
 
function main() {
  // This will add labels to and send emails about adgroups, keywords and ads. 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 entity_map = buildEntityMap(levels_to_tag[x]);
    for(var parent_id in entity_map) {
      var child_list = entity_map[parent_id];
      var stats_list = Object.keys(child_list[0].stats);
      for(var i in stats_list) {
        var mean = getMean(child_list,stats_list[i]);
        var stand_dev = getStandardDev(child_list,mean,stats_list[i]);
        var label_name = stats_list[i]+"_anomaly";
        report += addLabelToAnomalies(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 reporting and sends and email
//with the report as an attachment.
function sendResultsViaEmail(report,level) {
  var rows = report.match(/\n/g).length - 1;
  if(rows == 0) { return; }
  var options = { attachments: [Utilities.newBlob(report, 'text/csv', level+"_anomalies_"+_getDateString()+'.csv')] };
  var email_body = "There are " + rows + " " + level + "s that have abnormal performance. See attachment for details.";
  var subject = 'Abnormal ' + _initCap(level) + ' Entities Report - ' + _getDateString();
  for(var i in TO) {
    MailApp.sendEmail(TO[i], subject, email_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') ? entity.getName() : entity.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 add the labels to the entities based on the standard deviation and mean.
//It returns a csv formatted string for reporting
function addLabelToAnomalies(entites,mean,sd,stat_key,label_name,level) {
  createLabelIfNeeded(label_name);
  var report = '';
  for(var i in entites) {
    var entity = entites[i]['entity'];
    var deviation = Math.abs(entites[i]['stats'][stat_key] - mean);
    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 create the label if it does not already exist
function createLabelIfNeeded(name) {
  if(!AdWordsApp.labels().withCondition("Name = '"+name+"'").get().hasNext()) {
    AdWordsApp.createLabel(name);
  }
}
  
//This function returns the standard deviation 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] - mean,2);
  }
  if(Math.sqrt(entites.length-1) == 0) {
    return 0;
  }
  return round(Math.sqrt(total)/Math.sqrt(entites.length-1));
}
  
//Returns the mean (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 meant to work on AdGroups and Keywords
function buildEntityMap(entity_type) {
  var map = {}; // { parent_id : [ { entity : entity, stats : entity_stats } ], ... }
  var iter = getIterator(entity_type);
  while(iter.hasNext()) {
    var entity = iter.next();
    var stats = entity.getStatsFor(DATE_RANGE);
    var stats_map = getStatsMap(stats);
    var parent_id = getParentId(entity_type,entity);
    if(map[parent_id]) { 
      map[parent_id].push({entity : entity, stats : stats_map});
    } else {
      map[parent_id] = [{entity : entity, stats : stats_map}];
    }
  }
  return map;
}
  
//Given an entity type (adgroup or keyword) this will return the parent id
function getParentId(entity_type,entity) {
  switch(entity_type) {
    case 'adgroup' :
      return entity.getCampaign().getId();
    case 'keyword':
      return entity.getAdGroup().getId();
    case 'ad':
      return entity.getAdGroup().getId();
  }
}
  
//Given an entity 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 stats for a given entity.
//You can comment out the things you don't really care about.
function getStatsMap(stats) {
  return { // You can comment these out as needed.
          avg_cpc : stats.getAverageCpc(),
          avg_cpm : stats.getAverageCpm(),
          avg_pv : stats.getAveragePageviews(),
          avg_pos : stats.getAveragePosition(),
          avg_tos : stats.getAverageTimeOnSite(),
          bounce : stats.getBounceRate(),
          clicks : stats.getClicks(),
          cv : stats.getConversionRate(),
          conv : stats.getConversions(),
          cost : stats.getCost(),
          ctr : stats.getCtr(),
          imps : stats.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;
}