Em4e commited on
Commit
24a81d0
·
verified ·
1 Parent(s): 705adba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +119 -58
app.py CHANGED
@@ -3,41 +3,45 @@ import numpy as np
3
  import streamlit as st
4
  import requests
5
 
6
- # ▶️ Source CSV (with or without CPC column)
7
- SAMPLE_FILE_URL = "https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/resolve/main/sample_gsc_data.csv"
 
 
 
8
 
9
- st.set_page_config(page_title="SEO ROI + Savings Tool", layout="wide")
10
- st.title("📈 SEO ROI & Savings Forecasting for B2B SaaS")
11
 
12
- # — Info section on how it works
13
  with st.expander("ℹ️ How the app works", expanded=True):
14
  st.markdown("""
15
- 1. **Load your GSC data** (must include `Impressions`, `Position`, and `CPC`; if CPC is missing we simulate \$0.50–\$3.00).
16
- 2. **CTR benchmarks** by position map average CTR for positions 120.
 
17
  3. **Incremental clicks** =
18
    Projected_Clicks – Current_Clicks
19
-   • Current_Clicks = Impressions×Current_CTR
20
-   • Projected_Clicks = Impressions×Target_CTR
21
  4. **Financials**
22
-   • Avoided Paid Spend = Incremental_Clicks×CPC
23
    • Net Savings vs Paid = Avoided Paid Spend – SEO Investment
24
-   • Incremental MRR = Customers×MRR_per_Customer
25
    • SEO ROI = (Incremental MRR – SEO Investment) ÷ SEO Investment
26
  5. **Results**
27
- Top-line metrics + keyword-level table with Impact labels.
28
  """, unsafe_allow_html=True)
29
 
30
- # — Sidebar Inputs
31
  with st.sidebar:
32
  st.header("🔧 Assumptions & Inputs")
33
  uploaded_file = st.file_uploader("Upload GSC CSV", type="csv")
34
  target_position = st.slider("Target SERP Position", 1.0, 10.0, 4.0, 0.5)
35
- conversion_rate = st.slider("Conversion Rate (VisitorSignup %)", 0.1, 10.0, 2.0, 0.1)
36
- close_rate = st.slider("Close Rate (SignupCustomer %)", 1.0, 100.0, 20.0, 1.0)
37
- mrr_per_customer = st.slider("MRR per Customer ($)", 10, 1000, 200, 10)
38
- seo_cost = st.slider("Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000)
39
 
40
- # — Download sample button
41
  sample_bytes = requests.get(SAMPLE_FILE_URL).content
42
  st.download_button(
43
  label="📥 Download sample CSV",
@@ -46,75 +50,132 @@ st.download_button(
46
  mime="text/csv",
47
  )
48
 
49
- # === Load & prep data ===
50
  def load_csv():
51
  try:
52
- df = pd.read_csv(uploaded_file) if uploaded_file else pd.read_csv(SAMPLE_FILE_URL)
 
 
 
53
  except Exception as e:
54
  st.error(f"Error loading file: {e}")
55
  return None
56
 
57
- # simulate CPC if missing
58
- if 'CPC' not in df.columns:
59
- st.warning("No `CPC` column found—simulating CPC values between $0.50–$3.00.")
60
- df['CPC'] = np.round(np.random.uniform(0.5, 3.0, len(df)), 2)
 
 
 
 
61
  return df
62
 
63
  # === Core calculation ===
64
  def calculate(df):
65
- # map required cols (including CPC)
66
- cols = {c.lower(): c for c in df.columns}
67
  required = {
68
- 'query': ['query','keyword','queries'],
69
  'impressions': ['impressions'],
70
- 'position': ['position','avg. position'],
71
- 'cpc': ['cpc'] # we’ll end up with a lowercase 'cpc' column
72
  }
73
  found = {}
74
- for k, opts in required.items():
75
- for o in opts:
76
- if o in cols:
77
- found[k] = cols[o]
78
  break
79
- if k not in found:
80
- st.error(f"Missing column: {k}")
81
  return None, pd.DataFrame()
82
 
83
- # rename to our standard lowercase names
84
  df = df.rename(columns={found[k]: k for k in found})
85
 
86
- # CTR benchmarks & filtering omitted for brevity...
87
-
88
- # after you’ve computed Incremental_Clicks:
89
- # use direct key access to CPC:
90
- df['Avoided_Paid_Spend'] = df['Incremental_Clicks'] * df['cpc']
91
-
92
- total_avoided = df['Avoided_Paid_Spend'].sum()
93
- net_savings = total_avoided - seo_cost
94
-
95
- # build summary & table …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # when building your table, again reference the column by key:
98
- out = df[['query', 'MRR', 'Avoided_Paid_Spend']].copy()
99
- out.columns = ['Keyword', 'Projected Incremental MRR ($)', 'Avoided Paid Spend ($)']
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  return summary, out
102
 
103
-
104
- # === Run & display ===
105
  if st.button("Run Forecast"):
106
  df = load_csv()
107
  if df is not None:
108
  summary, table = calculate(df)
109
  if summary:
110
  c1,c2,c3,c4,c5,c6,c7 = st.columns(7)
111
- c1.metric("Incremental Clicks", summary['clicks'])
112
- c2.metric("Projected Signups", summary['signups'])
113
- c3.metric("New Customers", summary['customers'])
114
- c4.metric("Incremental MRR", summary['mrr'])
115
- c5.metric("SEO ROI", summary['roi'])
116
- c6.metric("Avoided Paid Spend", summary['avoid'])
117
- c7.metric("Net Savings vs Paid",summary['net'])
118
 
119
  st.subheader("📊 Opportunity Keywords")
120
  st.dataframe(table, use_container_width=True)
 
3
  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("📈 SEO ROI & Savings Forecasting Tool for B2B SaaS")
14
 
15
+ # — Info section explaining the math
16
  with st.expander("ℹ️ How the app works", expanded=True):
17
  st.markdown("""
18
+ 1. **Load your GSC data** (we lowercase all column names on load).
19
+ If no `cpc` column is present, we simulate values between \$0.50\$3.00.
20
+ 2. **CTR benchmarks** by position map an expected click-through rate for positions 1–20.
21
  3. **Incremental clicks** =
22
    Projected_Clicks – Current_Clicks
23
+   • Current_Clicks = Impressions × Current_CTR
24
+   • Projected_Clicks = Impressions × Target_CTR
25
  4. **Financials**
26
+   • Avoided Paid Spend = Incremental_Clicks × CPC
27
    • Net Savings vs Paid = Avoided Paid Spend – SEO Investment
28
+   • Incremental MRR = Customers × MRR_per_Customer
29
    • SEO ROI = (Incremental MRR – SEO Investment) ÷ SEO Investment
30
  5. **Results**
31
+ Top-line metrics + keyword-level table with impact labels.
32
  """, unsafe_allow_html=True)
33
 
34
+ # — Sidebar inputs
35
  with st.sidebar:
36
  st.header("🔧 Assumptions & Inputs")
37
  uploaded_file = st.file_uploader("Upload GSC CSV", type="csv")
38
  target_position = st.slider("Target SERP Position", 1.0, 10.0, 4.0, 0.5)
39
+ conversion_rate = st.slider("Conversion Rate (% signup)", 0.1, 10.0, 2.0, 0.1)
40
+ close_rate = st.slider("Close Rate (% customer)", 1.0, 100.0, 20.0, 1.0)
41
+ mrr_per_customer = st.slider("MRR per Customer ($)", 10, 1000, 200, 10)
42
+ seo_cost = st.slider("Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000)
43
 
44
+ # — Download sample CSV button
45
  sample_bytes = requests.get(SAMPLE_FILE_URL).content
46
  st.download_button(
47
  label="📥 Download sample CSV",
 
50
  mime="text/csv",
51
  )
52
 
53
+ # === Load & normalize CSV ===
54
  def load_csv():
55
  try:
56
+ if uploaded_file:
57
+ df = pd.read_csv(uploaded_file)
58
+ else:
59
+ df = pd.read_csv(SAMPLE_FILE_URL)
60
  except Exception as e:
61
  st.error(f"Error loading file: {e}")
62
  return None
63
 
64
+ # lowercase all column names
65
+ df.columns = [col.lower() for col in df.columns]
66
+
67
+ # ensure a 'cpc' column
68
+ if 'cpc' not in df.columns:
69
+ st.warning("No `cpc` column found—simulating CPC values between $0.50–$3.00.")
70
+ df['cpc'] = np.round(np.random.uniform(0.5, 3.0, size=len(df)), 2)
71
+
72
  return df
73
 
74
  # === Core calculation ===
75
  def calculate(df):
76
+ # required columns mapping
77
+ cols = {c: c for c in df.columns}
78
  required = {
79
+ 'query': ['query', 'keyword', 'queries'],
80
  'impressions': ['impressions'],
81
+ 'position': ['position', 'avg. position', 'average position'],
82
+ 'cpc': ['cpc']
83
  }
84
  found = {}
85
+ for key, opts in required.items():
86
+ for opt in opts:
87
+ if opt in df.columns:
88
+ found[key] = opt
89
  break
90
+ if key not in found:
91
+ st.error(f"Missing required column: {key}")
92
  return None, pd.DataFrame()
93
 
94
+ # rename to our standard keys
95
  df = df.rename(columns={found[k]: k for k in found})
96
 
97
+ # CTR benchmarks
98
+ 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])}
99
+ ctr.update({i: 0.005 for i in range(11,21)})
100
+ get_ctr = lambda p: ctr.get(int(round(p)), 0.005)
101
+
102
+ # filter positions 5–20
103
+ df = df[df['position'].between(5, 20)].copy()
104
+ if df.empty:
105
+ st.warning("No keywords in positions 5–20.")
106
+ return None, pd.DataFrame()
107
+
108
+ # clicks projections
109
+ df['current_ctr'] = df['position'].map(get_ctr)
110
+ df['target_ctr'] = get_ctr(target_position)
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 = df[df['incremental_clicks'] > 0]
115
+ if df.empty:
116
+ st.warning("No positive incremental clicks projected.")
117
+ return None, pd.DataFrame()
118
+
119
+ # conversions → MRR
120
+ conv = conversion_rate / 100
121
+ close = close_rate / 100
122
+ df['signups'] = df['incremental_clicks'] * conv
123
+ df['customers'] = df['signups'] * close
124
+ df['mrr'] = df['customers'] * mrr_per_customer
125
+
126
+ # financials: avoided spend & net savings
127
+ df['avoided_paid_spend'] = df['incremental_clicks'] * df['cpc']
128
+ total_avoided = df['avoided_paid_spend'].sum()
129
+ net_savings = total_avoided - seo_cost
130
+
131
+ # totals & ROI
132
+ tot_clicks = df['incremental_clicks'].sum()
133
+ tot_signups = df['signups'].sum()
134
+ tot_customers = df['customers'].sum()
135
+ tot_mrr = df['mrr'].sum()
136
+ seo_roi_pct = float('inf') if seo_cost == 0 else ((tot_mrr - seo_cost) / seo_cost) * 100
137
+
138
+ summary = {
139
+ "clicks": f"{tot_clicks:,.0f}",
140
+ "signups": f"{tot_signups:,.1f}",
141
+ "customers": f"{tot_customers:,.1f}",
142
+ "mrr": f"${tot_mrr:,.2f}",
143
+ "roi": f"{seo_roi_pct:,.2f}%",
144
+ "avoid": f"${total_avoided:,.2f}",
145
+ "net": f"${net_savings:,.2f}"
146
+ }
147
 
