Script 15: Budget Boost for Performing Non Brand Campaigns

Purpose

Boost the budget of non-brand campaigns if their CPA performance is better than the average by a certain threshold.

To Elaborate

The Python script aims to automate the process of boosting the budget for non-brand campaigns that have better CPA (Cost per Acquisition) performance compared to the average. The key business rules of the script are as follows:

  • The script looks at non-brand campaigns, identified by the presence of “Nonbrand” or “NB” in their names.
  • It considers a 33-day lookback period, excluding the most recent 3 days, to account for conversion lag.
  • The script calculates the CPA for each campaign by dividing the total cost by the total number of conversions.
  • It then calculates the average CPA for each account and campaign type.
  • The script compares the CPA of each campaign to the average CPA and calculates the performance difference.
  • If the performance difference is greater than a preset threshold, the script increases the campaign’s daily budget by a preset percentage.
  • The script generates an output table with the account, campaign, adjusted daily budget, and a message indicating the reason for the budget increase.

Walking Through the Code

  1. The script defines column constants and imports necessary libraries.
  2. It initializes variables for the performance threshold and budget increase percentage.
  3. The script reduces the input dataframe to only the needed columns and fills in missing campaign types.
  4. It filters the dataframe to include only non-brand campaigns.
  5. The script sets the start and end dates for the lookback period.
  6. It filters the dataframe to include only data within the specified date range.
  7. The script aggregates the data by account, campaign type, and campaign, taking the last campaign status and daily budget, and summing the publication cost and conversions.
  8. The script recalculates the CPA for each campaign.
  9. It calculates the average CPA for each account and campaign type.
  10. The script merges the average CPA data with the campaign data for comparison.
  11. It calculates the performance of each campaign’s CPA relative to the average.
  12. The script calculates the new daily budget for all campaigns by increasing the original daily budget by the budget increase percentage.
  13. It filters the data to include only campaigns that meet certain conditions, such as active status, spend, non-zero adjusted budget, and CPA performance above the threshold.
  14. If there are qualified campaigns, the script annotates them with a message indicating the reason for the budget increase.
  15. The script generates an output dataframe with the account, campaign, adjusted daily budget, and automation info.
  16. If there are no qualified campaigns, the script prints a message indicating that no campaigns exceeded the performance threshold.

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-05-15 07:44:05 GMT

comments powered by Disqus