UCS2014 commited on
Commit
745ba37
·
verified ·
1 Parent(s): c39d06d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -61
app.py CHANGED
@@ -1,5 +1,4 @@
1
  # app.py — ST_GeoMech_SMW
2
-
3
  import io, json, os, base64, math
4
  from pathlib import Path
5
  import streamlit as st
@@ -23,7 +22,7 @@ from sklearn.metrics import mean_squared_error, mean_absolute_error
23
  APP_NAME = "ST_GeoMech_SMW"
24
  TAGLINE = "Real-Time Upper/Lower Mud Weight (MW) Limits For Safe Drilling"
25
 
26
- # Defaults (can be overridden by meta files)
27
  FEATURES_DEFAULT = [
28
  "WOB (klbf)",
29
  "Torque (kft.lbf)",
@@ -45,7 +44,6 @@ BD_MODEL_PATH = MODELS_DIR / "bd_model.joblib"
45
  BO_META_PATH = MODELS_DIR / "bo_meta.json"
46
  BD_META_PATH = MODELS_DIR / "bd_meta.json"
47
 
48
- MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
49
  COLORS = {
50
  "pred_bo": "#1f77b4", # blue
51
  "pred_bd": "#d62728", # red
@@ -197,10 +195,10 @@ def _build_alias_map(canonical_features: list[str], tgt_bo: str, tgt_bd: str) ->
197
  can_ROP = pick(canonical_features, ["ROP (ft/h)", "ROP(ft/h)"])
198
  can_FR = pick(canonical_features, ["Flow Rate (gpm)", "Flow Rate, gpm", "Flow Rate,gpm", "Flow Rate , gpm"])
199
 
200
- # NEW: make Depth robust—map any of these to whatever your model expects
201
  can_DEPTH = pick(canonical_features, ["Depth", "Depth (ft)", "Depth, ft", "Depth(ft)", "DEPTH, ft"])
202
 
203
- alias = {
204
  "WOB (klbf)": can_WOB, "WOB, klbf": can_WOB, "WOB(klbf)": can_WOB, "WOB( klbf)": can_WOB,
205
  "Torque (kft.lbf)": can_TORQUE, "Torque(kft.lbf)": can_TORQUE, "TORQUE(kft.lbf)": can_TORQUE,
206
  "SPP (psi)": can_SPP, "SPP(psi)": can_SPP,
@@ -208,15 +206,15 @@ def _build_alias_map(canonical_features: list[str], tgt_bo: str, tgt_bd: str) ->
208
  "ROP (ft/h)": can_ROP, "ROP(ft/h)": can_ROP,
209
  "Flow Rate (gpm)": can_FR, "Flow Rate, gpm": can_FR, "Flow Rate,gpm": can_FR, "Flow Rate , gpm": can_FR,
210
 
211
- # Depth aliases (add plain "Depth" too)
212
  "Depth": can_DEPTH, "Depth (ft)": can_DEPTH, "Depth, ft": can_DEPTH, "Depth(ft)": can_DEPTH, "DEPTH, ft": can_DEPTH,
213
 
214
  # Targets
215
  "Breakout MW": tgt_bo, "BOMW": tgt_bo, "BO MW": tgt_bo,
216
  "Breakdown MW": tgt_bd, "BDMW": tgt_bd, "BD MW": tgt_bd,
217
  }
218
- return alias
219
 
 
220
 
221
  def _normalize_columns(df: pd.DataFrame, canonical_features: list[str], tgt_bo: str, tgt_bd: str) -> pd.DataFrame:
222
  out = df.copy()
@@ -225,6 +223,32 @@ def _normalize_columns(df: pd.DataFrame, canonical_features: list[str], tgt_bo:
225
  actual = {k: v for k, v in alias.items() if k in out.columns and k != v}
226
  return out.rename(columns=actual)
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  def ensure_cols(df: pd.DataFrame, cols: list[str]) -> bool:
229
  miss = [c for c in cols if c not in df.columns]
230
  if miss:
@@ -607,12 +631,13 @@ try:
607
  except Exception as e:
608
  st.error(f"Failed to load models: {e}"); st.stop()
609
 
610
- # Defaults
611
- FEATURES = FEATURES_DEFAULT[:]
 
 
612
  TARGET_BO = TARGET_BO_DEFAULT
613
  TARGET_BD = TARGET_BD_DEFAULT
614
 
615
- # Meta overrides
616
  def _load_meta(p: Path):
617
  if not p.exists(): return {}
618
  try:
@@ -623,19 +648,33 @@ def _load_meta(p: Path):
623
  meta_bo = _load_meta(BO_META_PATH)
624
  meta_bd = _load_meta(BD_META_PATH)
625
 
626
- # Use BO meta as primary feature source (or BD if BO missing metas)
627
- if meta_bo.get("features"): FEATURES = meta_bo["features"]
628
- elif meta_bd.get("features"): FEATURES = meta_bd["features"]
 
 
 
 
 
 
 
 
 
629
 
630
- if meta_bo.get("target"): TARGET_BO = meta_bo["target"]
631
- if meta_bd.get("target"): TARGET_BD = meta_bd["target"]
 
 
 
 
632
 
633
  # Session constants for easy access elsewhere
634
  st.session_state["FEATURES"] = FEATURES
635
  st.session_state["TARGET_BO"] = TARGET_BO
636
  st.session_state["TARGET_BD"] = TARGET_BD
 
637
 
638
- # Optional: strict version banner
639
  if STRICT_VERSION_CHECK:
640
  import numpy as _np, sklearn as _skl
641
  msgs=[]
@@ -744,27 +783,24 @@ if st.session_state.app_step == "dev":
744
  st.markdown('<div class="st-message-box st-error">Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets.</div>', unsafe_allow_html=True)
745
  st.stop()
746
 
747
- # Normalize + ensure cols
748
- tr = _normalize_columns(book[sh_train].copy(), FEATURES, TARGET_BO, TARGET_BD)
749
- te = _normalize_columns(book[sh_test].copy(), FEATURES, TARGET_BO, TARGET_BD)
750
- need = FEATURES + [TARGET_BO, TARGET_BD]
751
- if not (ensure_cols(tr, need) and ensure_cols(te, need)):
752
- st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
753
 
754
  # Predict with exact training feature order
755
- tr[PRED_BO] = model_bo.predict(_make_X(tr, FEATURES))
756
- tr[PRED_BD] = model_bd.predict(_make_X(tr, FEATURES))
757
- te[PRED_BO] = model_bo.predict(_make_X(te, FEATURES))
758
- te[PRED_BD] = model_bd.predict(_make_X(te, FEATURES))
759
 
760
  st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
761
- st.session_state.results["m_train_bo"]={"R": pearson_r(tr[TARGET_BO], tr[PRED_BO]), "RMSE": rmse(tr[TARGET_BO], tr[PRED_BO]), "MAE": mean_absolute_error(tr[TARGET_BO], tr[PRED_BO])}
762
- st.session_state.results["m_train_bd"]={"R": pearson_r(tr[TARGET_BD], tr[PRED_BD]), "RMSE": rmse(tr[TARGET_BD], tr[PRED_BD]), "MAE": mean_absolute_error(tr[TARGET_BD], tr[PRED_BD])}
763
- st.session_state.results["m_test_bo"] ={"R": pearson_r(te[TARGET_BO], te[PRED_BO]), "RMSE": rmse(te[TARGET_BO], te[PRED_BO]), "MAE": mean_absolute_error(te[TARGET_BO], te[PRED_BO])}
764
- st.session_state.results["m_test_bd"] ={"R": pearson_r(te[TARGET_BD], te[PRED_BD]), "RMSE": rmse(te[TARGET_BD], te[PRED_BD]), "MAE": mean_absolute_error(te[TARGET_BD], te[PRED_BD])}
765
 
766
- tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
767
- st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
768
  st.markdown('<div class="st-message-box st-success">Case has been built and results are displayed below.</div>', unsafe_allow_html=True)
769
 
770
  def _metrics_block(lbl, m):
@@ -782,11 +818,11 @@ if st.session_state.app_step == "dev":
782
  """, unsafe_allow_html=True)
783
  t1, t2, t3 = st.tabs(["Breakout", "Breakdown", "Combined"])
784
  with t1:
785
- st.plotly_chart(track_plot_single(df, PRED_BO, actual_col=TARGET_BO, title_suffix="Breakout"), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
786
- st.pyplot(cross_plot_static(df[TARGET_BO], df[PRED_BO], xlabel=f"Actual {TARGET_BO}", ylabel=f"Predicted {TARGET_BO}", color=COLORS["pred_bo"]), use_container_width=False)
787
  with t2:
788
- st.plotly_chart(track_plot_single(df, PRED_BD, actual_col=TARGET_BD, title_suffix="Breakdown"), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
789
- st.pyplot(cross_plot_static(df[TARGET_BD], df[PRED_BD], xlabel=f"Actual {TARGET_BD}", ylabel=f"Predicted {TARGET_BD}", color=COLORS["pred_bd"]), use_container_width=False)
790
  with t3:
791
  st.plotly_chart(track_plot_combined(df), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
792
 
@@ -822,29 +858,26 @@ if st.session_state.app_step == "validate":
822
  if go_btn and up is not None:
823
  book = read_book_bytes(up.getvalue())
824
  name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
825
- df = _normalize_columns(book[name].copy(), FEATURES, TARGET_BO, TARGET_BD)
826
- need = FEATURES + [TARGET_BO, TARGET_BD]
827
- if not ensure_cols(df, need):
828
- st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
829
 
830
- df[PRED_BO] = model_bo.predict(_make_X(df, FEATURES))
831
- df[PRED_BD] = model_bd.predict(_make_X(df, FEATURES))
832
  st.session_state.results["Validate"]=df
833
 
834
  ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
835
  if ranges:
836
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
837
  oor_pct = float(any_viol.mean()*100.0)
838
  if any_viol.any():
839
- tbl = df.loc[any_viol, FEATURES].copy()
840
- for c in FEATURES:
841
  if pd.api.types.is_numeric_dtype(tbl[c]): tbl[c] = tbl[c].round(2)
842
- tbl["Violations"] = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).loc[any_viol].apply(
843
  lambda r:", ".join([c for c,v in r.items() if v]), axis=1
844
  )
845
 
846
- st.session_state.results["m_val_bo"]={"R": pearson_r(df[TARGET_BO], df[PRED_BO]), "RMSE": rmse(df[TARGET_BO], df[PRED_BO]), "MAE": mean_absolute_error(df[TARGET_BO], df[PRED_BO])}
847
- st.session_state.results["m_val_bd"]={"R": pearson_r(df[TARGET_BD], df[PRED_BD]), "RMSE": rmse(df[TARGET_BD], df[PRED_BD]), "MAE": mean_absolute_error(df[TARGET_BD], df[PRED_BD])}
848
  st.session_state.results["sv_val"]={"n":len(df), "bo_min":float(df[PRED_BO].min()), "bo_max":float(df[PRED_BO].max()),
849
  "bd_min":float(df[PRED_BD].min()), "bd_max":float(df[PRED_BD].max()), "oor":oor_pct}
850
  st.session_state.results["oor_tbl"]=tbl
@@ -860,14 +893,14 @@ if st.session_state.app_step == "validate":
860
 
861
  t1, t2, t3 = st.tabs(["Breakout", "Breakdown", "Combined"])
862
  with t1:
863
- st.plotly_chart(track_plot_single(df, PRED_BO, actual_col=TARGET_BO, title_suffix="Breakout"),
864
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
865
- st.pyplot(cross_plot_static(df[TARGET_BO], df[PRED_BO], f"Actual {TARGET_BO}", f"Predicted {TARGET_BO}", COLORS["pred_bo"]),
866
  use_container_width=False)
867
  with t2:
868
- st.plotly_chart(track_plot_single(df, PRED_BD, actual_col=TARGET_BD, title_suffix="Breakdown"),
869
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
870
- st.pyplot(cross_plot_static(df[TARGET_BD], df[PRED_BD], f"Actual {TARGET_BD}", f"Predicted {TARGET_BD}", COLORS["pred_bd"]),
871
  use_container_width=False)
872
  with t3:
873
  st.plotly_chart(track_plot_combined(df), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
@@ -899,16 +932,15 @@ if st.session_state.app_step == "predict":
899
 
900
  if go_btn and up is not None:
901
  book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
902
- df = _normalize_columns(book[name].copy(), FEATURES, TARGET_BO, TARGET_BD)
903
- if not ensure_cols(df, FEATURES):
904
- st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
905
- df[PRED_BO] = model_bo.predict(_make_X(df, FEATURES))
906
- df[PRED_BD] = model_bd.predict(_make_X(df, FEATURES))
907
  st.session_state.results["PredictOnly"]=df
908
 
909
  ranges = st.session_state.train_ranges; oor_pct = 0.0
910
  if ranges:
911
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
912
  oor_pct = float(any_viol.mean()*100.0)
913
  st.session_state.results["sv_pred"]={
914
  "n":len(df),
@@ -962,12 +994,12 @@ if st.session_state.show_preview_modal:
962
  tabs = st.tabs(names)
963
  for t, name in zip(tabs, names):
964
  with t:
965
- df = _normalize_columns(book_to_preview[name], FEATURES, TARGET_BO, TARGET_BD)
966
  t1, t2 = st.tabs(["Tracks", "Summary"])
967
  with t1:
968
- st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
969
  with t2:
970
- feat_present = [c for c in FEATURES if c in df.columns]
971
  if not feat_present:
972
  st.info("No feature columns found to summarize.")
973
  else:
 
1
  # app.py — ST_GeoMech_SMW
 
2
  import io, json, os, base64, math
3
  from pathlib import Path
4
  import streamlit as st
 
22
  APP_NAME = "ST_GeoMech_SMW"
23
  TAGLINE = "Real-Time Upper/Lower Mud Weight (MW) Limits For Safe Drilling"
24
 
25
+ # Defaults (can be overridden by metas or model.feature_names_in_)
26
  FEATURES_DEFAULT = [
27
  "WOB (klbf)",
28
  "Torque (kft.lbf)",
 
44
  BO_META_PATH = MODELS_DIR / "bo_meta.json"
45
  BD_META_PATH = MODELS_DIR / "bd_meta.json"
46
 
 
47
  COLORS = {
48
  "pred_bo": "#1f77b4", # blue
49
  "pred_bd": "#d62728", # red
 
195
  can_ROP = pick(canonical_features, ["ROP (ft/h)", "ROP(ft/h)"])
196
  can_FR = pick(canonical_features, ["Flow Rate (gpm)", "Flow Rate, gpm", "Flow Rate,gpm", "Flow Rate , gpm"])
197
 
198
+ # Depth canonical = whatever is in training FEATURES (Depth or Depth (ft), etc.)
199
  can_DEPTH = pick(canonical_features, ["Depth", "Depth (ft)", "Depth, ft", "Depth(ft)", "DEPTH, ft"])
200
 
201
+ return {
202
  "WOB (klbf)": can_WOB, "WOB, klbf": can_WOB, "WOB(klbf)": can_WOB, "WOB( klbf)": can_WOB,
203
  "Torque (kft.lbf)": can_TORQUE, "Torque(kft.lbf)": can_TORQUE, "TORQUE(kft.lbf)": can_TORQUE,
204
  "SPP (psi)": can_SPP, "SPP(psi)": can_SPP,
 
206
  "ROP (ft/h)": can_ROP, "ROP(ft/h)": can_ROP,
207
  "Flow Rate (gpm)": can_FR, "Flow Rate, gpm": can_FR, "Flow Rate,gpm": can_FR, "Flow Rate , gpm": can_FR,
208
 
209
+ # Depth aliases
210
  "Depth": can_DEPTH, "Depth (ft)": can_DEPTH, "Depth, ft": can_DEPTH, "Depth(ft)": can_DEPTH, "DEPTH, ft": can_DEPTH,
211
 
212
  # Targets
213
  "Breakout MW": tgt_bo, "BOMW": tgt_bo, "BO MW": tgt_bo,
214
  "Breakdown MW": tgt_bd, "BDMW": tgt_bd, "BD MW": tgt_bd,
215
  }
 
216
 
217
+ DEPTH_CANDIDATES = ["Depth", "Depth (ft)", "Depth, ft", "Depth(ft)", "DEPTH, ft"]
218
 
219
  def _normalize_columns(df: pd.DataFrame, canonical_features: list[str], tgt_bo: str, tgt_bd: str) -> pd.DataFrame:
220
  out = df.copy()
 
223
  actual = {k: v for k, v in alias.items() if k in out.columns and k != v}
224
  return out.rename(columns=actual)
225
 
226
+ def _coerce_depth(df: pd.DataFrame, canon_depth: str | None) -> pd.DataFrame:
227
+ """If the model expects a Depth-like column, rename any variant to it."""
228
+ if not canon_depth:
229
+ return df
230
+ if canon_depth in df.columns:
231
+ return df
232
+ for c in DEPTH_CANDIDATES:
233
+ if c in df.columns:
234
+ return df.rename(columns={c: canon_depth})
235
+ return df
236
+
237
+ def _prepare_table(raw_df: pd.DataFrame, stage: str, features: list[str],
238
+ tgt_bo: str, tgt_bd: str, canon_depth: str | None) -> pd.DataFrame:
239
+ """Normalize headers, coerce depth name, and validate required columns."""
240
+ df = _normalize_columns(raw_df, features, tgt_bo, tgt_bd)
241
+ df = _coerce_depth(df, canon_depth)
242
+ missing = [c for c in features if c not in df.columns]
243
+ if missing:
244
+ st.error(
245
+ f"{stage}: Missing required column(s): {missing}\n\n"
246
+ f"Found columns: {list(df.columns)}\n\n"
247
+ f"Expected features: {features}"
248
+ )
249
+ st.stop()
250
+ return df
251
+
252
  def ensure_cols(df: pd.DataFrame, cols: list[str]) -> bool:
253
  miss = [c for c in cols if c not in df.columns]
254
  if miss:
 
631
  except Exception as e:
632
  st.error(f"Failed to load models: {e}"); st.stop()
633
 
634
+ # ---------------------------
635
+ # Resolve FEATURES & targets
636
+ # ---------------------------
637
+ FEATURES = FEATURES_DEFAULT[:] # fallback
638
  TARGET_BO = TARGET_BO_DEFAULT
639
  TARGET_BD = TARGET_BD_DEFAULT
640
 
 
641
  def _load_meta(p: Path):
642
  if not p.exists(): return {}
643
  try:
 
648
  meta_bo = _load_meta(BO_META_PATH)
649
  meta_bd = _load_meta(BD_META_PATH)
650
 
651
+ # Prefer the model's trained feature names (golden source)
652
+ try:
653
+ if hasattr(model_bo, "feature_names_in_"):
654
+ FEATURES = [str(x) for x in model_bo.feature_names_in_]
655
+ except Exception:
656
+ pass
657
+
658
+ # If still not set, fall back to BO meta, then BD meta, then defaults
659
+ if not FEATURES and meta_bo.get("features"):
660
+ FEATURES = [str(x) for x in meta_bo["features"]]
661
+ elif not FEATURES and meta_bd.get("features"):
662
+ FEATURES = [str(x) for x in meta_bd["features"]]
663
 
664
+ # Targets (from metas if provided)
665
+ if meta_bo.get("target"): TARGET_BO = str(meta_bo["target"])
666
+ if meta_bd.get("target"): TARGET_BD = str(meta_bd["target"])
667
+
668
+ # Canonical Depth name the model expects (if any)
669
+ CANON_DEPTH = next((c for c in FEATURES if str(c).strip().lower().startswith("depth")), None)
670
 
671
  # Session constants for easy access elsewhere
672
  st.session_state["FEATURES"] = FEATURES
673
  st.session_state["TARGET_BO"] = TARGET_BO
674
  st.session_state["TARGET_BD"] = TARGET_BD
675
+ st.session_state["CANON_DEPTH"] = CANON_DEPTH
676
 
677
+ # Optional strict version banner
678
  if STRICT_VERSION_CHECK:
679
  import numpy as _np, sklearn as _skl
680
  msgs=[]
 
783
  st.markdown('<div class="st-message-box st-error">Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets.</div>', unsafe_allow_html=True)
784
  st.stop()
785
 
786
+ # Normalize + ensure cols (robust depth handling)
787
+ tr = _prepare_table(book[sh_train].copy(), "Training", st.session_state["FEATURES"], st.session_state["TARGET_BO"], st.session_state["TARGET_BD"], st.session_state["CANON_DEPTH"])
788
+ te = _prepare_table(book[sh_test].copy(), "Testing", st.session_state["FEATURES"], st.session_state["TARGET_BO"], st.session_state["TARGET_BD"], st.session_state["CANON_DEPTH"])
 
 
 
789
 
790
  # Predict with exact training feature order
791
+ tr[PRED_BO] = model_bo.predict(_make_X(tr, st.session_state["FEATURES"]))
792
+ tr[PRED_BD] = model_bd.predict(_make_X(tr, st.session_state["FEATURES"]))
793
+ te[PRED_BO] = model_bo.predict(_make_X(te, st.session_state["FEATURES"]))
794
+ te[PRED_BD] = model_bd.predict(_make_X(te, st.session_state["FEATURES"]))
795
 
796
  st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
797
+ st.session_state.results["m_train_bo"]={"R": pearson_r(tr[st.session_state["TARGET_BO"]], tr[PRED_BO]), "RMSE": rmse(tr[st.session_state["TARGET_BO"]], tr[PRED_BO]), "MAE": mean_absolute_error(tr[st.session_state["TARGET_BO"]], tr[PRED_BO])}
798
+ st.session_state.results["m_train_bd"]={"R": pearson_r(tr[st.session_state["TARGET_BD"]], tr[PRED_BD]), "RMSE": rmse(tr[st.session_state["TARGET_BD"]], tr[PRED_BD]), "MAE": mean_absolute_error(tr[st.session_state["TARGET_BD"]], tr[PRED_BD])}
799
+ st.session_state.results["m_test_bo"] ={"R": pearson_r(te[st.session_state["TARGET_BO"]], te[PRED_BO]), "RMSE": rmse(te[st.session_state["TARGET_BO"]], te[PRED_BO]), "MAE": mean_absolute_error(te[st.session_state["TARGET_BO"]], te[PRED_BO])}
800
+ st.session_state.results["m_test_bd"] ={"R": pearson_r(te[st.session_state["TARGET_BD"]], te[PRED_BD]), "RMSE": rmse(te[st.session_state["TARGET_BD"]], te[PRED_BD]), "MAE": mean_absolute_error(te[st.session_state["TARGET_BD"]], te[PRED_BD])}
801
 
802
+ tr_min = tr[st.session_state["FEATURES"]].min().to_dict(); tr_max = tr[st.session_state["FEATURES"]].max().to_dict()
803
+ st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in st.session_state["FEATURES"]}
804
  st.markdown('<div class="st-message-box st-success">Case has been built and results are displayed below.</div>', unsafe_allow_html=True)
805
 
806
  def _metrics_block(lbl, m):
 
818
  """, unsafe_allow_html=True)
819
  t1, t2, t3 = st.tabs(["Breakout", "Breakdown", "Combined"])
820
  with t1:
821
+ st.plotly_chart(track_plot_single(df, PRED_BO, actual_col=st.session_state["TARGET_BO"], title_suffix="Breakout"), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
822
+ st.pyplot(cross_plot_static(df[st.session_state["TARGET_BO"]], df[PRED_BO], xlabel=f"Actual {st.session_state['TARGET_BO']}", ylabel=f"Predicted {st.session_state['TARGET_BO']}", color=COLORS["pred_bo"]), use_container_width=False)
823
  with t2:
824
+ st.plotly_chart(track_plot_single(df, PRED_BD, actual_col=st.session_state["TARGET_BD"], title_suffix="Breakdown"), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
825
+ st.pyplot(cross_plot_static(df[st.session_state["TARGET_BD"]], df[PRED_BD], xlabel=f"Actual {st.session_state['TARGET_BD']}", ylabel=f"Predicted {st.session_state['TARGET_BD']}", color=COLORS["pred_bd"]), use_container_width=False)
826
  with t3:
827
  st.plotly_chart(track_plot_combined(df), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
828
 
 
858
  if go_btn and up is not None:
859
  book = read_book_bytes(up.getvalue())
860
  name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
861
+ df = _prepare_table(book[name].copy(), "Validation", st.session_state["FEATURES"], st.session_state["TARGET_BO"], st.session_state["TARGET_BD"], st.session_state["CANON_DEPTH"])
 
 
 
862
 
863
+ df[PRED_BO] = model_bo.predict(_make_X(df, st.session_state["FEATURES"]))
864
+ df[PRED_BD] = model_bd.predict(_make_X(df, st.session_state["FEATURES"]))
865
  st.session_state.results["Validate"]=df
866
 
867
  ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
868
  if ranges:
869
+ any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in st.session_state["FEATURES"]}).any(axis=1)
870
  oor_pct = float(any_viol.mean()*100.0)
871
  if any_viol.any():
872
+ tbl = df.loc[any_viol, st.session_state["FEATURES"]].copy()
873
+ for c in st.session_state["FEATURES"]:
874
  if pd.api.types.is_numeric_dtype(tbl[c]): tbl[c] = tbl[c].round(2)
875
+ tbl["Violations"] = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in st.session_state["FEATURES"]}).loc[any_viol].apply(
876
  lambda r:", ".join([c for c,v in r.items() if v]), axis=1
877
  )
878
 
879
+ st.session_state.results["m_val_bo"]={"R": pearson_r(df[st.session_state["TARGET_BO"]], df[PRED_BO]), "RMSE": rmse(df[st.session_state["TARGET_BO"]], df[PRED_BO]), "MAE": mean_absolute_error(df[st.session_state["TARGET_BO"]], df[PRED_BO])}
880
+ st.session_state.results["m_val_bd"]={"R": pearson_r(df[st.session_state["TARGET_BD"]], df[PRED_BD]), "RMSE": rmse(df[st.session_state["TARGET_BD"]], df[PRED_BD]), "MAE": mean_absolute_error(df[st.session_state["TARGET_BD"]], df[PRED_BD])}
881
  st.session_state.results["sv_val"]={"n":len(df), "bo_min":float(df[PRED_BO].min()), "bo_max":float(df[PRED_BO].max()),
882
  "bd_min":float(df[PRED_BD].min()), "bd_max":float(df[PRED_BD].max()), "oor":oor_pct}
883
  st.session_state.results["oor_tbl"]=tbl
 
893
 
894
  t1, t2, t3 = st.tabs(["Breakout", "Breakdown", "Combined"])
895
  with t1:
896
+ st.plotly_chart(track_plot_single(df, PRED_BO, actual_col=st.session_state["TARGET_BO"], title_suffix="Breakout"),
897
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
898
+ st.pyplot(cross_plot_static(df[st.session_state["TARGET_BO"]], df[PRED_BO], f"Actual {st.session_state['TARGET_BO']}", f"Predicted {st.session_state['TARGET_BO']}", COLORS["pred_bo"]),
899
  use_container_width=False)
900
  with t2:
901
+ st.plotly_chart(track_plot_single(df, PRED_BD, actual_col=st.session_state["TARGET_BD"], title_suffix="Breakdown"),
902
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
903
+ st.pyplot(cross_plot_static(df[st.session_state["TARGET_BD"]], df[PRED_BD], f"Actual {st.session_state['TARGET_BD']}", f"Predicted {st.session_state['TARGET_BD']}", COLORS["pred_bd"]),
904
  use_container_width=False)
905
  with t3:
906
  st.plotly_chart(track_plot_combined(df), use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
 
932
 
933
  if go_btn and up is not None:
934
  book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
935
+ df = _prepare_table(book[name].copy(), "Prediction", st.session_state["FEATURES"], st.session_state["TARGET_BO"], st.session_state["TARGET_BD"], st.session_state["CANON_DEPTH"])
936
+
937
+ df[PRED_BO] = model_bo.predict(_make_X(df, st.session_state["FEATURES"]))
938
+ df[PRED_BD] = model_bd.predict(_make_X(df, st.session_state["FEATURES"]))
 
939
  st.session_state.results["PredictOnly"]=df
940
 
941
  ranges = st.session_state.train_ranges; oor_pct = 0.0
942
  if ranges:
943
+ any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in st.session_state["FEATURES"]}).any(axis=1)
944
  oor_pct = float(any_viol.mean()*100.0)
945
  st.session_state.results["sv_pred"]={
946
  "n":len(df),
 
994
  tabs = st.tabs(names)
995
  for t, name in zip(tabs, names):
996
  with t:
997
+ df = _prepare_table(book_to_preview[name], "Preview", st.session_state["FEATURES"], st.session_state["TARGET_BO"], st.session_state["TARGET_BD"], st.session_state["CANON_DEPTH"])
998
  t1, t2 = st.tabs(["Tracks", "Summary"])
999
  with t1:
1000
+ st.pyplot(preview_tracks(df, st.session_state["FEATURES"]), use_container_width=True)
1001
  with t2:
1002
+ feat_present = [c for c in st.session_state["FEATURES"] if c in df.columns]
1003
  if not feat_present:
1004
  st.info("No feature columns found to summarize.")
1005
  else: