Em4e commited on
Commit
e9b1c61
Β·
verified Β·
1 Parent(s): 0f1baac

Upload 2 files

Browse files
Files changed (1) hide show
  1. app.py +105 -130
app.py CHANGED
@@ -1,157 +1,132 @@
1
  import pandas as pd
2
- import gradio as gr
 
3
 
4
- # βœ… Direct raw URL from Hugging Face blob to raw
5
  SAMPLE_FILE_URL = "https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/raw/main/sample_gsc_data.csv"
6
 
7
- def calculate_seo_roi(
8
- gsc_file,
9
- target_position: float,
10
- conversion_rate_percent: float,
11
- close_rate_percent: float,
12
- mrr_per_customer: float,
13
- seo_cost: float
14
- ):
15
- empty_df = pd.DataFrame()
16
-
 
 
 
 
 
 
 
 
 
 
17
  try:
18
- if gsc_file is None or isinstance(gsc_file, bool):
19
- df = pd.read_csv(SAMPLE_FILE_URL)
20
  else:
21
- df = pd.read_csv(gsc_file.name)
 
 
 
22
 
23
- conversion_rate = conversion_rate_percent / 100
24
- close_rate = close_rate_percent / 100
 
 
 
 
25
 
26
  df_columns_lower = {col.lower(): col for col in df.columns}
27
- expected_cols_variations = {
28
  'query': ['query', 'queries', 'keyword', 'keywords'],
29
- 'page': ['page', 'pages', 'landing page', 'landing_page', 'url'],
30
  'impressions': ['impressions'],
31
  'position': ['position', 'avg. position', 'average position']
32
  }
33
 
34
- found_cols_map = {}
35
- for internal_name, variations in expected_cols_variations.items():
36
- for var in variations:
37
- if var in df_columns_lower:
38
- found_cols_map[internal_name] = df_columns_lower[var]
39
  break
40
- else:
41
- if internal_name in ['query', 'impressions', 'position']:
42
- return (f"Missing critical column for {internal_name}. Found: {df.columns.tolist()}", "", "", "", "", empty_df)
43
 
44
- df_processed = df.copy()
45
- rename_dict = {found_cols_map[k]: k for k in found_cols_map}
46
- df_processed.rename(columns=rename_dict, inplace=True)
47
 
48
- ctr_benchmarks = {
49
- 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])
50
- }
51
  ctr_benchmarks.update({i: 0.005 for i in range(11, 21)})
52
-
53
- def get_ctr(pos):
54
- return ctr_benchmarks.get(int(round(pos)), 0.005)
55
-
56
- df_filtered = df_processed[(df_processed['position'] >= 5) & (df_processed['position'] <= 20)].copy()
57
- if df_filtered.empty:
58
- return ("No keywords in positions 5–20.", "", "", "", "", empty_df)
59
-
60
- df_filtered['Current_CTR'] = df_filtered['position'].apply(get_ctr)
61
- df_filtered['Target_CTR'] = get_ctr(target_position)
62
- df_filtered['Projected_Clicks'] = df_filtered['impressions'] * df_filtered['Target_CTR']
63
- df_filtered['Current_Clicks'] = df_filtered['impressions'] * df_filtered['Current_CTR']
64
- df_filtered['Incremental_Clicks'] = df_filtered['Projected_Clicks'] - df_filtered['Current_Clicks']
65
- df_filtered = df_filtered[df_filtered['Incremental_Clicks'] > 0]
66
-
67
- if df_filtered.empty:
68
- return ("No incremental clicks projected. Try different assumptions.", "", "", "", "", empty_df)
69
-
70
- df_filtered['Signups'] = df_filtered['Incremental_Clicks'] * conversion_rate
71
- df_filtered['New_Customers'] = df_filtered['Signups'] * close_rate
72
- df_filtered['Incremental_MRR'] = df_filtered['New_Customers'] * mrr_per_customer
73
-
74
- total_clicks = df_filtered['Incremental_Clicks'].sum()
75
- total_signups = df_filtered['Signups'].sum()
76
- total_customers = df_filtered['New_Customers'].sum()
77
- total_mrr = df_filtered['Incremental_MRR'].sum()
78
  roi = float('inf') if seo_cost == 0 else ((total_mrr - seo_cost) / seo_cost) * 100
