Em4e commited on
Commit
56d0ce0
Β·
verified Β·
1 Parent(s): 2ecf6a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +249 -130
app.py CHANGED
@@ -4,18 +4,164 @@ import streamlit as st
4
  import requests
5
 
6
  # ▢️ URL for your sample GSC CSV
 
7
  SAMPLE_FILE_URL = (
8
  "https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/"
9
  "resolve/main/sample_gsc_data.csv"
10
  )
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  st.set_page_config(page_title="SEO ROI & Savings Forecasting", layout="wide")
13
  st.title("πŸ“ˆ B2B SaaS SEO ROI & Savings Simulator")
14
 
15
  # ---
16
  # ℹ️ How the app works
 
17
  with st.expander("ℹ️ How the app works", expanded=True):
18
- st.markdown("""
 
19
  <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px;">
20
  <p>1. <b>Load your GSC data</b> (we lowercase all column names on load). If no file is uploaded, we use the default sample data. If no <code>cpc</code> column is present, we simulate values between 0.50 and 3.00 dollars.</p>
21
  <p>2. <b>CTR benchmarks</b> by position map an expected click-through rate for positions 1–20.</p>
@@ -37,25 +183,37 @@ with st.expander("ℹ️ How the app works", expanded=True):
37
  <p>5. <b>Results</b></p>
38
  <p>Top-line metrics and keyword-level table with impact labels.</p>
39
  </div>
40
- """, unsafe_allow_html=True)
 
 
41
 
42
  # β€” Sidebar inputs
 
 
43
  with st.sidebar:
44
- st.header("πŸ”§ Assumptions & Inputs") # Moved st.header inside with block
45
- uploaded_file = st.file_uploader("Upload GSC CSV", type="csv")
46
- target_position = st.slider("Target SERP Position",
47
- 1.0, 10.0, 4.0, 0.5)
48
- conversion_rate = st.slider("Conversion Rate (% β†’ signup)", 0.1, 10.0, 2.0, 0.1)
49
- close_rate = st.slider("Close Rate (% β†’ customer)",
50
- 1.0, 100.0, 20.0, 1.0)
51
- mrr_per_customer = st.slider("MRR per Customer ($)",
52
- 10, 1000, 200, 10)
53
- seo_cost = st.slider("Total SEO Investment ($)",
54
- 1_000, 100_000, 10_000, 1_000)
55
- add_spend = st.slider("Additional Ad Spend ($)",
56
- 0, 50_000, 0, 1_000)
 
 
 
 
 
 
 
57
 
58
  # β€” Download sample CSV button
 
59
  sample_bytes = requests.get(SAMPLE_FILE_URL).content
