Script 15: Budget Boost for Performing Non Brand Campaigns

Purpose

The Python script adjusts the budget for non-brand advertising campaigns based on their cost-per-acquisition (CPA) performance compared to peer averages.

To Elaborate

The script is designed to optimize the budget allocation for advertising campaigns that are identified as non-brand by analyzing their CPA performance over a specific period. It focuses on campaigns with ‘Nonbrand’ or ‘NB’ in their names and evaluates their performance over a 33-day period, excluding the most recent 3 days to account for conversion lag. If a campaign’s CPA is at least 10% better than the average CPA of similar campaigns within the same account, the script increases its budget by 5%. This approach aims to enhance the efficiency of budget spending by allocating more resources to better-performing campaigns, thereby potentially increasing overall advertising effectiveness.

Walking Through the Code

  1. Initial Setup and Configuration
    • The script begins by setting up constants for column names and initializes the output DataFrame with placeholders for daily budget and automation information.
    • User-changeable parameters include performance_threshold (set to 0.10) and budget_increase (set to 0.05), which determine the CPA performance threshold and the budget increase percentage, respectively.
  2. Data Preparation
    • The script reduces the input data to necessary columns and fills missing campaign types with ‘NA’.
    • It filters the data to include only non-brand campaigns by checking for ‘NB’ or ‘nonbrand’ in campaign names.
  3. Date Filtering
    • The script applies a 33-day lookback period, excluding the most recent 3 days, to account for conversion lag, and filters the data accordingly.
  4. Data Aggregation
    • It aggregates the data by summing the cost per conversion and taking the latest campaign budget, while removing entries with zero conversions.
    • The CPA is recalculated for each campaign.
  5. CPA Comparison
    • The script calculates the average CPA by account and campaign type, then merges this data with the campaign data to facilitate comparison.
    • It computes the CPA performance relative to the average CPA.
  6. Budget Adjustment
    • The script calculates a new daily budget for campaigns and identifies those that meet specific conditions: active status, non-zero spend, non-zero adjusted budget, and CPA performance above the threshold.
    • For qualifying campaigns, it annotates the budget increase and prepares the output DataFrame with updated budget information.
  7. Output Generation
    • If no campaigns meet the criteria, it outputs an empty DataFrame; otherwise, it formats and outputs the adjusted budget data.

Vitals

  • Script ID : 15
  • Client ID / Customer ID: 1306920543 / 60268855
  • Action Type: Bulk Upload (Preview)
  • Item Changed: Campaign
  • Output Columns: Account, Campaign, Daily Budget, AUTOMATION - INFO
  • Linked Datasource: M1 Report
  • Reference Datasource: None
  • Owner: Michael Huang (mhuang@marinsoftware.com)
  • Created by Michael Huang on 2023-02-15 22:39
  • Last Updated by Autumn Archibald on 2024-03-13 04:02
> See it in Action

Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#
# Boost Campaign Budget if CPA performance is better than peer average by preset threshold
#
#
# Author: Michael S. Huang
# Date: 2023-02-18

RPT_COL_CAMPAIGN = 'Campaign'
RPT_COL_DATE = 'Date'
RPT_COL_ACCOUNT = 'Account'
RPT_COL_CAMPAIGN_ID = 'Campaign ID'
RPT_COL_CAMPAIGN_TYPE = 'Campaign Type'
RPT_COL_CAMPAIGN_STATUS = 'Campaign Status'
RPT_COL_PUB_COST = 'Pub. Cost $'
RPT_COL_COST_PER_CONV = 'Cost/Conv. $'
RPT_COL_ROAS = 'ROAS'
RPT_COL_AVG_CPC = 'Avg. CPC $'
RPT_COL_CONV_RATE = 'Conv. Rate %'
RPT_COL_CTR = 'CTR %'
RPT_COL_CONV = 'Conv.'
RPT_COL_REVENUE = 'Revenue $'
RPT_COL_SEARCH_LOSTTOPISBUDGET = 'Search Lost Top IS (Budget) %'
RPT_COL_SEARCH_LOSTTOPISRANK = 'Search Lost Top IS (Rank) %'
RPT_COL_LOST_IMPRSHAREBUDGET = 'Lost Impr. Share (Budget) %'
RPT_COL_LOST_IMPRSHARERANK = 'Lost Impr. Share (Rank) %'
RPT_COL_DAILY_BUDGET = 'Daily Budget'
RPT_COL_AVG_BID = 'Avg. Bid $'
RPT_COL_HIST_QS = 'Hist. QS'
RPT_COL_IMPR = 'Impr.'
RPT_COL_CLICKS = 'Clicks'
BULK_COL_ACCOUNT = 'Account'
BULK_COL_CAMPAIGN = 'Campaign'
BULK_COL_DAILY_BUDGET = 'Daily Budget'
BULK_COL_AUTOMATION_INFO = 'AUTOMATION - INFO'

outputDf[BULK_COL_DAILY_BUDGET] = numpy.nan
outputDf[BULK_COL_AUTOMATION_INFO] = numpy.nan

# CPA Performance Threshold and Campaign Budget increase step size
performance_threshold = 0.10
budget_increase = 0.05

# when missing CONV, CPA is INF. Treat it as missing value.
pd.set_option('mode.use_inf_as_na', True)

# reduce to needed columns, fill in missing campaign type
df_reduced = inputDf[[RPT_COL_ACCOUNT, RPT_COL_CAMPAIGN_TYPE, RPT_COL_CAMPAIGN, RPT_COL_CAMPAIGN_STATUS, RPT_COL_DATE, RPT_COL_DAILY_BUDGET, RPT_COL_PUB_COST, RPT_COL_CONV]].copy()
df_reduced.fillna({RPT_COL_CAMPAIGN_TYPE: 'NA'}, inplace=True)

