Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -18,14 +18,12 @@ class DataLoader:
|
|
| 18 |
self.sample_file_url = sample_file_url
|
| 19 |
|
| 20 |
@st.cache_data
|
| 21 |
-
|
| 22 |
-
def load_csv(_self, uploaded_file_obj: st.runtime.uploaded_file_manager.UploadedFile | None) -> pd.DataFrame | None:
|
| 23 |
"""
|
| 24 |
Loads the GSC data from an uploaded CSV or a sample URL,
|
| 25 |
normalizes column names, and ensures a 'cpc' column exists.
|
| 26 |
-
|
| 27 |
Args:
|
| 28 |
-
|
| 29 |
uploaded_file_obj (streamlit.runtime.uploaded_file_manager.UploadedFile): The file object
|
| 30 |
uploaded by the user, or None.
|
| 31 |
Returns:
|
|
@@ -35,8 +33,8 @@ class DataLoader:
|
|
| 35 |
if uploaded_file_obj:
|
| 36 |
df = pd.read_csv(uploaded_file_obj)
|
| 37 |
else:
|
| 38 |
-
# Use
|
| 39 |
-
df = pd.read_csv(
|
| 40 |
except Exception as e:
|
| 41 |
st.error(f"Error loading file: {e}")
|
| 42 |
return None
|
|
@@ -83,9 +81,8 @@ class SeoCalculator:
|
|
| 83 |
return df.rename(columns={found_columns[k]: k for k in found_columns})
|
| 84 |
|
| 85 |
@st.cache_data
|
| 86 |
-
# _self is correct for the instance itself
|
| 87 |
def calculate_metrics(
|
| 88 |
-
|
| 89 |
df: pd.DataFrame,
|
| 90 |
target_position: float,
|
| 91 |
conversion_rate: float,
|
|
@@ -96,18 +93,16 @@ class SeoCalculator:
|
|
| 96 |
) -> tuple[dict, pd.DataFrame] | tuple[None, pd.DataFrame]:
|
| 97 |
"""
|
| 98 |
Performs core calculations for SEO forecasting based on GSC data and user inputs.
|
| 99 |
-
|
| 100 |
Returns:
|
| 101 |
tuple: A dictionary of calculated metrics and a DataFrame with detailed results.
|
| 102 |
Returns (None, pd.DataFrame()) if required columns are missing.
|
| 103 |
"""
|
| 104 |
-
|
| 105 |
-
df_processed = _self._validate_and_rename_columns(df.copy())
|
| 106 |
if df_processed is None:
|
| 107 |
return None, pd.DataFrame()
|
| 108 |
|
| 109 |
-
df_processed["current_ctr"] = df_processed["position"].apply(
|
| 110 |
-
target_ctr_value =
|
| 111 |
df_processed["target_ctr"] = target_ctr_value
|
| 112 |
|
| 113 |
df_processed["current_clicks"] = df_processed["impressions"] * df_processed["current_ctr"]
|
|
@@ -199,6 +194,7 @@ class SeoAppUI:
|
|
| 199 |
def _get_sidebar_inputs(self) -> tuple:
|
| 200 |
with st.sidebar:
|
| 201 |
st.header("🔧 Assumptions & Inputs")
|
|
|
|
| 202 |
uploaded_file = st.file_uploader("Upload queries CSV data", type="csv")
|
| 203 |
target_position = st.slider(
|
| 204 |
"Target SERP Position",
|
|
@@ -214,13 +210,6 @@ class SeoAppUI:
|
|
| 214 |
seo_cost = st.slider("Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000)
|
| 215 |
add_spend = st.slider("Additional Ad Spend ($)", 0, 50_000, 0, 1_000, help="A **hypothetical budget** for extra paid ad spend, not from your GSC data. Use it to directly compare SEO's projected incremental MRR with a potential ad investment.")
|
| 216 |
|
| 217 |
-
sample_bytes = requests.get(SAMPLE_FILE_URL).content
|
| 218 |
-
st.download_button(
|
| 219 |
-
label="📥 Download sample CSV",
|
| 220 |
-
data=sample_bytes,
|
| 221 |
-
file_name="sample_gsc_data.csv",
|
| 222 |
-
mime="text/csv",
|
| 223 |
-
)
|
| 224 |
return uploaded_file, target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend
|
| 225 |
|
| 226 |
def _display_summary_metrics(self, metrics: dict):
|
|
@@ -293,13 +282,23 @@ class SeoAppUI:
|
|
| 293 |
|
| 294 |
def run(self):
|
| 295 |
self._display_info_expander()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
uploaded_file, target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend = self._get_sidebar_inputs()
|
| 297 |
|
| 298 |
-
# FIX: Call load_csv normally, Python handles the _self
|
| 299 |
df = self.data_loader.load_csv(uploaded_file)
|
| 300 |
|
| 301 |
if df is not None:
|
| 302 |
-
# FIX: Call calculate_metrics normally, Python handles the _self
|
| 303 |
metrics, df_results = self.seo_calculator.calculate_metrics(
|
| 304 |
df,
|
| 305 |
target_position,
|
|
|
|
| 18 |
self.sample_file_url = sample_file_url
|
| 19 |
|
| 20 |
@st.cache_data
|
| 21 |
+
def load_csv(self, uploaded_file_obj: st.runtime.uploaded_file_manager.UploadedFile | None) -> pd.DataFrame | None:
|
|
|
|
| 22 |
"""
|
| 23 |
Loads the GSC data from an uploaded CSV or a sample URL,
|
| 24 |
normalizes column names, and ensures a 'cpc' column exists.
|
|
|
|
| 25 |
Args:
|
| 26 |
+
self: The instance of the DataLoader class.
|
| 27 |
uploaded_file_obj (streamlit.runtime.uploaded_file_manager.UploadedFile): The file object
|
| 28 |
uploaded by the user, or None.
|
| 29 |
Returns:
|
|
|
|
| 33 |
if uploaded_file_obj:
|
| 34 |
df = pd.read_csv(uploaded_file_obj)
|
| 35 |
else:
|
| 36 |
+
# Use self.sample_file_url since self is the instance
|
| 37 |
+
df = pd.read_csv(self.sample_file_url)
|
| 38 |
except Exception as e:
|
| 39 |
st.error(f"Error loading file: {e}")
|
| 40 |
return None
|
|
|
|
| 81 |
return df.rename(columns={found_columns[k]: k for k in found_columns})
|
| 82 |
|
| 83 |
@st.cache_data
|
|
|
|
| 84 |
def calculate_metrics(
|
| 85 |
+
self,
|
| 86 |
df: pd.DataFrame,
|
| 87 |
target_position: float,
|
| 88 |
conversion_rate: float,
|
|
|
|
| 93 |
) -> tuple[dict, pd.DataFrame] | tuple[None, pd.DataFrame]:
|
| 94 |
"""
|
| 95 |
Performs core calculations for SEO forecasting based on GSC data and user inputs.
|
|
|
|
| 96 |
Returns:
|
| 97 |
tuple: A dictionary of calculated metrics and a DataFrame with detailed results.
|
| 98 |
Returns (None, pd.DataFrame()) if required columns are missing.
|
| 99 |
"""
|
| 100 |
+
df_processed = self._validate_and_rename_columns(df.copy())
|
|
|
|
| 101 |
if df_processed is None:
|
| 102 |
return None, pd.DataFrame()
|
| 103 |
|
| 104 |
+
df_processed["current_ctr"] = df_processed["position"].apply(self._get_ctr)
|
| 105 |
+
target_ctr_value = self._get_ctr(target_position)
|
| 106 |
df_processed["target_ctr"] = target_ctr_value
|
| 107 |
|
| 108 |
df_processed["current_clicks"] = df_processed["impressions"] * df_processed["current_ctr"]
|
|
|
|
| 194 |
def _get_sidebar_inputs(self) -> tuple:
|
| 195 |
with st.sidebar:
|
| 196 |
st.header("🔧 Assumptions & Inputs")
|
| 197 |
+
# The upload file will remain in the sidebar as it's an input
|
| 198 |
uploaded_file = st.file_uploader("Upload queries CSV data", type="csv")
|
| 199 |
target_position = st.slider(
|
| 200 |
"Target SERP Position",
|
|
|
|
| 210 |
seo_cost = st.slider("Total SEO Investment ($)", 1_000, 100_000, 10_000, 1_000)
|
| 211 |
add_spend = st.slider("Additional Ad Spend ($)", 0, 50_000, 0, 1_000, help="A **hypothetical budget** for extra paid ad spend, not from your GSC data. Use it to directly compare SEO's projected incremental MRR with a potential ad investment.")
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
return uploaded_file, target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend
|
| 214 |
|
| 215 |
def _display_summary_metrics(self, metrics: dict):
|
|
|
|
| 282 |
|
| 283 |
def run(self):
|
| 284 |
self._display_info_expander()
|
| 285 |
+
|
| 286 |
+
# Moved the download button out of the sidebar to the main area
|
| 287 |
+
sample_bytes = requests.get(SAMPLE_FILE_URL).content
|
| 288 |
+
st.download_button(
|
| 289 |
+
label="📥 Download sample CSV",
|
| 290 |
+
data=sample_bytes,
|
| 291 |
+
file_name="sample_gsc_data.csv",
|
| 292 |
+
mime="text/csv",
|
| 293 |
+
key="download_sample_main" # Added a key to avoid potential duplicate widget issues
|
| 294 |
+
)
|
| 295 |
+
st.markdown("---") # Add a separator for better visual organization
|
| 296 |
+
|
| 297 |
uploaded_file, target_position, conversion_rate, close_rate, mrr_per_customer, seo_cost, add_spend = self._get_sidebar_inputs()
|
| 298 |
|
|
|
|
| 299 |
df = self.data_loader.load_csv(uploaded_file)
|
| 300 |
|
| 301 |
if df is not None:
|
|
|
|
| 302 |
metrics, df_results = self.seo_calculator.calculate_metrics(
|
| 303 |
df,
|
| 304 |
target_position,
|