Bidadjustments for audiences

Apply automatic bid adjustments to target groups in campaigns.

Start Now!
Bidadjustments for audiences
Bidadjustments Get started!

To gain more insight into the performance of your visitors and customers, I always add audiences to campaigns. This allows you to make bid adjustments based on the performance of these audiences. Depending on the customer, we sometimes added 100 different audiences to a campaign. Instead of tracking these groups every week and changing bid adjustments, we have automated this process.

The latest changes to this script make it possible to decide how the bidadjustment is calculated. You can go for CPA or ROAS. A bidadjustment is set based on the CPA or ROAS of an audience vs. the CPA or ROAS of it's campaign.

We also have scripts for bidadjustments for devices, locations or adschedule. Start automating all of your bidadjustments today.

Settings

In the script you can only adjust the following things:

  • LOG: Specify whether the script should report the intermediate steps by adjusting the value to 'true'.
  • DATE_RANGE: The script looks at the conversions and costs over this period.
  • MINIMUM_CONVERSIONS: Set a minimum number of conversions for an audience. If an audience has less conversions, we won't set a bidadjustment.
  • MINIMUM_CONVERSIONVALUE: Set a minimum conversionvalue for an audience. If an audience has less revenue, we won't set a bidadjustment.
  • MINIMUM_COST: Set a minimum amount of cost for an audience. If an audience has spend less, we won't set a bidadjustment.
  • MINIMUM_CLICKS: Set a minimum number of clicks for an audience. If an audience has less clicks, we won't set a bidadjustment.
  • KPI: Calculate the bidadjustment based on 'CPA' or 'ROAS'.
  • CAMPAIGN_LABEL: Select campaigns by using a label.
  • MAX_BID: Bidadjustment won't be higher then set here.
  • MIN_BID: Bidadjustment won't be lower then set here.

Scheduling: To generate sufficient data we advise to run the script once a day or once a week.

The script
// Copyright 2021. Increase BV. All Rights Reserved.
//
// Created By: Tibbe van Asten
// for Increase B.V.
// 
// Created: 01-03-2019
// Last update: 23-03-2021
//
// ABOUT THE SCRIPT
// With this script we adjust the biddings for audiences in 
// active campaigns.
//
////////////////////////////////////////////////////////////////////

var config = {

  LOG : true,
  DATE_RANGE : "LAST_30_DAYS",

  // Optional: Use minimum conversions and/or cost to select audiences
  // Leave empty to skip
  MINIMUM_CONVERSIONS : "",
  MINIMUM_CONVERSIONVALUE : "",
  MINIMUM_COST : "",
  MINIMUM_CLICKS : "",
  
  // Set bidadjustments based on CPA or ROAS
  KPI : "ROAS",
  
  // Optional: Use a campaignlabel to make a selection.
  // Leave empty to skip
  CAMPAIGN_LABEL : "",
  
  // Bidadjustments won't be higher then MAX_BID and not lower then MIN_BID
  // Examples: 1.2 = +20%, 0.8 = -20%, 2 = +100%
  MAX_BID : 1.2,
  MIN_BID : 0.8
  
}


////////////////////////////////////////////////////////////////////

function main() {
  
  if(config.KPI != "CPA" && config.KPI != "ROAS"){
    throw Error("Set KPI to 'CPA' or 'ROAS' only!");
  }
  
  var map = {};
  
  // Depending on use of campaign label, a function is triggered
  if(config.CAMPAIGN_LABEL == ""){ map = collectAudiences(map) }
  else{ map = collectAudiencesLabel(map) }
  
  if(config.LOG === true){
    Logger.log(" ");
    Logger.log("...Loop through audiences");
    Logger.log(Object.keys(map).length + " items found");
    Logger.log(" ");
  }
  
  
  // Set bidadjustments
  for (var key in map) {
    
    var campaignIterator = AdsApp
      .campaigns()
      .withIds([key.split(",")[1]])
      .get();
    
    while(campaignIterator.hasNext()){
      var campaign = campaignIterator.next();
      
        var ids = [];
        ids.push(key.split(",")[1]);
        ids.push(key.split(",")[0]);
      
      var audienceIterator = campaign
        .targeting()
        .audiences()
        .withIds([ids])
        .get();

      while(audienceIterator.hasNext()){
        var audience = audienceIterator.next();

        audience.bidding().setBidModifier(parseFloat(map[key][0][2])); 
        Logger.log("Bidadjustment of " + map[key][0][2] + " set for " + map[key][0][0] + " in " + map[key][0][1]);
        
      } // audienceIterator
      
    } // campaignIterator
    
  } // keyIterator
  
} // function main()

////////////////////////////////////////////////////////////////////

function collectAudiences(map){
  
  var report = AdsApp.report(
   "SELECT Id, UserListName, CampaignName, CampaignId, BidModifier, Conversions, ConversionValue, Cost, Clicks " +
    "FROM AUDIENCE_PERFORMANCE_REPORT " +
    "WHERE BiddingStrategyType IN ['MANUAL_CPC','MANUAL_CPV','MANUAL_CPM'] " +
    "AND CampaignStatus = ENABLED " +
    "DURING " + config.DATE_RANGE
  );
  
  var rows = report.rows();
  while(rows.hasNext()){
    var row = rows.next();
    
    // Check thresholds
    if(config.MINIMUM_CONVERSIONS != "" || config.MINIMUM_CONVERSIONVALUE != "" ||config.MINIMUM_COST != "" || config.MINIMUM_CLICKS != ""){      
      if(row["Conversions"] < config.MINIMUM_CONVERSIONS || row["ConversionValue"] < config.MINIMUM_CONVERSIONVALUE || row["Cost"] < config.MINIMUM_COST || row["Clicks"] < config.MINIMUM_CLICKS) continue;
    }
    
    var campaignKpi = parseFloat(getCampaignKpi(row["CampaignId"])).toFixed(2);
    var oldBid = parseFloat(1 + (row["BidModifier"].split("%")[0] / 100)).toFixed(2);
    
    // Calculate KPI's and new bid
    if(config.KPI == "CPA"){ var audienceKpi = parseFloat(row["Cost"] / row["Conversions"]).toFixed(2); var newBid = parseFloat(campaignKpi / audienceKpi).toFixed(2); }
    if(config.KPI == "ROAS"){ var audienceKpi = parseFloat(row["ConversionValue"] / row["Cost"]).toFixed(2); var newBid = parseFloat(audienceKpi / campaignKpi).toFixed(2); }
    
    // If there is a CPA, we will set a bidadjustment
    if(isNaN(newBid) === false && isFinite(audienceKpi) === true) {
      if(newBid < config.MIN_BID) { newBid = parseFloat(config.MIN_BID).toFixed(2); }
      if(newBid > config.MAX_BID) { newBid = parseFloat(config.MAX_BID).toFixed(2); }  
      
      // Add to map
      if(newBid != oldBid) {
        if(map[row["Id"] + "," + row["CampaignId"]] == null) {
          map[row["Id"] + "," + row["CampaignId"]] = [];
        }
        map[row["Id"] + "," + row["CampaignId"]].push([row["UserListName"], row["CampaignName"], newBid]);
      
          if(config.LOG === true){
            Logger.log("Audience " + row["UserListName"] + " in " + row["CampaignName"]);
            Logger.log("Current bidadjustment: " + row["BidModifier"] + " = " + oldBid);
            Logger.log("Audience " + config.KPI + ": " + audienceKpi + ", Campain " + config.KPI + ": " + campaignKpi + ", New bid: " + newBid);
            Logger.log("------------");
          }   
      }

    } // Check bid
    
  } // rowIterator 
  
  return map
  
}

////////////////////////////////////////////////////////////////////

function collectAudiencesLabel(map){
  
  var campaignIterator = AdsApp
    .campaigns()
    .withCondition("Status = ENABLED")
    .withCondition("LabelNames CONTAINS_ANY ['" + config.CAMPAIGN_LABEL + "']")
    .get();
  
  while(campaignIterator.hasNext()){
    var campaign = campaignIterator.next();
    
    // Collecting all audience data
    var report = AdsApp.report(
      "SELECT Id, UserListName, CampaignName, CampaignId, BidModifier, Conversions, ConversionValue, Cost, Clicks " +
      "FROM AUDIENCE_PERFORMANCE_REPORT " +
      "WHERE BiddingStrategyType IN ['MANUAL_CPC','MANUAL_CPV','MANUAL_CPM'] " +
      "AND CampaignId = " + campaign.getId() +
      " DURING " + config.DATE_RANGE
    );
    
    var rows = report.rows();
    while(rows.hasNext()){
      var row = rows.next();

      // Check thresholds
      if(config.MINIMUM_CONVERSIONS != "" || config.MINIMUM_CONVERSIONVALUE != "" ||config.MINIMUM_COST != "" || config.MINIMUM_CLICKS != ""){
        if(row["Conversions"] < config.MINIMUM_CONVERSIONS || row["ConversionValue"] < config.MINIMUM_CONVERSIONVALUE || row["Cost"] < config.MINIMUM_COST || row["Clicks"] < config.MINIMUM_CLICKS) continue;
      }
      
      // Campaign performance
      var campaignKpi = parseFloat(getCampaignKpi(row["CampaignId"])).toFixed(2);
      var oldBid = parseFloat(1 + (row["BidModifier"].split("%")[0] / 100)).toFixed(2);

      // Calculate KPI's and new bid
      if(config.KPI == "CPA"){ var audienceKpi = parseFloat(row["Cost"] / row["Conversions"]).toFixed(2); var newBid = parseFloat(campaignKpi / audienceKpi).toFixed(2); }
      if(config.KPI == "ROAS"){ var audienceKpi = parseFloat(row["ConversionValue"] / row["Cost"]).toFixed(2); var newBid = parseFloat(audienceKpi / campaignKpi).toFixed(2); }

      // If there is a CPA, we will set a bidadjustment
      if(isNaN(newBid) === false && isFinite(audienceKpi) === true) {
        if(newBid < config.MIN_BID) { newBid = parseFloat(config.MIN_BID).toFixed(2); }
        if(newBid > config.MAX_BID) { newBid = parseFloat(config.MAX_BID).toFixed(2); }  

        // Add to map
        if(newBid != oldBid) {
          if(map[row["Id"] + "," + row["CampaignId"]] == null) {
            map[row["Id"] + "," + row["CampaignId"]] = [];
          }
          map[row["Id"] + "," + row["CampaignId"]].push([row["UserListName"], row["CampaignName"], newBid]);

            if(config.LOG === true){
              Logger.log("Audience " + row["UserListName"] + " in " + row["CampaignName"]);
              Logger.log("Current bidadjustment: " + row["BidModifier"] + " = " + oldBid);
              Logger.log("Audience " + config.KPI + ": " + audienceKpi + ", Campain " + config.KPI + ": " + campaignKpi + ", New bid: " + newBid);
              Logger.log("------------");
            }   
        }

      } // Check bid

    } // rowIterator     
    
  } // campaignIterator
  
  return map
  
}

////////////////////////////////////////////////////////////////////

function getCampaignKpi(campaignId){
  
  var report = AdsApp.report(
    "SELECT Cost, Conversions, ConversionValue " +
    "FROM CAMPAIGN_PERFORMANCE_REPORT " +
    "WHERE CampaignId = '" + campaignId + "' " +
    "DURING " + config.DATE_RANGE
  );
  
  var rows = report.rows();
  while(rows.hasNext()){
    var campaign = rows.next();
    
    if(config.KPI == "CPA"){ var campaignKpi = campaign["Cost"] / campaign["Conversions"]; }
    if(config.KPI == "ROAS"){ var campaignKpi = campaign["ConversionValue"] / campaign["Cost"]; }
    
  } // campaignIterator 
  
  return campaignKpi;
  
} // function getCampaignKpi()
Show whole script!
Loading Comments
The Experts
Tibbe van Asten Head of PPC @ Increase
Nils Rooijmans Water Cooler Topics
Martijn Kraan Freelance PPC Specialist
Bas Baudoin Teamlead SEA @ Happy Leads
How about you? JOIN US!
Sharing Knowledge
Caring

Sharing Knowledge

Adsscripts.com is all about sharing knowledge. In the current market, PPC specialists like to keep their knowledge and experience to themselves. We're convinced that sharing knowledge can ensure that everyone gets better at their work. We want to change this by sharing our knowledge about scripts with everyone.

Do you also want to contribute? We are open to new ideas and feedback on everything you find on Adsscripts.com.

Contact us

Training &
Workshop
Contact us!
Adsscripts Training & Workshop