UCS2014 commited on
Commit
36729cc
·
verified ·
1 Parent(s): fc3fc49

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -55
app.py CHANGED
@@ -25,10 +25,11 @@ MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
25
 
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
- # exact pixel sizes (no container stretching)
29
- CROSS_W = 420; CROSS_H = 420 # square cross-plot
30
- TRACK_W = 220; TRACK_H = 700 # match preview strip
31
  FONT_SZ = 13
 
32
 
33
  # =========================
34
  # Page / CSS
@@ -48,6 +49,14 @@ st.markdown(
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,7 +154,14 @@ def parse_excel(data_bytes: bytes):
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:
@@ -175,14 +191,14 @@ def cross_plot(actual, pred):
175
  ))
176
  fig.update_layout(
177
  width=CROSS_W, height=CROSS_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
178
- margin=dict(l=64, r=18, t=10, b=48), hovermode="closest",
179
  font=dict(size=FONT_SZ)
180
  )
181
- fig.update_xaxes(title_text="<b>Actual UCS</b>", range=[x0, x1],
182
  ticks="outside", tickformat=",.0f",
183
  showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
184
  automargin=True)
185
- fig.update_yaxes(title_text="<b>Predicted UCS</b>", range=[x0, x1],
186
  ticks="outside", tickformat=",.0f",
187
  showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
188
  scaleanchor="x", scaleratio=1, automargin=True)
@@ -197,30 +213,30 @@ def track_plot(df, include_actual=True):
197
 
198
  fig = go.Figure()
199
  fig.add_trace(go.Scatter(
200
- x=df["UCS_Pred"], y=y, mode="lines",
201
  line=dict(color=COLORS["pred"], width=1.8),
202
- name="UCS_Pred",
203
- hovertemplate="UCS_Pred: %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
204
  ))
205
  if include_actual and TARGET in df.columns:
206
  fig.add_trace(go.Scatter(
207
  x=df[TARGET], y=y, mode="lines",
208
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
209
- name="UCS (actual)",
210
- hovertemplate="UCS (actual): %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
211
  ))
212
 
213
  fig.update_layout(
214
  width=TRACK_W, height=TRACK_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
215
- margin=dict(l=72, r=18, t=36, b=48), hovermode="closest",
216
  font=dict(size=FONT_SZ),
217
  legend=dict(
218
  x=0.98, y=0.05, xanchor="right", yanchor="bottom",
219
- bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1
220
  ),
221
  legend_title_text=""
222
  )
223
- fig.update_xaxes(title_text="<b>UCS</b>", side="top",
224
  ticks="outside", tickformat=",.0f",
225
  showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
226
  automargin=True)
@@ -315,6 +331,13 @@ for k, v in {
315
  }.items():
316
  st.session_state.setdefault(k, v)
317
 
 
 
 
 
 
 
 
318
  # =========================
319
  # Hero
320
  # =========================
@@ -331,6 +354,9 @@ st.markdown(
331
  unsafe_allow_html=True,
332
  )
333
 
 
 
 
334
  # =========================
335
  # INTRO
336
  # =========================
@@ -368,17 +394,19 @@ if st.session_state.app_step == "dev":
368
  st.session_state.dev_preview = True
369
 
370
  run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
371
- # always available nav
372
  if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
373
  if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
374
 
 
375
  st.subheader("Case Building (Development)")
376
- if st.session_state.dev_file_loaded and st.session_state.dev_preview:
377
- st.info("Previewed now click **Run Model**.")
 
 
378
  elif st.session_state.dev_file_loaded:
379
- st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
380
  else:
381
- st.write("**Upload your data to build a case, then run the model to review development performance.**")
382
 
383
  if run and st.session_state.dev_file_bytes:
384
  book = read_book_bytes(st.session_state.dev_file_bytes)
@@ -387,18 +415,17 @@ if st.session_state.app_step == "dev":
387
  if sh_train is None or sh_test is None:
388
  st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
389
  tr = book[sh_train].copy(); te = book[sh_test].copy()
390
- if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
391
- st.error("Missing required columns."); st.stop()
392
- tr["UCS_Pred"] = model.predict(tr[FEATURES])
393
- te["UCS_Pred"] = model.predict(te[FEATURES])
394
 
395
  st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
396
- st.session_state.results["m_train"]={"R2":r2_score(tr[TARGET],tr["UCS_Pred"]), "RMSE":rmse(tr[TARGET],tr["UCS_Pred"]), "MAE":mean_absolute_error(tr[TARGET],tr["UCS_Pred"])}
397
- st.session_state.results["m_test"] ={"R2":r2_score(te[TARGET],te["UCS_Pred"]), "RMSE":rmse(te[TARGET],te["UCS_Pred"]), "MAE":mean_absolute_error(te[TARGET],te["UCS_Pred"])}
398
 
399
  tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
400
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
401
- st.success("Case has been built and results are displayed below.")
402
 
403
  if "Train" in st.session_state.results or "Test" in st.session_state.results:
404
  tab1, tab2 = st.tabs(["Training", "Testing"])
@@ -406,13 +433,15 @@ if st.session_state.app_step == "dev":
406
  def dev_block(df, m):
407
  c1,c2,c3 = st.columns(3)
408
  c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
409
- col_left, col_right = st.columns(2)
410
- with col_left:
411
- st.plotly_chart(cross_plot(df[TARGET], df["UCS_Pred"]),
412
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
413
- with col_right:
 
 
414
  st.plotly_chart(track_plot(df, include_actual=True),
415
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
416
 
417
  if "Train" in st.session_state.results:
418
  with tab1: dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
@@ -436,15 +465,16 @@ if st.session_state.app_step == "validate":
436
  if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
437
  if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
438
 
 
439
  st.subheader("Validate the Model")
440
- st.write("Upload a dataset with the same **features** and **UCS** to evaluate performance.")
441
 
442
  if go_btn and up is not None:
443
  book = read_book_bytes(up.getvalue())
444
  name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
445
  df = book[name].copy()
446
- if not ensure_cols(df, FEATURES+[TARGET]): st.error("Missing required columns."); st.stop()
447
- df["UCS_Pred"] = model.predict(df[FEATURES])
448
  st.session_state.results["Validate"]=df
449
 
450
  ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
@@ -454,25 +484,28 @@ if st.session_state.app_step == "validate":
454
  if any_viol.any():
455
  tbl = df.loc[any_viol, FEATURES].copy()
456
  tbl["Violations"] = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).loc[any_viol].apply(lambda r:", ".join([c for c,v in r.items() if v]), axis=1)
457
- 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"])}
458
- 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}
459
  st.session_state.results["oor_tbl"]=tbl
 
460
 
461
  if "Validate" in st.session_state.results:
462
  m = st.session_state.results["m_val"]; sv = st.session_state.results["sv_val"]
463
  c1,c2,c3 = st.columns(3)
464
  c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
465
 
466
- col_left, col_right = st.columns(2)
467
- with col_left:
468
  st.plotly_chart(cross_plot(st.session_state.results["Validate"][TARGET],
469
- st.session_state.results["Validate"]["UCS_Pred"]),
470
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
471
- with col_right:
 
 
472
  st.plotly_chart(track_plot(st.session_state.results["Validate"], include_actual=True),
473
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
474
 
475
- if sv["oor"] > 0: st.warning("Some inputs fall outside **training min–max** ranges.")
476
  if st.session_state.results["oor_tbl"] is not None:
477
  st.write("*Out-of-range rows (vs. Training min–max):*")
478
  st.dataframe(st.session_state.results["oor_tbl"], use_container_width=True)
@@ -493,13 +526,14 @@ if st.session_state.app_step == "predict":
493
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
494
  if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
495
 
 
496
  st.subheader("Prediction")
497
- st.write("Upload a dataset with the feature columns (no **UCS**).")
498
 
499
  if go_btn and up is not None:
500
  book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
501
  df = book[name].copy()
502
- if not ensure_cols(df, FEATURES): st.error("Missing required columns."); st.stop()
503
  df["UCS_Pred"] = model.predict(df[FEATURES])
504
  st.session_state.results["PredictOnly"]=df
505
 
@@ -509,18 +543,19 @@ if st.session_state.app_step == "predict":
509
  oor_pct = float(any_viol.mean()*100.0)
510
  st.session_state.results["sv_pred"]={
511
  "n":len(df),
512
- "pred_min":float(df["UCS_Pred"].min()),
513
- "pred_max":float(df["UCS_Pred"].max()),
514
- "pred_mean":float(df["UCS_Pred"].mean()),
515
- "pred_std":float(df["UCS_Pred"].std(ddof=0)),
516
  "oor":oor_pct
517
  }
 
518
 
519
  if "PredictOnly" in st.session_state.results:
520
  df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
521
 
522
- col_left, col_right = st.columns(2)
523
- with col_left:
524
  table = pd.DataFrame({
525
  "Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
526
  "Value": [sv["n"], sv["pred_min"], sv["pred_max"], sv["pred_mean"], sv["pred_std"], f'{sv["oor"]:.1f}%']
@@ -528,9 +563,11 @@ if st.session_state.app_step == "predict":
528
  st.success("Predictions ready ✓")
529
  st.dataframe(table, use_container_width=True, hide_index=True)
530
  st.caption("**★ OOR** = % of rows whose input features fall outside the training min–max range.")
531
- with col_right:
 
 
532
  st.plotly_chart(track_plot(df, include_actual=False),
533
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
534
 
535
  # =========================
536
  # Footer
 
25
 
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
+ # ---- Plot sizing controls (edit here) ----
29
+ CROSS_W = 520; CROSS_H = 520 # square cross-plot
30
+ TRACK_W = 260; TRACK_H = 950 # log-strip style (tall, slightly wider)
31
  FONT_SZ = 13
32
+ PLOT_COLS = [10, 3, 10] # 3-column band: left • spacer • right
33
 
34
  # =========================
35
  # Page / CSS
 
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
  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:
 
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, psi</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, psi</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)
 
213
 
214
  fig = go.Figure()
215
  fig.add_trace(go.Scatter(
216
+ x=df["UCS_Pred, psi"], y=y, mode="lines",
217
  line=dict(color=COLORS["pred"], width=1.8),
218
+ name="UCS_Pred, psi",
219
+ hovertemplate="UCS_Pred, psi: %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
220
  ))
221
  if include_actual and TARGET in df.columns:
222
  fig.add_trace(go.Scatter(
223
  x=df[TARGET], y=y, mode="lines",
224
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
225
+ name="UCS (actual), psi",
226
+ hovertemplate="UCS (actual), psi: %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
227
  ))
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, psi</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)
 
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
343
  # =========================
 
354
  unsafe_allow_html=True,
355
  )
356
 
357
+ # reuse plot config
358
+ PLOT_CFG = {"displayModeBar": False, "scrollZoom": True}
359
+
360
  # =========================
361
  # INTRO
362
  # =========================
 
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
  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, psi"] = model.predict(tr[FEATURES])
420
+ te["UCS_Pred, psi"] = model.predict(te[FEATURES])
 
421
 
422
  st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
423
+ st.session_state.results["m_train"]={"R2":r2_score(tr[TARGET],tr["UCS_Pred, psi"]), "RMSE":rmse(tr[TARGET],tr["UCS_Pred, psi"]), "MAE":mean_absolute_error(tr[TARGET],tr["UCS_Pred"])}
424
+ st.session_state.results["m_test"] ={"R2":r2_score(te[TARGET],te["UCS_Pred, psi"]), "RMSE":rmse(te[TARGET],te["UCS_Pred, psi"]), "MAE":mean_absolute_error(te[TARGET],te["UCS_Pred"])}
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"])
 
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, psi"]),
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"])
 
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, psi</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, psi"] = model.predict(df[FEATURES])
478
  st.session_state.results["Validate"]=df
479
 
480
  ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
 
484
  if any_viol.any():
485
  tbl = df.loc[any_viol, FEATURES].copy()
486
  tbl["Violations"] = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).loc[any_viol].apply(lambda r:", ".join([c for c,v in r.items() if v]), axis=1)
487
+ st.session_state.results["m_val"]={"R2":r2_score(df[TARGET],df["UCS_Pred, psi"]), "RMSE":rmse(df[TARGET],df["UCS_Pred, psi"]), "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, psi"].min()),"pred_max":float(df["UCS_Pred, psi"].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, psi"]),
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
  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, psi</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
 
 
543
  oor_pct = float(any_viol.mean()*100.0)
544
  st.session_state.results["sv_pred"]={
545
  "n":len(df),
546
+ "pred_min":float(df["UCS_Pred, psi"].min()),
547
+ "pred_max":float(df["UCS_Pred, psi"].max()),
548
+ "pred_mean":float(df["UCS_Pred, psi"].mean()),
549
+ "pred_std":float(df["UCS_Pred, psi"].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 %"],
561
  "Value": [sv["n"], sv["pred_min"], sv["pred_max"], sv["pred_mean"], sv["pred_std"], f'{sv["oor"]:.1f}%']
 
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