79
 
80
- output_df = df_filtered[['query', 'Incremental_MRR']].copy()
81
- output_df.rename(columns={'query': 'Keyword', 'Incremental_MRR': 'Projected Incremental MRR ($)'}, inplace=True)
82
 
83
- def label_impact(mrr):
84
- if mrr >= 2000:
85
  return "High ROI"
86
- elif mrr >= 500:
87
  return "Moderate ROI"
88
- else:
89
- return "Low Priority"
90
-
91
- output_df['Business Impact'] = output_df['Projected Incremental MRR ($)'].apply(label_impact)
92
 
93
- sort_priority = {"High ROI": 0, "Moderate ROI": 1, "Low Priority": 2}
94
- output_df['__sort__'] = output_df['Business Impact'].map(sort_priority)
95
- output_df.sort_values(by=['__sort__', 'Projected Incremental MRR ($)'], ascending=[True, False], inplace=True)
96
- output_df.drop(columns='__sort__', inplace=True)
97
 
98
- return (
99
- f"{total_clicks:,.0f}",
100
- f"{total_signups:,.1f}",
101
- f"{total_customers:,.1f}",
102
- f"${total_mrr:,.2f}",
103
- f"{roi:,.2f}%",
104
- output_df
105
- )
106
 
107
  except Exception as e:
108
- return (f"Error: {e}", "", "", "", "", empty_df)
109
-
110
-
111
- # Gradio UI components
112
- input_gsc_file = gr.File(label="Upload Google Search Console CSV Export", file_types=[".csv"])
113
- input_target_position = gr.Slider(minimum=1, maximum=10, step=0.5, value=4, label="Target SERP Position")
114
- input_conversion_rate = gr.Slider(minimum=0.1, maximum=10.0, step=0.1, value=2.0, label="Conversion Rate (Visitor to Signup %)")
115
- input_close_rate = gr.Slider(minimum=1.0, maximum=100.0, step=1.0, value=20.0, label="Close Rate (Signup to Customer %)")
116
- input_mrr = gr.Slider(minimum=10, maximum=1000, step=10, value=200, label="MRR per Customer ($)")
117
- input_cost = gr.Slider(minimum=1000, maximum=100000, step=1000, value=10000, label="Total SEO Investment ($)")
118
-
119
- output_clicks = gr.Textbox(label="Incremental Clicks")
120
- output_signups = gr.Textbox(label="Projected Signups")
121
- output_customers = gr.Textbox(label="Projected New Customers")
122
- output_mrr = gr.Textbox(label="Projected Incremental MRR")
123
- output_roi = gr.Textbox(label="SEO ROI")
124
- output_df = gr.Dataframe(label="Opportunity Keywords & Business Impact", row_count=(5, "dynamic"), interactive=False)
125
-
126
- app = gr.Interface(
127
- fn=calculate_seo_roi,
128
- inputs=[
129
- input_gsc_file, input_target_position, input_conversion_rate,
130
- input_close_rate, input_mrr, input_cost
131
- ],
132
- outputs=[
133
- output_clicks, output_signups, output_customers,
134
- output_mrr, output_roi, output_df
135
- ],
136
- title="SEO ROI Forecasting Tool for B2B SaaS",
137
- description="""
138
- <h3>πŸ“Š How This Tool Works:</h3>
139
- <p>This tool helps B2B SaaS teams translate SEO performance into financial impact. It acts as a 'what-if' planner to estimate how better keyword rankings can drive leads, MRR, and overall return on investment.</p>
140
-
141
- <ul>
142
- <li><b><span style='color: blue;'>1. Upload Your Data (or use default):</span></b> If you don’t upload a CSV, the app uses a sample file automatically: <a href='https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/blob/main/sample_gsc_data.csv' target='_blank'>sample_gsc_data.csv</a></li>
143
- <li><b><span style='color: blue;'>2. Set Your Assumptions:</span></b> Define funnel conversion rates, MRR, and SEO budget.</li>
144
- <li><b><span style='color: blue;'>3. Identify Opportunities:</span></b> The tool filters keywords ranked between positions 5–20 and forecasts click + revenue uplift if improved.</li>
145
- <li><b><span style='color: blue;'>4. Prioritize by ROI:</span></b> Keywords are labeled with business impact and sorted by MRR gain.</li>
146
- </ul>
147
-
148
- <h4>Assumptions:</h4>
149
- <ul>
150
- <li><b>CTR Benchmarks:</b> Position 1: 25%, Position 2: 15%, ..., Position 10: 1%, beyond 10: 0.5%</li>
151
- <li><b>Conversion Rates:</b> Enter as percentages (e.g., 2 for 2%)</li>
152
- </ul>
153
- """
154
- )
155
-
156
- if __name__ == "__main__":
157
- app.launch(share=True)
 
