Aprendizaje automático con secuencias de comandos de AdWords y API de predicción de Google

En esta útil guía práctica, el columnista Russell Savage explica cómo usar la API de predicción de Google junto con los scripts de AdWords para obtener información sobre sus datos de PPC.

google-adwords-bigA9-1920

Para muchos de nosotros, el análisis de nuestros datos de AdWords comienza con la descarga de un archivo .CSV masivo en Excel y la ejecución de varios cálculos y la creación de gráficos. Después de eso, nos convertimos en adivinos, tratando de leer las hojas de té analíticas de nuestros datos y predecir qué cambios hacer.

Ese análisis requiere mucho tiempo, es difícil y está sesgado por nuestras experiencias y emociones personales. El aprendizaje automático puede ayudarnos a solucionarlo. Hoy vamos a utilizar la API de predicción de Google y los scripts de AdWords para predecir el futuro.

Pidiendo una predicción

Con el API de predicción de Google, ya no necesita un equipo dedicado de doctores para construir y mantener un modelo analítico para sus datos de pago por clic (PPC). Todo lo que necesita hacer es formatear e insertar sus datos, luego pedirle una predicción. Cuantos más datos pueda proporcionar, más precisa debería ser la predicción.

El mundo del aprendizaje automático parece un poco abrumador al principio, pero te daré un curso intensivo para ayudarte a comenzar rápidamente. Empezaré diciendo que nunca he realizado cursos avanzados de estadística ni he programado nada en R, pero puedo usar la API Prediction sin ningún problema, y ​​tú también puedes.

Necesitamos comenzar con una pregunta o algo que queremos que nuestro modelo pueda predecir. Voy a crear un modelo que pueda predecir cuál será el CPC promedio para una temperatura, velocidad del viento y condiciones climáticas determinadas en mi cuenta. Por supuesto, todos sabemos que el clima impacta nuestras ofertas, pero esto me dirá exactamente cuánto impacto debo esperar.

Recopilar datos históricos

Para que mi modelo haga una predicción, al igual que un estudiante, necesito enseñarlo (o entrenarlo) con ejemplos. Eso significa que tendré que recopilar datos meteorológicos históricos para mi cuenta. Esto permitirá que mi modelo comprenda las relaciones entre los datos. Utilizará esos ejemplos de entrenamiento para devolver una predicción para una nueva consulta que nunca antes había visto.

En esta publicación, vamos a escribir dos scripts a nivel de cuenta. El primero es simplemente recopilar y almacenar datos de entrenamiento para la cuenta. El segundo usará esos datos de entrenamiento para construir y actualizar un modelo.

Los datos de entrenamiento se componen de dos partes: el valor de ejemplo, que será la respuesta devuelta, y un conjunto de características. En este ejemplo, mi valor será el CPC promedio para una ubicación específica y las funciones incluirán toda la información que conozco en ese momento. (Este es solo un ejemplo, por lo que no tengo todos los datos, pero esto debería ayudarlo a comenzar).

Vamos al guión uno. Estamos viendo que el clima se basa en la ubicación, por lo que un buen lugar para comenzar sería una función que extraiga el Informe de rendimiento geográfico. Podemos usar esos datos para tener una idea de dónde proviene nuestro tráfico.

Por supuesto, si tiene objetivos de campaña muy específicos en su cuenta, simplemente podría proporcionar una lista de ubicaciones que le interesan, pero ¿qué tiene de divertido? Aquí hay una función que nos ayuda a obtener los datos de rendimiento por ubicación geográfica.

/*******************************
 * Grab the GEO_PERFORMANCE_REPORT for the given
 * date range. dateRange can be things like LAST_30_DAYS
 * or more specific like 20150101,20150201 
 *******************************/
function getGeoReport(dateRange) {
  // The columns are in a function so I can call them
  // later when I build the spreadsheet.
  var cols = getReportColumns();
  var report="GEO_PERFORMANCE_REPORT";
  var query = ['select',cols.join(','),'from',report,
               'where CampaignStatus = ENABLED',
               'and AdGroupStatus = ENABLED',
               'and Clicks > 0', // You can increase this to 
                                 // reduce the number of results
               'during',dateRange].join(' ');
  var reportIter = AdWordsApp.report(query).rows();
  var retVal = [];
  while(reportIter.hasNext()) {
    var row = reportIter.next();
    // If we don't have city level data, let's ignore it
    if(row['CityCriteriaId'] == 'Unspecified') { continue; }
    retVal.push(row);
  }
  return retVal;
}

// Helper function to return the report columns
function getReportColumns() {
  return ['Date','DayOfWeek',
          'CampaignId','CampaignName',
          'AdGroupId','AdGroupName',
          'CountryCriteriaId','RegionCriteriaId','MetroCriteriaId','CityCriteriaId',
          'Impressions','Clicks','Cost',
          'ConvertedClicks','ConversionValue',
          'AverageCpc'];
}

Puede usar esa función para capturar datos para cualquier rango de fechas que desee. Para la extracción de datos inicial, es posible que desee mirar hacia atrás 30 días o más. Después de eso, puede programar la secuencia de comandos para que se ejecute diariamente para continuar recopilando información nueva.

Para extraer datos meteorológicos históricos, usaré Clima subterráneo. Es gratis registrarse y comenzar con una clave API, pero alcanzará los límites bastante rápido. Otra opcion es la API de mapa meteorológico abierto, pero lo encontré un poco más confuso de usar. Solo estamos tratando de obtener algunos datos de entrenamiento, por lo que los límites no son tan importantes en este momento. He agregado variables y almacenamiento en caché a la versión final del script para ayudar a lidiar con los límites que pueda encontrar.

Tendremos que traducir las ubicaciones en el informe de AdWords a ubicaciones que Weather Underground pueda comprender. Para eso, podemos usar su API de Autocompletar. El siguiente código usa la información CityCriteriaId, RegionCriteriaId y CountryCriteriaId del informe geográfico y busca la URL de ubicación meteorológica para usar con Weather Underground.

/*********************************
 * Given a city, region, and country, return
 * the location information from WeatherUnderground.
 * Uses CACHEs to improve performance and reduce
 * calls to API.
 *********************************/
var CITY_LOOKUP_CACHE = {};
var COUNTRY_TO_CODE_MAP = getCountryCodesMap();
var WU_AUTOCOMPLETE_BASE_URL = 'http://autocomplete.wunderground.com/aq';
function getWeatherUndergroundLocation(city,region,country) {
  // Create a key for looking up data in our cache
  var cityCacheKey = [city,region,country].join('-');
  if(CITY_LOOKUP_CACHE[cityCacheKey]) {
    return CITY_LOOKUP_CACHE[cityCacheKey];
  }
  var urlParams = { 'cities': 1, 'h': 0 };
  if(country) {
    urlParams['c'] = COUNTRY_TO_CODE_MAP[country];
  }
  var urlsToCheck = [];
  // We check the more specific location first
  if(region && region != 'Unspecified') {
    urlParams['query'] = city+', '+region;
    urlsToCheck.push(WU_AUTOCOMPLETE_BASE_URL+'?'+ toQueryString(urlParams));
  }
  // But also try just the city
  urlParams['query'] = city;
  urlsToCheck.push(WU_AUTOCOMPLETE_BASE_URL+'?'+ toQueryString(urlParams));
  
  for(var i in urlsToCheck) {
    var urlToCheck = urlsToCheck[i];
    (ENABLE_LOGGING && Logger.log('Checking Url: '+urlToCheck));
    var resp = UrlFetchApp.fetch(urlToCheck,{muteHttpExceptions:true});
    if(resp.getResponseCode() == 200) {
      var jsonResults = JSON.parse(resp.getContentText());
      if(jsonResults.RESULTS.length > 0) {
        CITY_LOOKUP_CACHE[cityCacheKey] = jsonResults.RESULTS[0];
        return jsonResults.RESULTS[0];
      }
    }
    // Otherwise sleep a bit and try the 
    // other url.
    Utilities.sleep(500);
  }
  // If we can't find the city, just ignore it
  (ENABLE_LOGGING && Logger.log('Could not find data for: '+cityCacheKey));
  CITY_LOOKUP_CACHE[cityCacheKey] = false;
  return {};
}

// Converts {a:b,c:d} to a=b&c=d
// Taken from: http://goo.gl/5pG5oY
function toQueryString(hash) {
  var parts = [];
  for (var key in hash) {
    parts.push(key + "=" + encodeURIComponent(hash[key]));
  }
  return parts.join("&");
}

Una cosa que notará es que el informe geográfico de AdWords muestra los nombres completos de los países, mientras que Weather Underground usa solo el código de país ISO de dos dígitos. Aquí hay una función rápida que creará un mapeo de nombres completos de países a códigos de dos dígitos según los datos de Conocimiento abierto.

/**********************************
 * This function returns a mapping of country codes
 * to two digit country codes.
 * { "United States" : "US", ... }
 **********************************/
function getCountryCodesMap() {
  var url="http://data.okfn.org/data/core/country-codes/r/country-codes.json";
  var resp = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
  if(resp.getResponseCode() == 200) {
    var jsonData = JSON.parse(resp.getContentText());
    var retVal = {};
    for(var i in jsonData) {
      var country = jsonData[i];
      if(!country){ continue; }
      retVal[country.name] = country['ISO3166-1-Alpha-2'];
    }
    // Fixing some values. There may be more but
    // Weather Undergrounds' country mapping is 
    // pretty arbitrary. This page might help
    // http://goo.gl/J17Ve6 but it always accurate.
    retVal['South Korea'] = 'KR';
    retVal['Japan'] = 'JA';
    retVal['Isreal'] = 'IS';
    retVal['Spain'] = 'SP';
    retVal['United Kingdom'] = 'UK';
    retVal['Switzerland'] = 'SW';
    return retVal;
  } else {
    throw 'ERROR: Could not fetch country mapping. Response Code: '+
          resp.getResponseCode();
  }
}

Este código utiliza cachés para acelerar las búsquedas de ciudades y reducir las llamadas a la API. Una vez que encontramos la ciudad, la almacenamos en la variable CITY_LOOKUP_CACHE para que no necesitemos volver a solicitarla.

Ahora que tenemos los datos geográficos de AdWords y la información de ubicación de Weather Underground, podemos buscar los datos meteorológicos históricos de la ubicación. La siguiente función busca la información meteorológica histórica para la fecha y el lugar dados. Nuevamente, estamos usando un caché para limitar la cantidad de llamadas a la API.

/************************************
 * Calls the Weather Underground history api
 * for a given location, date, and timezone
 * and returns the weather information. It 
 * utilizes a cache to conserve api calls
 ************************************/
var WU_API_KEY = 'YOUR WU API KEY HERE';
var WEATHER_LOOKUP_CACHE = {};
function getHistoricalTemp(wuUrl,date,timeZone) {
  if(!wuUrl) { return {}; }
  if(WU_TOTAL_CALLS_PER_DAY <= 0) {
    throw 'Out of WU API calls for today.';
  }
  var weatherCacheKey = [wuUrl,date,timeZone].join('-');
  if(WEATHER_LOOKUP_CACHE[weatherCacheKey]) {
    return WEATHER_LOOKUP_CACHE[weatherCacheKey];
  }
  var formattedDate = date.replace(/-/g,'');
  var url = ['http://api.wunderground.com/api/',
             WU_API_KEY,
             '/history_',
             formattedDate,
             wuUrl,'.json'].join('');
  (ENABLE_LOGGING && Logger.log('Checking Url: '+url));
  var resp = UrlFetchApp.fetch(url,{muteHttpExceptions:true});
  
  // This keeps you within the WU API guidelines
  WU_TOTAL_CALLS_PER_DAY--;
  Utilities.sleep(1000*(60/WU_CALLS_PER_MIN));
  
  if(resp.getResponseCode() == 200) {
    var jsonResults = JSON.parse(resp.getContentText());
    WEATHER_LOOKUP_CACHE[weatherCacheKey] = jsonResults.history.dailysummary[0];
    return jsonResults.history.dailysummary[0];
  }
  (ENABLE_LOGGING && Logger.log('Could not find historical weather for: '+weatherCacheKey));
  WEATHER_LOOKUP_CACHE[weatherCacheKey] = false;
  return {};
}

