UCS2014 commited on
Commit
5cca3cc
·
verified ·
1 Parent(s): 22c14c0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -72
app.py CHANGED
@@ -29,8 +29,6 @@ COLORS = {
29
  # Page / Theme
30
  # =========================
31
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
32
-
33
- # Hide Streamlit default header/footer and tighten layout
34
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
35
  st.markdown(
36
  """
@@ -53,11 +51,10 @@ st.markdown(
53
  # =========================
54
  # Helpers
55
  # =========================
56
-
57
- # Prefer real dialog (Streamlit ≥1.31). Fallback to expander if older.
58
  try:
59
  dialog = st.dialog
60
  except AttributeError:
 
61
  def dialog(title):
62
  def deco(fn):
63
  def wrapper(*args, **kwargs):
@@ -102,31 +99,23 @@ def find_sheet(book, names):
102
  return None
103
 
104
  def cross_plot(actual, pred, title, size=(3.9, 3.9)):
105
- """Compact, square cross-plot with a 1:1 reference line."""
106
  fig, ax = plt.subplots(figsize=size, dpi=100)
107
  ax.scatter(actual, pred, s=14, alpha=0.85, color=COLORS["pred"])
108
  lo = float(np.nanmin([actual.min(), pred.min()]))
109
  hi = float(np.nanmax([actual.max(), pred.max()]))
110
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
111
- ax.plot([lo - pad, hi + pad], [lo - pad, hi + pad],
112
- '--', lw=1.2, color=COLORS["ref"])
113
- ax.set_xlim(lo - pad, hi + pad)
114
- ax.set_ylim(lo - pad, hi + pad)
115
- ax.set_aspect('equal', 'box') # perfect 1:1
116
  ax.set_xlabel("Actual UCS"); ax.set_ylabel("Predicted UCS"); ax.set_title(title)
117
  ax.grid(True, ls=":", alpha=0.4)
118
  return fig
119
 
120
  def depth_or_index_track(df, title=None, include_actual=True):
121
- """
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:
@@ -140,7 +129,6 @@ def depth_or_index_track(df, title=None, include_actual=True):
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")
@@ -182,9 +170,7 @@ def inline_logo(path="logo.png") -> str:
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:
@@ -192,14 +178,13 @@ def make_index_tracks(df: pd.DataFrame, cols: list[str]):
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)
@@ -212,47 +197,37 @@ def stats_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
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_dev(book: dict[str, pd.DataFrame], feature_cols: list[str]):
220
  if not book:
221
- st.info("No data loaded yet.")
222
- return
223
- # Auto detect Train/Test-like sheets
224
  sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
225
  sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
226
- tabs = []
227
- data = []
228
  if sh_train: tabs.append("Train"); data.append(book[sh_train])
229
  if sh_test: tabs.append("Test"); data.append(book[sh_test])
230
- if not tabs: # nothing matched, just show the first sheet
231
  first_name = list(book.keys())[0]
232
  tabs = [first_name]; data = [book[first_name]]
233
-
234
  st.write("Use the tabs to switch between Train/Test views (if available).")
235
  t_objs = st.tabs(tabs)
236
  for t, df in zip(t_objs, data):
237
  with t:
238
- tab_tracks, tab_summary = st.tabs(["Tracks", "Summary"])
239
- with tab_tracks:
240
- st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
241
- with tab_summary:
242
- st.dataframe(stats_table(df, feature_cols), use_container_width=True)
243
 
244
  @dialog("Preview data")
245
  def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
246
  if not book:
247
- st.info("No data loaded yet.")
248
- return
249
  vname = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
250
  df = book[vname]
251
- tab_tracks, tab_summary = st.tabs(["Tracks", "Summary"])
252
- with tab_tracks:
253
- st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
254
- with tab_summary:
255
- st.dataframe(stats_table(df, feature_cols), use_container_width=True)
256
 
257
  # =========================
258
  # Model presence (local or optional download)
@@ -308,11 +283,14 @@ if "app_step" not in st.session_state: st.session_state.app_step = "intro"
308
  if "results" not in st.session_state: st.session_state.results = {}
309
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
310
  if "dev_ready" not in st.session_state: st.session_state.dev_ready = False
 
 
 
311
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
312
  st.session_state.dev_ready = True
313
 
314
  # =========================
315
- # Hero header (logo + title) — website link moved to footer
316
  # =========================
317
  st.markdown(
318
  f"""
@@ -345,9 +323,10 @@ if st.session_state.app_step == "intro":
345
  )
346
  st.subheader("How It Works")
347
  st.markdown(
348
- "1. **Upload your development data (Excel)** and click **Run Model** to compute metrics and review plots. \n"
349
- "2. Click **Proceed to Prediction** to upload a new dataset for validation and view results. \n"
350
- "3. Export results to Excel at any time."
 
351
  )
352
  if st.button("Start Showcase", type="primary", key="start_showcase"):
353
  st.session_state.app_step = "dev"; st.rerun()
@@ -357,27 +336,32 @@ if st.session_state.app_step == "intro":
357
  # =========================
358
  if st.session_state.app_step == "dev":
359
  st.sidebar.header("Model Development Data")
360
- dev_label = "Upload Data (Excel)"
361
- if st.session_state.get("dev_file_name"):
362
- dev_label = "Replace data (Excel)"
363
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
 
 
364
  if train_test_file is not None:
365
  st.session_state.dev_file_name = train_test_file.name
366
- # small status line in sidebar
367
  _book_tmp = read_book(train_test_file)
368
  if _book_tmp:
369
  first_df = next(iter(_book_tmp.values()))
370
  st.sidebar.caption(f"**Data loaded:** {train_test_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
 
 
 
 
371
 
372
  # Preview button (modal)
373
- preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(train_test_file is None))
374
- if preview_btn and train_test_file is not None:
375
  _book = read_book(train_test_file)
 
376
  preview_modal_dev(_book, FEATURES)
377
 
378
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
379
 
380
- # Proceed button BELOW run, always visible; enables after first successful run
381
  st.sidebar.button(
382
  "Proceed to Prediction ▶",
383
  use_container_width=True,
@@ -385,9 +369,16 @@ if st.session_state.app_step == "dev":
385
  on_click=(lambda: st.session_state.update(app_step="predict")) if st.session_state.dev_ready else None,
386
  )
387
 
388
- # Header + helper sentence under the header
389
  st.subheader("Model Development")
390
- st.write("**Upload your data to build a case, run the model, and review the development performance.**")
 
 
 
 
 
 
 
391
 
392
  if run_btn and train_test_file is not None:
393
  with st.status("Processing…", expanded=False) as status:
@@ -427,8 +418,9 @@ if st.session_state.app_step == "dev":
427
 
428
  st.session_state.dev_ready = True # enable Proceed button immediately
429
  status.update(label="Done ✓", state="complete"); toast("Model run complete 🚀")
430
- st.rerun() # refresh to enable the sidebar button without a second click
431
 
 
432
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
433
  tab1, tab2 = st.tabs(["Training", "Testing"])
434
  if "Train" in st.session_state.results:
@@ -436,12 +428,11 @@ if st.session_state.app_step == "dev":
436
  df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
437
  c1,c2,c3 = st.columns(3)
438
  c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
439
- left, right = st.columns([0.9, 0.55]) # slim track column
440
  with left:
441
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
442
  with right:
443
  st.pyplot(depth_or_index_track(df, title=None, include_actual=True), use_container_width=True)
444
-
445
  if "Test" in st.session_state.results:
446
  with tab2:
447
  df = st.session_state.results["Test"]; m = st.session_state.results["metrics_test"]
@@ -475,9 +466,7 @@ if st.session_state.app_step == "dev":
475
  # =========================
476
  if st.session_state.app_step == "predict":
477
  st.sidebar.header("Prediction (Validation)")
478
- val_label = "Upload Validation Excel"
479
- if st.session_state.get("val_file_name"):
480
- val_label = "Replace data (Excel)"
481
  validation_file = st.sidebar.file_uploader(val_label, type=["xlsx","xls"], key="val_upload")
482
  if validation_file is not None:
483
  st.session_state.val_file_name = validation_file.name
@@ -486,7 +475,6 @@ if st.session_state.app_step == "predict":
486
  first_df = next(iter(_book_tmp.values()))
487
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
488
 
489
- # Preview button for validation data
490
  preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
491
  if preview_val_btn and validation_file is not None:
492
  _book = read_book(validation_file)
@@ -547,17 +535,13 @@ if st.session_state.app_step == "predict":
547
  c1.metric("points", f"{sv['n_points']}"); c2.metric("Pred min", f"{sv['pred_min']:.2f}")
548
  c3.metric("Pred max", f"{sv['pred_max']:.2f}"); c4.metric("OOR %", f"{sv['oor_pct']:.1f}%")
549
 
550
- left, right = st.columns([0.9, 0.55]) # slim log-look track
551
  with left:
552
  if TARGET in st.session_state.results["Validate"].columns:
553
- st.pyplot(
554
- cross_plot(
555
- st.session_state.results["Validate"][TARGET],
556
- st.session_state.results["Validate"]["UCS_Pred"],
557
- "Validation: Actual vs Predicted"
558
- ),
559
- use_container_width=True
560
- )
561
  else:
562
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
563
  with right:
 
29
  # Page / Theme
30
  # =========================
31
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
 
 
32
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
33
  st.markdown(
34
  """
 
51
  # =========================
52
  # Helpers
53
  # =========================
 
 
54
  try:
55
  dialog = st.dialog
56
  except AttributeError:
57
+ # Back-compat shim if st.dialog isn't available
58
  def dialog(title):
59
  def deco(fn):
60
  def wrapper(*args, **kwargs):
 
99
  return None
100
 
101
  def cross_plot(actual, pred, title, size=(3.9, 3.9)):
 
102
  fig, ax = plt.subplots(figsize=size, dpi=100)
103
  ax.scatter(actual, pred, s=14, alpha=0.85, color=COLORS["pred"])
104
  lo = float(np.nanmin([actual.min(), pred.min()]))
105
  hi = float(np.nanmax([actual.max(), pred.max()]))
106
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
107
+ ax.plot([lo - pad, hi + pad], [lo - pad, hi + pad], '--', lw=1.2, color=COLORS["ref"])
108
+ ax.set_xlim(lo - pad, hi + pad); ax.set_ylim(lo - pad, hi + pad)
109
+ ax.set_aspect('equal', 'box')
 
 
110
  ax.set_xlabel("Actual UCS"); ax.set_ylabel("Predicted UCS"); ax.set_title(title)
111
  ax.grid(True, ls=":", alpha=0.4)
112
  return fig
113
 
114
  def depth_or_index_track(df, title=None, include_actual=True):
 
 
 
 
115
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
116
  fig_w = 3.1
117
  fig_h = 7.6 if depth_col is not None else 7.2
118
  fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=100)
 
119
  if depth_col is not None:
120
  ax.plot(df["UCS_Pred"], df[depth_col], '-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
121
  if include_actual and TARGET in df.columns:
 
129
  ax.plot(df[TARGET], idx, ':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
130
  ax.set_ylabel("Point Index"); ax.set_xlabel("UCS")
131
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
 
132
  ax.grid(True, linestyle=":", alpha=0.4)
133
  if title: ax.set_title(title, pad=8)
134
  ax.legend(loc="best")
 
170
  return ""
171
 
172
  # ---------- Preview modal helpers ----------
 
173
  def make_index_tracks(df: pd.DataFrame, cols: list[str]):
 
174
  cols = [c for c in cols if c in df.columns]
175
  n = len(cols)
176
  if n == 0:
 
178
  ax.text(0.5, 0.5, "No selected columns in sheet", ha="center", va="center")
179
  ax.axis("off")
180
  return fig
181
+ width_per = 2.2
182
  fig_h = 7.0
183
  fig, axes = plt.subplots(1, n, figsize=(width_per * n, fig_h), sharey=True, dpi=100)
184
+ if n == 1: axes = [axes]
 
185
  idx = np.arange(1, len(df) + 1)
186
  for ax, col in zip(axes, cols):
187
+ ax.plot(df[col], idx, '-', lw=1.4, color="#333")
188
  ax.set_xlabel(col)
189
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
190
  ax.grid(True, linestyle=":", alpha=0.3)
 
197
  return pd.DataFrame({"Feature": [], "Min": [], "Max": [], "Mean": [], "Std": []})
198
  out = df[cols].agg(['min', 'max', 'mean', 'std']).T
199
  out = out.rename(columns={"min": "Min", "max": "Max", "mean": "Mean", "std": "Std"})
200
+ return out.reset_index().rename(columns={"index": "Feature"})
 
201
 
202
  @dialog("Preview data")
203
  def preview_modal_dev(book: dict[str, pd.DataFrame], feature_cols: list[str]):
204
  if not book:
205
+ st.info("No data loaded yet."); return
 
 
206
  sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
207
  sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
208
+ tabs, data = [], []
 
209
  if sh_train: tabs.append("Train"); data.append(book[sh_train])
210
  if sh_test: tabs.append("Test"); data.append(book[sh_test])
211
+ if not tabs:
212
  first_name = list(book.keys())[0]
213
  tabs = [first_name]; data = [book[first_name]]
 
214
  st.write("Use the tabs to switch between Train/Test views (if available).")
215
  t_objs = st.tabs(tabs)
216
  for t, df in zip(t_objs, data):
217
  with t:
218
+ t1, t2 = st.tabs(["Tracks", "Summary"])
219
+ with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
220
+ with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
 
 
221
 
222
  @dialog("Preview data")
223
  def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
224
  if not book:
225
+ st.info("No data loaded yet."); return
 
226
  vname = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
227
  df = book[vname]
228
+ t1, t2 = st.tabs(["Tracks", "Summary"])
229
+ with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
230
+ with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
 
 
231
 
232
  # =========================
233
  # Model presence (local or optional download)
 
283
  if "results" not in st.session_state: st.session_state.results = {}
284
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
285
  if "dev_ready" not in st.session_state: st.session_state.dev_ready = False
286
+ if "dev_file_loaded" not in st.session_state: st.session_state.dev_file_loaded = False
287
+ if "dev_previewed" not in st.session_state: st.session_state.dev_previewed = False
288
+
289
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
290
  st.session_state.dev_ready = True
291
 
292
  # =========================
293
+ # Hero header (logo + title)
294
  # =========================
295
  st.markdown(
296
  f"""
 
323
  )
324
  st.subheader("How It Works")
325
  st.markdown(
326
+ "1. **Upload your data to build the case and preview the performance of our model.** \n"
327
+ "2. Click **Run Model** to compute metrics and plots. \n"
328
+ "3. Click **Proceed to Prediction** to validate on a new dataset. \n"
329
+ "4. Export results to Excel at any time."
330
  )
331
  if st.button("Start Showcase", type="primary", key="start_showcase"):
332
  st.session_state.app_step = "dev"; st.rerun()
 
336
  # =========================
337
  if st.session_state.app_step == "dev":
338
  st.sidebar.header("Model Development Data")
339
+
340
+ dev_label = "Upload Data (Excel)" if not st.session_state.get("dev_file_name") else "Replace data (Excel)"
 
341
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
342
+
343
+ # If a file is uploaded, parse once and set flags
344
  if train_test_file is not None:
345
  st.session_state.dev_file_name = train_test_file.name
 
346
  _book_tmp = read_book(train_test_file)
347
  if _book_tmp:
348
  first_df = next(iter(_book_tmp.values()))
349
  st.sidebar.caption(f"**Data loaded:** {train_test_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
350
+ st.session_state.dev_file_loaded = True
351
+ # Reset preview + readiness on new upload
352
+ st.session_state.dev_previewed = False
353
+ st.session_state.dev_ready = False
354
 
355
  # Preview button (modal)
356
+ preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded)
357
+ if preview_btn and st.session_state.dev_file_loaded and train_test_file is not None:
358
  _book = read_book(train_test_file)
359
+ st.session_state.dev_previewed = True # mark previewed
360
  preview_modal_dev(_book, FEATURES)
361
 
362
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
363
 
364
+ # Proceed button BELOW run, visible always; enables after first successful run
365
  st.sidebar.button(
366
  "Proceed to Prediction ▶",
367
  use_container_width=True,
 
369
  on_click=(lambda: st.session_state.update(app_step="predict")) if st.session_state.dev_ready else None,
370
  )
371
 
372
+ # ----- Title & helper (ALWAYS visible; state-aware) -----
373
  st.subheader("Model Development")
374
+ if st.session_state.dev_ready:
375
+ st.success("Case has been built and results are displayed below.")
376
+ elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
377
+ st.info("Previewed ✓ — now click **Run Model** to build the case.")
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_btn and train_test_file is not None:
384
  with st.status("Processing…", expanded=False) as status:
 
418
 
419
  st.session_state.dev_ready = True # enable Proceed button immediately
420
  status.update(label="Done ✓", state="complete"); toast("Model run complete 🚀")
421
+ st.rerun() # refresh UI to reflect new helper & enabled button
422
 
423
+ # Results
424
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
425
  tab1, tab2 = st.tabs(["Training", "Testing"])
426
  if "Train" in st.session_state.results:
 
428
  df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
429
  c1,c2,c3 = st.columns(3)
430
  c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
431
+ left, right = st.columns([0.9, 0.55])
432
  with left:
433
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
434
  with right:
435
  st.pyplot(depth_or_index_track(df, title=None, include_actual=True), use_container_width=True)
 
436
  if "Test" in st.session_state.results:
437
  with tab2:
438
  df = st.session_state.results["Test"]; m = st.session_state.results["metrics_test"]
 
466
  # =========================
467
  if st.session_state.app_step == "predict":
468
  st.sidebar.header("Prediction (Validation)")
469
+ val_label = "Upload Validation Excel" if not st.session_state.get("val_file_name") else "Replace data (Excel)"
 
 
470
  validation_file = st.sidebar.file_uploader(val_label, type=["xlsx","xls"], key="val_upload")
471
  if validation_file is not None:
472
  st.session_state.val_file_name = validation_file.name
 
475
  first_df = next(iter(_book_tmp.values()))
476
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
477
 
 
478
  preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
479
  if preview_val_btn and validation_file is not None:
480
  _book = read_book(validation_file)
 
535
  c1.metric("points", f"{sv['n_points']}"); c2.metric("Pred min", f"{sv['pred_min']:.2f}")
536
  c3.metric("Pred max", f"{sv['pred_max']:.2f}"); c4.metric("OOR %", f"{sv['oor_pct']:.1f}%")
537
 
538
+ left, right = st.columns([0.9, 0.55])
539
  with left:
540
  if TARGET in st.session_state.results["Validate"].columns:
541
+ st.pyplot(cross_plot(st.session_state.results["Validate"][TARGET],
542
+ st.session_state.results["Validate"]["UCS_Pred"],
543
+ "Validation: Actual vs Predicted"),
544
+ use_container_width=True)
 
 
 
 
545
  else:
546
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
547
  with right: