UCS2014 commited on
Commit
063ad01
·
verified ·
1 Parent(s): 99ae896

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -0
app.py CHANGED
@@ -4,6 +4,7 @@ import streamlit as st
4
  import pandas as pd
5
  import numpy as np
6
  import joblib
 
7
 
8
  # Matplotlib for PREVIEW modal and for the CROSS-PLOT (static)
9
  import matplotlib
@@ -231,6 +232,147 @@ def df_centered_rounded(df: pd.DataFrame, hide_index=True):
231
  .set_table_styles(TABLE_CENTER_CSS)
232
  )
233
  st.dataframe(styler, use_container_width=True, hide_index=hide_index)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  # =========================
236
  # Cross plot (Matplotlib, fixed limits & ticks)
@@ -772,6 +914,17 @@ if st.session_state.show_preview_modal:
772
  df_centered_rounded(tbl.reset_index(names="Feature"))
773
  # Reset the state variable after the modal is displayed
774
  st.session_state.show_preview_modal = False
 
 
 
 
 
 
 
 
 
 
 
775
  # =========================
776
  # Footer
777
  # =========================
 
4
  import pandas as pd
5
  import numpy as np
6
  import joblib
7
+ from datetime import datetime
8
 
9
  # Matplotlib for PREVIEW modal and for the CROSS-PLOT (static)
10
  import matplotlib
 
232
  .set_table_styles(TABLE_CENTER_CSS)
233
  )
234
  st.dataframe(styler, use_container_width=True, hide_index=hide_index)
