UCS2014 commited on
Commit
6f5e7f7
·
verified ·
1 Parent(s): 1c2fd4b

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -658
app.py DELETED
@@ -1,658 +0,0 @@
1
- import io, json, os, base64, math
2
- from pathlib import Path
3
- import streamlit as st
4
- import pandas as pd
5
- import numpy as np
6
- import joblib
7
-
8
- # matplotlib only for PREVIEW modal
9
- import matplotlib
10
- matplotlib.use("Agg")
11
- import matplotlib.pyplot as plt
12
-
13
- import plotly.graph_objects as go
14
- from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
15
-
16
- # =========================
17
- # Constants (simple & robust)
18
- # =========================
19
- FEATURES = ["Q, gpm", "SPP(psi)", "T (kft.lbf)", "WOB (klbf)", "ROP (ft/h)"]
20
- TARGET = "UCS"
21
- MODELS_DIR = Path("models")
22
- DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
23
- MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
24
- COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
25
-
26
- # ---- Plot sizing controls (edit here) ----
27
- CROSS_W = 450; CROSS_H = 450 # square cross-plot (Build + Validate)
28
- TRACK_W = 400; TRACK_H = 950 # log-strip style (all pages)
29
- FONT_SZ = 15
30
- PLOT_COLS = [30, 1, 20] # 3-column band: left • spacer • right (Build + Validate)
31
- CROSS_NUDGE = 0.02 # push cross-plot to the RIGHT inside its band:
32
- # inner columns [CROSS_NUDGE : 1] → bigger = more right
33
-
34
- # =========================
35
- # Page / CSS
36
- # =========================
37
- st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
38
- st.markdown("""
39
- <style>
40
- /* ✅ Hides 'Drag and drop file here' and file size note */
41
- section[data-testid="stFileUploader"] div[data-testid="stMarkdownContainer"] {
42
- display: none !important;
43
- }
44
- </style>
45
- """, unsafe_allow_html=True)
46
- st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
47
- st.markdown(
48
- """
49
- <style>
50
- .stApp { background:#fff; }
51
- section[data-testid="stSidebar"] { background:#F6F9FC; }
52
- .block-container { padding-top:.5rem; padding-bottom:.5rem; }
53
- .stButton>button { background:#007bff; color:#fff; font-weight:600; border-radius:8px; border:none; }
54
- .stButton>button:hover { background:#0056b3; }
55
- .st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
56
- .st-hero .brand { width:110px; height:110px; object-fit:contain; }
57
- .st-hero h1 { margin:0; line-height:1.05; }
58
- .st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
59
- [data-testid="stBlock"]{ margin-top:0 !important; }
60
- </style>
61
- """,
62
- unsafe_allow_html=True
63
- )
64
-
65
- # =========================
66
- # Password gate (define first, then call)
67
- # =========================
68
- def inline_logo(path="logo.png") -> str:
69
- try:
70
- p = Path(path)
71
- if not p.exists(): return ""
72
- return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
73
- except Exception:
74
- return ""
75
-
76
- def add_password_gate() -> None:
77
- try:
78
- required = st.secrets.get("APP_PASSWORD", "")
79
- except Exception:
80
- required = os.environ.get("APP_PASSWORD", "")
81
-
82
- if not required:
83
- st.markdown(
84
- f"""
85
- <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
86
- <img src="{inline_logo()}" style="width:56px;height:56px;object-fit:contain"/>
87
- <div>
88
- <div style="font-size:1.9rem;font-weight:800;">ST_GeoMech_UCS</div>
89
- <div style="color:#667085;">Smart Thinking • Secure Access</div>
90
- </div>
91
- </div>
92
- <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
93
- <div style="color:#6b7280;margin-bottom:14px;">
94
- Set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b> (or environment) and restart.
95
- </div>
96
- """,
97
- unsafe_allow_html=True,
98
- )
99
- st.stop()
100
-
101
- if st.session_state.get("auth_ok", False):
102
- return
103
-
104
- st.markdown(
105
- f"""
106
- <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
107
- <img src="{inline_logo()}" style="width:56px;height:56px;object-fit:contain"/>
108
- <div>
109
- <div style="font-size:1.9rem;font-weight:800;">ST_GeoMech_UCS</div>
110
- <div style="color:#667085;">Smart Thinking • Secure Access</div>
111
- </div>
112
- </div>
113
- <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected</div>
114
- <div style="color:#6b7280;margin-bottom:14px;">Please enter your access key to continue.</div>
115
- """,
116
- unsafe_allow_html=True
117
- )
118
-
119
- pwd = st.text_input("Access key", type="password", placeholder="••••••••")
120
- if st.button("Unlock", type="primary"):
121
- if pwd == required:
122
- st.session_state.auth_ok = True
123
- st.rerun()
124
- else:
125
- st.error("Incorrect key.")
126
- st.stop()
127
-
128
- add_password_gate()
129
-
130
- # =========================
131
- # Utilities
132
- # =========================
133
- try:
134
- dialog = st.dialog
135
- except AttributeError:
136
- def dialog(title):
137
- def deco(fn):
138
- def wrapper(*args, **kwargs):
139
- with st.expander(title, expanded=True):
140
- return fn(*args, **kwargs)
141
- return wrapper
142
- return deco
143
-
144
- def rmse(y_true, y_pred):
145
- return float(np.sqrt(mean_squared_error(y_true, y_pred)))
146
-
147
- @st.cache_resource(show_spinner=False)
148
- def load_model(model_path: str):
149
- return joblib.load(model_path)
150
-
151
- @st.cache_data(show_spinner=False)
152
- def parse_excel(data_bytes: bytes):
153
- bio = io.BytesIO(data_bytes)
154
- xl = pd.ExcelFile(bio)
155
- return {sh: xl.parse(sh) for sh in xl.sheet_names}
156
-
157
- def read_book_bytes(b: bytes):
158
- return parse_excel(b) if b else {}
159
-
160
- def ensure_cols(df, cols):
161
- miss = [c for c in cols if c not in df.columns]
162
- if miss:
163
- st.error(f"Missing columns: {miss}\nFound: {list(df.columns)}")
164
- return False
165
- return True
166
-
167
- def find_sheet(book, names):
168
- low2orig = {k.lower(): k for k in book.keys()}
169
- for nm in names:
170
- if nm.lower() in low2orig:
171
- return low2orig[nm.lower()]
172
- return None
173
-
174
- def _nice_tick0(xmin: float, step: int = 100) -> float:
175
- return step * math.floor(xmin / step) if np.isfinite(xmin) else xmin
176
-
177
- # ---------- cross_plot ----------
178
- def cross_plot(actual, pred):
179
- a = pd.Series(actual).astype(float)
180
- p = pd.Series(pred).astype(float)
181
-
182
- fixed_min = 6000
183
- fixed_max = 10000
184
- tick_spacing = 500
185
- tick_start = 6000
186
-
187
- fig = go.Figure()
188
-
189
- # Scatter points
190
- fig.add_trace(go.Scatter(
191
- x=a, y=p, mode="markers",
192
- marker=dict(size=6, color=COLORS["pred"]),
193
- hovertemplate="Actual: %{x:.0f}<br>Pred: %{y:.0f}<extra></extra>",
194
- showlegend=False
195
- ))
196
-
197
- # 1:1 diagonal line
198
- fig.add_trace(go.Scatter(
199
- x=[fixed_min, fixed_max], y=[fixed_min, fixed_max], mode="lines",
200
- line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
201
- hoverinfo="skip", showlegend=False
202
- ))
203
-
204
- fig.update_layout(
205
- width=CROSS_W, height=CROSS_H,
206
- paper_bgcolor="#fff", plot_bgcolor="#fff",
207
- margin=dict(l=64, r=18, t=10, b=48),
208
- hovermode="closest",
209
- font=dict(size=FONT_SZ),
210
- dragmode=False
211
- )
212
-
213
- fig.update_xaxes(
214
- title_text="<b>Actual UCS (psi)</b>",
215
- title_font=dict(size=18, family="Arial", color="#000"),
216
- range=[6000, 10000],
217
- tick0=6000,
218
- dtick=500,
219
- ticks="outside",
220
- tickformat=",.0f",
221
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
222
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
223
- constrain="domain", # ensures no stretching
224
- scaleanchor="y", scaleratio=1, # enforce 1:1 with y-axis
225
- automargin=False
226
- )
227
-
228
- fig.update_yaxes(
229
- title_text="<b>Predicted UCS (psi)</b>",
230
- title_font=dict(size=18, family="Arial", color="#000"),
231
- range=[6000, 10000],
232
- tick0=6000,
233
- dtick=500,
234
- ticks="outside",
235
- tickformat=",.0f",
236
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
237
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
238
- constrain="domain", # ensures no stretching
239
- automargin=False
240
- )
241
-
242
- return fig
243
-
244
- # ---------- track_plot ----------
245
- def track_plot(df, include_actual=True):
246
- depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
247
- if depth_col:
248
- y = pd.Series(df[depth_col]).astype(float)
249
- ylab = depth_col
250
- else:
251
- y = pd.Series(np.arange(1, len(df) + 1))
252
- ylab = "Point Index"
253
-
254
- y_range = [float(y.max()), float(y.min())]
255
-
256
- x_series = pd.Series(df.get("UCS_Pred", pd.Series(dtype=float))).astype(float)
257
- if include_actual and TARGET in df.columns:
258
- x_series = pd.concat([x_series, pd.Series(df[TARGET]).astype(float)], ignore_index=True)
259
-
260
- x_lo, x_hi = float(x_series.min()), float(x_series.max())
261
- x_pad = 0.03 * (x_hi - x_lo if x_hi > x_lo else 1.0)
262
- xmin, xmax = x_lo - x_pad, x_hi + x_pad
263
- tick0 = _nice_tick0(xmin, step=100)
264
-
265
- fig = go.Figure()
266
-
267
- fig.add_trace(go.Scatter(
268
- x=df["UCS_Pred"], y=y, mode="lines",
269
- line=dict(color=COLORS["pred"], width=1.8),
270
- name="UCS_Pred",
271
- hovertemplate="UCS_Pred: %{x:.0f}<br>" + ylab + ": %{y}<extra></extra>"
272
- ))
273
-
274
- if include_actual and TARGET in df.columns:
275
- fig.add_trace(go.Scatter(
276
- x=df[TARGET], y=y, mode="lines",
277
- line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
278
- name="UCS (actual)",
279
- hovertemplate="UCS (actual): %{x:.0f}<br>" + ylab + ": %{y}<extra></extra>"
280
- ))
281
-
282
- fig.update_layout(
283
- width=TRACK_W, height=TRACK_H,
284
- paper_bgcolor="#fff", plot_bgcolor="#fff",
285
- margin=dict(l=72, r=18, t=36, b=48),
286
- hovermode="closest",
287
- font=dict(size=FONT_SZ),
288
- legend=dict(
289
- x=0.98, y=0.05, xanchor="right", yanchor="bottom",
290
- bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1
291
- ),
292
- legend_title_text=""
293
- )
294
-
295
- fig.update_xaxes(
296
- title_text="<b>UCS (psi)</b>",
297
- title_font=dict(size=18, family="Arial", color="#000"),
298
- side="top", range=[xmin, xmax],
299
- tick0=tick0, tickmode="auto", tickformat=",.0f",
300
- ticks="outside",
301
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
302
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
303
- )
304
-
305
- fig.update_yaxes(
306
- title_text=f"<b>{ylab}</b>",
307
- title_font=dict(size=18, family="Arial", color="#000"),
308
- range=y_range,
309
- ticks="outside",
310
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
311
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
312
- )
313
-
314
- return fig
315
- # ---------- Preview modal (matplotlib) ----------
316
- def preview_tracks(df: pd.DataFrame, cols: list[str]):
317
- cols = [c for c in cols if c in df.columns]
318
- n = len(cols)
319
- if n == 0:
320
- fig, ax = plt.subplots(figsize=(4, 2))
321
- ax.text(0.5,0.5,"No selected columns",ha="center",va="center")
322
- ax.axis("off")
323
- return fig
324
- fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
325
- if n == 1: axes = [axes]
326
- idx = np.arange(1, len(df) + 1)
327
- for ax, col in zip(axes, cols):
328
- ax.plot(df[col], idx, '-', lw=1.4, color="#333")
329
- ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
330
- ax.grid(True, linestyle=":", alpha=0.3)
331
- for s in ax.spines.values(): s.set_visible(True)
332
- axes[0].set_ylabel("Point Index")
333
- return fig
334
-
335
- try:
336
- dialog = st.dialog
337
- except AttributeError:
338
- def dialog(title):
339
- def deco(fn):
340
- def wrapper(*args, **kwargs):
341
- with st.expander(title, expanded=True):
342
- return fn(*args, **kwargs)
343
- return wrapper
344
- return deco
345
-
346
- @dialog("Preview data")
347
- def preview_modal(book: dict[str, pd.DataFrame]):
348
- if not book:
349
- st.info("No data loaded yet."); return
350
- names = list(book.keys())
351
- tabs = st.tabs(names)
352
- for t, name in zip(tabs, names):
353
- with t:
354
- df = book[name]
355
- t1, t2 = st.tabs(["Tracks", "Summary"])
356
- with t1: st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
357
- with t2:
358
- tbl = df[FEATURES].agg(['min','max','mean','std']).T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
359
- st.dataframe(tbl.reset_index(names="Feature"), use_container_width=True)
360
-
361
- # =========================
362
- # Load model (simple)
363
- # =========================
364
- def ensure_model() -> Path|None:
365
- for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
366
- if p.exists() and p.stat().st_size > 0: return p
367
- url = os.environ.get("MODEL_URL", "")
368
- if not url: return None
369
- try:
370
- import requests
371
- DEFAULT_MODEL.parent.mkdir(parents=True, exist_ok=True)
372
- with requests.get(url, stream=True, timeout=30) as r:
373
- r.raise_for_status()
374
- with open(DEFAULT_MODEL, "wb") as f:
375
- for chunk in r.iter_content(1<<20):
376
- if chunk: f.write(chunk)
377
- return DEFAULT_MODEL
378
- except Exception:
379
- return None
380
-
381
- mpath = ensure_model()
382
- if not mpath:
383
- st.error("Model not found. Upload models/ucs_rf.joblib (or set MODEL_URL).")
384
- st.stop()
385
- try:
386
- model = load_model(str(mpath))
387
- except Exception as e:
388
- st.error(f"Failed to load model: {e}")
389
- st.stop()
390
-
391
- meta_path = MODELS_DIR / "meta.json"
392
- if meta_path.exists():
393
- try:
394
- meta = json.loads(meta_path.read_text(encoding="utf-8"))
395
- FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
396
- except Exception:
397
- pass
398
-
399
- # =========================
400
- # Session state
401
- # =========================
402
- st.session_state.setdefault("app_step", "intro")
403
- st.session_state.setdefault("results", {})
404
- st.session_state.setdefault("train_ranges", None)
405
- st.session_state.setdefault("dev_file_name","")
406
- st.session_state.setdefault("dev_file_bytes",b"")
407
- st.session_state.setdefault("dev_file_loaded",False)
408
- st.session_state.setdefault("dev_preview",False)
409
-
410
- # =========================
411
- # Hero
412
- # =========================
413
- st.markdown(
414
- f"""
415
- <div class="st-hero">
416
- <img src="{inline_logo()}" class="brand" />
417
- <div>
418
- <h1>ST_GeoMech_UCS</h1>
419
- <div class="tagline">Real-Time UCS Tracking While Drilling</div>
420
- </div>
421
- </div>
422
- """,
423
- unsafe_allow_html=True,
424
- )
425
-
426
- # =========================
427
- # INTRO
428
- # =========================
429
- if st.session_state.app_step == "intro":
430
- st.header("Welcome!")
431
- st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
432
- st.subheader("How It Works")
433
- st.markdown(
434
- "1) **Upload your data to build the case and preview the performance of our model.** \n"
435
- "2) Click **Run Model** to compute metrics and plots. \n"
436
- "3) **Proceed to Validation** (with actual UCS) or **Proceed to Prediction** (no UCS)."
437
- )
438
- if st.button("Start Showcase", type="primary"):
439
- st.session_state.app_step = "dev"; st.rerun()
440
-
441
- # =========================
442
- # CASE BUILDING
443
- # =========================
444
- if st.session_state.app_step == "dev":
445
- st.sidebar.header("Case Building")
446
- up = st.sidebar.file_uploader("Upload Train/Test Excel", type=["xlsx","xls"])
447
- if up is not None:
448
- st.session_state.dev_file_bytes = up.getvalue()
449
- st.session_state.dev_file_name = up.name
450
- st.session_state.dev_file_loaded = True
451
- st.session_state.dev_preview = False
452
- if st.session_state.dev_file_loaded:
453
- tmp = read_book_bytes(st.session_state.dev_file_bytes)
454
- if tmp:
455
- df0 = next(iter(tmp.values()))
456
- st.sidebar.caption(f"**Data loaded:** {st.session_state.dev_file_name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
457
-
458
- if st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded):
459
- preview_modal(read_book_bytes(st.session_state.dev_file_bytes))
460
- st.session_state.dev_preview = True
461
-
462
- run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
463
- # always available nav
464
- if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
465
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
466
-
467
- # ---- Pinned helper at the very top of the page ----
468
- helper_top = st.container()
469
- with helper_top:
470
- st.subheader("Case Building")
471
- if st.session_state.dev_file_loaded and st.session_state.dev_preview:
472
- st.info("Previewed ✓ — now click **Run Model**.")
473
- elif st.session_state.dev_file_loaded:
474
- st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
475
- else:
476
- st.write("**Upload your data to build a case, then run the model to review development performance.**")
477
-
478
- if run and st.session_state.dev_file_bytes:
479
- book = read_book_bytes(st.session_state.dev_file_bytes)
480
- sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
481
- sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
482
- if sh_train is None or sh_test is None:
483
- st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
484
- tr = book[sh_train].copy(); te = book[sh_test].copy()
485
- if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
486
- st.error("Missing required columns."); st.stop()
487
- tr["UCS_Pred"] = model.predict(tr[FEATURES])
488
- te["UCS_Pred"] = model.predict(te[FEATURES])
489
-
490
- st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
491
- 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"])}
492
- 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"])}
493
-
494
- tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
495
- st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
496
- st.success("Case has been built and results are displayed below.")
497
-
498
- def _dev_block(df, m):
499
- c1,c2,c3 = st.columns(3)
500
- c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
501
- left, spacer, right = st.columns(PLOT_COLS)
502
- with left:
503
- pad, plotcol = left.columns([CROSS_NUDGE, 1]) # shift cross-plot right inside its band
504
- with plotcol:
505
- st.plotly_chart(
506
- cross_plot(df[TARGET], df["UCS_Pred"]),
507
- use_container_width=False,
508
- config={"displayModeBar": False, "scrollZoom": True}
509
- )
510
- with right:
511
- st.plotly_chart(
512
- track_plot(df, include_actual=True),
513
- use_container_width=False,
514
- config={"displayModeBar": False, "scrollZoom": True}
515
- )
516
-
517
- if "Train" in st.session_state.results or "Test" in st.session_state.results:
518
- tab1, tab2 = st.tabs(["Training", "Testing"])
519
- if "Train" in st.session_state.results:
520
- with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
521
- if "Test" in st.session_state.results:
522
- with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
523
-
524
- # =========================
525
- # VALIDATION (with actual UCS)
526
- # =========================
527
- if st.session_state.app_step == "validate":
528
- st.sidebar.header("Validate the Model")
529
- up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"])
530
- if up is not None:
531
- book = read_book_bytes(up.getvalue())
532
- if book:
533
- df0 = next(iter(book.values()))
534
- st.sidebar.caption(f"**Data loaded:** {up.name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
535
- if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
536
- preview_modal(read_book_bytes(up.getvalue()))
537
- go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
538
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
539
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
540
-
541
- st.subheader("Validate the Model")
542
- st.write("Upload a dataset with the same **features** and **UCS** to evaluate performance.")
543
-
544
- if go_btn and up is not None:
545
- book = read_book_bytes(up.getvalue())
546
- name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
547
- df = book[name].copy()
548
- if not ensure_cols(df, FEATURES+[TARGET]): st.error("Missing required columns."); st.stop()
549
- df["UCS_Pred"] = model.predict(df[FEATURES])
550
- st.session_state.results["Validate"]=df
551
-
552
- ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
553
- if ranges:
554
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
555
- oor_pct = float(any_viol.mean()*100.0)
556
- if any_viol.any():
557
- tbl = df.loc[any_viol, FEATURES].copy()
558
- 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)
559
- 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"])}
560
- 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}
561
- st.session_state.results["oor_tbl"]=tbl
562
-
563
- if "Validate" in st.session_state.results:
564
- m = st.session_state.results["m_val"]
565
- c1,c2,c3 = st.columns(3)
566
- c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
567
-
568
- left, spacer, right = st.columns(PLOT_COLS)
569
- with left:
570
- pad, plotcol = left.columns([CROSS_NUDGE, 1]) # same nudge
571
- with plotcol:
572
- st.plotly_chart(
573
- cross_plot(st.session_state.results["Validate"][TARGET],
574
- st.session_state.results["Validate"]["UCS_Pred"]),
575
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
576
- )
577
- with right:
578
- st.plotly_chart(
579
- track_plot(st.session_state.results["Validate"], include_actual=True),
580
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
581
- )
582
-
583
- sv = st.session_state.results["sv_val"]
584
- if sv["oor"] > 0: st.warning("Some inputs fall outside **training min–max** ranges.")
585
- if st.session_state.results["oor_tbl"] is not None:
586
- st.write("*Out-of-range rows (vs. Training min–max):*")
587
- st.dataframe(st.session_state.results["oor_tbl"], use_container_width=True)
588
-
589
- # =========================
590
- # PREDICTION (no actual UCS)
591
- # =========================
592
- if st.session_state.app_step == "predict":
593
- st.sidebar.header("Prediction (No Actual UCS)")
594
- up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"])
595
- if up is not None:
596
- book = read_book_bytes(up.getvalue())
597
- if book:
598
- df0 = next(iter(book.values()))
599
- st.sidebar.caption(f"**Data loaded:** {up.name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
600
- if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
601
- preview_modal(read_book_bytes(up.getvalue()))
602
- go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
603
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
604
-
605
- st.subheader("Prediction")
606
- st.write("Upload a dataset with the feature columns (no **UCS**).")
607
-
608
- if go_btn and up is not None:
609
- book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
610
- df = book[name].copy()
611
- if not ensure_cols(df, FEATURES): st.error("Missing required columns."); st.stop()
612
- df["UCS_Pred"] = model.predict(df[FEATURES])
613
- st.session_state.results["PredictOnly"]=df
614
-
615
- ranges = st.session_state.train_ranges; oor_pct = 0.0
616
- if ranges:
617
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
618
- oor_pct = float(any_viol.mean()*100.0)
619
- st.session_state.results["sv_pred"]={
620
- "n":len(df),
621
- "pred_min":float(df["UCS_Pred"].min()),
622
- "pred_max":float(df["UCS_Pred"].max()),
623
- "pred_mean":float(df["UCS_Pred"].mean()),
624
- "pred_std":float(df["UCS_Pred"].std(ddof=0)),
625
- "oor":oor_pct
626
- }
627
-
628
- if "PredictOnly" in st.session_state.results:
629
- df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
630
-
631
- left, spacer, right = st.columns(PLOT_COLS)
632
- with left:
633
- table = pd.DataFrame({
634
- "Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
635
- "Value": [sv["n"], sv["pred_min"], sv["pred_max"], sv["pred_mean"], sv["pred_std"], f'{sv["oor"]:.1f}%']
636
- })
637
- st.success("Predictions ready ✓")
638
- st.dataframe(table, use_container_width=True, hide_index=True)
639
- st.caption("**★ OOR** = % of rows whose input features fall outside the training min–max range.")
640
- with right:
641
- st.plotly_chart(
642
- track_plot(df, include_actual=False),
643
- use_container_width=False, config={"displayModeBar": False, "scrollZoom": True}
644
- )
645
-
646
- # =========================
647
- # Footer
648
- # =========================
649
- st.markdown("---")
650
- st.markdown(
651
- """
652
- <div style='text-align:center; color:#6b7280; line-height:1.6'>
653
- ST_GeoMech_UCS • © Smart Thinking<br/>
654
- <strong>Visit our website:</strong> <a href='https://www.smartthinking.com.sa' target='_blank'>smartthinking.com.sa</a>
655
- </div>
656
- """,
657
- unsafe_allow_html=True
658
- )