# limit to non-brand campaigns
non_brand = (df_reduced[RPT_COL_CAMPAIGN].map(lambda x: 'NB' in x)) | (df_reduced[RPT_COL_CAMPAIGN].map(lambda x: 'nonbrand' in x.lower()))
df_reduced = df_reduced[non_brand]

# print("reduced input\\n", tableize(df_reduced))

# 30-day lookback without most recent 3 days due to conversion lag
start_date = pd.to_datetime(datetime.date.today() - datetime.timedelta(days=33))
end_date = pd.to_datetime(datetime.date.today() - datetime.timedelta(days=3))
df_reduced = df_reduced[ (df_reduced[RPT_COL_DATE] >= start_date) & (df_reduced[RPT_COL_DATE] <= end_date) ]
if (df_reduced.shape[0] > 0):
    print("reduced dates\\n", min(df_reduced[RPT_COL_DATE]), max(df_reduced[RPT_COL_DATE]))
else:
    print("no more input to process")

# aggregate over remaining date range, summing cost/conv and take latest campaign budget
# remove 0 conv
agg_func_selection = {
    RPT_COL_CAMPAIGN_STATUS: ['last'],
    RPT_COL_DAILY_BUDGET: ['last'],
    RPT_COL_PUB_COST: ['sum'],
    RPT_COL_CONV: ['sum']
}
df_campaign_agg = df_reduced.groupby([RPT_COL_ACCOUNT, RPT_COL_CAMPAIGN_TYPE, RPT_COL_CAMPAIGN]).agg(agg_func_selection)

# recalc cpa
df_campaign_agg[RPT_COL_COST_PER_CONV] = df_campaign_agg[RPT_COL_PUB_COST] / df_campaign_agg[RPT_COL_CONV]
# print("recalc cpa for campaign w conv\\n", tableize(df_campaign_agg))

# calc avg cpa by account and campaign type
agg_func_selection = {
    RPT_COL_DAILY_BUDGET: ['mean'],
    RPT_COL_PUB_COST: ['sum'],
    RPT_COL_CONV: ['sum']
}
df_cpa_agg = df_reduced.groupby([RPT_COL_ACCOUNT, RPT_COL_CAMPAIGN_TYPE]).agg(agg_func_selection)
df_cpa_agg[RPT_COL_COST_PER_CONV] = df_cpa_agg[RPT_COL_PUB_COST] / df_cpa_agg[RPT_COL_CONV]
# print("avg cpa\\n", tableize(df_cpa_agg))

# merge avg cpa to input df to make comparison
df_comparison = df_campaign_agg.join(df_cpa_agg, how='left', on=[RPT_COL_ACCOUNT, RPT_COL_CAMPAIGN_TYPE], lsuffix="", rsuffix="_agg")
# print("merged avg cpa\\n", tableize(df_comparison))

# calculate performance of CPA relative to categorical average
df_comparison['cpa_perf'] = (df_comparison[RPT_COL_COST_PER_CONV + '_agg'] - df_comparison[RPT_COL_COST_PER_CONV]) / df_comparison[RPT_COL_COST_PER_CONV + '_agg']

# print("daily budget\\n", df_comparison[RPT_COL_DAILY_BUDGET]['last'])

# calc new daily budget for all campaigns
df_comparison[BULK_COL_DAILY_BUDGET + '_'] = df_comparison[RPT_COL_DAILY_BUDGET]['last'] * (1+budget_increase)

print("df_comparison\\n", tableize(df_comparison))

# Make budget adj if campaign meets these conditions:
#  - campaign status is ACTIVE
#  - campaign has spend
#  - adjusted campaign budget is non-zero
#  - campaign cpa perf is over threshold
df_adj = df_comparison[ \
        (df_comparison[RPT_COL_CAMPAIGN_STATUS]['last'] == 'Active') & \
        (df_comparison[RPT_COL_PUB_COST]['sum'] > 0) & \
        (df_comparison[BULK_COL_DAILY_BUDGET + '_'] > 0) & \
        (df_comparison['cpa_perf'] > performance_threshold) \
    ].copy() 

if not df_adj.empty:

    df_adj.reset_index(level='Campaign', inplace=True)

    # annotate via Marin Dimensions
    def rowFunc(row):
        return 'CPA is {:,.2f}% lower than avg ${:,.2f} for Account/CampaignType {}. Budget increased {:,.2f}% on {}'.format(
            100*row['cpa_perf'].values[0], \
            row[RPT_COL_COST_PER_CONV + "_agg"].values[0], \
            row.name, \
            budget_increase * 100, \
            datetime.date.today()
        )

    df_adj[BULK_COL_AUTOMATION_INFO] = df_adj.apply(rowFunc, axis=1)

    df_adj.reset_index(inplace=True)

    # print("df_adj\\n", tableize(df_adj))

    # bulk output
    outputDf = df_adj[[RPT_COL_ACCOUNT, RPT_COL_CAMPAIGN, RPT_COL_DAILY_BUDGET + '_', BULK_COL_AUTOMATION_INFO]].copy()
    outputDf.rename(columns={RPT_COL_DAILY_BUDGET + '_': RPT_COL_DAILY_BUDGET}, inplace=True)
    outputDf = outputDf.round(2).droplevel(1, axis=1)
    # print("output\\n", tableize(outputDf))

else:

    print("No qualified campaigns exceeded performance threshold of {:,.2f}%".format(performance_threshold * 100))
    outputDf = outputDf.iloc[0:0]

Post generated on 2024-11-27 06:58:46 GMT

comments powered by Disqus