235
+ # === NEW: Excel export helpers =================================================
236
+
237
+ def _excel_engine() -> str:
238
+ """Prefer xlsxwriter for better formatting; fall back to openpyxl if missing."""
239
+ try:
240
+ import xlsxwriter # noqa: F401
241
+ return "xlsxwriter"
242
+ except Exception:
243
+ return "openpyxl"
244
+
245
+ def _excel_safe_name(name: str) -> str:
246
+ """Excel sheet names: max 31 chars, no []:*?/\\."""
247
+ bad = '[]:*?/\\'
248
+ safe = ''.join('_' if ch in bad else ch for ch in str(name))
249
+ return safe[:31]
250
+
251
+ def _round_numeric(df: pd.DataFrame, ndigits: int = 2) -> pd.DataFrame:
252
+ out = df.copy()
253
+ for c in out.columns:
254
+ if pd.api.types.is_float_dtype(out[c]) or pd.api.types.is_integer_dtype(out[c]):
255
+ out[c] = pd.to_numeric(out[c], errors="coerce").round(ndigits)
256
+ return out
257
+
258
+ def _summary_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
259
+ cols = [c for c in cols if c in df.columns]
260
+ if not cols:
261
+ return pd.DataFrame()
262
+ tbl = (df[cols]
263
+ .agg(['min','max','mean','std'])
264
+ .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
265
+ .reset_index(names="Field"))
266
+ return _round_numeric(tbl)
267
+
268
+ def _train_ranges_df(ranges: dict[str, tuple[float, float]]) -> pd.DataFrame:
269
+ if not ranges:
270
+ return pd.DataFrame()
271
+ df = pd.DataFrame(ranges).T.reset_index()
272
+ df.columns = ["Feature", "Min", "Max"]
273
+ return _round_numeric(df)
274
+
275
+ def build_export_workbook() -> tuple[bytes|None, str|None, list[str]]:
276
+ """
277
+ Build a multi-sheet Excel workbook (as bytes) from what's currently in session state.
278
+ Returns: (bytes_or_None, filename_or_None, [sheet_names])
279
+ """
280
+ res = st.session_state.get("results", {})
281
+ if not res:
282
+ return None, None, []
283
+
284
+ sheets: dict[str, pd.DataFrame] = {}
285
+ order: list[str] = []
286
+
287
+ # Training
288
+ if "Train" in res:
289
+ tr = _round_numeric(res["Train"])
290
+ sheets["Training"] = tr; order.append("Training")
291
+ m = res.get("m_train", {})
292
+ if m:
293
+ sheets["Training_Metrics"] = _round_numeric(pd.DataFrame([m])); order.append("Training_Metrics")
294
+ tr_cols = FEATURES + ([TARGET] if TARGET in tr.columns else []) + (["UCS_Pred"] if "UCS_Pred" in tr.columns else [])
295
+ s = _summary_table(tr, tr_cols)
296
+ if not s.empty:
297
+ sheets["Training_Summary"] = s; order.append("Training_Summary")
298
+
299
+ # Testing
300
+ if "Test" in res:
301
+ te = _round_numeric(res["Test"])
302
+ sheets["Testing"] = te; order.append("Testing")
303
+ m = res.get("m_test", {})
304
+ if m:
305
+ sheets["Testing_Metrics"] = _round_numeric(pd.DataFrame([m])); order.append("Testing_Metrics")
306
+ te_cols = FEATURES + ([TARGET] if TARGET in te.columns else []) + (["UCS_Pred"] if "UCS_Pred" in te.columns else [])
307
+ s = _summary_table(te, te_cols)
308
+ if not s.empty:
309
+ sheets["Testing_Summary"] = s; order.append("Testing_Summary")
310
+
311
+ # Validation
312
+ if "Validate" in res:
313
+ va = _round_numeric(res["Validate"])
314
+ sheets["Validation"] = va; order.append("Validation")
315
+ m = res.get("m_val", {})
316
+ if m:
317
+ sheets["Validation_Metrics"] = _round_numeric(pd.DataFrame([m])); order.append("Validation_Metrics")
318
+ sv = res.get("sv_val", {})
319
+ if sv:
320
+ sheets["Validation_Summary"] = _round_numeric(pd.DataFrame([sv])); order.append("Validation_Summary")
321
+ oor_tbl = res.get("oor_tbl")
322
+ if oor_tbl is not None and isinstance(oor_tbl, pd.DataFrame) and not oor_tbl.empty:
323
+ sheets["Validation_OOR"] = _round_numeric(oor_tbl.reset_index(drop=True)); order.append("Validation_OOR")
324
+
325
+ # Prediction (no actual)
326
+ if "PredictOnly" in res:
327
+ pr = _round_numeric(res["PredictOnly"])
328
+ sheets["Prediction"] = pr; order.append("Prediction")
329
+ sv = res.get("sv_pred", {})
330
+ if sv:
331
+ sheets["Prediction_Summary"] = _round_numeric(pd.DataFrame([sv])); order.append("Prediction_Summary")
332
+
333
+ # Training ranges (from dev step)
334
+ tr_ranges = st.session_state.get("train_ranges")
335
+ if tr_ranges:
336
+ rr = _train_ranges_df(tr_ranges)
337
+ if not rr.empty:
338
+ sheets["Training_Ranges"] = rr; order.append("Training_Ranges")
339
+
340
+ # Info sheet
341
+ info = pd.DataFrame([
342
+ {"Key": "Target", "Value": TARGET},
343
+ {"Key": "Features", "Value": ", ".join(FEATURES)},
344
+ {"Key": "ExportedAt", "Value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
345
+ ])
346
+ sheets["Info"] = info; order.append("Info")
347
+
348
+ # Write workbook to memory
349
+ bio = io.BytesIO()
350
+ with pd.ExcelWriter(bio, engine=_excel_engine()) as writer:
351
+ for name in order:
352
+ df = sheets[name]
353
+ df.to_excel(writer, sheet_name=_excel_safe_name(name), index=False)
354
+ bio.seek(0)
355
+
356
+ fname = f"UCS_Export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
357
+ return bio.getvalue(), fname, order
358
+
359
+ def render_export_button(key: str = "export_main") -> None:
360
+ """Bottom-of-page export button (main content area)."""
361
+ data, fname, names = build_export_workbook()
362
+ st.divider()
363
+ st.markdown("### Export to Excel")
364
+ if names:
365
+ st.caption("Includes sheets: " + ", ".join(names))
366
+ st.download_button(
367
+ label="⬇️ Export Excel",
368
+ data=(data or b""),
369
+ file_name=(fname or "UCS_Export.xlsx"),
370
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
371
+ disabled=(data is None),
372
+ help="Exports all available results, metrics, summaries, OOR, training ranges, and info.",
373
+ key=key,
374
+ )
375
+ # ================================================================================
376
 
377
  # =========================
378
  # Cross plot (Matplotlib, fixed limits & ticks)
 
914
  df_centered_rounded(tbl.reset_index(names="Feature"))
915
  # Reset the state variable after the modal is displayed
916
  st.session_state.show_preview_modal = False
917
+
918
+ # === Bottom-of-page Export (per step) =========================================
919
+ if st.session_state.app_step in ("dev", "validate", "predict"):
920
+ has_results = any(
921
+ k in st.session_state.results
922
+ for k in ("Train", "Test", "Validate", "PredictOnly")
923
+ )
924
+ if has_results:
925
+ # Unique key per step avoids duplicate-widget clashes when switching steps
926
+ render_export_button(key=f"export_{st.session_state.app_step}")
927
+ # ==============================================================================
928
  # =========================
929
  # Footer
930
  # =========================