UCS2014 commited on
Commit
000b7bd
·
verified ·
1 Parent(s): 53297b4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -145
app.py CHANGED
@@ -4,14 +4,17 @@ import streamlit as st
4
  import pandas as pd
5
  import numpy as np
6
  import joblib
 
 
7
  import matplotlib
8
  matplotlib.use("Agg")
9
  import matplotlib.pyplot as plt
10
- from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
11
 
12
- # NEW: Plotly for interactive charts (keeps styling the same)
13
  import plotly.graph_objects as go
14
 
 
 
15
  # =========================
16
  # Defaults
17
  # =========================
@@ -95,46 +98,9 @@ def find_sheet(book, names):
95
  if nm.lower() in low2orig: return low2orig[nm.lower()]
96
  return None
97
 
98
- # ---------- ORIGINAL Matplotlib plotters (kept for reference) ----------
99
- def cross_plot(actual, pred, title, size=(3.9, 3.9)):
100
- fig, ax = plt.subplots(figsize=size, dpi=100)
101
- ax.scatter(actual, pred, s=14, alpha=0.85, color=COLORS["pred"])
102
- lo = float(np.nanmin([actual.min(), pred.min()]))
103
- hi = float(np.nanmax([actual.max(), pred.max()]))
104
- pad = 0.03 * (hi - lo if hi > lo else 1.0)
105
- ax.plot([lo - pad, hi + pad], [lo - pad, hi + pad], '--', lw=1.2, color=COLORS["ref"])
106
- ax.set_xlim(lo - pad, hi + pad); ax.set_ylim(lo - pad, hi + pad)
107
- ax.set_aspect('equal', 'box')
108
- ax.set_xlabel("Actual UCS"); ax.set_ylabel("Predicted UCS"); ax.set_title(title)
109
- ax.grid(True, ls=":", alpha=0.4)
110
- return fig
111
-
112
- def depth_or_index_track(df, title=None, include_actual=True):
113
- depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
114
- fig_w = 3.1
115
- fig_h = 7.6 if depth_col is not None else 7.2
116
- fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=100)
117
- if depth_col is not None:
118
- ax.plot(df["UCS_Pred"], df[depth_col], '-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
119
- if include_actual and TARGET in df.columns:
120
- ax.plot(df[TARGET], df[depth_col], ':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
121
- ax.set_ylabel(depth_col); ax.set_xlabel("UCS")
122
- ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
123
- else:
124
- idx = np.arange(1, len(df) + 1)
125
- ax.plot(df["UCS_Pred"], idx, '-', lw=1.8, color=COLORS["pred"], label="UCS_Pred")
126
- if include_actual and TARGET in df.columns:
127
- ax.plot(df[TARGET], idx, ':', lw=2.0, color=COLORS["actual"], alpha=0.95, label="UCS (actual)")
128
- ax.set_ylabel("Point Index"); ax.set_xlabel("UCS")
129
- ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
130
- ax.grid(True, linestyle=":", alpha=0.4)
131
- if title: ax.set_title(title, pad=8)
132
- ax.legend(loc="best")
133
- return fig
134
-
135
- # ---------- NEW: Plotly equivalents (interactive, same specs) ----------
136
- def cross_plot_interactive(actual, pred, title, size=(3.9, 3.9)):
137
- """Interactive cross-plot with the same look: blue points, dashed 1:1, equal axes, grid, title."""
138
  a = pd.Series(actual).astype(float)
139
  p = pd.Series(pred).astype(float)
140
  lo = float(np.nanmin([a.min(), p.min()]))
@@ -144,96 +110,80 @@ def cross_plot_interactive(actual, pred, title, size=(3.9, 3.9)):
144
 
145
  fig = go.Figure()
146
 
147
- # points
148
  fig.add_trace(go.Scatter(
149
- x=a, y=p,
150
- mode="markers",
151
  marker=dict(size=6, color=COLORS["pred"]),
152
  hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
153
- name="Points",
154
- showlegend=False
155
  ))
156
 
157
- # 1:1
158
  fig.add_trace(go.Scatter(
159
- x=[x0, x1], y=[x0, x1],
160
- mode="lines",
161
  line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
162
- hoverinfo="skip",
163
- name="1:1",
164
- showlegend=False
165
  ))
166
 
167
  fig.update_layout(
168
- title=title,
169
- paper_bgcolor="#ffffff",
170
- plot_bgcolor="#ffffff",
171
- margin=dict(l=50, r=10, t=36, b=36),
172
- hovermode="closest",
173
- font=dict(size=13)
174
  )
175
  fig.update_xaxes(
176
  title_text="<b>Actual UCS</b>",
177
- range=[x0, x1],
178
- ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
179
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
180
- automargin=True
181
  )
182
  fig.update_yaxes(
183
  title_text="<b>Predicted UCS</b>",
184
- range=[x0, x1],
185
- ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
186
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
187
- scaleanchor="x", scaleratio=1,
188
  automargin=True
189
  )
190
-
191
- # match your size ~ inches * 100 dpi
192
- w = int(size[0] * 100)
193
- h = int(size[1] * 100)
194
  fig.update_layout(width=w, height=h)
195
  return fig
196
 
197
  def depth_or_index_track_interactive(df, title=None, include_actual=True):
198
- """Interactive narrow/tall UCS track: blue solid pred, yellow dotted actual, top x-axis, inverted y."""
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 = df[depth_col]
202
- y_label = depth_col
203
  else:
204
- y = np.arange(1, len(df) + 1)
205
- y_label = "Point Index"
206
 
207
  fig = go.Figure()
208
 
209
  # Predicted (solid blue)
210
  fig.add_trace(go.Scatter(
211
- x=df["UCS_Pred"], y=y,
212
- mode="lines",
213
  line=dict(color=COLORS["pred"], width=1.8),
214
  name="UCS_Pred",
215
  hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
216
  ))
217
-
218
  # Actual (dotted yellow)
219
  if include_actual and TARGET in df.columns:
220
  fig.add_trace(go.Scatter(
221
- x=df[TARGET], y=y,
222
- mode="lines",
223
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
224
  name="UCS (actual)",
225
  hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
226
  ))
227
 
228
  fig.update_layout(
229
- title=title if title else None,
230
- paper_bgcolor="#ffffff",
231
- plot_bgcolor="#ffffff",
232
- margin=dict(l=60, r=10, t=36, b=36),
233
- hovermode="closest",
234
- legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
235
- font=dict(size=13),
236
- # keep it tall & narrow like your Matplotlib version (~3.1in x 7.6in @100dpi)
237
  width=int(3.1 * 100),
238
  height=int((7.6 if depth_col is not None else 7.2) * 100),
239
  )
@@ -241,53 +191,17 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
241
  title_text="<b>UCS</b>", side="top",
242
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
243
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
244
- automargin=True
245
  )
246
  fig.update_yaxes(
247
- title_text=f"<b>{y_label}</b>",
248
- autorange="reversed",
249
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
250
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
251
  automargin=True
252
  )
253
  return fig
254
 
255
- def export_workbook(sheets_dict, summary_df=None):
256
- try: import openpyxl # noqa
257
- except Exception: raise RuntimeError("Export requires openpyxl. Please add it to requirements or install it.")
258
- buf = io.BytesIO()
259
- with pd.ExcelWriter(buf, engine="openpyxl") as xw:
260
- for name, frame in sheets_dict.items():
261
- frame.to_excel(xw, sheet_name=name[:31], index=False)
262
- if summary_df is not None: summary_df.to_excel(xw, sheet_name="Summary", index=False)
263
- return buf.getvalue()
264
-
265
- def toast(msg):
266
- try: st.toast(msg)
267
- except Exception: st.info(msg)
268
-
269
- def infer_features_from_model(m):
270
- try:
271
- if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
272
- return [str(x) for x in m.feature_names_in_]
273
- except Exception: pass
274
- try:
275
- if hasattr(m, "steps") and len(m.steps):
276
- last = m.steps[-1][1]
277
- if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
278
- return [str(x) for x in last.feature_names_in_]
279
- except Exception: pass
280
- return None
281
-
282
- def inline_logo(path="logo.png") -> str:
283
- try:
284
- p = Path(path)
285
- if not p.exists(): return ""
286
- return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
287
- except Exception:
288
- return ""
289
-
290
- # ---------- Preview modal helpers (unchanged; still Matplotlib) ----------
291
  def make_index_tracks(df: pd.DataFrame, cols: list[str]):
292
  cols = [c for c in cols if c in df.columns]
293
  n = len(cols)
@@ -390,7 +304,12 @@ if meta_path.exists():
390
  FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
391
  except Exception: pass
392
  else:
393
- infer = infer_features_from_model(model)
 
 
 
 
 
394
  if infer: FEATURES = infer
395
 
396
  # =========================
@@ -485,7 +404,6 @@ if st.session_state.app_step == "dev":
485
  st.session_state.dev_file_signature = sig
486
  st.session_state.dev_file_name = train_test_file.name
487
  st.session_state.dev_file_bytes = file_bytes
488
- # Inspect first sheet for rows/cols
489
  _book_tmp = read_book_bytes(file_bytes)
490
  if _book_tmp:
491
  first_df = next(iter(_book_tmp.values()))
@@ -530,7 +448,7 @@ if st.session_state.app_step == "dev":
530
  else:
531
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
532
 
533
- # If user clicked preview, open modal *after* helper so helper stays on top
534
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
535
  _book = read_book_bytes(st.session_state.dev_file_bytes)
536
  st.session_state.dev_previewed = True
@@ -571,10 +489,9 @@ if st.session_state.app_step == "dev":
571
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
572
 
573
  st.session_state.dev_ready = True
574
- status.update(label="Done ✓", state="complete"); toast("Model run complete 🚀")
575
- st.rerun()
576
 
577
- # Results (NOW USING INTERACTIVE PLOTS)
578
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
579
  tab1, tab2 = st.tabs(["Training", "Testing"])
580
  if "Train" in st.session_state.results:
@@ -585,7 +502,7 @@ if st.session_state.app_step == "dev":
585
  left, right = st.columns([0.9, 0.55])
586
  with left:
587
  st.plotly_chart(
588
- cross_plot_interactive(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted", size=(3.9,3.9)),
589
  use_container_width=True, config={"displayModeBar": False}
590
  )
591
  with right:
@@ -601,7 +518,7 @@ if st.session_state.app_step == "dev":
601
  left, right = st.columns([0.9, 0.55])
602
  with left:
603
  st.plotly_chart(
604
- cross_plot_interactive(df[TARGET], df["UCS_Pred"], "Testing: Actual vs Predicted", size=(3.9,3.9)),
605
  use_container_width=True, config={"displayModeBar": False}
606
  )
607
  with right:
@@ -620,11 +537,17 @@ if st.session_state.app_step == "dev":
620
  rows.append({"Split":"Test", **{k:round(v,6) for k,v in st.session_state.results["metrics_test"].items()}})
621
  summary_df = pd.DataFrame(rows) if rows else None
622
  try:
623
- data_bytes = export_workbook(sheets, summary_df)
 
 
 
 
 
 
624
  st.download_button("Export Development Results to Excel",
625
- data=data_bytes, file_name="UCS_Dev_Results.xlsx",
626
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
627
- except RuntimeError as e:
628
  st.warning(str(e))
629
 
630
  # =========================
@@ -703,10 +626,11 @@ if st.session_state.app_step == "predict":
703
  with left:
704
  if TARGET in st.session_state.results["Validate"].columns:
705
  st.plotly_chart(
706
- cross_plot_interactive(st.session_state.results["Validate"][TARGET],
707
- st.session_state.results["Validate"]["UCS_Pred"],
708
- "Validation: Actual vs Predicted",
709
- size=(3.9,3.9)),
 
710
  use_container_width=True, config={"displayModeBar": False}
711
  )
712
  else:
@@ -714,7 +638,8 @@ if st.session_state.app_step == "predict":
714
  with right:
715
  st.plotly_chart(
716
  depth_or_index_track_interactive(
717
- st.session_state.results["Validate"], title=None,
 
718
  include_actual=(TARGET in st.session_state.results["Validate"].columns)
719
  ),
720
  use_container_width=True, config={"displayModeBar": False}
@@ -732,11 +657,17 @@ if st.session_state.app_step == "predict":
732
  if m: rows.append({"Split": name, **{k: round(v,6) for k,v in m.items()}})
733
  summary_df = pd.DataFrame(rows) if rows else None
734
  try:
735
- data_bytes = export_workbook(sheets, summary_df)
 
 
 
 
 
 
736
  st.download_button("Export Validation Results to Excel",
737
- data=data_bytes, file_name="UCS_Validation_Results.xlsx",
738
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
739
- except RuntimeError as e:
740
  st.warning(str(e))
741
 
742
  # =========================
 
4
  import pandas as pd
5
  import numpy as np
6
  import joblib
7
+
8
+ # Matplotlib remains for the preview modal (tracks + stats)
9
  import matplotlib
10
  matplotlib.use("Agg")
11
  import matplotlib.pyplot as plt
 
12
 
13
+ # Plotly for the main interactive charts
14
  import plotly.graph_objects as go
15
 
16
+ from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
17
+
18
  # =========================
19
  # Defaults
20
  # =========================
 
98
  if nm.lower() in low2orig: return low2orig[nm.lower()]
99
  return None
100
 
101
+ # ---------- Plotly (interactive) charts ----------
102
+ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
103
+ """Interactive cross-plot: blue points, dashed 1:1, equal axes, no title, numeric ticks."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  a = pd.Series(actual).astype(float)
105
  p = pd.Series(pred).astype(float)
106
  lo = float(np.nanmin([a.min(), p.min()]))
 
110
 
111
  fig = go.Figure()
112
 
113
+ # Points (blue)
114
  fig.add_trace(go.Scatter(
115
+ x=a, y=p, mode="markers",
 
116
  marker=dict(size=6, color=COLORS["pred"]),
117
  hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
118
+ showlegend=False, name="Points"
 
119
  ))
120
 
121
+ # 1:1 line (dashed grey)
122
  fig.add_trace(go.Scatter(
123
+ x=[x0, x1], y=[x0, x1], mode="lines",
 
124
  line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
125
+ hoverinfo="skip", showlegend=False, name="1:1"
 
 
126
  ))
127
 
128
  fig.update_layout(
129
+ paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
130
+ margin=dict(l=50, r=10, t=10, b=36),
131
+ hovermode="closest", font=dict(size=13)
 
 
 
132
  )
133
  fig.update_xaxes(
134
  title_text="<b>Actual UCS</b>",
135
+ range=[x0, x1], ticks="outside",
136
+ showline=True, linewidth=1.2, linecolor="#444",
137
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
138
+ tickformat=",.0f", automargin=True
139
  )
140
  fig.update_yaxes(
141
  title_text="<b>Predicted UCS</b>",
142
+ range=[x0, x1], ticks="outside",
143
+ showline=True, linewidth=1.2, linecolor="#444",
144
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
145
+ tickformat=",.0f", scaleanchor="x", scaleratio=1,
146
  automargin=True
147
  )
148
+ w = int(size[0] * 100); h = int(size[1] * 100)
 
 
 
149
  fig.update_layout(width=w, height=h)
150
  return fig
151
 
152
  def depth_or_index_track_interactive(df, title=None, include_actual=True):
153
+ """Interactive UCS track: blue solid pred, yellow dotted actual, legend inside; x on top; y inverted."""
154
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
155
  if depth_col is not None:
156
+ y = df[depth_col]; y_label = depth_col
 
157
  else:
158
+ y = np.arange(1, len(df) + 1); y_label = "Point Index"
 
159
 
160
  fig = go.Figure()
161
 
162
  # Predicted (solid blue)
163
  fig.add_trace(go.Scatter(
164
+ x=df["UCS_Pred"], y=y, mode="lines",
 
165
  line=dict(color=COLORS["pred"], width=1.8),
166
  name="UCS_Pred",
167
  hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
168
  ))
 
169
  # Actual (dotted yellow)
170
  if include_actual and TARGET in df.columns:
171
  fig.add_trace(go.Scatter(
172
+ x=df[TARGET], y=y, mode="lines",
 
173
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
174
  name="UCS (actual)",
175
  hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
176
  ))
177
 
178
  fig.update_layout(
179
+ paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
180
+ margin=dict(l=60, r=10, t=10, b=36),
181
+ hovermode="closest", font=dict(size=13),
182
+ legend=dict(
183
+ x=0.98, y=0.05, xanchor="right", yanchor="bottom",
184
+ bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1
185
+ ),
186
+ legend_title_text="",
187
  width=int(3.1 * 100),
188
  height=int((7.6 if depth_col is not None else 7.2) * 100),
189
  )
 
191
  title_text="<b>UCS</b>", side="top",
192
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
193
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
194
+ tickformat=",.0f", automargin=True
195
  )
196
  fig.update_yaxes(
197
+ title_text=f"<b>{y_label}</b>", autorange="reversed",
 
198
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444",
199
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
200
  automargin=True
201
  )
202
  return fig
203
 
204
+ # ---------- Preview modal helpers (matplotlib) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  def make_index_tracks(df: pd.DataFrame, cols: list[str]):
206
  cols = [c for c in cols if c in df.columns]
207
  n = len(cols)
 
304
  FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
305
  except Exception: pass
306
  else:
307
+ infer = None
308
+ try:
309
+ if hasattr(model, "feature_names_in_") and len(getattr(model, "feature_names_in_")):
310
+ infer = [str(x) for x in model.feature_names_in_]
311
+ except Exception:
312
+ pass
313
  if infer: FEATURES = infer
314
 
315
  # =========================
 
404
  st.session_state.dev_file_signature = sig
405
  st.session_state.dev_file_name = train_test_file.name
406
  st.session_state.dev_file_bytes = file_bytes
 
407
  _book_tmp = read_book_bytes(file_bytes)
408
  if _book_tmp:
409
  first_df = next(iter(_book_tmp.values()))
 
448
  else:
449
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
450
 
451
+ # If user clicked preview, open modal after helper (so helper stays on top)
452
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
453
  _book = read_book_bytes(st.session_state.dev_file_bytes)
454
  st.session_state.dev_previewed = True
 
489
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
490
 
491
  st.session_state.dev_ready = True
492
+ status.update(label="Done ✓", state="complete"); st.rerun()
 
493
 
494
+ # Results (if available)
495
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
496
  tab1, tab2 = st.tabs(["Training", "Testing"])
497
  if "Train" in st.session_state.results:
 
502
  left, right = st.columns([0.9, 0.55])
503
  with left:
504
  st.plotly_chart(
505
+ cross_plot_interactive(df[TARGET], df["UCS_Pred"], size=(3.9,3.9)),
506
  use_container_width=True, config={"displayModeBar": False}
507
  )
508
  with right:
 
518
  left, right = st.columns([0.9, 0.55])
519
  with left:
520
  st.plotly_chart(
521
+ cross_plot_interactive(df[TARGET], df["UCS_Pred"], size=(3.9,3.9)),
522
  use_container_width=True, config={"displayModeBar": False}
523
  )
524
  with right:
 
537
  rows.append({"Split":"Test", **{k:round(v,6) for k,v in st.session_state.results["metrics_test"].items()}})
538
  summary_df = pd.DataFrame(rows) if rows else None
539
  try:
540
+ buf = io.BytesIO()
541
+ import openpyxl # noqa
542
+ with pd.ExcelWriter(buf, engine="openpyxl") as xw:
543
+ for name, frame in sheets.items():
544
+ frame.to_excel(xw, sheet_name=name[:31], index=False)
545
+ if summary_df is not None:
546
+ summary_df.to_excel(xw, sheet_name="Summary", index=False)
547
  st.download_button("Export Development Results to Excel",
548
+ data=buf.getvalue(), file_name="UCS_Dev_Results.xlsx",
549
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
550
+ except Exception as e:
551
  st.warning(str(e))
552
 
553
  # =========================
 
626
  with left:
627
  if TARGET in st.session_state.results["Validate"].columns:
628
  st.plotly_chart(
629
+ cross_plot_interactive(
630
+ st.session_state.results["Validate"][TARGET],
631
+ st.session_state.results["Validate"]["UCS_Pred"],
632
+ size=(3.9,3.9)
633
+ ),
634
  use_container_width=True, config={"displayModeBar": False}
635
  )
636
  else:
 
638
  with right:
639
  st.plotly_chart(
640
  depth_or_index_track_interactive(
641
+ st.session_state.results["Validate"],
642
+ title=None,
643
  include_actual=(TARGET in st.session_state.results["Validate"].columns)
644
  ),
645
  use_container_width=True, config={"displayModeBar": False}
 
657
  if m: rows.append({"Split": name, **{k: round(v,6) for k,v in m.items()}})
658
  summary_df = pd.DataFrame(rows) if rows else None
659
  try:
660
+ buf = io.BytesIO()
661
+ import openpyxl # noqa
662
+ with pd.ExcelWriter(buf, engine="openpyxl") as xw:
663
+ for nm, fr in sheets.items():
664
+ fr.to_excel(xw, sheet_name=nm[:31], index=False)
665
+ if summary_df is not None:
666
+ summary_df.to_excel(xw, sheet_name="Summary", index=False)
667
  st.download_button("Export Validation Results to Excel",
668
+ data=buf.getvalue(), file_name="UCS_Validation_Results.xlsx",
669
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
670
+ except Exception as e:
671
  st.warning(str(e))
672
 
673
  # =========================