60
  st.download_button(
61
  label="πŸ“₯ Download sample CSV",
@@ -64,152 +222,113 @@ st.download_button(
64
  mime="text/csv",
65
  )
66
 
67
- # === Load & normalize CSV ===
68
- def load_csv():
69
- try:
70
- if uploaded_file:
71
- df = pd.read_csv(uploaded_file)
72
- else:
73
- df = pd.read_csv(SAMPLE_FILE_URL)
74
- except Exception as e:
75
- st.error(f"Error loading file: {e}")
76
- return None
77
-
78
- # lowercase all column names
79
- df.columns = [col.lower() for col in df.columns]
80
-
81
- # ensure a 'cpc' column
82
- if 'cpc' not in df.columns:
83
- st.warning("No `cpc` column foundοΏ½οΏ½οΏ½simulating CPC values between $0.50–$3.00.")
84
- df['cpc'] = np.round(np.random.uniform(0.5, 3.0, size=len(df)), 2)
85
- return df
86
-
87
- # === Core calculation ===
88
- def calculate(df, target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend):
89
- # required columns mapping
90
- cols = {c: c for c in df.columns}
91
- required = {
92
- 'query': ['query', 'keyword', 'queries'],
93
- 'impressions': ['impressions'],
94
- 'position': ['position', 'avg. position', 'average position'],
95
- 'cpc': ['cpc']
96
- }
97
- found = {}
98
- for key, opts in required.items():
99
- for opt in opts:
100
- if opt in df.columns:
101
- found[key] = opt
102
- break
103
- if key not in found:
104
- st.error(f"Missing required column: {key}")
105
- return None, pd.DataFrame()
106
-
107
- # rename to our standard keys
108
- df = df.rename(columns={found[k]: k for k in found})
109
-
110
- # CTR benchmarks
111
- ctr = {i: v for i, v in zip(range(1, 11), [0.25,0.15,0.10,0.08,0.06,0.04,0.03,0.02,0.015,0.01])}
112
- ctr.update({i: 0.005 for i in range(11,21)})
113
- get_ctr = lambda p: ctr.get(int(round(p)), 0.005)
114
 
115
- df['current_ctr'] = df['position'].apply(get_ctr)
116
- df['target_ctr'] = df['position'].apply(lambda x: ctr.get(int(round(target_position)), 0.005))
 
117
 
118
- df['current_clicks'] = df['impressions'] * df['current_ctr']
119
- df['projected_clicks'] = df['impressions'] * df['target_ctr']
120
- df['incremental_clicks'] = df['projected_clicks'] - df['current_clicks']
121
- df['avoided_paid_spend'] = df['incremental_clicks'] * df['cpc']
 
 
 
 
 
 
 
122
 
123
- # Financial calculations
124
- total_avoided_paid_spend = df['avoided_paid_spend'].sum()
125
- net_savings_vs_paid = total_avoided_paid_spend - seo_cost
126
-
127
- total_incremental_conversions = df['incremental_clicks'].sum() * (conversion_rate / 100)
128
- total_incremental_customers = total_incremental_conversions * (close_rate / 100)
129
- incremental_mrr = total_incremental_customers * mrr_per_customer
130
-
131
- # SEO ROI calculation
132
- if seo_cost > 0:
133
- seo_roi = (incremental_mrr - seo_cost) / seo_cost
134
- else:
135
- seo_roi = np.inf # Undefined or very high if no SEO cost
136
-
137
- # Categorize impact for each query
138
- def categorize_impact(row):
139
- if row['position'] > target_position:
140
- return 'πŸš€ Improvement'
141
- elif row['position'] <= target_position and row['incremental_clicks'] > 0:
142
- return 'βœ… Maintain & Grow'
143
- else:
144
- return '🎯 Reached Target'
145
-
146
- df['impact_category'] = df.apply(categorize_impact, axis=1)
147
-
148
- return {
149
- 'total_avoided_paid_spend': total_avoided_paid_spend,
150
- 'net_savings_vs_paid': net_savings_vs_paid,
151
- 'total_incremental_conversions': total_incremental_conversions,
152
- 'total_incremental_customers': total_incremental_customers,
153
- 'incremental_mrr': incremental_mrr,
154
- 'seo_roi': seo_roi
155
- }, df
156
-
157
- # ---
158
- # Main app logic
159
- df = load_csv()
160
- if df is not None:
161
- metrics, df_results = calculate(df.copy(), target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend)
162
-
163
- if metrics is not None:
164
  st.write("---")
165
  st.header("πŸ“Š SEO Performance Summary")
166
 
 
167
  col1, col2, col3 = st.columns(3)
168
  with col1:
169
- st.metric(label="Total Avoided Paid Spend πŸ’°", value=f"${metrics['total_avoided_paid_spend']:,.2f}")
 
 
 
170
  with col2:
171
- st.metric(label="Net Savings vs Paid πŸ“ˆ", value=f"${metrics['net_savings_vs_paid']:,.2f}")
 
 
 
172
  with col3:
173
- st.metric(label="Incremental MRR recurrent revenue πŸš€", value=f"${metrics['incremental_mrr']:,.2f}")
 
 
 
174
 
175
  col4, col5, col6 = st.columns(3)
176
  with col4:
177
- st.metric(label="Total Incremental Conversions 🎯", value=f"{metrics['total_incremental_conversions']:,.0f}")
 
 
 
178
  with col5:
179
- st.metric(label="Total Incremental Customers 🀝", value=f"{metrics['total_incremental_customers']:,.0f}")
 
 
 
180
  with col6:
181
- st.metric(label="SEO ROI (Return on Investment) πŸ’°", value=f"{metrics['seo_roi']:.2%}")
 
 
 
182
 
183
  st.write("---")
184
  st.header("Hypothetical Comparison: SEO vs. Additional Ad Spend")
185
 
186
- col_ad1, col_ad2, col_advice = st.columns([1, 1, 1]) # Added a third column for advice
187
-
188
  with col_ad1:
189
- st.metric(label="Incremental MRR from SEO", value=f"${metrics['incremental_mrr']:,.2f}")
190
-
 
 
191
  with col_ad2:
192
- st.metric(label="Additional Ad Spend", value=f"${add_spend:,.2f}")
 
 
193
 
194
  with col_advice:
195
- if metrics['incremental_mrr'] > add_spend:
196
  advice_message = "SEO is a better investment!"
197
  advice_color = "green"
198
  else:
199
  advice_message = "Ad Spend may yield higher returns."
200
  advice_color = "red"
201
- st.markdown(f"""
 
202
  <div style="text-align: center;">
203
  <p style="font-size: 1.2em; margin-bottom: 0;">Advice</p>
204
  <p style="color:{advice_color}; font-weight:bold; font-size: 1.5em; margin-top: 0;">{advice_message}</p>
205
  </div>
206
- """, unsafe_allow_html=True)
207
-
 
208
 
209
  st.write("---")
210
- st.header("Detailed Keyword Performance") # Kept as st.header for prominence
211
- st.dataframe(df_results[[
212
- 'query', 'impressions', 'position', 'current_ctr', 'target_ctr',
213
- 'current_clicks', 'projected_clicks', 'incremental_clicks',
214
- 'cpc', 'avoided_paid_spend', 'impact_category'
215
- ]].sort_values(by='incremental_clicks', ascending=False), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import requests
5
 
6
  # ▢️ URL for your sample GSC CSV
7
+ # This URL points to a sample Google Search Console (GSC) data CSV file hosted on Hugging Face.
8
  SAMPLE_FILE_URL = (
9
  "https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/"
10
  "resolve/main/sample_gsc_data.csv"
11
  )
12
 
13
+ # === Helper Functions ===
14
+
15
+ # === Load & normalize CSV ===
16
+ # This function handles loading the GSC data, either from an uploaded file or the sample URL.
17
+ # It also standardizes column names and simulates CPC values if missing.
18
+ def load_csv(uploaded_file_obj):
19
+ """
20
+ Loads the GSC data from an uploaded CSV or a sample URL,
21
+ normalizes column names, and ensures a 'cpc' column exists.
22
+
23
+ Args:
24
+ uploaded_file_obj (streamlit.uploaded_file_manager.UploadedFile): The file object
25
+ uploaded by the user, or None.
26
+
27
+ Returns:
28
+ pd.DataFrame: The loaded and processed DataFrame, or None if an error occurs.
29
+ """
30
+ try:
31
+ if uploaded_file_obj:
32
+ df = pd.read_csv(uploaded_file_obj)
33
+ else:
34
+ df = pd.read_csv(SAMPLE_FILE_URL)
35
+ except Exception as e:
36
+ st.error(f"Error loading file: {e}")
37
+ return None
38
+
39
+ # Convert all column names to lowercase for consistency
40
+ df.columns = [col.lower() for col in df.columns]
41
+
42
+ # Check for 'cpc' column; if missing, simulate values
43
+ if "cpc" not in df.columns:
44
+ st.warning("No `cpc` column foundβ€”simulating CPC values between $0.50–$3.00.")
45
+ df["cpc"] = np.round(np.random.uniform(0.5, 3.0, size=len(df)), 2)
46
+ return df
47
+
48
+
49
+ # === Core calculation ===
50
+ # This function performs the main calculations for SEO performance, including
51
+ # click-through rates, incremental clicks, avoided paid spend, and ROI.
52
+ def calculate(
53
+ df,
54
+ target_position,
55
+ conversion_rate,
56
+ close_rate,
57
+ mrr_per_customer,
58
+ seo_cost,
59
+ add_spend,
60
+ ):
61
+ """
62
+ Performs core calculations for SEO forecasting based on GSC data and user inputs.
63
+
64
+ Args:
65
+ df (pd.DataFrame): The input DataFrame containing GSC data.
66
+ target_position (float): The desired average search engine result page position.
67
+ conversion_rate (float): Percentage of clicks that convert to signups.
68
+ close_rate (float): Percentage of signups that become paying customers.
69
+ mrr_per_customer (int): Monthly Recurring Revenue per customer.
70
+ seo_cost (int): Total investment in SEO efforts.
71
+ add_spend (int): Hypothetical additional ad spend for comparison.
72
+
73
+ Returns:
74
+ tuple: A dictionary of calculated metrics and a DataFrame with detailed results.
75
+ Returns (None, pd.DataFrame()) if required columns are missing.
76
+ """
77
+ # Define required column mappings for flexibility in input CSVs
78
+ required_columns = {
79
+ "query": ["query", "keyword", "queries"],
80
+ "impressions": ["impressions"],
81
+ "position": ["position", "avg. position", "average position"],
82
+ "cpc": ["cpc"],
83
+ }
84
+
85
+ found_columns = {}
86
+ for key, options in required_columns.items():
87
+ for opt in options:
88
+ if opt in df.columns:
89
+ found_columns[key] = opt
90
+ break
91
+ if key not in found_columns:
92
+ st.error(f"Missing required column: {key}. Please ensure your CSV has one of {options}.")
93
+ return None, pd.DataFrame()
94
+
95
+ # Rename columns to a standardized format for easier processing
96
+ df = df.rename(columns={found_columns[k]: k for k in found_columns})
97
+
98
+ # Define Click-Through Rate (CTR) benchmarks by position
99
+ # These are illustrative CTRs for positions 1-20
100
+ ctr_benchmarks = {i: v for i, v in zip(range(1, 11), [0.25, 0.15, 0.10, 0.08, 0.06, 0.04, 0.03, 0.02, 0.015, 0.01])}
101
+ ctr_benchmarks.update({i: 0.005 for i in range(11, 21)})
102
+
103
+ # Helper function to get CTR based on position, defaulting to 0.005 for positions > 20
104
+ get_ctr = lambda p: ctr_benchmarks.get(int(round(p)), 0.005)
105
+
106
+ # Calculate current and target CTRs, and projected clicks
107
+ df["current_ctr"] = df["position"].apply(get_ctr)
108
+ df["target_ctr"] = df["position"].apply(
109
+ lambda x: ctr_benchmarks.get(int(round(target_position)), 0.005)
110
+ )
111
+ df["current_clicks"] = df["impressions"] * df["current_ctr"]
112
+ df["projected_clicks"] = df["impressions"] * df["target_ctr"]
113
+ df["incremental_clicks"] = df["projected_clicks"] - df["current_clicks"]
114
+ df["avoided_paid_spend"] = df["incremental_clicks"] * df["cpc"]
115
+
116
+ # --- Financial calculations ---
117
+ total_avoided_paid_spend = df["avoided_paid_spend"].sum()
118
+ net_savings_vs_paid = total_avoided_paid_spend - seo_cost
119
+ total_incremental_conversions = df["incremental_clicks"].sum() * (
120
+ conversion_rate / 100
121
+ )
122
+ total_incremental_customers = total_incremental_conversions * (close_rate / 100)
123
+ incremental_mrr = total_incremental_customers * mrr_per_customer
124
+
125
+ # SEO ROI calculation, handling division by zero for seo_cost
126
+ if seo_cost > 0:
127
+ seo_roi = (incremental_mrr - seo_cost) / seo_cost
128
+ else:
129
+ seo_roi = np.inf # Undefined or very high if no SEO cost
130
+
131
+ # Categorize impact for each query based on its current position relative to the target
132
+ def categorize_impact(row):
133
+ if row["position"] > target_position:
134
+ return "πŸš€ Improvement" # Position is worse than target, room for improvement
135
+ elif (
136
+ row["position"] <= target_position and row["incremental_clicks"] > 0
137
+ ):
138
+ return "βœ… Maintain & Grow" # Position is at or better than target, still gaining clicks
139
+ else:
140
+ return "🎯 Reached Target" # Position is at or better than target, no further incremental clicks expected
141
+
142
+ df["impact_category"] = df.apply(categorize_impact, axis=1)
143
+
144
+ # Return calculated metrics and the detailed DataFrame
145
+ return {
146
+ "total_avoided_paid_spend": total_avoided_paid_spend,
147
+ "net_savings_vs_paid": net_savings_vs_paid,
148
+ "total_incremental_conversions": total_incremental_conversions,
149
+ "total_incremental_customers": total_incremental_customers,
150
+ "incremental_mrr": incremental_mrr,
151
+ "seo_roi": seo_roi,
152
+ }, df
153
+
154
+
155
+ # Set Streamlit page configuration for a wider layout and a descriptive title.
156
  st.set_page_config(page_title="SEO ROI & Savings Forecasting", layout="wide")
157
  st.title("πŸ“ˆ B2B SaaS SEO ROI & Savings Simulator")
158
 
159
  # ---
160
  # ℹ️ How the app works
161
+ # This section provides an expandable information box explaining the app's methodology.
162
  with st.expander("ℹ️ How the app works", expanded=True):
163
+ st.markdown(
164
+ """
165
  <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px;">
166
  <p>1. <b>Load your GSC data</b> (we lowercase all column names on load). If no file is uploaded, we use the default sample data. If no <code>cpc</code> column is present, we simulate values between 0.50 and 3.00 dollars.</p>
167
  <p>2. <b>CTR benchmarks</b> by position map an expected click-through rate for positions 1–20.</p>
 
183
  <p>5. <b>Results</b></p>
184
  <p>Top-line metrics and keyword-level table with impact labels.</p>
185
  </div>
186
+ """,
187
+ unsafe_allow_html=True,
188
+ )
189
 
190
  # β€” Sidebar inputs
191
+ # This section defines the input controls in the Streamlit sidebar, allowing users to
192
+ # adjust various parameters for the SEO forecasting.
193
  with st.sidebar:
194
+ st.header("πŸ”§ Assumptions & Inputs")
195
+ uploaded_file = st.file_uploader("Upload GSC CSV", type="csv")
196
+ target_position = st.slider(
197
+ "Target SERP Position", 1.0, 10.0, 4.0, 0.5
198
+ ) # Desired average search engine result page position
199
+ conversion_rate = st.slider(
200
+ "Conversion Rate (% β†’ signup)", 0.1, 10.0, 2.0, 0.1
201
+ ) # Percentage of clicks that convert to signups
202
+ close_rate = st.slider(
203
+ "Close Rate (% β†’ customer)", 1.0, 100.0, 20.0, 1.0
204
+ ) # Percentage of signups that become paying customers
205
+ mrr_per_customer = st.slider(
206
+ "MRR per Customer ($)", 10, 1000, 200, 10
207
+ ) # Monthly Recurring Revenue per customer
208
+ seo_cost = st.slider(
209
+ "Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000
210
+ ) # Total investment in SEO efforts
211
+ add_spend = st.slider(
212
+ "Additional Ad Spend ($)", 0, 50_000, 0, 1_000
213
+ ) # Hypothetical additional ad spend for comparison
214
 
215
  # β€” Download sample CSV button
216
+ # Provides a button for users to download the sample GSC data CSV.
217
  sample_bytes = requests.get(SAMPLE_FILE_URL).content
218
  st.download_button(
219
  label="πŸ“₯ Download sample CSV",
 
222
  mime="text/csv",
223
  )
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ # --- Main app logic ---
227
+ # This block orchestrates the flow of the Streamlit application.
228
+ df = load_csv(uploaded_file) # Load the data first, passing the uploaded file object
229
 
230
+ if df is not None: # Proceed only if data loading was successful
231
+ # Perform the core calculations
232
+ metrics, df_results = calculate(
233
+ df.copy(),
234
+ target_position,
235
+ conversion_rate,
236
+ close_rate,
237
+ mrr_per_customer,
238
+ seo_cost,
239
+ add_spend,
240
+ )
241
 
242
+ if metrics is not None: # Proceed only if calculations were successful
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  st.write("---")
244
  st.header("πŸ“Š SEO Performance Summary")
245
 
246
+ # Display key performance metrics in a 3-column layout
247
  col1, col2, col3 = st.columns(3)
248
  with col1:
249
+ st.metric(
250
+ label="Total Avoided Paid Spend πŸ’°",
251
+ value=f"${metrics['total_avoided_paid_spend']:,.2f}",
252
+ )
253
  with col2:
254
+ st.metric(
255
+ label="Net Savings vs Paid πŸ“ˆ",
256
+ value=f"${metrics['net_savings_vs_paid']:,.2f}",
257
+ )
258
  with col3:
259
+ st.metric(
260
+ label="Incremental MRR (Monthly Recurring Revenue) πŸš€",
261
+ value=f"${metrics['incremental_mrr']:,.2f}",
262
+ )
263
 
264
  col4, col5, col6 = st.columns(3)
265
  with col4:
266
+ st.metric(
267
+ label="Total Incremental Conversions 🎯",
268
+ value=f"{metrics['total_incremental_conversions']:,.0f}",
269
+ )
270
  with col5:
271
+ st.metric(
272
+ label="Total Incremental Customers 🀝",
273
+ value=f"{metrics['total_incremental_customers']:,.0f}",
274
+ )
275
  with col6:
276
+ st.metric(
277
+ label="SEO ROI (Return on Investment) πŸ’°",
278
+ value=f"{metrics['seo_roi']:.2%}",
279
+ )
280
 
281
  st.write("---")
282
  st.header("Hypothetical Comparison: SEO vs. Additional Ad Spend")
283
 
284
+ # Compare SEO's incremental MRR with a hypothetical additional ad spend
285
+ col_ad1, col_ad2, col_advice = st.columns([1, 1, 1])
286
  with col_ad1:
287
+ st.metric(
288
+ label="Incremental MRR from SEO",
289
+ value=f"${metrics['incremental_mrr']:,.2f}",
290
+ )
291
  with col_ad2:
292
+ st.metric(
293
+ label="Additional Ad Spend", value=f"${add_spend:,.2f}"
294
+ )
295
 
296
  with col_advice:
297
+ if metrics["incremental_mrr"] > add_spend:
298
  advice_message = "SEO is a better investment!"
299
  advice_color = "green"
300
  else:
301
  advice_message = "Ad Spend may yield higher returns."
302
  advice_color = "red"
303
+ st.markdown(
304
+ f"""
305
  <div style="text-align: center;">
306
  <p style="font-size: 1.2em; margin-bottom: 0;">Advice</p>
307
  <p style="color:{advice_color}; font-weight:bold; font-size: 1.5em; margin-top: 0;">{advice_message}</p>
308
  </div>
309
+ """,
310
+ unsafe_allow_html=True,
311
+ )
312
 
313
  st.write("---")
314
+ st.header("Detailed Keyword Performance")
315
+
316
+ # Display a detailed table of keyword performance, sorted by incremental clicks
317
+ st.dataframe(
318
+ df_results[
319
+ [
320
+ "query",
321
+ "impressions",
322
+ "position",
323
+ "current_ctr",
324
+ "target_ctr",
325
+ "current_clicks",
326
+ "projected_clicks",
327
+ "incremental_clicks",
328
+ "cpc",
329
+ "avoided_paid_spend",
330
+ "impact_category",
331
+ ]
332
+ ].sort_values(by="incremental_clicks", ascending=False),
333
+ use_container_width=True,
334
+ )