1
  import pandas as pd
2
+ import streamlit as st
3
+ import io
4
 
5
+ # βœ… Raw sample file URL
6
  SAMPLE_FILE_URL = "https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/raw/main/sample_gsc_data.csv"
7
 
8
+ st.set_page_config(page_title="SEO ROI Forecasting Tool for B2B SaaS", layout="wide")
9
+ st.title("πŸ“ˆ SEO ROI Forecasting Tool for B2B SaaS")
10
+
11
+ st.markdown("""
12
+ This app helps you estimate the **financial upside** of ranking improvements for your SEO keywords.
13
+ If no file is uploaded, a [sample CSV](https://huggingface.co/spaces/Em4e/seo-b2b-saas-forecasting-tool/blob/main/sample_gsc_data.csv) will be used.
14
+ """)
15
+
16
+ # === Inputs ===
17
+ with st.sidebar:
18
+ st.header("πŸ”§ Assumptions")
19
+ uploaded_file = st.file_uploader("Upload Google Search Console CSV", type="csv")
20
+ target_position = st.slider("Target SERP Position", min_value=1.0, max_value=10.0, step=0.5, value=4.0)
21
+ conversion_rate = st.slider("Conversion Rate (Visitor β†’ Signup %)", min_value=0.1, max_value=10.0, step=0.1, value=2.0)
22
+ close_rate = st.slider("Close Rate (Signup β†’ Customer %)", min_value=1.0, max_value=100.0, step=1.0, value=20.0)
23
+ mrr_per_customer = st.slider("MRR per Customer ($)", min_value=10, max_value=1000, step=10, value=200)
24
+ seo_cost = st.slider("Total SEO Investment ($)", min_value=1000, max_value=100000, step=1000, value=10000)
25
+
26
+ # === Load CSV ===
27
+ def load_csv():
28
  try:
29
+ if uploaded_file is not None:
30
+ return pd.read_csv(uploaded_file)
31
  else:
32
+ return pd.read_csv(SAMPLE_FILE_URL)
33
+ except Exception as e:
34
+ st.error(f"Error loading file: {e}")
35
+ return None
36
 
37
+ # === Main ROI Logic ===
38
+ def calculate_roi(df):
39
+ empty_df = pd.DataFrame()
40
+ try:
41
+ conversion = conversion_rate / 100
42
+ close = close_rate / 100
43
 
44
  df_columns_lower = {col.lower(): col for col in df.columns}
45
+ expected_cols = {
46
  'query': ['query', 'queries', 'keyword', 'keywords'],
 
47
  'impressions': ['impressions'],
48
  'position': ['position', 'avg. position', 'average position']
49
  }
50
 
51
+ found_cols = {}
52
+ for k, v_list in expected_cols.items():
53
+ for v in v_list:
54
+ if v in df_columns_lower:
55
+ found_cols[k] = df_columns_lower[v]
56
  break
57
+ if k not in found_cols:
58
+ st.error(f"Missing required column for {k.upper()}")
59
+ return None, empty_df
60
 
61
+ df.rename(columns={found_cols[k]: k for k in found_cols}, inplace=True)
 
 
62
 
63
+ 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])}
 
 
64
  ctr_benchmarks.update({i: 0.005 for i in range(11, 21)})
65
+ get_ctr = lambda pos: ctr_benchmarks.get(int(round(pos)), 0.005)
66
+
67
+ df = df[(df['position'] >= 5) & (df['position'] <= 20)].copy()
68
+ if df.empty:
69
+ st.warning("No keywords between position 5–20.")
70
+ return None, empty_df
71
+
72
+ df['Current_CTR'] = df['position'].apply(get_ctr)
73
+ df['Target_CTR'] = get_ctr(target_position)
74
+ df['Projected_Clicks'] = df['impressions'] * df['Target_CTR']
75
+ df['Current_Clicks'] = df['impressions'] * df['Current_CTR']
76
+ df['Incremental_Clicks'] = df['Projected_Clicks'] - df['Current_Clicks']
77
+ df = df[df['Incremental_Clicks'] > 0]
78
+
79
+ if df.empty:
80
+ st.warning("No incremental clicks projected. Adjust assumptions.")
81
+ return None, empty_df
82
+
83
+ df['Signups'] = df['Incremental_Clicks'] * conversion
84
+ df['Customers'] = df['Signups'] * close
85
+ df['MRR'] = df['Customers'] * mrr_per_customer
86
+
87
+ total_clicks = df['Incremental_Clicks'].sum()
88
+ total_signups = df['Signups'].sum()
89
+ total_customers = df['Customers'].sum()
90
+ total_mrr = df['MRR'].sum()
91
  roi = float('inf') if seo_cost == 0 else ((total_mrr - seo_cost) / seo_cost) * 100
92
 
93
+ output_df = df[['query', 'MRR']].copy()
94
+ output_df.rename(columns={'query': 'Keyword', 'MRR': 'Projected Incremental MRR ($)'}, inplace=True)
95
 
96
+ def label(m):
97
+ if m >= 2000:
98
  return "High ROI"
99
+ elif m >= 500:
100
  return "Moderate ROI"
101
+ return "Low Priority"
 
 
 
102
 
103
+ output_df['Impact'] = output_df['Projected Incremental MRR ($)'].apply(label)
104
+ output_df.sort_values(by=['Impact', 'Projected Incremental MRR ($)'], ascending=[True, False], inplace=True)
 
 
105
 
106
+ return {
107
+ "clicks": f"{total_clicks:,.0f}",
108
+ "signups": f"{total_signups:,.1f}",
109
+ "customers": f"{total_customers:,.1f}",
110
+ "mrr": f"${total_mrr:,.2f}",
111
+ "roi": f"{roi:,.2f}%"
112
+ }, output_df
 
113
 
114
  except Exception as e:
115
+ st.error(f"Error during ROI calculation: {e}")
116
+ return None, empty_df
117
+
118
+
119
+ if st.button("Run Forecast"):
120
+ df = load_csv()
121
+ if df is not None:
122
+ summary, table = calculate_roi(df)
123
+ if summary:
124
+ col1, col2, col3, col4, col5 = st.columns(5)
125
+ col1.metric("Incremental Clicks", summary['clicks'])
126
+ col2.metric("Projected Signups", summary['signups'])
127
+ col3.metric("New Customers", summary['customers'])
128
+ col4.metric("Incremental MRR", summary['mrr'])
129
+ col5.metric("SEO ROI", summary['roi'])
130
+
131
+ st.subheader("πŸ“Š Opportunity Keywords")
132
+ st.dataframe(table, use_container_width=True)