UCS2014 commited on
Commit
0d83d15
·
verified ·
1 Parent(s): c155002

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -107
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
- # ---------- Plot builders (fixed pixel sizes; full outline; crisp) ----------
 
 
 
 
 
 
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=8, b=48), hovermode="closest",
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, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
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, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
 
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]; ylab = depth_col; autorange = "reversed"
 
 
 
211
  else:
212
- y = np.arange(1, len(df) + 1); ylab = "Point Index"; autorange = "reversed"
 
 
 
 
 
 
 
 
 
 
 
 
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=30, b=48), hovermode="closest",
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.78)", bordercolor="#ccc", borderwidth=1
236
  ),
237
  legend_title_text=""
238
  )
239
- fig.update_xaxes(title_text="<b>UCS</b>", side="top",
240
- ticks="outside", tickformat=",.0f",
241
- showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
242
- automargin=True)
243
- fig.update_yaxes(title_text=f"<b>{ylab}</b>", autorange=autorange,
244
- ticks="outside",
245
- showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
246
- automargin=True)
 
 
 
 
 
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)); ax.text(0.5,0.5,"No selected columns",ha="center",va="center"); ax.axis("off"); return fig
 
 
 
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
- # dev file state
328
- for k, v in {
329
- "dev_file_name":"", "dev_file_bytes":b"", "dev_file_loaded":False,
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
- info, success = make_notice()
401
- st.subheader("Case Building (Development)")
402
- if "Train" in st.session_state.results or "Test" in st.session_state.results:
403
- success("Case has been built and results are displayed below.")
404
- elif st.session_state.dev_file_loaded and st.session_state.dev_preview:
405
- info("Previewed ✓ — now click <b>Run Model</b>.")
406
- elif st.session_state.dev_file_loaded:
407
- info("📄 <b>Preview uploaded data</b> using the sidebar button, then click <b>Run Model</b>.")
408
- else:
409
- info("<b>Upload your data</b> to build a case, then run the model to review development performance.")
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])): st.stop()
 
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.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
448
  if "Test" in st.session_state.results:
449
- with tab2: dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
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
- info("Upload a dataset with the same <b>features</b> and <b>UCS</b> to evaluate performance.")
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, mid, right = st.columns(PLOT_COLS, gap="small")
498
  with left:
499
- st.plotly_chart(cross_plot(st.session_state.results["Validate"][TARGET],
500
- st.session_state.results["Validate"]["UCS_Pred"]),
501
- use_container_width=False, config=PLOT_CFG)
502
- with mid:
503
- st.write("")
504
  with right:
505
- st.plotly_chart(track_plot(st.session_state.results["Validate"], include_actual=True),
506
- use_container_width=False, config=PLOT_CFG)
 
 
507
 
508
- if sv["oor"] > 0: st.warning("Some inputs fall outside <b>training min–max</b> ranges.", icon="⚠️")
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
- info("Upload a dataset with the feature columns (no <b>UCS</b>).")
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, mid, right = st.columns(PLOT_COLS, gap="small")
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(track_plot(df, include_actual=False),
570
- use_container_width=False, config=PLOT_CFG)
 
 
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