148
+ # keyword-level table
149
+ out = df[['query', 'mrr', 'avoided_paid_spend']].copy()
150
+ out.columns = [
151
+ 'Keyword',
152
+ 'Projected Incremental MRR ($)',
153
+ 'Avoided Paid Spend ($)'
154
+ ]
155
+ out['Impact'] = pd.cut(
156
+ out['Projected Incremental MRR ($)'],
157
+ bins=[-1, 500, 2000, float('inf')],
158
+ labels=['Low Priority','Moderate ROI','High ROI']
159
+ )
160
+ out = out.sort_values(['Impact','Projected Incremental MRR ($)'],
161
+ ascending=[True, False])
162
 
163
  return summary, out
164
 
165
+ # === Run forecast & display ===
 
166
  if st.button("Run Forecast"):
167
  df = load_csv()
168
  if df is not None:
169
  summary, table = calculate(df)
170
  if summary:
171
  c1,c2,c3,c4,c5,c6,c7 = st.columns(7)
172
+ c1.metric("Incremental Clicks", summary['clicks'])
173
+ c2.metric("Projected Signups", summary['signups'])
174
+ c3.metric("New Customers", summary['customers'])
175
+ c4.metric("Incremental MRR", summary['mrr'])
176
+ c5.metric("SEO ROI", summary['roi'])
177
+ c6.metric("Avoided Paid Spend", summary['avoid'])
178
+ c7.metric("Net Savings vs Paid", summary['net'])
179
 
180
  st.subheader("📊 Opportunity Keywords")
181
  st.dataframe(table, use_container_width=True)