商品やサービスによっては、天候により需要が大きく変動することがあります。たとえば、遊園地に関する情報は、寒い雨の日よりも晴れて暑い日の方が検索される可能性は大幅に高くなります。アミューズメント パークの運営会社は、天気の良いときに入札単価を引き上げたいと思うかもしれませんが、毎日これを行うには多くの手作業が必要となります。そこで、Google 広告 スクリプトを活用すると、プログラムによって気象情報を取得し、その情報に応じて入札単価をすばやく調整することができます。
このスクリプトでは、Google スプレッドシートを使って、キャンペーンとその掲載対象地域のリストを保存します。場所ごとに OpenWeatherMap API への呼び出しが行われ、いくつかの基本的なルールを使用して気象条件が計算されます。ルールの条件が true と評価されると、対応する地域の入札単価調整比がキャンペーンの地域ターゲティングに適用されます。
仕組み
スクリプトは、スプレッドシートからデータを読み取って処理を実行します。スプレッドシートは、次の 3 つのシートで構成されています。
1. キャンペーン データ
一連のルールに基づいて、気象条件が一致する場合に単価調整比をキャンペーンに適用するよう指定します。以下の列が必須です。
- キャンペーン名: 変更するキャンペーンの名前。
- Weather Location: 気象条件をチェックする場所。
- Weather Condition: このルールが適用される気象条件。
- Bid Modifier: 気象条件が満たされた場合に適用する地域の入札単価調整比。
- Apply Modifier To: 入札単価調整比を、Weather Location に一致するキャンペーンの対象地域のみに適用するか、すべてのキャンペーンの対象地域に適用するかを指定します。
- 有効: ルールを有効にするには
Yes
を指定し、無効にするにはNo
を指定します。
例
下記の例では、3 つのキャンペーンを設定しています。
Test Campaign 1 は典型的な利用例です。キャンペーンはマサチューセッツ州ボストンをターゲットとし、
- ボストン(マサチューセッツ州)の天気が
Sunny
の場合は、1.3
の入札単価調整比を適用する。 - ボストン(マサチューセッツ州)の天気が
Rainy
の場合は、0.8
の入札単価調整比を適用する。
テスト キャンペーン 2 は、テスト キャンペーン 1 と同じ入札ルールを使用しますが、ターゲットはコネチカットです。
テスト キャンペーン 3 でも同じ入札ルールを使用しますが、ターゲットはフロリダ州です。フロリダの気象ルールは、キャンペーンが明示的にターゲットとする地域ではない州全体にマッピングされているため、[Apply Modifier To] を All Geo Targets
に設定することで、キャンペーンのターゲットとなっている都市が影響を受けます。
2. 気象データ
このシートでは、キャンペーン データシートで使用する気象条件を定義します。 次の列は必須です。
- 条件名: 気象条件名(例:
Sunny
)。 - 温度: 温度(華氏)。
- 降水量: 過去 3 時間の降水量(ミリメートル)。
- 風速: 風速(マイル/時)。
上記のシートには、次の 2 つの気象条件が定義されています。
Sunny
: 気温が華氏 65 ~ 80 度で、過去 3 時間の降水量が 1 mm 未満、風速が時速 5 マイル未満。Rainy
: 過去 3 時間の降水量が 0 mm を上回り、風速が時速 16 マイル未満。
悪天候
気象条件を定義する場合は、次のように値を指定します。
below x
: 指定された値はbelow x
です(例:below 10
)。above x
: 指定された値はabove x
です(例:above 70
)。x to y
: 指定する値はx
~y
の範囲内となります(例:65 to 80
)。
セルを空のままにすると、そのパラメータは計算で考慮されません。この例では、Rainy
の気象条件には空の温度列があるため、この気象条件の計算時に気温は考慮されません。
気象状態を計算する際は、気象条件は AND で結合されます。この例では、Sunny
の気象条件は次のように評価されます。
const isSunny = (temperature >= 65 && temperature <= 80) && (precipitation < 1) && (wind < 5);
3. 気象確認地域データ
このシートは、キャンペーン データシートで使用される気象確認地域を定義するもので、次の 2 つの列で構成されています。
- Weather Location: OpenWeatherMap API で認識される気象場所の名前です。
- Geo Target Code: Google 広告が認識する地域コードです。
このスクリプトでは、1 つの気象確認地域に複数の地域ターゲティング コードを指定できます。これは、気象確認地域が Google 広告で使用可能なターゲティング オプションほど細かく設定されていない可能性があるためです。1 つの気象場所を複数の位置情報にマッピングするには、同じ気象場所で、行ごとに異なる地域コードを持つ複数の行を作成します。
この例では、次の 3 つの気象確認地域が定義されています。
Boston, MA
: 地域コード10108127
Connecticut
: 地域コード1014778
、1014743
、1014843
。コネチカットの 3 つの都市に対応します。Florida
: 地域コード21142
近隣地域ターゲットの設定
Matching Geo Targets
を使用するキャンペーン ルールは、TARGETING
フラグを使用して対象地域と近隣地域ターゲティング、またはその両方に適用できます。
地域ターゲティングが、地域コードと地域 ID を照合する場合。
近隣地域ターゲティングでは、指定された緯度と経度の座標が近隣半径の範囲内にあるかどうか、ヘイバサイン式を使って検証します。
スクリプトのロジック
スクリプトはまず、上記の 3 つのすべてのシートからルールを読み込みます。そして、キャンペーンのシートから順に各ルールの処理を実行します。
実行されたルールごとに、スクリプトはキャンペーンが指定された地域をターゲットにしているかどうかを確認します。ある場合は、現在の入札単価調整比を取得します。
次に、OpenWeatherMap API を呼び出してその場所の気象条件を取得します。その後、気象条件のルールが評価され、ロケーションの気象条件がルールで指定されているものと一致するかどうかが確認されます。気象条件が一致し、新しい単価調整比が現在の単価調整比と異なる場合は、新しい単価調整比をその地域に適用します。
気象条件が一致しない場合、入札単価調整比の値が同じである場合、またはルールの [Apply Modifier To] が Matching Geo
Targets
であるにもかかわらず、ルールにマッピングされた地域がキャンペーンのターゲットに設定されていない場合、変更は行われません。
設定
- openweathermap.org で API キーを登録します。
- テンプレートのスプレッドシートのコピーを作成し、キャンペーンと天気のルールを編集します。
- 下記のソースコードを使って新しいスクリプトを作成します。
- スクリプト内の
OPEN_WEATHER_MAP_API_KEY
変数、SPREADSHEET_URL
変数、TARGETING
変数を更新します。 - 必要に応じて、スクリプトのスケジュール設定を行います。
ソースコード
// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @name Bid By Weather
*
* @overview The Bid By Weather script adjusts campaign bids by weather
* conditions of their associated locations. See
* https://developers.google.com/google-ads/scripts/docs/solutions/weather-based-campaign-management#bid-by-weather
* for more details.
*
* @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 2.0
*
* @changelog
* - version 2.0
* - Updated to use new Google Ads Scripts features.
* - version 1.2.2
* - Add support for video and shopping campaigns.
* - version 1.2.1
* - Added validation for external spreadsheet setup.
* - version 1.2
* - Added proximity based targeting. Targeting flag allows location
* targeting, proximity targeting or both.
* - version 1.1
* - Added flag allowing bid adjustments on all locations targeted by
* a campaign rather than only those that match the campaign rule
* - version 1.0
* - Released initial version.
*/
// Register for an API key at http://openweathermap.org/appid
// and enter the key below.
const OPEN_WEATHER_MAP_API_KEY = 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE';
// Create a copy of https://goo.gl/A59Uuc and enter the URL below.
const SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';
// A cache to store the weather for locations already lookedup earlier.
const WEATHER_LOOKUP_CACHE = {};
// Flag to pick which kind of targeting "LOCATION", "PROXIMITY", or "ALL".
const TARGETING = 'ALL';
/**
* According to the list of campaigns and their associated locations, the script
* makes a call to the OpenWeatherMap API for each location.
* Based on the weather conditions, the bids are adjusted.
*/
function main() {
validateApiKey();
// Load data from spreadsheet.
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
const campaignRuleData = getSheetData(spreadsheet, 1);
const weatherConditionData = getSheetData(spreadsheet, 2);
const geoMappingData = getSheetData(spreadsheet, 3);
// Convert the data into dictionaries for convenient usage.
const campaignMapping = buildCampaignRulesMapping(campaignRuleData);
const weatherConditionMapping =
buildWeatherConditionMapping(weatherConditionData);
const locationMapping = buildLocationMapping(geoMappingData);
// Apply the rules.
for (const campaignName in campaignMapping) {
applyRulesForCampaign(campaignName, campaignMapping[campaignName],
locationMapping, weatherConditionMapping);
}
}
/**
* Retrieves the data for a worksheet.
*
* @param {Object} spreadsheet The spreadsheet.
* @param {number} sheetIndex The sheet index.
* @return {Array} The data as a two dimensional array.
*/
function getSheetData(spreadsheet, sheetIndex) {
const sheet = spreadsheet.getSheets()[sheetIndex];
const range =
sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
return range.getValues();
}
/**
* Builds a mapping between the list of campaigns and the rules
* being applied to them.
*
* @param {Array} campaignRulesData The campaign rules data, from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>> } A map, with key as campaign name,
* and value as an array of rules that apply to this campaign.
*/
function buildCampaignRulesMapping(campaignRulesData) {
const campaignMapping = {};
for (const rules of campaignRulesData) {
// Skip rule if not enabled.
if (rules[5].toLowerCase() == 'yes') {
const campaignName = rules[0];
const campaignRules = campaignMapping[campaignName] || [];
campaignRules.push({
'name': campaignName,
// location for which this rule applies.
'location': rules[1],
// the weather condition (e.g. Sunny).
'condition': rules[2],
// bid modifier to be applied.
'bidModifier': rules[3],
// whether bid adjustments should by applied only to geo codes
// matching the location of the rule or to all geo codes that
// the campaign targets.
'targetedOnly': rules[4].toLowerCase() ==
'matching geo targets'
});
campaignMapping[campaignName] = campaignRules;
}
}
Logger.log('Campaign Mapping: %s', campaignMapping);
return campaignMapping;
}
/**
* Builds a mapping between a weather condition name (e.g. Sunny) and the rules
* that correspond to that weather condition.
*
* @param {Array} weatherConditionData The weather condition data from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a weather
* condition name, and value as the set of rules corresponding to that
* weather condition.
*/
function buildWeatherConditionMapping(weatherConditionData) {
const weatherConditionMapping = {};
for (const weatherCondition of weatherConditionData) {
const weatherConditionName = weatherCondition[0];
weatherConditionMapping[weatherConditionName] = {
// Condition name (e.g. Sunny)
'condition': weatherConditionName,
// Temperature (e.g. 50 to 70)
'temperature': weatherCondition[1],
// Precipitation (e.g. below 70)
'precipitation': weatherCondition[2],
// Wind speed (e.g. above 5)
'wind': weatherCondition[3]
};
}
Logger.log('Weather condition mapping: %s', weatherConditionMapping);
return weatherConditionMapping;
}
/**
* Builds a mapping between a location name (as understood by OpenWeatherMap
* API) and a list of geo codes as identified by Google Ads scripts.
*
* @param {Array} geoTargetData The geo target data from the spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a locaton name,
* and value as an array of geo codes that correspond to that location
* name.
*/
function buildLocationMapping(geoTargetData) {
const locationMapping = {};
for (const geoTarget of geoTargetData) {
const locationName = geoTarget[0];
const locationDetails = locationMapping[locationName] || {
'geoCodes': [] // List of geo codes understood by Google Ads scripts.
};
locationDetails.geoCodes.push(geoTarget[1]);
locationMapping[locationName] = locationDetails;
}
Logger.log('Location Mapping: %s', locationMapping);
return locationMapping;
}
/**
* Applies rules to a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Object} campaignRules The details of the campaign. See
* buildCampaignMapping for details.
* @param {Object} locationMapping Mapping between a location name (as
* understood by OpenWeatherMap API) and a list of geo codes as
* identified by Google Ads scripts. See buildLocationMapping for details.
* @param {Object} weatherConditionMapping Mapping between a weather condition
* name (e.g. Sunny) and the rules that correspond to that weather
* condition. See buildWeatherConditionMapping for details.
*/
function applyRulesForCampaign(campaignName, campaignRules, locationMapping,
weatherConditionMapping) {
for (const rules of campaignRules) {
let bidModifier = 1;
const campaignRule = rules;
// Get the weather for the required location.
const locationDetails = locationMapping[campaignRule.location];
const weather = getWeather(campaignRule.location);
Logger.log('Weather for %s: %s', locationDetails, weather);
// Get the weather rules to be checked.
const weatherConditionName = campaignRule.condition;
const weatherConditionRules = weatherConditionMapping[weatherConditionName];
// Evaluate the weather rules.
if (evaluateWeatherRules(weatherConditionRules, weather)) {
Logger.log('Matching Rule found: Campaign Name = %s, location = %s, ' +
'weatherName = %s,weatherRules = %s, noticed weather = %s.',
campaignRule.name, campaignRule.location,
weatherConditionName, weatherConditionRules, weather);
bidModifier = campaignRule.bidModifier;
if (TARGETING == 'LOCATION' || TARGETING == 'ALL') {
// Get the geo codes that should have their bids adjusted.
const geoCodes = campaignRule.targetedOnly ?
locationDetails.geoCodes : null;
adjustBids(campaignName, geoCodes, bidModifier);
}
if (TARGETING == 'PROXIMITY' || TARGETING == 'ALL') {
const location = campaignRule.targetedOnly ? campaignRule.location : null;
adjustProximityBids(campaignName, location, bidModifier);
}
}
}
return;
}
/**
* Converts a temperature value from kelvin to fahrenheit.
*
* @param {number} kelvin The temperature in Kelvin scale.
* @return {number} The temperature in Fahrenheit scale.
*/
function toFahrenheit(kelvin) {
return (kelvin - 273.15) * 1.8 + 32;
}
/**
* Evaluates the weather rules.
*
* @param {Object} weatherRules The weather rules to be evaluated.
* @param {Object.<string, string>} weather The actual weather.
* @return {boolean} True if the rule matches current weather conditions,
* False otherwise.
*/
function evaluateWeatherRules(weatherRules, weather) {
// See https://openweathermap.org/weather-data
// for values returned by OpenWeatherMap API.
let precipitation = 0;
if (weather.rain && weather.rain['3h']) {
precipitation = weather.rain['3h'];
}
const temperature = toFahrenheit(weather.main.temp);
const windspeed = weather.wind.speed;
return evaluateMatchRules(weatherRules.temperature, temperature) &&
evaluateMatchRules(weatherRules.precipitation, precipitation) &&
evaluateMatchRules(weatherRules.wind, windspeed);
}
/**
* Evaluates a condition for a value against a set of known evaluation rules.
*
* @param {string} condition The condition to be checked.
* @param {Object} value The value to be checked.
* @return {boolean} True if an evaluation rule matches, false otherwise.
*/
function evaluateMatchRules(condition, value) {
// No condition to evaluate, rule passes.
if (condition == '') {
return true;
}
const rules = [matchesBelow, matchesAbove, matchesRange];
for (const rule of rules) {
if (rule(condition, value)) {
return true;
}
}
return false;
}
/**
* Evaluates whether a value is below a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. below 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is less than what is specified in
* condition, false otherwise.
*/
function matchesBelow(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'below') {
return false;
}
if (value < conditionParts[1]) {
return true;
}
return false;
}
/**
* Evaluates whether a value is above a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. above 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is greater than what is specified in
* condition, false otherwise.
*/
function matchesAbove(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'above') {
return false;
}
if (value > conditionParts[1]) {
return true;
}
return false;
}
/**
* Evaluates whether a value is within a range of values.
*
* @param {string} condition The condition to be checked (e.g. 5 to 18).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is in the desired range, false otherwise.
*/
function matchesRange(condition, value) {
conditionParts = condition.replace('w+', ' ').split(' ');
if (conditionParts.length != 3) {
return false;
}
if (conditionParts[1] != 'to') {
return false;
}
if (conditionParts[0] <= value && value <= conditionParts[2]) {
return true;
}
return false;
}
/**
* Retrieves the weather for a given location, using the OpenWeatherMap API.
*
* @param {string} location The location to get the weather for.
* @return {Object.<string, string>} The weather attributes and values, as
* defined in the API.
*/
function getWeather(location) {
if (location in WEATHER_LOOKUP_CACHE) {
Logger.log('Cache hit...');
return WEATHER_LOOKUP_CACHE[location];
}
const url=`http://api.openweathermap.org/data/2.5/weather?APPID=${OPEN_WEATHER_MAP_API_KEY}&q=${location}`;
const response = UrlFetchApp.fetch(url);
if (response.getResponseCode() != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
const result = JSON.parse(response.getContentText());
// OpenWeatherMap's way of returning errors.
if (result.cod != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
WEATHER_LOOKUP_CACHE[location] = result;
return result;
}
/**
* Adjusts the bidModifier for a list of geo codes for a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Array} geoCodes The list of geo codes for which bids should be
* adjusted. If null, all geo codes on the campaign are adjusted.
* @param {number} bidModifier The bid modifier to use.
*/
function adjustBids(campaignName, geoCodes, bidModifier) {
// Get the campaign.
const campaign = getCampaign(campaignName);
if (!campaign) return null;
// Get the targeted locations.
const locations = campaign.targeting().targetedLocations().get();
for (const location of locations) {
const currentBidModifier = location.getBidModifier().toFixed(2);
// Apply the bid modifier only if the campaign has a custom targeting
// for this geo location or if all locations are to be modified.
if (!geoCodes || (geoCodes.indexOf(location.getId()) != -1 &&
currentBidModifier != bidModifier)) {
Logger.log('Setting bidModifier = %s for campaign name = %s, ' +
'geoCode = %s. Old bid modifier is %s.', bidModifier,
campaignName, location.getId(), currentBidModifier);
location.setBidModifier(bidModifier);
}
}
}
/**
* Adjusts the bidModifier for campaigns targeting by proximity location
* for a given weather location.
*
* @param {string} campaignName The name of the campaign.
* @param {string} weatherLocation The weather location for which bids should be
* adjusted. If null, all proximity locations on the campaign are adjusted.
* @param {number} bidModifier The bid modifier to use.
*/
function adjustProximityBids(campaignName, weatherLocation, bidModifier) {
// Get the campaign.
const campaign = getCampaign(campaignName);
if(campaign === null) return;
// Get the proximity locations.
const proximities = campaign.targeting().targetedProximities().get();
for (const proximity of proximities) {
const currentBidModifier = proximity.getBidModifier().toFixed(2);
// Apply the bid modifier only if the campaign has a custom targeting
// for this geo location or if all locations are to be modified.
if (!weatherLocation ||
(weatherNearProximity(proximity, weatherLocation) &&
currentBidModifier != bidModifier)) {
Logger.log('Setting bidModifier = %s for campaign name = %s, with ' +
'weatherLocation = %s in proximity area. Old bid modifier is %s.',
bidModifier, campaignName, weatherLocation, currentBidModifier);
proximity.setBidModifier(bidModifier);
}
}
}
/**
* Checks if weather location is within the radius of the proximity location.
*
* @param {Object} proximity The targeted proximity of campaign.
* @param {string} weatherLocation Name of weather location to check within
* radius.
* @return {boolean} Returns true if weather location is within radius.
*/
function weatherNearProximity(proximity, weatherLocation) {
// See https://en.wikipedia.org/wiki/Haversine_formula for details on how
// to compute spherical distance.
const earthRadiusInMiles = 3960.0;
const degreesToRadians = Math.PI / 180.0;
const radiansToDegrees = 180.0 / Math.PI;
const kmToMiles = 0.621371;
const radiusInMiles = proximity.getRadiusUnits() == 'MILES' ?
proximity.getRadius() : proximity.getRadius() * kmToMiles;
// Compute the change in latitude degrees for the radius.
const deltaLat = (radiusInMiles / earthRadiusInMiles) * radiansToDegrees;
// Find the radius of a circle around the earth at given latitude.
const r = earthRadiusInMiles * Math.cos(proximity.getLatitude() *
degreesToRadians);
// Compute the change in longitude degrees for the radius.
const deltaLon = (radiusInMiles / r) * radiansToDegrees;
// Retrieve weather location for lat/lon coordinates.
const weather = getWeather(weatherLocation);
// Check if weather condition is within the proximity boundaries.
return (weather.coord.lat >= proximity.getLatitude() - deltaLat &&
weather.coord.lat <= proximity.getLatitude() + deltaLat &&
weather.coord.lon >= proximity.getLongitude() - deltaLon &&
weather.coord.lon <= proximity.getLongitude() + deltaLon);
}
/**
* Finds a campaign by name, whether it is a regular, video, or shopping
* campaign, by trying all in sequence until it finds one.
*
* @param {string} campaignName The campaign name to find.
* @return {Object} The campaign found, or null if none was found.
*/
function getCampaign(campaignName) {
const selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
AdsApp.shoppingCampaigns()];
for (const selector of selectors) {
const campaignIter = selector.
withCondition(`CampaignName = "${campaignName}"`).
get();
if (campaignIter.hasNext()) {
return campaignIter.next();
}
}
return null;
}
/**
* DO NOT EDIT ANYTHING BELOW THIS LINE.
* Please modify your spreadsheet URL and API key at the top of the file only.
*/
/**
* Validates the provided spreadsheet URL to make sure that it's set up
* properly. Throws a descriptive error message if validation fails.
*
* @param {string} spreadsheeturl The URL of the spreadsheet to open.
* @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
* @throws {Error} If the spreadsheet URL hasn't been set
*/
function validateAndGetSpreadsheet(spreadsheeturl) {
if (spreadsheeturl == 'INSERT_SPREADSHEET_URL_HERE') {
throw new Error('Please specify a valid Spreadsheet URL. You can find' +
' a link to a template in the associated guide for this script.');
}
const spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
return spreadsheet;
}
/**
* Validates the provided API key to make sure that it's not the default. Throws
* a descriptive error message if validation fails.
*
* @throws {Error} If the configured API key hasn't been set.
*/
function validateApiKey() {
if (OPEN_WEATHER_MAP_API_KEY == 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE') {
throw new Error('Please specify a valid API key for OpenWeatherMap. You ' +
'can acquire one here: http://openweathermap.org/appid');
}
}