Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# app.py
|
| 2 |
-
import io, json, os, base64
|
| 3 |
from pathlib import Path
|
| 4 |
import streamlit as st
|
| 5 |
import pandas as pd
|
|
@@ -22,14 +22,13 @@ TARGET = "UCS"
|
|
| 22 |
MODELS_DIR = Path("models")
|
| 23 |
DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
|
| 24 |
MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
|
| 25 |
-
|
| 26 |
COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
|
| 27 |
|
| 28 |
# ---- Plot sizing controls (edit here) ----
|
| 29 |
CROSS_W = 400; CROSS_H = 400 # square cross-plot
|
| 30 |
TRACK_W = 400; TRACK_H = 950 # log-strip style (tall, slightly wider)
|
| 31 |
FONT_SZ = 13
|
| 32 |
-
PLOT_COLS = [14,1, 10] # 3-column band: left • spacer • right
|
| 33 |
|
| 34 |
# =========================
|
| 35 |
# Page / CSS
|
|
@@ -49,14 +48,6 @@ st.markdown(
|
|
| 49 |
.st-hero h1 { margin:0; line-height:1.05; }
|
| 50 |
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 51 |
[data-testid="stBlock"]{ margin-top:0 !important; }
|
| 52 |
-
|
| 53 |
-
/* sticky helper notice */
|
| 54 |
-
.helper-sticky { position: sticky; top: 64px; z-index: 50; }
|
| 55 |
-
.helper-sticky .box {
|
| 56 |
-
border-radius: 8px; padding: 12px 14px; margin: 6px 0 10px 0; font-size: 0.98rem;
|
| 57 |
-
}
|
| 58 |
-
.helper-sticky .info { background:#eaf2ff; border:1px solid #c9defa; color:#0b4aa2; }
|
| 59 |
-
.helper-sticky .success { background:#eaf7ea; border:1px solid #c7e8c8; color:#1b6e22; }
|
| 60 |
</style>
|
| 61 |
""",
|
| 62 |
unsafe_allow_html=True
|
|
@@ -154,21 +145,20 @@ def parse_excel(data_bytes: bytes):
|
|
| 154 |
return {sh: xl.parse(sh) for sh in xl.sheet_names}
|
| 155 |
|
| 156 |
def read_book_bytes(b: bytes): return parse_excel(b) if b else {}
|
| 157 |
-
|
| 158 |
-
def ensure_cols(df, cols):
|
| 159 |
-
miss = [c for c in cols if c not in df.columns]
|
| 160 |
-
if miss:
|
| 161 |
-
st.error(f"Missing columns: {miss}")
|
| 162 |
-
return False
|
| 163 |
-
return True
|
| 164 |
-
|
| 165 |
def find_sheet(book, names):
|
| 166 |
low2orig = {k.lower(): k for k in book.keys()}
|
| 167 |
for nm in names:
|
| 168 |
if nm.lower() in low2orig: return low2orig[nm.lower()]
|
| 169 |
return None
|
| 170 |
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
def cross_plot(actual, pred):
|
| 173 |
a = pd.Series(actual).astype(float)
|
| 174 |
p = pd.Series(pred).astype(float)
|
|
@@ -191,25 +181,41 @@ def cross_plot(actual, pred):
|
|
| 191 |
))
|
| 192 |
fig.update_layout(
|
| 193 |
width=CROSS_W, height=CROSS_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
|
| 194 |
-
margin=dict(l=64, r=18, t=
|
| 195 |
font=dict(size=FONT_SZ)
|
| 196 |
)
|
| 197 |
-
fig.update_xaxes(title_text="<b>Actual UCS</b>", range=[x0, x1],
|
| 198 |
ticks="outside", tickformat=",.0f",
|
| 199 |
-
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
| 200 |
-
automargin=True)
|
| 201 |
-
fig.update_yaxes(title_text="<b>Predicted UCS</b>", range=[x0, x1],
|
| 202 |
ticks="outside", tickformat=",.0f",
|
| 203 |
-
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
|
|
|
| 204 |
scaleanchor="x", scaleratio=1, automargin=True)
|
| 205 |
return fig
|
| 206 |
|
| 207 |
def track_plot(df, include_actual=True):
|
| 208 |
depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
|
| 209 |
if depth_col is not None:
|
| 210 |
-
y = df[depth_col]
|
|
|
|
|
|
|
|
|
|
| 211 |
else:
|
| 212 |
-
y = np.arange(1, len(df) + 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
fig = go.Figure()
|
| 215 |
fig.add_trace(go.Scatter(
|
|
@@ -228,22 +234,27 @@ def track_plot(df, include_actual=True):
|
|
| 228 |
|
| 229 |
fig.update_layout(
|
| 230 |
width=TRACK_W, height=TRACK_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
|
| 231 |
-
margin=dict(l=72, r=18, t=
|
| 232 |
font=dict(size=FONT_SZ),
|
| 233 |
legend=dict(
|
| 234 |
x=0.98, y=0.05, xanchor="right", yanchor="bottom",
|
| 235 |
-
bgcolor="rgba(255,255,255,0.
|
| 236 |
),
|
| 237 |
legend_title_text=""
|
| 238 |
)
|
| 239 |
-
fig.update_xaxes(
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
return fig
|
| 248 |
|
| 249 |
# ---------- Preview modal (matplotlib) ----------
|
|
@@ -251,7 +262,10 @@ def preview_tracks(df: pd.DataFrame, cols: list[str]):
|
|
| 251 |
cols = [c for c in cols if c in df.columns]
|
| 252 |
n = len(cols)
|
| 253 |
if n == 0:
|
| 254 |
-
fig, ax = plt.subplots(figsize=(4, 2))
|
|
|
|
|
|
|
|
|
|
| 255 |
fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
|
| 256 |
if n == 1: axes = [axes]
|
| 257 |
idx = np.arange(1, len(df) + 1)
|
|
@@ -263,6 +277,17 @@ def preview_tracks(df: pd.DataFrame, cols: list[str]):
|
|
| 263 |
axes[0].set_ylabel("Point Index")
|
| 264 |
return fig
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
@dialog("Preview data")
|
| 267 |
def preview_modal(book: dict[str, pd.DataFrame]):
|
| 268 |
if not book:
|
|
@@ -308,7 +333,6 @@ except Exception as e:
|
|
| 308 |
st.error(f"Failed to load model: {e}")
|
| 309 |
st.stop()
|
| 310 |
|
| 311 |
-
# Try to pull features from model if provided
|
| 312 |
meta_path = MODELS_DIR / "meta.json"
|
| 313 |
if meta_path.exists():
|
| 314 |
try:
|
|
@@ -323,20 +347,10 @@ if meta_path.exists():
|
|
| 323 |
st.session_state.setdefault("app_step", "intro")
|
| 324 |
st.session_state.setdefault("results", {})
|
| 325 |
st.session_state.setdefault("train_ranges", None)
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
"dev_preview":False
|
| 331 |
-
}.items():
|
| 332 |
-
st.session_state.setdefault(k, v)
|
| 333 |
-
|
| 334 |
-
# helper notice anchor (sticky)
|
| 335 |
-
def make_notice():
|
| 336 |
-
anchor = st.empty()
|
| 337 |
-
def info(msg_html): anchor.markdown(f"<div class='helper-sticky'><div class='box info'>{msg_html}</div></div>", unsafe_allow_html=True)
|
| 338 |
-
def success(msg_html): anchor.markdown(f"<div class='helper-sticky'><div class='box success'>{msg_html}</div></div>", unsafe_allow_html=True)
|
| 339 |
-
return info, success
|
| 340 |
|
| 341 |
# =========================
|
| 342 |
# Hero
|
|
@@ -354,9 +368,6 @@ st.markdown(
|
|
| 354 |
unsafe_allow_html=True,
|
| 355 |
)
|
| 356 |
|
| 357 |
-
# reuse plot config
|
| 358 |
-
PLOT_CFG = {"displayModeBar": False, "scrollZoom": True}
|
| 359 |
-
|
| 360 |
# =========================
|
| 361 |
# INTRO
|
| 362 |
# =========================
|
|
@@ -394,19 +405,20 @@ if st.session_state.app_step == "dev":
|
|
| 394 |
st.session_state.dev_preview = True
|
| 395 |
|
| 396 |
run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
|
|
|
|
| 397 |
if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
|
| 398 |
if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
|
| 399 |
|
| 400 |
-
|
| 401 |
-
st.
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
|
| 411 |
if run and st.session_state.dev_file_bytes:
|
| 412 |
book = read_book_bytes(st.session_state.dev_file_bytes)
|
|
@@ -415,7 +427,8 @@ if st.session_state.app_step == "dev":
|
|
| 415 |
if sh_train is None or sh_test is None:
|
| 416 |
st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
|
| 417 |
tr = book[sh_train].copy(); te = book[sh_test].copy()
|
| 418 |
-
if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
|
|
|
|
| 419 |
tr["UCS_Pred"] = model.predict(tr[FEATURES])
|
| 420 |
te["UCS_Pred"] = model.predict(te[FEATURES])
|
| 421 |
|
|
@@ -425,28 +438,31 @@ if st.session_state.app_step == "dev":
|
|
| 425 |
|
| 426 |
tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
|
| 427 |
st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
|
| 428 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
|
| 430 |
if "Train" in st.session_state.results or "Test" in st.session_state.results:
|
| 431 |
tab1, tab2 = st.tabs(["Training", "Testing"])
|
| 432 |
-
|
| 433 |
-
def dev_block(df, m):
|
| 434 |
-
c1,c2,c3 = st.columns(3)
|
| 435 |
-
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 436 |
-
left, mid, right = st.columns(PLOT_COLS, gap="small")
|
| 437 |
-
with left:
|
| 438 |
-
st.plotly_chart(cross_plot(df[TARGET], df["UCS_Pred"]),
|
| 439 |
-
use_container_width=False, config=PLOT_CFG)
|
| 440 |
-
with mid:
|
| 441 |
-
st.write("") # spacer
|
| 442 |
-
with right:
|
| 443 |
-
st.plotly_chart(track_plot(df, include_actual=True),
|
| 444 |
-
use_container_width=False, config=PLOT_CFG)
|
| 445 |
-
|
| 446 |
if "Train" in st.session_state.results:
|
| 447 |
-
with tab1:
|
| 448 |
if "Test" in st.session_state.results:
|
| 449 |
-
with tab2:
|
| 450 |
|
| 451 |
# =========================
|
| 452 |
# VALIDATION (with actual UCS)
|
|
@@ -465,15 +481,14 @@ if st.session_state.app_step == "validate":
|
|
| 465 |
if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
|
| 466 |
if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
|
| 467 |
|
| 468 |
-
info, success = make_notice()
|
| 469 |
st.subheader("Validate the Model")
|
| 470 |
-
|
| 471 |
|
| 472 |
if go_btn and up is not None:
|
| 473 |
book = read_book_bytes(up.getvalue())
|
| 474 |
name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
|
| 475 |
df = book[name].copy()
|
| 476 |
-
if not ensure_cols(df, FEATURES+[TARGET]): st.stop()
|
| 477 |
df["UCS_Pred"] = model.predict(df[FEATURES])
|
| 478 |
st.session_state.results["Validate"]=df
|
| 479 |
|
|
@@ -487,25 +502,26 @@ if st.session_state.app_step == "validate":
|
|
| 487 |
st.session_state.results["m_val"]={"R2":r2_score(df[TARGET],df["UCS_Pred"]), "RMSE":rmse(df[TARGET],df["UCS_Pred"]), "MAE":mean_absolute_error(df[TARGET],df["UCS_Pred"])}
|
| 488 |
st.session_state.results["sv_val"]={"n":len(df),"pred_min":float(df["UCS_Pred"].min()),"pred_max":float(df["UCS_Pred"].max()),"oor":oor_pct}
|
| 489 |
st.session_state.results["oor_tbl"]=tbl
|
| 490 |
-
st.rerun()
|
| 491 |
|
| 492 |
if "Validate" in st.session_state.results:
|
| 493 |
m = st.session_state.results["m_val"]; sv = st.session_state.results["sv_val"]
|
| 494 |
c1,c2,c3 = st.columns(3)
|
| 495 |
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 496 |
|
| 497 |
-
left,
|
| 498 |
with left:
|
| 499 |
-
st.plotly_chart(
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
with right:
|
| 505 |
-
st.plotly_chart(
|
| 506 |
-
|
|
|
|
|
|
|
| 507 |
|
| 508 |
-
if sv["oor"] > 0: st.warning("Some inputs fall outside
|
| 509 |
if st.session_state.results["oor_tbl"] is not None:
|
| 510 |
st.write("*Out-of-range rows (vs. Training min–max):*")
|
| 511 |
st.dataframe(st.session_state.results["oor_tbl"], use_container_width=True)
|
|
@@ -526,14 +542,13 @@ if st.session_state.app_step == "predict":
|
|
| 526 |
go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
|
| 527 |
if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
|
| 528 |
|
| 529 |
-
info, _success = make_notice()
|
| 530 |
st.subheader("Prediction")
|
| 531 |
-
|
| 532 |
|
| 533 |
if go_btn and up is not None:
|
| 534 |
book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
|
| 535 |
df = book[name].copy()
|
| 536 |
-
if not ensure_cols(df, FEATURES): st.stop()
|
| 537 |
df["UCS_Pred"] = model.predict(df[FEATURES])
|
| 538 |
st.session_state.results["PredictOnly"]=df
|
| 539 |
|
|
@@ -549,12 +564,11 @@ if st.session_state.app_step == "predict":
|
|
| 549 |
"pred_std":float(df["UCS_Pred"].std(ddof=0)),
|
| 550 |
"oor":oor_pct
|
| 551 |
}
|
| 552 |
-
st.rerun()
|
| 553 |
|
| 554 |
if "PredictOnly" in st.session_state.results:
|
| 555 |
df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
|
| 556 |
|
| 557 |
-
left,
|
| 558 |
with left:
|
| 559 |
table = pd.DataFrame({
|
| 560 |
"Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
|
|
@@ -563,11 +577,11 @@ if st.session_state.app_step == "predict":
|
|
| 563 |
st.success("Predictions ready ✓")
|
| 564 |
st.dataframe(table, use_container_width=True, hide_index=True)
|
| 565 |
st.caption("**★ OOR** = % of rows whose input features fall outside the training min–max range.")
|
| 566 |
-
with mid:
|
| 567 |
-
st.write("")
|
| 568 |
with right:
|
| 569 |
-
st.plotly_chart(
|
| 570 |
-
|
|
|
|
|
|
|
| 571 |
|
| 572 |
# =========================
|
| 573 |
# Footer
|
|
|
|
| 1 |
# app.py
|
| 2 |
+
import io, json, os, base64, math
|
| 3 |
from pathlib import Path
|
| 4 |
import streamlit as st
|
| 5 |
import pandas as pd
|
|
|
|
| 22 |
MODELS_DIR = Path("models")
|
| 23 |
DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
|
| 24 |
MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
|
|
|
|
| 25 |
COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
|
| 26 |
|
| 27 |
# ---- Plot sizing controls (edit here) ----
|
| 28 |
CROSS_W = 400; CROSS_H = 400 # square cross-plot
|
| 29 |
TRACK_W = 400; TRACK_H = 950 # log-strip style (tall, slightly wider)
|
| 30 |
FONT_SZ = 13
|
| 31 |
+
PLOT_COLS = [14, 1, 10] # 3-column band: left • spacer • right
|
| 32 |
|
| 33 |
# =========================
|
| 34 |
# Page / CSS
|
|
|
|
| 48 |
.st-hero h1 { margin:0; line-height:1.05; }
|
| 49 |
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 50 |
[data-testid="stBlock"]{ margin-top:0 !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
</style>
|
| 52 |
""",
|
| 53 |
unsafe_allow_html=True
|
|
|
|
| 145 |
return {sh: xl.parse(sh) for sh in xl.sheet_names}
|
| 146 |
|
| 147 |
def read_book_bytes(b: bytes): return parse_excel(b) if b else {}
|
| 148 |
+
def ensure_cols(df, cols): return not [c for c in cols if c not in df.columns] or False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def find_sheet(book, names):
|
| 150 |
low2orig = {k.lower(): k for k in book.keys()}
|
| 151 |
for nm in names:
|
| 152 |
if nm.lower() in low2orig: return low2orig[nm.lower()]
|
| 153 |
return None
|
| 154 |
|
| 155 |
+
def _nice_tick0(xmin: float, step: int = 100) -> float:
|
| 156 |
+
"""Round xmin down to a sensible multiple so the first tick sits at the left edge."""
|
| 157 |
+
if not np.isfinite(xmin):
|
| 158 |
+
return xmin
|
| 159 |
+
return step * math.floor(xmin / step)
|
| 160 |
+
|
| 161 |
+
# ---------- Plot builders ----------
|
| 162 |
def cross_plot(actual, pred):
|
| 163 |
a = pd.Series(actual).astype(float)
|
| 164 |
p = pd.Series(pred).astype(float)
|
|
|
|
| 181 |
))
|
| 182 |
fig.update_layout(
|
| 183 |
width=CROSS_W, height=CROSS_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
|
| 184 |
+
margin=dict(l=64, r=18, t=10, b=48), hovermode="closest",
|
| 185 |
font=dict(size=FONT_SZ)
|
| 186 |
)
|
| 187 |
+
fig.update_xaxes(title_text="<b>Actual UCS (psi)</b>", range=[x0, x1],
|
| 188 |
ticks="outside", tickformat=",.0f",
|
| 189 |
+
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
| 190 |
+
showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True)
|
| 191 |
+
fig.update_yaxes(title_text="<b>Predicted UCS (psi)</b>", range=[x0, x1],
|
| 192 |
ticks="outside", tickformat=",.0f",
|
| 193 |
+
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
| 194 |
+
showgrid=True, gridcolor="rgba(0,0,0,0.12)",
|
| 195 |
scaleanchor="x", scaleratio=1, automargin=True)
|
| 196 |
return fig
|
| 197 |
|
| 198 |
def track_plot(df, include_actual=True):
|
| 199 |
depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
|
| 200 |
if depth_col is not None:
|
| 201 |
+
y = pd.Series(df[depth_col]).astype(float)
|
| 202 |
+
ylab = depth_col
|
| 203 |
+
y_min, y_max = float(y.min()), float(y.max())
|
| 204 |
+
y_range = [y_max, y_min] # reversed for log profile style
|
| 205 |
else:
|
| 206 |
+
y = pd.Series(np.arange(1, len(df) + 1))
|
| 207 |
+
ylab = "Point Index"
|
| 208 |
+
y_min, y_max = float(y.min()), float(y.max())
|
| 209 |
+
y_range = [y_max, y_min]
|
| 210 |
+
|
| 211 |
+
# X (UCS) range & ticks
|
| 212 |
+
x_series = pd.Series(df.get("UCS_Pred", pd.Series(dtype=float))).astype(float)
|
| 213 |
+
if include_actual and TARGET in df.columns:
|
| 214 |
+
x_series = pd.concat([x_series, pd.Series(df[TARGET]).astype(float)], ignore_index=True)
|
| 215 |
+
x_lo, x_hi = float(x_series.min()), float(x_series.max())
|
| 216 |
+
x_pad = 0.03 * (x_hi - x_lo if x_hi > x_lo else 1.0)
|
| 217 |
+
xmin, xmax = x_lo - x_pad, x_hi + x_pad
|
| 218 |
+
tick0 = _nice_tick0(xmin, step=100) # sensible first tick at left border
|
| 219 |
|
| 220 |
fig = go.Figure()
|
| 221 |
fig.add_trace(go.Scatter(
|
|
|
|
| 234 |
|
| 235 |
fig.update_layout(
|
| 236 |
width=TRACK_W, height=TRACK_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
|
| 237 |
+
margin=dict(l=72, r=18, t=36, b=48), hovermode="closest",
|
| 238 |
font=dict(size=FONT_SZ),
|
| 239 |
legend=dict(
|
| 240 |
x=0.98, y=0.05, xanchor="right", yanchor="bottom",
|
| 241 |
+
bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1
|
| 242 |
),
|
| 243 |
legend_title_text=""
|
| 244 |
)
|
| 245 |
+
fig.update_xaxes(
|
| 246 |
+
title_text="<b>UCS (psi)</b>", side="top", range=[xmin, xmax],
|
| 247 |
+
ticks="outside", tickformat=",.0f",
|
| 248 |
+
tickmode="auto", tick0=tick0,
|
| 249 |
+
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
| 250 |
+
showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
|
| 251 |
+
)
|
| 252 |
+
fig.update_yaxes(
|
| 253 |
+
title_text=f"<b>{ylab}</b>", range=y_range,
|
| 254 |
+
ticks="outside",
|
| 255 |
+
showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
| 256 |
+
showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
|
| 257 |
+
)
|
| 258 |
return fig
|
| 259 |
|
| 260 |
# ---------- Preview modal (matplotlib) ----------
|
|
|
|
| 262 |
cols = [c for c in cols if c in df.columns]
|
| 263 |
n = len(cols)
|
| 264 |
if n == 0:
|
| 265 |
+
fig, ax = plt.subplots(figsize=(4, 2))
|
| 266 |
+
ax.text(0.5,0.5,"No selected columns",ha="center",va="center")
|
| 267 |
+
ax.axis("off")
|
| 268 |
+
return fig
|
| 269 |
fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
|
| 270 |
if n == 1: axes = [axes]
|
| 271 |
idx = np.arange(1, len(df) + 1)
|
|
|
|
| 277 |
axes[0].set_ylabel("Point Index")
|
| 278 |
return fig
|
| 279 |
|
| 280 |
+
try:
|
| 281 |
+
dialog = st.dialog
|
| 282 |
+
except AttributeError:
|
| 283 |
+
def dialog(title):
|
| 284 |
+
def deco(fn):
|
| 285 |
+
def wrapper(*args, **kwargs):
|
| 286 |
+
with st.expander(title, expanded=True):
|
| 287 |
+
return fn(*args, **kwargs)
|
| 288 |
+
return wrapper
|
| 289 |
+
return deco
|
| 290 |
+
|
| 291 |
@dialog("Preview data")
|
| 292 |
def preview_modal(book: dict[str, pd.DataFrame]):
|
| 293 |
if not book:
|
|
|
|
| 333 |
st.error(f"Failed to load model: {e}")
|
| 334 |
st.stop()
|
| 335 |
|
|
|
|
| 336 |
meta_path = MODELS_DIR / "meta.json"
|
| 337 |
if meta_path.exists():
|
| 338 |
try:
|
|
|
|
| 347 |
st.session_state.setdefault("app_step", "intro")
|
| 348 |
st.session_state.setdefault("results", {})
|
| 349 |
st.session_state.setdefault("train_ranges", None)
|
| 350 |
+
st.session_state.setdefault("dev_file_name","")
|
| 351 |
+
st.session_state.setdefault("dev_file_bytes",b"")
|
| 352 |
+
st.session_state.setdefault("dev_file_loaded",False)
|
| 353 |
+
st.session_state.setdefault("dev_preview",False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
# =========================
|
| 356 |
# Hero
|
|
|
|
| 368 |
unsafe_allow_html=True,
|
| 369 |
)
|
| 370 |
|
|
|
|
|
|
|
|
|
|
| 371 |
# =========================
|
| 372 |
# INTRO
|
| 373 |
# =========================
|
|
|
|
| 405 |
st.session_state.dev_preview = True
|
| 406 |
|
| 407 |
run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
|
| 408 |
+
# always available nav
|
| 409 |
if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
|
| 410 |
if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
|
| 411 |
|
| 412 |
+
# ---- Pinned helper at the very top of the page ----
|
| 413 |
+
helper_top = st.container()
|
| 414 |
+
with helper_top:
|
| 415 |
+
st.subheader("Case Building (Development)")
|
| 416 |
+
if st.session_state.dev_file_loaded and st.session_state.dev_preview:
|
| 417 |
+
st.info("Previewed ✓ — now click **Run Model**.")
|
| 418 |
+
elif st.session_state.dev_file_loaded:
|
| 419 |
+
st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
|
| 420 |
+
else:
|
| 421 |
+
st.write("**Upload your data to build a case, then run the model to review development performance.**")
|
| 422 |
|
| 423 |
if run and st.session_state.dev_file_bytes:
|
| 424 |
book = read_book_bytes(st.session_state.dev_file_bytes)
|
|
|
|
| 427 |
if sh_train is None or sh_test is None:
|
| 428 |
st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
|
| 429 |
tr = book[sh_train].copy(); te = book[sh_test].copy()
|
| 430 |
+
if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
|
| 431 |
+
st.error("Missing required columns."); st.stop()
|
| 432 |
tr["UCS_Pred"] = model.predict(tr[FEATURES])
|
| 433 |
te["UCS_Pred"] = model.predict(te[FEATURES])
|
| 434 |
|
|
|
|
| 438 |
|
| 439 |
tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
|
| 440 |
st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
|
| 441 |
+
st.success("Case has been built and results are displayed below.")
|
| 442 |
+
|
| 443 |
+
def _dev_block(df, m):
|
| 444 |
+
c1,c2,c3 = st.columns(3)
|
| 445 |
+
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 446 |
+
left, spacer, right = st.columns(PLOT_COLS)
|
| 447 |
+
with left:
|
| 448 |
+
st.plotly_chart(
|
| 449 |
+
cross_plot(df[TARGET], df["UCS_Pred"]),
|
| 450 |
+
use_container_width=False,
|
| 451 |
+
config={"displayModeBar": False, "scrollZoom": True}
|
| 452 |
+
)
|
| 453 |
+
with right:
|
| 454 |
+
st.plotly_chart(
|
| 455 |
+
track_plot(df, include_actual=True),
|
| 456 |
+
use_container_width=False,
|
| 457 |
+
config={"displayModeBar": False, "scrollZoom": True}
|
| 458 |
+
)
|
| 459 |
|
| 460 |
if "Train" in st.session_state.results or "Test" in st.session_state.results:
|
| 461 |
tab1, tab2 = st.tabs(["Training", "Testing"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
if "Train" in st.session_state.results:
|
| 463 |
+
with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
|
| 464 |
if "Test" in st.session_state.results:
|
| 465 |
+
with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
|
| 466 |
|
| 467 |
# =========================
|
| 468 |
# VALIDATION (with actual UCS)
|
|
|
|
| 481 |
if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
|
| 482 |
if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
|
| 483 |
|
|
|
|
| 484 |
st.subheader("Validate the Model")
|
| 485 |
+
st.write("Upload a dataset with the same **features** and **UCS** to evaluate performance.")
|
| 486 |
|
| 487 |
if go_btn and up is not None:
|
| 488 |
book = read_book_bytes(up.getvalue())
|
| 489 |
name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
|
| 490 |
df = book[name].copy()
|
| 491 |
+
if not ensure_cols(df, FEATURES+[TARGET]): st.error("Missing required columns."); st.stop()
|
| 492 |
df["UCS_Pred"] = model.predict(df[FEATURES])
|
| 493 |
st.session_state.results["Validate"]=df
|
| 494 |
|
|
|
|
| 502 |
st.session_state.results["m_val"]={"R2":r2_score(df[TARGET],df["UCS_Pred"]), "RMSE":rmse(df[TARGET],df["UCS_Pred"]), "MAE":mean_absolute_error(df[TARGET],df["UCS_Pred"])}
|
| 503 |
st.session_state.results["sv_val"]={"n":len(df),"pred_min":float(df["UCS_Pred"].min()),"pred_max":float(df["UCS_Pred"].max()),"oor":oor_pct}
|
| 504 |
st.session_state.results["oor_tbl"]=tbl
|
|
|
|
| 505 |
|
| 506 |
if "Validate" in st.session_state.results:
|
| 507 |
m = st.session_state.results["m_val"]; sv = st.session_state.results["sv_val"]
|
| 508 |
c1,c2,c3 = st.columns(3)
|
| 509 |
c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
|
| 510 |
|
| 511 |
+
left, spacer, right = st.columns(PLOT_COLS)
|
| 512 |
with left:
|
| 513 |
+
st.plotly_chart(
|
| 514 |
+
cross_plot(st.session_state.results["Validate"][TARGET],
|
| 515 |
+
st.session_state.results["Validate"]["UCS_Pred"]),
|
| 516 |
+
use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
|
| 517 |
+
)
|
| 518 |
with right:
|
| 519 |
+
st.plotly_chart(
|
| 520 |
+
track_plot(st.session_state.results["Validate"], include_actual=True),
|
| 521 |
+
use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
|
| 522 |
+
)
|
| 523 |
|
| 524 |
+
if sv["oor"] > 0: st.warning("Some inputs fall outside **training min–max** ranges.")
|
| 525 |
if st.session_state.results["oor_tbl"] is not None:
|
| 526 |
st.write("*Out-of-range rows (vs. Training min–max):*")
|
| 527 |
st.dataframe(st.session_state.results["oor_tbl"], use_container_width=True)
|
|
|
|
| 542 |
go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
|
| 543 |
if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
|
| 544 |
|
|
|
|
| 545 |
st.subheader("Prediction")
|
| 546 |
+
st.write("Upload a dataset with the feature columns (no **UCS**).")
|
| 547 |
|
| 548 |
if go_btn and up is not None:
|
| 549 |
book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
|
| 550 |
df = book[name].copy()
|
| 551 |
+
if not ensure_cols(df, FEATURES): st.error("Missing required columns."); st.stop()
|
| 552 |
df["UCS_Pred"] = model.predict(df[FEATURES])
|
| 553 |
st.session_state.results["PredictOnly"]=df
|
| 554 |
|
|
|
|
| 564 |
"pred_std":float(df["UCS_Pred"].std(ddof=0)),
|
| 565 |
"oor":oor_pct
|
| 566 |
}
|
|
|
|
| 567 |
|
| 568 |
if "PredictOnly" in st.session_state.results:
|
| 569 |
df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
|
| 570 |
|
| 571 |
+
left, spacer, right = st.columns(PLOT_COLS)
|
| 572 |
with left:
|
| 573 |
table = pd.DataFrame({
|
| 574 |
"Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
|
|
|
|
| 577 |
st.success("Predictions ready ✓")
|
| 578 |
st.dataframe(table, use_container_width=True, hide_index=True)
|
| 579 |
st.caption("**★ OOR** = % of rows whose input features fall outside the training min–max range.")
|
|
|
|
|
|
|
| 580 |
with right:
|
| 581 |
+
st.plotly_chart(
|
| 582 |
+
track_plot(df, include_actual=False),
|
| 583 |
+
use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
|
| 584 |
+
)
|
| 585 |
|
| 586 |
# =========================
|
| 587 |
# Footer
|