Cuando juntamos todas estas piezas, tenemos un script que podemos ejecutar a diario y que almacenará nuestros datos de entrenamiento en una hoja de cálculo, a la que luego podemos acceder desde nuestro script de modelado. Aquí está la función principal y algunos ayudantes más para unir las cosas.

// Set this to false to disable all logging
var ENABLE_LOGGING = true;
// The name of the spreadsheet you want to store your training
// data in. Should be unique.
var TRAINING_DATA_SPREADSHEET_NAME = 'Super Cool Training Data';
// The date range for looking up data. On the first run, you can 
// set this value to be longer. When scheduling for daily runs, this
// should be set for YESTERDAY
var DATE_RANGE = 'YESTERDAY';
// These values help you stay within the limits of
// the Weather Underground API. More details can be found
// in your Weather Underground account.
var WU_CALLS_PER_MIN = 10; // or 100 or 1000 for paid plans
var WU_TOTAL_CALLS_PER_DAY = 500; // or 5000, or 100,000 for paid plans

function main() {
  var sheet = getSheet(TRAINING_DATA_SPREADSHEET_NAME);
  // If the sheet is blank, let's add the headers
  if(sheet.getDataRange().getValues()[0][0] == '') {
    sheet.appendRow(getReportColumns().concat(['Mean Temp','Mean Windspeed','Conditions']));
  }
  var results = getGeoReport(DATE_RANGE);
  for(var key in results) {
    var row = results[key];
    var loc = getWeatherUndergroundLocation(row.CityCriteriaId,
                                            row.RegionCriteriaId,
                                            row.CountryCriteriaId);
    var historicalWeather = getHistoricalTemp(loc.l,results[key].Date,loc.tz);
    // See below. This pulls the info out of the weather results
    // and translates the average conditions.
    var translatedConditions = translateConditions(historicalWeather);
    sheet.appendRow(translateRowToArray(row).concat(translatedConditions));
    // Break before you run out of quota
    if(AdWordsApp.getExecutionInfo().getRemainingTime() < 10/*seconds*/) { break; }
  }
}

// Helper function to get or create a spreadsheet 
function getSheet(spreadsheetName) {
  var fileIter = DriveApp.getFilesByName(spreadsheetName);
  if(fileIter.hasNext()) {
    return SpreadsheetApp.openByUrl(fileIter.next().getUrl()).getActiveSheet();
  } else {
    return SpreadsheetApp.create(spreadsheetName).getActiveSheet();
  }
}

// Helper function to convert a report row to an array
function translateRowToArray(row) {
  var cols = getReportColumns();
  var ssRow = [];
  for(var i in cols) {
    ssRow.push(row[cols[i]]);
  }
  return ssRow;
}

/**********************************
 * Given a result from the Weather Underground
 * history API, it pulls out the average temp,
 * windspeed, and translates the conditons for 
 * rain, snow, etc. It returns an array of values.
 **********************************/
function translateConditions(historicalWeather) {
  var retVal = [];
  // in meantempi, the i is for Imperial. use c for Celcius.
  if(historicalWeather && historicalWeather['meantempi']) {
    retVal.push(historicalWeather['meantempi']);
    retVal.push(historicalWeather['meanwindspdi']);
    if(historicalWeather['rain'] == 1) {
      retVal.push('rain');
    } else if(historicalWeather['snow'] == 1) {
      retVal.push('snow');
    } else if(historicalWeather['hail'] == 1) {
      retVal.push('hail');
    } else if(historicalWeather['thunder'] == 1) {
      retVal.push('thunder');
    } else if(historicalWeather['tornado'] == 1) {
      retVal.push('tornado');
    } else if(historicalWeather['fog'] == 1) {
      retVal.push('fog');
    } else {
      retVal.push('clear');
    }
    return retVal;
  }
  return [];
}

Ahora que tenemos un script para construir y agregar continuamente a nuestros datos de entrenamiento, podemos crear el segundo script que realmente construirá nuestro modelo. Para hacer esto, necesitamos habilitar la API Prediction para nuestro script en la Botón API avanzadas y siga el enlace a Developers Console para habilitarlo allí también.

