Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,6 +4,8 @@ import streamlit as st
|
|
| 4 |
import pandas as pd
|
| 5 |
import numpy as np
|
| 6 |
import joblib
|
|
|
|
|
|
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
|
| 9 |
|
|
@@ -35,11 +37,11 @@ st.markdown(
|
|
| 35 |
<style>
|
| 36 |
.stApp { background: #FFFFFF; }
|
| 37 |
section[data-testid="stSidebar"] { background: #F6F9FC; }
|
| 38 |
-
.block-container { padding-top: .5rem; padding-bottom: .5rem; }
|
| 39 |
.stButton>button{ background:#007bff; color:#fff; font-weight:bold; border-radius:8px; border:none; padding:10px 24px; }
|
| 40 |
.stButton>button:hover{ background:#0056b3; }
|
| 41 |
.st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
|
| 42 |
-
.st-hero .brand { width:110px; height:110px; object-fit:contain; } /*
|
| 43 |
.st-hero h1 { margin:0; line-height:1.05; }
|
| 44 |
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 45 |
[data-testid="stBlock"]{ margin-top:0 !important; }
|
|
@@ -51,6 +53,19 @@ st.markdown(
|
|
| 51 |
# =========================
|
| 52 |
# Helpers
|
| 53 |
# =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def _get_model_url():
|
| 55 |
"""Read optional MODEL_URL from environment only (avoid st.secrets banner)."""
|
| 56 |
return (os.environ.get("MODEL_URL", "") or "").strip()
|
|
@@ -107,34 +122,27 @@ def depth_or_index_track(df, title=None, include_actual=True):
|
|
| 107 |
Narrow, tall track: predicted solid blue; actual dotted yellow.
|
| 108 |
Works for either Depth on Y or Index on Y.
|
| 109 |
"""
|
| 110 |
-
# Find depth-like column if available
|
| 111 |
depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
|
| 112 |
-
|
| 113 |
-
# Narrow width, tall height for logging look
|
| 114 |
fig_w = 3.1
|
| 115 |
fig_h = 7.6 if depth_col is not None else 7.2
|
| 116 |
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=100)
|
| 117 |
|
| 118 |
if depth_col is not None:
|
| 119 |
-
ax.plot(df["UCS_Pred"], df[depth_col],
|
| 120 |
-
'-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
|
| 121 |
if include_actual and TARGET in df.columns:
|
| 122 |
-
ax.plot(df[TARGET], df[depth_col],
|
| 123 |
-
':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
|
| 124 |
ax.set_ylabel(depth_col); ax.set_xlabel("UCS")
|
| 125 |
ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
|
| 126 |
else:
|
| 127 |
idx = np.arange(1, len(df) + 1)
|
| 128 |
-
ax.plot(df["UCS_Pred"], idx,
|
| 129 |
-
'-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
|
| 130 |
if include_actual and TARGET in df.columns:
|
| 131 |
-
ax.plot(df[TARGET], idx,
|
| 132 |
-
':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
|
| 133 |
ax.set_ylabel("Point Index"); ax.set_xlabel("UCS")
|
| 134 |
ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
|
| 135 |
|
| 136 |
ax.grid(True, linestyle=":", alpha=0.4)
|
| 137 |
-
if title: ax.set_title(title, pad=8)
|
| 138 |
ax.legend(loc="best")
|
| 139 |
return fig
|
| 140 |
|
|
@@ -173,6 +181,54 @@ def inline_logo(path="logo.png") -> str:
|
|
| 173 |
except Exception:
|
| 174 |
return ""
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
# =========================
|
| 177 |
# Model presence (local or optional download)
|
| 178 |
# =========================
|
|
@@ -278,6 +334,12 @@ if st.session_state.app_step == "dev":
|
|
| 278 |
st.sidebar.header("Model Development Data")
|
| 279 |
train_test_file = st.sidebar.file_uploader("Upload Data (Excel)", type=["xlsx","xls"], key="dev_upload")
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
|
| 282 |
|
| 283 |
# Proceed button BELOW run, always visible; enables immediately after first successful run
|
|
@@ -339,8 +401,7 @@ if st.session_state.app_step == "dev":
|
|
| 339 |
df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
|
| 340 |
c1,c2,c3 = st.columns(3)
|
| 341 |
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 342 |
-
#
|
| 343 |
-
left, right = st.columns([0.9, 0.55])
|
| 344 |
with left:
|
| 345 |
st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
|
| 346 |
with right:
|
|
@@ -380,6 +441,13 @@ if st.session_state.app_step == "dev":
|
|
| 380 |
if st.session_state.app_step == "predict":
|
| 381 |
st.sidebar.header("Prediction (Validation)")
|
| 382 |
validation_file = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"], key="val_upload")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
|
| 384 |
st.sidebar.button("⬅ Back", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
|
| 385 |
|
|
@@ -428,7 +496,6 @@ if st.session_state.app_step == "predict":
|
|
| 428 |
st.subheader("Validation Results")
|
| 429 |
sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
|
| 430 |
|
| 431 |
-
# Show OOR warning above the plots when applicable
|
| 432 |
if sv["oor_pct"] > 0:
|
| 433 |
st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
|
| 434 |
|
|
|
|
| 4 |
import pandas as pd
|
| 5 |
import numpy as np
|
| 6 |
import joblib
|
| 7 |
+
import matplotlib
|
| 8 |
+
matplotlib.use("Agg") # safe non-GUI backend for cloud
|
| 9 |
import matplotlib.pyplot as plt
|
| 10 |
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
|
| 11 |
|
|
|
|
| 37 |
<style>
|
| 38 |
.stApp { background: #FFFFFF; }
|
| 39 |
section[data-testid="stSidebar"] { background: #F6F9FC; }
|
| 40 |
+
.block-container { padding-top: .5rem; padding-bottom: .5rem; }
|
| 41 |
.stButton>button{ background:#007bff; color:#fff; font-weight:bold; border-radius:8px; border:none; padding:10px 24px; }
|
| 42 |
.stButton>button:hover{ background:#0056b3; }
|
| 43 |
.st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
|
| 44 |
+
.st-hero .brand { width:110px; height:110px; object-fit:contain; } /* bigger logo */
|
| 45 |
.st-hero h1 { margin:0; line-height:1.05; }
|
| 46 |
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 47 |
[data-testid="stBlock"]{ margin-top:0 !important; }
|
|
|
|
| 53 |
# =========================
|
| 54 |
# Helpers
|
| 55 |
# =========================
|
| 56 |
+
|
| 57 |
+
# Modal dialog fallback for older Streamlit
|
| 58 |
+
try:
|
| 59 |
+
dialog = st.dialog # Streamlit ≥1.31
|
| 60 |
+
except AttributeError:
|
| 61 |
+
def dialog(title):
|
| 62 |
+
def deco(fn):
|
| 63 |
+
def wrapper(*args, **kwargs):
|
| 64 |
+
with st.expander(title, expanded=True):
|
| 65 |
+
return fn(*args, **kwargs)
|
| 66 |
+
return wrapper
|
| 67 |
+
return deco
|
| 68 |
+
|
| 69 |
def _get_model_url():
|
| 70 |
"""Read optional MODEL_URL from environment only (avoid st.secrets banner)."""
|
| 71 |
return (os.environ.get("MODEL_URL", "") or "").strip()
|
|
|
|
| 122 |
Narrow, tall track: predicted solid blue; actual dotted yellow.
|
| 123 |
Works for either Depth on Y or Index on Y.
|
| 124 |
"""
|
|
|
|
| 125 |
depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
|
|
|
|
|
|
|
| 126 |
fig_w = 3.1
|
| 127 |
fig_h = 7.6 if depth_col is not None else 7.2
|
| 128 |
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=100)
|
| 129 |
|
| 130 |
if depth_col is not None:
|
| 131 |
+
ax.plot(df["UCS_Pred"], df[depth_col], '-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
|
|
|
|
| 132 |
if include_actual and TARGET in df.columns:
|
| 133 |
+
ax.plot(df[TARGET], df[depth_col], ':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
|
|
|
|
| 134 |
ax.set_ylabel(depth_col); ax.set_xlabel("UCS")
|
| 135 |
ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
|
| 136 |
else:
|
| 137 |
idx = np.arange(1, len(df) + 1)
|
| 138 |
+
ax.plot(df["UCS_Pred"], idx, '-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
|
|
|
|
| 139 |
if include_actual and TARGET in df.columns:
|
| 140 |
+
ax.plot(df[TARGET], idx, ':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
|
|
|
|
| 141 |
ax.set_ylabel("Point Index"); ax.set_xlabel("UCS")
|
| 142 |
ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
|
| 143 |
|
| 144 |
ax.grid(True, linestyle=":", alpha=0.4)
|
| 145 |
+
if title: ax.set_title(title, pad=8)
|
| 146 |
ax.legend(loc="best")
|
| 147 |
return fig
|
| 148 |
|
|
|
|
| 181 |
except Exception:
|
| 182 |
return ""
|
| 183 |
|
| 184 |
+
# ---------- Preview modal helpers ----------
|
| 185 |
+
|
| 186 |
+
def make_index_tracks(df: pd.DataFrame, cols: list[str]):
|
| 187 |
+
"""Build a side-by-side index track figure (log-style) for given columns."""
|
| 188 |
+
cols = [c for c in cols if c in df.columns]
|
| 189 |
+
n = len(cols)
|
| 190 |
+
if n == 0:
|
| 191 |
+
fig, ax = plt.subplots(figsize=(4, 2))
|
| 192 |
+
ax.text(0.5, 0.5, "No selected columns in sheet", ha="center", va="center")
|
| 193 |
+
ax.axis("off")
|
| 194 |
+
return fig
|
| 195 |
+
width_per = 2.2 # width per track
|
| 196 |
+
fig_h = 7.0
|
| 197 |
+
fig, axes = plt.subplots(1, n, figsize=(width_per * n, fig_h), sharey=True, dpi=100)
|
| 198 |
+
if n == 1:
|
| 199 |
+
axes = [axes]
|
| 200 |
+
idx = np.arange(1, len(df) + 1)
|
| 201 |
+
for ax, col in zip(axes, cols):
|
| 202 |
+
ax.plot(df[col], idx, '-', lw=1.4, color="#333333")
|
| 203 |
+
ax.set_xlabel(col)
|
| 204 |
+
ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
|
| 205 |
+
ax.grid(True, linestyle=":", alpha=0.3)
|
| 206 |
+
axes[0].set_ylabel("Point Index")
|
| 207 |
+
return fig
|
| 208 |
+
|
| 209 |
+
def stats_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
|
| 210 |
+
cols = [c for c in cols if c in df.columns]
|
| 211 |
+
if not cols:
|
| 212 |
+
return pd.DataFrame({"Feature": [], "Min": [], "Max": [], "Mean": [], "Std": []})
|
| 213 |
+
out = df[cols].agg(['min', 'max', 'mean', 'std']).T
|
| 214 |
+
out = out.rename(columns={"min": "Min", "max": "Max", "mean": "Mean", "std": "Std"})
|
| 215 |
+
out = out.reset_index().rename(columns={"index": "Feature"})
|
| 216 |
+
return out
|
| 217 |
+
|
| 218 |
+
@dialog("Preview data")
|
| 219 |
+
def preview_modal(book: dict[str, pd.DataFrame], default_cols: list[str]):
|
| 220 |
+
if not book:
|
| 221 |
+
st.info("No data loaded yet.")
|
| 222 |
+
return
|
| 223 |
+
sheet = st.selectbox("Sheet", list(book.keys()), index=0)
|
| 224 |
+
df = book[sheet]
|
| 225 |
+
present = [c for c in default_cols if c in df.columns]
|
| 226 |
+
tab_tracks, tab_summary = st.tabs(["Tracks", "Summary"])
|
| 227 |
+
with tab_tracks:
|
| 228 |
+
st.pyplot(make_index_tracks(df, present), use_container_width=True)
|
| 229 |
+
with tab_summary:
|
| 230 |
+
st.dataframe(stats_table(df, present), use_container_width=True)
|
| 231 |
+
|
| 232 |
# =========================
|
| 233 |
# Model presence (local or optional download)
|
| 234 |
# =========================
|
|
|
|
| 334 |
st.sidebar.header("Model Development Data")
|
| 335 |
train_test_file = st.sidebar.file_uploader("Upload Data (Excel)", type=["xlsx","xls"], key="dev_upload")
|
| 336 |
|
| 337 |
+
# Preview button (uses modal)
|
| 338 |
+
preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(train_test_file is None))
|
| 339 |
+
if preview_btn and train_test_file is not None:
|
| 340 |
+
_book = read_book(train_test_file)
|
| 341 |
+
preview_modal(_book, FEATURES)
|
| 342 |
+
|
| 343 |
run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
|
| 344 |
|
| 345 |
# Proceed button BELOW run, always visible; enables immediately after first successful run
|
|
|
|
| 401 |
df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
|
| 402 |
c1,c2,c3 = st.columns(3)
|
| 403 |
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 404 |
+
left, right = st.columns([0.9, 0.55]) # slim track column
|
|
|
|
| 405 |
with left:
|
| 406 |
st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
|
| 407 |
with right:
|
|
|
|
| 441 |
if st.session_state.app_step == "predict":
|
| 442 |
st.sidebar.header("Prediction (Validation)")
|
| 443 |
validation_file = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"], key="val_upload")
|
| 444 |
+
|
| 445 |
+
# Preview button for validation data
|
| 446 |
+
preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
|
| 447 |
+
if preview_val_btn and validation_file is not None:
|
| 448 |
+
_book = read_book(validation_file)
|
| 449 |
+
preview_modal(_book, FEATURES)
|
| 450 |
+
|
| 451 |
predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
|
| 452 |
st.sidebar.button("⬅ Back", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
|
| 453 |
|
|
|
|
| 496 |
st.subheader("Validation Results")
|
| 497 |
sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
|
| 498 |
|
|
|
|
| 499 |
if sv["oor_pct"] > 0:
|
| 500 |
st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
|
| 501 |
|