UCS2014 commited on
Commit
73b13cf
·
verified ·
1 Parent(s): d99ed4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +96 -70
app.py CHANGED
@@ -13,6 +13,7 @@ import matplotlib.pyplot as plt
13
 
14
  import plotly.graph_objects as go
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
 
16
 
17
  # =========================
18
  # Defaults
@@ -30,7 +31,6 @@ COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
30
  # =========================
31
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
32
 
33
- # ---------- inline logo (used by password gate + header) ----------
34
  def inline_logo(path="logo.png") -> str:
35
  try:
36
  p = Path(path)
@@ -43,18 +43,11 @@ def inline_logo(path="logo.png") -> str:
43
  # Password (brand-gated)
44
  # =========================
45
  def add_password_gate() -> bool:
46
- """
47
- Shows a branded access screen until the correct password is entered.
48
- Requires APP_PASSWORD in Secrets (or environment).
49
- """
50
- # 1) Read password
51
- required = ""
52
  try:
53
  required = st.secrets.get("APP_PASSWORD", "")
54
  except Exception:
55
  required = os.environ.get("APP_PASSWORD", "")
56
 
57
- # 2) If not configured, BLOCK (admin instruction)
58
  if not required:
59
  st.markdown(
60
  f"""
@@ -75,11 +68,9 @@ def add_password_gate() -> bool:
75
  )
76
  st.stop()
77
 
78
- # 3) Already authenticated?
79
  if st.session_state.get("auth_ok", False):
80
  return True
81
 
82
- # 4) Branded prompt
83
  st.markdown(
84
  f"""
85
  <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
@@ -107,10 +98,9 @@ def add_password_gate() -> bool:
107
  st.error("Incorrect key. Please try again.")
108
  st.stop()
109
 
110
- # 🔒 Gate the app
111
  add_password_gate()
112
 
113
- # CSS
114
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
115
  st.markdown(
116
  """
@@ -136,7 +126,6 @@ st.markdown(
136
  try:
137
  dialog = st.dialog
138
  except AttributeError:
139
- # Fallback (expander) if st.dialog is unavailable
140
  def dialog(title):
141
  def deco(fn):
142
  def wrapper(*args, **kwargs):
@@ -179,7 +168,24 @@ def find_sheet(book, names):
179
  if nm.lower() in low2orig: return low2orig[nm.lower()]
180
  return None
181
 
182
- # ---------- Interactive plotting (full outline, bold axis titles) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
184
  a = pd.Series(actual).astype(float)
185
  p = pd.Series(pred).astype(float)
@@ -187,12 +193,13 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
187
  hi = float(np.nanmax([a.max(), p.max()]))
188
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
189
  x0, x1 = lo - pad, hi + pad
 
190
 
191
  fig = go.Figure()
192
  fig.add_trace(go.Scatter(
193
  x=a, y=p, mode="markers",
194
  marker=dict(size=6, color=COLORS["pred"]),
195
- hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
196
  showlegend=False
197
  ))
198
  fig.add_trace(go.Scatter(
@@ -203,18 +210,18 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
203
  fig.update_layout(
204
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
205
  margin=dict(l=50, r=10, t=10, b=36),
206
- hovermode="closest", font=dict(size=13)
207
  )
208
  fig.update_xaxes(
209
  title_text="<b>Actual UCS</b>",
210
- range=[x0, x1], ticks="outside",
211
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
212
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
213
  tickformat=",.0f", automargin=True
214
  )
215
  fig.update_yaxes(
216
  title_text="<b>Predicted UCS</b>",
217
- range=[x0, x1], ticks="outside",
218
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
219
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
220
  tickformat=",.0f", scaleanchor="x", scaleratio=1,
@@ -224,7 +231,7 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
224
  fig.update_layout(width=w, height=h)
225
  return fig
226
 
227
- def depth_or_index_track_interactive(df, title=None, include_actual=True):
228
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
229
  if depth_col is not None:
230
  y = df[depth_col]; y_label = depth_col
@@ -236,16 +243,17 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
236
  x=df["UCS_Pred"], y=y, mode="lines",
237
  line=dict(color=COLORS["pred"], width=1.8),
238
  name="UCS_Pred",
239
- hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
240
  ))
241
  if include_actual and TARGET in df.columns:
242
  fig.add_trace(go.Scatter(
243
  x=df[TARGET], y=y, mode="lines",
244
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
245
  name="UCS (actual)",
246
- hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
247
  ))
248
 
 
249
  fig.update_layout(
250
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
251
  margin=dict(l=60, r=10, t=10, b=36),
@@ -255,14 +263,17 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
255
  bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1
256
  ),
257
  legend_title_text="",
258
- width=int(3.1 * 100),
259
- height=int((7.6 if depth_col is not None else 7.2) * 100),
 
260
  )
261
  fig.update_xaxes(
262
  title_text="<b>UCS</b>", side="top",
263
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
264
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
265
- tickformat=",.0f", automargin=True
 
 
266
  )
267
  fig.update_yaxes(
268
  title_text=f"<b>{y_label}</b>", autorange="reversed",
@@ -398,26 +409,15 @@ if "app_step" not in st.session_state: st.session_state.app_step = "intro"
398
  if "results" not in st.session_state: st.session_state.results = {}
399
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
400
 
401
- # Dev page state (persist file)
402
- for k, v in {
403
- "dev_ready": False,
404
- "dev_file_loaded": False,
405
- "dev_previewed": False,
406
- "dev_file_signature": None,
407
- "dev_preview_request": False,
408
- "dev_file_bytes": b"",
409
- "dev_file_name": "",
410
- "dev_file_rows": 0,
411
- "dev_file_cols": 0,
412
- # validation (was predict)
413
- "val_file_bytes": b"",
414
- "val_file_loaded": False,
415
- "val_preview_request": False,
416
- # prediction (new)
417
- "pred_file_bytes": b"",
418
- "pred_file_loaded": False,
419
- "pred_preview_request": False,
420
- }.items():
421
  if k not in st.session_state: st.session_state[k] = v
422
 
423
  # =========================
@@ -441,9 +441,7 @@ st.markdown(
441
  # =========================
442
  if st.session_state.app_step == "intro":
443
  st.header("Welcome!")
444
- st.markdown(
445
- "This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data."
446
- )
447
  st.subheader("Expected Input Features (in Order)")
448
  st.markdown(
449
  "- Q, gpm — Flow rate (gallons per minute) \n"
@@ -471,7 +469,6 @@ if st.session_state.app_step == "dev":
471
  dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
472
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
473
 
474
- # Detect new/changed file and PERSIST BYTES
475
  if train_test_file is not None:
476
  try:
477
  file_bytes = train_test_file.getvalue(); size = len(file_bytes)
@@ -503,7 +500,7 @@ if st.session_state.app_step == "dev":
503
 
504
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
505
 
506
- # Always enabled so users can jump ahead
507
  proceed_val = st.sidebar.button("Proceed to Validation ▶", use_container_width=True)
508
  proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
509
  if proceed_val:
@@ -511,7 +508,6 @@ if st.session_state.app_step == "dev":
511
  if proceed_pred:
512
  st.session_state.app_step = "predict"; st.rerun()
513
 
514
- # Helper (always at top)
515
  with st.container():
516
  st.subheader("Case Building")
517
  if st.session_state.dev_ready:
@@ -578,8 +574,20 @@ if st.session_state.app_step == "dev":
578
  use_container_width=True, config={"displayModeBar": False}
579
  )
580
  with right:
 
 
 
 
 
 
 
 
 
 
 
 
581
  st.plotly_chart(
582
- depth_or_index_track_interactive(df, title=None, include_actual=True),
583
  use_container_width=True, config={"displayModeBar": False}
584
  )
585
  if "Test" in st.session_state.results:
@@ -594,8 +602,19 @@ if st.session_state.app_step == "dev":
594
  use_container_width=True, config={"displayModeBar": False}
595
  )
596
  with right:
 
 
 
 
 
 
 
 
 
 
 
597
  st.plotly_chart(
598
- depth_or_index_track_interactive(df, title=None, include_actual=True),
599
  use_container_width=True, config={"displayModeBar": False}
600
  )
601
 
@@ -625,7 +644,7 @@ if st.session_state.app_step == "dev":
625
  st.warning(str(e))
626
 
627
  # =========================
628
- # 2) VALIDATE THE MODEL (was predict)
629
  # =========================
630
  if st.session_state.app_step == "validate":
631
  st.sidebar.header("Validate the model")
@@ -644,14 +663,11 @@ if st.session_state.app_step == "validate":
644
  st.session_state.val_preview_request = True
645
 
646
  predict_btn = st.sidebar.button("Run Validation", type="primary", use_container_width=True)
647
-
648
- # Always enabled
649
  proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
650
  st.sidebar.button("⬅ Back to Case Building", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
651
  if proceed_pred:
652
  st.session_state.app_step = "predict"; st.rerun()
653
 
654
- # Helper
655
  with st.container():
656
  st.subheader("Validate the model")
657
  st.write("Upload a validation dataset (with actual UCS if available), preview it, then run to view metrics and plots.")
@@ -661,7 +677,6 @@ if st.session_state.app_step == "validate":
661
  st.session_state.val_preview_request = False
662
  preview_modal_val(_book, FEATURES)
663
 
664
- # Run validation
665
  if predict_btn and st.session_state.val_file_bytes:
666
  with st.status("Validating…", expanded=False) as status:
667
  vbook = read_book_bytes(st.session_state.val_file_bytes)
@@ -700,7 +715,6 @@ if st.session_state.app_step == "validate":
700
  st.session_state.results["oor_table"] = oor_table
701
  status.update(label="Validation ready ✓", state="complete")
702
 
703
- # Display
704
  if "Validate" in st.session_state.results:
705
  sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
706
 
@@ -733,11 +747,23 @@ if st.session_state.app_step == "validate":
733
  else:
734
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
735
  with right:
 
 
 
 
 
 
 
 
 
 
 
 
736
  st.plotly_chart(
737
  depth_or_index_track_interactive(
738
- st.session_state.results["Validate"],
739
- title=None,
740
- include_actual=(TARGET in st.session_state.results["Validate"].columns)
741
  ),
742
  use_container_width=True, config={"displayModeBar": False}
743
  )
@@ -770,7 +796,7 @@ if st.session_state.app_step == "validate":
770
  st.warning(str(e))
771
 
772
  # =========================
773
- # 3) PREDICTION (production scoring, no actual UCS)
774
  # =========================
775
  if st.session_state.app_step == "predict":
776
  st.sidebar.header("Prediction")
@@ -798,10 +824,8 @@ if st.session_state.app_step == "predict":
798
  if st.session_state.pred_preview_request and st.session_state.pred_file_bytes:
799
  _book = read_book_bytes(st.session_state.pred_file_bytes)
800
  st.session_state.pred_preview_request = False
801
- # Reuse the same previewer (no special sheet naming required)
802
  preview_modal_val(_book, FEATURES)
803
 
804
- # Run prediction
805
  if predict_btn and st.session_state.pred_file_bytes:
806
  with st.status("Predicting…", expanded=False) as status:
807
  pbook = read_book_bytes(st.session_state.pred_file_bytes)
@@ -814,7 +838,6 @@ if st.session_state.app_step == "predict":
814
  df_pred["UCS_Pred"] = model.predict(df_pred[FEATURES])
815
  st.session_state.results["Prediction"] = df_pred
816
 
817
- # OOR vs training ranges if available
818
  ranges = st.session_state.train_ranges; oor_table = None; oor_pct = 0.0
819
  if ranges:
820
  viol = {f: (df_pred[f] < ranges[f][0]) | (df_pred[f] > ranges[f][1]) for f in FEATURES}
@@ -835,13 +858,11 @@ if st.session_state.app_step == "predict":
835
  st.session_state.results["oor_table_pred"] = oor_table
836
  status.update(label="Predictions ready ✓", state="complete")
837
 
838
- # Display prediction results (no cross-plot)
839
  if "Prediction" in st.session_state.results:
840
  sv = st.session_state.results["summary_pred"]
841
  if sv.get("oor_pct", 0) > 0:
842
  st.warning("Some inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
843
 
844
- # Two columns: table (left), track (right)
845
  left, right = st.columns([0.6, 0.9])
846
  with left:
847
  table = pd.DataFrame(
@@ -858,21 +879,26 @@ if st.session_state.app_step == "predict":
858
  }
859
  )
860
  st.dataframe(table, use_container_width=True, hide_index=True)
 
 
861
  with right:
 
 
 
 
 
862
  st.plotly_chart(
863
  depth_or_index_track_interactive(
864
- st.session_state.results["Prediction"], title=None, include_actual=False
865
  ),
866
  use_container_width=True, config={"displayModeBar": False}
867
  )
868
 
869
- # OOR table if any
870
  if st.session_state.results.get("oor_table_pred") is not None:
871
  st.write("*Out-of-range rows (vs. Training min–max):*")
872
  st.dataframe(st.session_state.results["oor_table_pred"], use_container_width=True)
873
 
874
  st.markdown("---")
875
- # Export predictions + summary
876
  try:
877
  buf = io.BytesIO()
878
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
 
13
 
14
  import plotly.graph_objects as go
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
16
+ from math import floor, log10
17
 
18
  # =========================
19
  # Defaults
 
31
  # =========================
32
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
33
 
 
34
  def inline_logo(path="logo.png") -> str:
35
  try:
36
  p = Path(path)
 
43
  # Password (brand-gated)
44
  # =========================
45
  def add_password_gate() -> bool:
 
 
 
 
 
 
46
  try:
47
  required = st.secrets.get("APP_PASSWORD", "")
48
  except Exception:
49
  required = os.environ.get("APP_PASSWORD", "")
50
 
 
51
  if not required:
52
  st.markdown(
53
  f"""
 
68
  )
69
  st.stop()
70
 
 
71
  if st.session_state.get("auth_ok", False):
72
  return True
73
 
 
74
  st.markdown(
75
  f"""
76
  <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
 
98
  st.error("Incorrect key. Please try again.")
99
  st.stop()
100
 
 
101
  add_password_gate()
102
 
103
+ # CSS
104
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
105
  st.markdown(
106
  """
 
126
  try:
127
  dialog = st.dialog
128
  except AttributeError:
 
129
  def dialog(title):
130
  def deco(fn):
131
  def wrapper(*args, **kwargs):
 
168
  if nm.lower() in low2orig: return low2orig[nm.lower()]
169
  return None
170
 
171
+ # ----- Nice tick step for cross-plot -----
172
+ def _nice_dtick(data_range: float) -> float:
173
+ if data_range <= 0 or np.isnan(data_range): return 1.0
174
+ raw = data_range / 6.0 # aim ~6 ticks
175
+ k = floor(log10(raw))
176
+ base = 10 ** k
177
+ m = raw / base
178
+ if m <= 1.5:
179
+ step = 1 * base
180
+ elif m <= 3.5:
181
+ step = 2 * base
182
+ elif m <= 7.5:
183
+ step = 5 * base
184
+ else:
185
+ step = 10 * base
186
+ return step
187
+
188
+ # ---------- Interactive plotting ----------
189
  def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
190
  a = pd.Series(actual).astype(float)
191
  p = pd.Series(pred).astype(float)
 
193
  hi = float(np.nanmax([a.max(), p.max()]))
194
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
195
  x0, x1 = lo - pad, hi + pad
196
+ dtick = _nice_dtick(x1 - x0)
197
 
198
  fig = go.Figure()
199
  fig.add_trace(go.Scatter(
200
  x=a, y=p, mode="markers",
201
  marker=dict(size=6, color=COLORS["pred"]),
202
+ hovertemplate="Actual: %{x:.0f}<br>Pred: %{y:.0f}<extra></extra>",
203
  showlegend=False
204
  ))
205
  fig.add_trace(go.Scatter(
 
210
  fig.update_layout(
211
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
212
  margin=dict(l=50, r=10, t=10, b=36),
213
+ hovermode="closest", font=dict(size=13), dragmode="zoom"
214
  )
215
  fig.update_xaxes(
216
  title_text="<b>Actual UCS</b>",
217
+ range=[x0, x1], tickmode="linear", dtick=dtick, ticks="outside",
218
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
219
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
220
  tickformat=",.0f", automargin=True
221
  )
222
  fig.update_yaxes(
223
  title_text="<b>Predicted UCS</b>",
224
+ range=[x0, x1], tickmode="linear", dtick=dtick, ticks="outside",
225
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
226
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
227
  tickformat=",.0f", scaleanchor="x", scaleratio=1,
 
231
  fig.update_layout(width=w, height=h)
232
  return fig
233
 
234
+ def depth_or_index_track_interactive(df, title=None, include_actual=True, x_range=None):
235
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
236
  if depth_col is not None:
237
  y = df[depth_col]; y_label = depth_col
 
243
  x=df["UCS_Pred"], y=y, mode="lines",
244
  line=dict(color=COLORS["pred"], width=1.8),
245
  name="UCS_Pred",
246
+ hovertemplate="UCS_Pred: %{x:.0f}<br>"+y_label+": %{y}<extra></extra>"
247
  ))
248
  if include_actual and TARGET in df.columns:
249
  fig.add_trace(go.Scatter(
250
  x=df[TARGET], y=y, mode="lines",
251
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
252
  name="UCS (actual)",
253
+ hovertemplate="UCS (actual): %{x:.0f}<br>"+y_label+": %{y}<extra></extra>"
254
  ))
255
 
256
+ # slimmer & taller like a log profile
257
  fig.update_layout(
258
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
259
  margin=dict(l=60, r=10, t=10, b=36),
 
263
  bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1
264
  ),
265
  legend_title_text="",
266
+ width=int(2.4 * 100), # narrower
267
+ height=int(8.4 * 100), # taller
268
+ dragmode="zoom"
269
  )
270
  fig.update_xaxes(
271
  title_text="<b>UCS</b>", side="top",
272
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
273
  showgrid=True, gridcolor="rgba(0,0,0,0.12)",
274
+ tickformat=",.0f",
275
+ automargin=True,
276
+ range=x_range
277
  )
278
  fig.update_yaxes(
279
  title_text=f"<b>{y_label}</b>", autorange="reversed",
 
409
  if "results" not in st.session_state: st.session_state.results = {}
410
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
411
 
412
+ # Dev/Val/Pred state
413
+ defaults = {
414
+ "dev_ready": False, "dev_file_loaded": False, "dev_previewed": False,
415
+ "dev_file_signature": None, "dev_preview_request": False,
416
+ "dev_file_bytes": b"", "dev_file_name": "", "dev_file_rows": 0, "dev_file_cols": 0,
417
+ "val_file_bytes": b"", "val_file_loaded": False, "val_preview_request": False,
418
+ "pred_file_bytes": b"", "pred_file_loaded": False, "pred_preview_request": False,
419
+ }
420
+ for k, v in defaults.items():
 
 
 
 
 
 
 
 
 
 
 
421
  if k not in st.session_state: st.session_state[k] = v
422
 
423
  # =========================
 
441
  # =========================
442
  if st.session_state.app_step == "intro":
443
  st.header("Welcome!")
444
+ st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
 
 
445
  st.subheader("Expected Input Features (in Order)")
446
  st.markdown(
447
  "- Q, gpm — Flow rate (gallons per minute) \n"
 
469
  dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
470
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
471
 
 
472
  if train_test_file is not None:
473
  try:
474
  file_bytes = train_test_file.getvalue(); size = len(file_bytes)
 
500
 
501
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
502
 
503
+ # jump links
504
  proceed_val = st.sidebar.button("Proceed to Validation ▶", use_container_width=True)
505
  proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
506
  if proceed_val:
 
508
  if proceed_pred:
509
  st.session_state.app_step = "predict"; st.rerun()
510
 
 
511
  with st.container():
512
  st.subheader("Case Building")
513
  if st.session_state.dev_ready:
 
574
  use_container_width=True, config={"displayModeBar": False}
575
  )
576
  with right:
577
+ # Zoom control for UCS axis
578
+ pr_min = float(df["UCS_Pred"].min())
579
+ xs = [pr_min]
580
+ if TARGET in df: xs.append(float(df[TARGET].min()))
581
+ x_min = min(xs)
582
+ pr_max = float(df["UCS_Pred"].max())
583
+ xs = [pr_max]
584
+ if TARGET in df: xs.append(float(df[TARGET].max()))
585
+ x_max = max(xs)
586
+ with st.expander("Zoom (UCS axis)", expanded=False):
587
+ z = st.slider("UCS range", min_value=float(x_min), max_value=float(x_max),
588
+ value=(float(x_min), float(x_max)), step=10.0, key="zoom_train")
589
  st.plotly_chart(
590
+ depth_or_index_track_interactive(df, title=None, include_actual=True, x_range=z),
591
  use_container_width=True, config={"displayModeBar": False}
592
  )
593
  if "Test" in st.session_state.results:
 
602
  use_container_width=True, config={"displayModeBar": False}
603
  )
604
  with right:
605
+ pr_min = float(df["UCS_Pred"].min())
606
+ xs = [pr_min]
607
+ if TARGET in df: xs.append(float(df[TARGET].min()))
608
+ x_min = min(xs)
609
+ pr_max = float(df["UCS_Pred"].max())
610
+ xs = [pr_max]
611
+ if TARGET in df: xs.append(float(df[TARGET].max()))
612
+ x_max = max(xs)
613
+ with st.expander("Zoom (UCS axis)", expanded=False):
614
+ z2 = st.slider("UCS range", min_value=float(x_min), max_value=float(x_max),
615
+ value=(float(x_min), float(x_max)), step=10.0, key="zoom_test")
616
  st.plotly_chart(
617
+ depth_or_index_track_interactive(df, title=None, include_actual=True, x_range=z2),
618
  use_container_width=True, config={"displayModeBar": False}
619
  )
620
 
 
644
  st.warning(str(e))
645
 
646
  # =========================
647
+ # 2) VALIDATE THE MODEL
648
  # =========================
649
  if st.session_state.app_step == "validate":
650
  st.sidebar.header("Validate the model")
 
663
  st.session_state.val_preview_request = True
664
 
665
  predict_btn = st.sidebar.button("Run Validation", type="primary", use_container_width=True)
 
 
666
  proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
667
  st.sidebar.button("⬅ Back to Case Building", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
668
  if proceed_pred:
669
  st.session_state.app_step = "predict"; st.rerun()
670
 
 
671
  with st.container():
672
  st.subheader("Validate the model")
673
  st.write("Upload a validation dataset (with actual UCS if available), preview it, then run to view metrics and plots.")
 
677
  st.session_state.val_preview_request = False
678
  preview_modal_val(_book, FEATURES)
679
 
 
680
  if predict_btn and st.session_state.val_file_bytes:
681
  with st.status("Validating…", expanded=False) as status:
682
  vbook = read_book_bytes(st.session_state.val_file_bytes)
 
715
  st.session_state.results["oor_table"] = oor_table
716
  status.update(label="Validation ready ✓", state="complete")
717
 
 
718
  if "Validate" in st.session_state.results:
719
  sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
720
 
 
747
  else:
748
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
749
  with right:
750
+ df = st.session_state.results["Validate"]
751
+ pr_min = float(df["UCS_Pred"].min())
752
+ xs = [pr_min]
753
+ if TARGET in df: xs.append(float(df[TARGET].min()))
754
+ x_min = min(xs)
755
+ pr_max = float(df["UCS_Pred"].max())
756
+ xs = [pr_max]
757
+ if TARGET in df: xs.append(float(df[TARGET].max()))
758
+ x_max = max(xs)
759
+ with st.expander("Zoom (UCS axis)", expanded=False):
760
+ zv = st.slider("UCS range", min_value=float(x_min), max_value=float(x_max),
761
+ value=(float(x_min), float(x_max)), step=10.0, key="zoom_val")
762
  st.plotly_chart(
763
  depth_or_index_track_interactive(
764
+ df, title=None,
765
+ include_actual=(TARGET in df.columns),
766
+ x_range=zv
767
  ),
768
  use_container_width=True, config={"displayModeBar": False}
769
  )
 
796
  st.warning(str(e))
797
 
798
  # =========================
799
+ # 3) PREDICTION (no actual UCS)
800
  # =========================
801
  if st.session_state.app_step == "predict":
802
  st.sidebar.header("Prediction")
 
824
  if st.session_state.pred_preview_request and st.session_state.pred_file_bytes:
825
  _book = read_book_bytes(st.session_state.pred_file_bytes)
826
  st.session_state.pred_preview_request = False
 
827
  preview_modal_val(_book, FEATURES)
828
 
 
829
  if predict_btn and st.session_state.pred_file_bytes:
830
  with st.status("Predicting…", expanded=False) as status:
831
  pbook = read_book_bytes(st.session_state.pred_file_bytes)
 
838
  df_pred["UCS_Pred"] = model.predict(df_pred[FEATURES])
839
  st.session_state.results["Prediction"] = df_pred
840
 
 
841
  ranges = st.session_state.train_ranges; oor_table = None; oor_pct = 0.0
842
  if ranges:
843
  viol = {f: (df_pred[f] < ranges[f][0]) | (df_pred[f] > ranges[f][1]) for f in FEATURES}
 
858
  st.session_state.results["oor_table_pred"] = oor_table
859
  status.update(label="Predictions ready ✓", state="complete")
860
 
 
861
  if "Prediction" in st.session_state.results:
862
  sv = st.session_state.results["summary_pred"]
863
  if sv.get("oor_pct", 0) > 0:
864
  st.warning("Some inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
865
 
 
866
  left, right = st.columns([0.6, 0.9])
867
  with left:
868
  table = pd.DataFrame(
 
879
  }
880
  )
881
  st.dataframe(table, use_container_width=True, hide_index=True)
882
+ # ★ footnote under table
883
+ st.caption("★ OOR % = percentage of rows where at least one input feature is outside the training set's min–max range.")
884
  with right:
885
+ # Optional zoom
886
+ dfp = st.session_state.results["Prediction"]
887
+ pmin, pmax = float(dfp["UCS_Pred"].min()), float(dfp["UCS_Pred"].max())
888
+ with st.expander("Zoom (UCS axis)", expanded=False):
889
+ zp = st.slider("UCS range", min_value=pmin, max_value=pmax, value=(pmin, pmax), step=10.0, key="zoom_pred")
890
  st.plotly_chart(
891
  depth_or_index_track_interactive(
892
+ dfp, title=None, include_actual=False, x_range=zp
893
  ),
894
  use_container_width=True, config={"displayModeBar": False}
895
  )
896
 
 
897
  if st.session_state.results.get("oor_table_pred") is not None:
898
  st.write("*Out-of-range rows (vs. Training min–max):*")
899
  st.dataframe(st.session_state.results["oor_table_pred"], use_container_width=True)
900
 
901
  st.markdown("---")
 
902
  try:
903
  buf = io.BytesIO()
904
  with pd.ExcelWriter(buf, engine="openpyxl") as xw: