Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,46 +1,46 @@
|
|
| 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 |
-
st.header("π§ Assumptions & Inputs")
|
| 38 |
-
uploaded_file
|
| 39 |
-
target_position
|
| 40 |
-
conversion_rate
|
| 41 |
-
close_rate
|
| 42 |
-
mrr_per_customer = st.slider("MRR per Customer ($)",
|
| 43 |
-
seo_cost
|
| 44 |
|
| 45 |
# === Load & prep data ===
|
| 46 |
def load_csv():
|
|
@@ -65,10 +65,10 @@ def calculate_roi(df):
|
|
| 65 |
# map and rename
|
| 66 |
cols = {c.lower(): c for c in df.columns}
|
| 67 |
need = {
|
| 68 |
-
'query':
|
| 69 |
'impressions': ['impressions'],
|
| 70 |
-
'position':
|
| 71 |
-
'cpc':
|
| 72 |
}
|
| 73 |
found = {}
|
| 74 |
for k, opts in need.items():
|
|
@@ -93,8 +93,8 @@ def calculate_roi(df):
|
|
| 93 |
return None, pd.DataFrame()
|
| 94 |
|
| 95 |
# compute clicks
|
| 96 |
-
df['Current_CTR']
|
| 97 |
-
df['Target_CTR']
|
| 98 |
df['Current_Clicks'] = df.impressions * df.Current_CTR
|
| 99 |
df['Projected_Clicks'] = df.impressions * df.Target_CTR
|
| 100 |
df['Incremental_Clicks'] = df.Projected_Clicks - df.Current_Clicks
|
|
@@ -104,40 +104,40 @@ def calculate_roi(df):
|
|
| 104 |
return None, pd.DataFrame()
|
| 105 |
|
| 106 |
# monetize
|
| 107 |
-
conv
|
| 108 |
close = close_rate / 100
|
| 109 |
-
df['Signups']
|
| 110 |
-
df['Customers']
|
| 111 |
-
df['MRR']
|
| 112 |
|
| 113 |
# paid-ads cost & savings
|
| 114 |
-
df['Paid_Cost']
|
| 115 |
-
total_paid_cost
|
| 116 |
-
savings_vs_paid_ads
|
| 117 |
|
| 118 |
# totals & ROI
|
| 119 |
-
tot_clicks
|
| 120 |
-
tot_signups
|
| 121 |
tot_customers = df.Customers.sum()
|
| 122 |
-
tot_mrr
|
| 123 |
-
seo_roi_pct
|
| 124 |
|
| 125 |
summary = {
|
| 126 |
-
"clicks":
|
| 127 |
-
"signups":
|
| 128 |
-
"customers":
|
| 129 |
-
"mrr":
|
| 130 |
-
"roi":
|
| 131 |
-
"paid_cost":
|
| 132 |
-
"savings":
|
| 133 |
}
|
| 134 |
|
| 135 |
# table
|
| 136 |
out = df[['query','MRR','Paid_Cost']].copy()
|
| 137 |
out.rename(columns={
|
| 138 |
-
'query':
|
| 139 |
-
'MRR':
|
| 140 |
-
'Paid_Cost':
|
| 141 |
}, inplace=True)
|
| 142 |
out['Impact'] = pd.cut(
|
| 143 |
out['Projected Incremental MRR ($)'],
|
|
@@ -155,13 +155,13 @@ if st.button("Run Forecast"):
|
|
| 155 |
summary, table = calculate_roi(df)
|
| 156 |
if summary:
|
| 157 |
c1, c2, c3, c4, c5, c6, c7 = st.columns(7)
|
| 158 |
-
c1.metric("Incremental Clicks",
|
| 159 |
-
c2.metric("Projected Signups",
|
| 160 |
-
c3.metric("New Customers",
|
| 161 |
-
c4.metric("Incremental MRR",
|
| 162 |
-
c5.metric("SEO ROI",
|
| 163 |
-
c6.metric("Paid Ads Cost",
|
| 164 |
-
c7.metric("Savings vs Paid Ads",
|
| 165 |
|
| 166 |
st.subheader("π Opportunity Keywords")
|
| 167 |
-
st.dataframe(table, use_container_width=True)
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import requests
|
| 5 |
+
|
| 6 |
+
# βΆοΈ Use the URL you provided
|
| 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 Forecasting Tool for B2B SaaS", layout="wide")
|
| 10 |
+
st.title("π SEO ROI Forecasting Tool for B2B SaaS")
|
| 11 |
+
|
| 12 |
+
st.markdown("""
|
| 13 |
+
This app helps you estimate the **financial upside** of ranking improvements for your SEO keywords,
|
| 14 |
+
and compare that to what it would cost you in paid ads.
|
| 15 |
+
<br>
|
| 16 |
+
|
| 17 |
+
π **Make sure your CSV has a `CPC` column** (cost per click in $).
|
| 18 |
+
If you donβt, weβll simulate one for you.
|
| 19 |
+
<br>
|
| 20 |
+
|
| 21 |
+
Developed by: [Emilija Gjorgjevska](https://www.linkedin.com/in/emilijagjorgjevska/)
|
| 22 |
+
""", unsafe_allow_html=True)
|
| 23 |
+
|
| 24 |
+
# βββββββ
|
| 25 |
+
# Download button for the sample file
|
| 26 |
+
sample_bytes = requests.get(SAMPLE_FILE_URL).content
|
| 27 |
+
st.download_button(
|
| 28 |
+
label="π₯ Download sample CSV",
|
| 29 |
+
data=sample_bytes,
|
| 30 |
+
file_name="sample_gsc_data.csv",
|
| 31 |
+
mime="text/csv",
|
| 32 |
+
)
|
| 33 |
+
# βββββββ
|
| 34 |
+
|
| 35 |
+
# === Sidebar inputs ===
|
| 36 |
+
with st.sidebar:
|
| 37 |
+
st.header("π§ Assumptions & Inputs")
|
| 38 |
+
uploaded_file = st.file_uploader("Upload Google Search Console CSV", type="csv")
|
| 39 |
+
target_position = st.slider("Target SERP Position", 1.0, 10.0, 4.0, 0.5)
|
| 40 |
+
conversion_rate = st.slider("Conversion Rate (% β signup)", 0.1, 10.0, 2.0, 0.1)
|
| 41 |
+
close_rate = st.slider("Close Rate (% β customer)", 1.0, 100.0, 20.0, 1.0)
|
| 42 |
+
mrr_per_customer = st.slider("MRR per Customer ($)", 10, 1000, 200, 10)
|
| 43 |
+
seo_cost = st.slider("Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000)
|
| 44 |
|
| 45 |
# === Load & prep data ===
|
| 46 |
def load_csv():
|
|
|
|
| 65 |
# map and rename
|
| 66 |
cols = {c.lower(): c for c in df.columns}
|
| 67 |
need = {
|
| 68 |
+
'query': ['query','keyword','keywords','queries'],
|
| 69 |
'impressions': ['impressions'],
|
| 70 |
+
'position': ['position','avg. position','average position'],
|
| 71 |
+
'cpc': ['cpc']
|
| 72 |
}
|
| 73 |
found = {}
|
| 74 |
for k, opts in need.items():
|
|
|
|
| 93 |
return None, pd.DataFrame()
|
| 94 |
|
| 95 |
# compute clicks
|
| 96 |
+
df['Current_CTR'] = df.position.apply(get_ctr)
|
| 97 |
+
df['Target_CTR'] = get_ctr(target_position)
|
| 98 |
df['Current_Clicks'] = df.impressions * df.Current_CTR
|
| 99 |
df['Projected_Clicks'] = df.impressions * df.Target_CTR
|
| 100 |
df['Incremental_Clicks'] = df.Projected_Clicks - df.Current_Clicks
|
|
|
|
| 104 |
return None, pd.DataFrame()
|
| 105 |
|
| 106 |
# monetize
|
| 107 |
+
conv = conversion_rate / 100
|
| 108 |
close = close_rate / 100
|
| 109 |
+
df['Signups'] = df.Incremental_Clicks * conv
|
| 110 |
+
df['Customers'] = df.Signups * close
|
| 111 |
+
df['MRR'] = df.Customers * mrr_per_customer
|
| 112 |
|
| 113 |
# paid-ads cost & savings
|
| 114 |
+
df['Paid_Cost'] = df.Incremental_Clicks * df.cpc
|
| 115 |
+
total_paid_cost = df.Paid_Cost.sum()
|
| 116 |
+
savings_vs_paid_ads = total_paid_cost - seo_cost
|
| 117 |
|
| 118 |
# totals & ROI
|
| 119 |
+
tot_clicks = df.Incremental_Clicks.sum()
|
| 120 |
+
tot_signups = df.Signups.sum()
|
| 121 |
tot_customers = df.Customers.sum()
|
| 122 |
+
tot_mrr = df.MRR.sum()
|
| 123 |
+
seo_roi_pct = float('inf') if seo_cost == 0 else ((tot_mrr - seo_cost) / seo_cost) * 100
|
| 124 |
|
| 125 |
summary = {
|
| 126 |
+
"clicks": f"{tot_clicks:,.0f}",
|
| 127 |
+
"signups": f"{tot_signups:,.1f}",
|
| 128 |
+
"customers": f"{tot_customers:,.1f}",
|
| 129 |
+
"mrr": f"${tot_mrr:,.2f}",
|
| 130 |
+
"roi": f"{seo_roi_pct:,.2f}%",
|
| 131 |
+
"paid_cost": f"${total_paid_cost:,.2f}",
|
| 132 |
+
"savings": f"${savings_vs_paid_ads:,.2f}"
|
| 133 |
}
|
| 134 |
|
| 135 |
# table
|
| 136 |
out = df[['query','MRR','Paid_Cost']].copy()
|
| 137 |
out.rename(columns={
|
| 138 |
+
'query': 'Keyword',
|
| 139 |
+
'MRR': 'Projected Incremental MRR ($)',
|
| 140 |
+
'Paid_Cost': 'Equivalent Paid Ads Cost ($)'
|
| 141 |
}, inplace=True)
|
| 142 |
out['Impact'] = pd.cut(
|
| 143 |
out['Projected Incremental MRR ($)'],
|
|
|
|
| 155 |
summary, table = calculate_roi(df)
|
| 156 |
if summary:
|
| 157 |
c1, c2, c3, c4, c5, c6, c7 = st.columns(7)
|
| 158 |
+
c1.metric("Incremental Clicks", summary['clicks'])
|
| 159 |
+
c2.metric("Projected Signups", summary['signups'])
|
| 160 |
+
c3.metric("New Customers", summary['customers'])
|
| 161 |
+
c4.metric("Incremental MRR", summary['mrr'])
|
| 162 |
+
c5.metric("SEO ROI", summary['roi'])
|
| 163 |
+
c6.metric("Paid Ads Cost", summary['paid_cost'])
|
| 164 |
+
c7.metric("Savings vs Paid Ads", summary['savings'])
|
| 165 |
|
| 166 |
st.subheader("π Opportunity Keywords")
|
| 167 |
+
st.dataframe(table, use_container_width=True)
|