Una vez hecho esto, podemos crear nuestro modelo. El siguiente código extraerá los datos de la misma hoja de cálculo que construimos en la parte uno y creará un modelo.

Una cosa a tener en cuenta es el campo para ignorar columnas de nuestros datos de entrenamiento. Cuando un campo es único para cada fila, como una fecha, realmente no nos ayuda con una predicción. Además, los elementos que tienen el mismo nivel de singularidad, como el nombre de la campaña y el identificador de la campaña, tampoco agregan mucho. Muchos de estos en realidad hacen que su modelo sea un poco menos flexible porque necesitará pasar esos valores con su consulta. Por lo tanto, he ignorado cualquier columna que no afecte el costo por clic (CPC) promedio.

También he excluido valores como impresiones, clics y costo, solo porque son elementos que no sabré cuando consulto el modelo. Por supuesto, puede pasar los valores deseados para estos campos en su consulta para ver cómo reacciona la salida. Hay muchas cosas con las que jugar aquí, y puede crear y entrenar tantas variaciones de modelo como desee si desea comparar el rendimiento. Simplemente cambie los nombres.

/***********************************
 * This function accepts a sheet full of training
 * data and creates a trained model for you to query.
 ***********************************/
// Unique name for your model. Maybe add a date here
// if you are doing iterations on your model.
var MODEL_NAME = 'Weather Training Model';
// This Id should be listed in your Developers Console
// when you authorize the script
var PROJECT_ID = 'PROJECT ID FROM DEVELOPER CONSOLE';
// These are the names of your columns from the training
// data to ignore. Change these to create variations of your
// model for testing
var COLS_TO_IGNORE = [
  'Date','CampaignId','CampaignName','AdGroupId','AdGroupName',
  'MetroCriteriaId','Impressions','Clicks','Cost'
];
// This is the output column for your training data, or
// what value the model is supposed to predict
var OUTPUT_COLUMN = 'AverageCpc';

function createTrainingModel(sheet) {
  var trainingInstances = [];
  // get the spreadsheet values
  var trainingData = sheet.getDataRange().getValues();
  var headers = trainingData.shift();
  for(var r in trainingData) {
    var inputs = [];
    var row = trainingData[r];
    for(var i in headers) {
      if(COLS_TO_IGNORE.indexOf(headers[i]) == -1 && headers[i] != OUTPUT_COLUMN) {
        inputs.push(row[i])
      }
    }
    var output = row[headers.indexOf(OUTPUT_COLUMN)];
    trainingInstances.push(createTrainingInstance(inputs,output));
  }

  var insert = Prediction.newInsert();
  insert.id = MODEL_NAME;
  insert.trainingInstances = trainingInstances;

  var insertReply = Prediction.Trainedmodels.insert(insert, PROJECT_ID);
  Logger.log('Trained model with data.');
}

// Helper function to create the training instance.
function createTrainingInstance(inputs,output) {
  var trainingInstances = Prediction.newInsertTrainingInstances();
  trainingInstances.csvInstance = inputs;
  trainingInstances.output = output;
  return trainingInstances;
}

El código para crear el modelo solo debe ejecutarse una vez, luego debe desactivarse. Puede llamarlo desde la función principal de esta manera.

// The name of the spreadsheet containing your training data
var TRAINING_DATA_SPREADSHEET_NAME = 'Weather Model Training Data';
function main() {
  var sheet = getSheet(TRAINING_DATA_SPREADSHEET_NAME);
  createTrainingModel(sheet);
  //makePrediction();
}

// Helper function to get an existing sheet
// Throws an error if the sheet doesn't exist
function getSheet(spreadsheetName) {
  var fileIter = DriveApp.getFilesByName(spreadsheetName);
  if(fileIter.hasNext()) {
    return SpreadsheetApp.openByUrl(fileIter.next().getUrl()).getActiveSheet();
  }
  throw 'Sheet not found: '+spreadsheetName;
}

Ahora que ha creado su modelo, puede continuar actualizándolo a medida que se agregan más datos a su hoja de cálculo de entrenamiento. Puede usar el siguiente código, que es muy similar a la función de entrenamiento, para agregar nuevos datos de entrenamiento a su modelo a diario.

function updateTrainedModelData(sheet) {
  var updateData = sheet.getDataRange().getValues();
  var headers = updateData.shift();
  for(var r in updateData) {
    var inputs = [];
    var row = updateData[r];
    for(var i in headers) {
      if(COLS_TO_IGNORE.indexOf(headers[i]) == -1 && headers[i] != OUTPUT_COLUMN) {
        inputs.push(row[i])
      }
    }
    var output = row[headers.indexOf(OUTPUT_COLUMN)];
    var update = createUpdateInstance(inputs,output)
    var updateResponse = Prediction.Trainedmodels.update(update, PROJECT_ID, MODEL_NAME);
    Logger.log('Trained model updated with new data.');
  }
}

// Helper function to create the update instance.
function createUpdateInstance(inputs,output) {
  var updateInstance = Prediction.newUpdate();
  updateInstance.csvInstance = inputs;
  updateInstance.output = output;
  return updateInstance;
}

Solo asegúrese de no continuar actualizando su modelo con datos de entrenamiento anteriores. Una vez que los datos están en el modelo, debe moverlos a una hoja de cálculo diferente o simplemente borrarlos.

Ahora podemos comenzar a consultar el modelo con bastante facilidad. El siguiente código aceptará una matriz de consultas (que son solo matrices de valores, las mismas que las características de los datos de entrenamiento) y devolverá los resultados de la predicción para cada fila. Una forma de probar su modelo es tomar una parte de sus datos de entrenamiento, eliminar la columna de salida y pasarla a esta función.

/***************************
 * Accepts a 2d array of query data and returns the
 * predicted output in an array.
 ***************************/
function makePrediction(data) {
  var retVal = [];
  for(var r in data) {
    var request = Prediction.newInput();
    request.input = Prediction.newInputInput();
    request.input.csvInstance = data[r];
    var predictionResult = Prediction.Trainedmodels.predict(
      request, PROJECT_ID, MODEL_NAME);
    Logger.log("Prediction for data: %s is %s",
               JSON.stringify(data[r]), predictionResult.outputValue);
    retVal.push(predictionResult.outputValue);
  }
  return retVal;
}

Y eso es exactamente lo que hago aquí como ejemplo. Nuestro principal ahora se ve así:

function main() {
  var sheet = getSheet(TRAINING_DATA_SPREADSHEET_NAME);
  //createTrainingModel(sheet);
  //updateTrainedModelData(sheet);
  var queries = [];
  // We are going to test it by querying with training data
  var testData = sheet.getDataRange().getValues();
  var headers = testData.shift();
  for(var r in testData) {
    var query = [];
    var row = testData[r];
    for(var i in headers) {
      if(COLS_TO_IGNORE.indexOf(headers[i]) == -1 && headers[i] != OUTPUT_COLUMN) {
        query.push(row[i])
      }
    }
    queries.push(query);
  }
  Logger.log(makePrediction(queries));
}

Y eso es todo lo que hay que hacer. Si llegaste tan lejos, ¡felicidades! Aún necesitará ajustar y probar el modelo para asegurarse de que los datos que se devuelven tengan sentido. Si no es así, eche un vistazo a los datos de entrenamiento y vea si hay cosas allí que no tengan ningún sentido.

Es bastante sorprendente pensar que lo que a un equipo de científicos de datos de doctorado le tomaba meses producir ahora se puede construir usando scripts de AdWords y algunas líneas de código. Y esto es solo el comienzo. Puede utilizar cualquier dato externo que desee para construir su modelo. Ahora puede pasar de «si es mayor que x, haga y» a utilizar la salida de su propio algoritmo de aprendizaje automático para determinar sus acciones.

Por supuesto, un gran poder conlleva una gran responsabilidad. Se necesitará tiempo y una gran cantidad de datos antes de que su modelo sea un buen predictor del comportamiento, ¡así que es mejor que comience a entrenar ahora!


Las opiniones expresadas en este artículo pertenecen al autor invitado y no necesariamente a El Blog informatico. Los autores del personal se enumeran aquí.


Deja un comentario