Spaces:
Running
Running
| import warnings | |
| # Suppress deprecation and future warnings from third-party libraries (like MLflow) | |
| warnings.filterwarnings("ignore", category=FutureWarning) | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import os | |
| import logging | |
| import requests | |
| import subprocess | |
| import sys | |
| from datetime import datetime | |
| import altair as alt | |
| import mlflow | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| # Determine robust file paths relative to script location | |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..")) | |
| if PROJECT_ROOT not in sys.path: | |
| sys.path.append(PROJECT_ROOT) | |
| from src.models.export_forecasts import get_economic_factors | |
| CLEAN_DATA_PATH = os.path.join(PROJECT_ROOT, "data", "processed", "clean_prices.csv") | |
| DRIFT_STATUS_PATH = os.path.join(PROJECT_ROOT, "data", "processed", "drift_status.json") | |
| MLFLOW_DIR = os.path.join(PROJECT_ROOT, "mlruns") | |
| # Configure Streamlit page layout and title | |
| st.set_page_config( | |
| page_title="Sri Lankan Commodity Price Forecast", | |
| page_icon="📈", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Inject modern premium styling (Google Font Inter/Outfit and clean card UI) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap'); | |
| /* Global Font Settings */ | |
| html, body, [class*="css"] { | |
| font-family: 'Outfit', sans-serif; | |
| } | |
| /* Header Gradient styling */ | |
| .main-title { | |
| font-weight: 700; | |
| font-size: 2.5rem; | |
| background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 50%, #60a5fa 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 0.2rem; | |
| } | |
| .subtitle { | |
| color: #4b5563; | |
| font-size: 1.1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| /* Metric Cards UI */ | |
| .metric-container { | |
| display: flex; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .card { | |
| flex: 1; | |
| background-color: #ffffff; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02); | |
| } | |
| .card-title { | |
| font-size: 0.85rem; | |
| color: #6b7280; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 0.5rem; | |
| } | |
| .card-value { | |
| font-size: 1.85rem; | |
| font-weight: 700; | |
| color: #1e3a8a; | |
| } | |
| .card-delta { | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| margin-top: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .delta-up { | |
| color: #dc2626; /* Red for price increase */ | |
| } | |
| .delta-down { | |
| color: #16a34a; /* Green for price decrease */ | |
| } | |
| .delta-neutral { | |
| color: #4b5563; | |
| } | |
| /* Status Badges */ | |
| .badge { | |
| display: inline-block; | |
| padding: 0.25rem 0.6rem; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| border-radius: 9999px; | |
| margin-left: 0.5rem; | |
| } | |
| .badge-success { | |
| background-color: #d1fae5; | |
| color: #065f46; | |
| } | |
| .badge-error { | |
| background-color: #fee2e2; | |
| color: #991b1b; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Helper function to render a custom styled metric card | |
| def render_metric_card(label, value_str, pct_change): | |
| if pct_change > 0: | |
| delta_class = "delta-up" | |
| delta_str = f"▲ +{pct_change:.2f}%" | |
| elif pct_change < 0: | |
| delta_class = "delta-down" | |
| delta_str = f"▼ {pct_change:.2f}%" | |
| else: | |
| delta_class = "delta-neutral" | |
| delta_str = "● 0.00%" | |
| return f""" | |
| <div class="card"> | |
| <div class="card-title">{label}</div> | |
| <div class="card-value">{value_str}</div> | |
| <div class="card-delta {delta_class}">{delta_str} vs. Day 0</div> | |
| </div> | |
| """ | |
| # ----------------- DATA LOADING ----------------- | |
| def load_historical_prices(): | |
| if os.path.exists(CLEAN_DATA_PATH): | |
| df = pd.read_csv(CLEAN_DATA_PATH) | |
| df["Date"] = pd.to_datetime(df["Date"]) | |
| return df | |
| else: | |
| st.warning(f"Cleaned CSV file not found at {CLEAN_DATA_PATH}. Using mock data.") | |
| # Return empty df with columns to prevent crashes | |
| return pd.DataFrame(columns=["Date", "Commodity", "Price"]) | |
| df_historical = load_historical_prices() | |
| # Get list of commodities | |
| if not df_historical.empty: | |
| commodities_list = sorted(list(df_historical["Commodity"].unique())) | |
| else: | |
| commodities_list = ["Samba", "Kekulu", "Big Onion", "Potato", "Dried Chilli", "Coconut"] | |
| # ----------------- SIDEBAR ----------------- | |
| st.sidebar.title("📈 CBSL Forecast Engine") | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("🎯 Parameter Selection") | |
| commodity_choice = st.sidebar.selectbox( | |
| "Select Target Commodity", | |
| commodities_list | |
| ) | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("📡 Backend API Status") | |
| # Check FastAPI Health Status | |
| api_url = os.getenv("API_URL", "http://localhost:8000") | |
| api_live = False | |
| try: | |
| health_resp = requests.get(f"{api_url}/", timeout=1) | |
| if health_resp.status_code == 200 and health_resp.json().get("status") == "healthy": | |
| api_live = True | |
| except Exception: | |
| pass | |
| if api_live: | |
| st.sidebar.markdown( | |
| 'Status: <span class="badge badge-success">● FastAPI Live</span>', | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.sidebar.markdown( | |
| 'Status: <span class="badge badge-error">● API Offline</span>', | |
| unsafe_allow_html=True | |
| ) | |
| st.sidebar.warning(" FastAPI service is offline! Start it via terminal: `uvicorn src.models.api:app --reload` to fetch predictions.") | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("🛠️ Pipeline Controls") | |
| # Button to trigger drift check simulation and retraining | |
| if st.sidebar.button("⚡ Run Retrain Simulation"): | |
| st.sidebar.info("Running drift monitor with simulated inflation shock...") | |
| # Run python src/models/monitor.py --simulate-shock | |
| process = subprocess.Popen( | |
| [sys.executable, "src/models/monitor.py", "--simulate-shock"], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| cwd=PROJECT_ROOT | |
| ) | |
| # Display running logs in the sidebar | |
| log_placeholder = st.sidebar.empty() | |
| log_text = "" | |
| if process.stdout is not None: | |
| while True: | |
| output = process.stdout.readline() | |
| if output == '' and process.poll() is not None: | |
| break | |
| if output: | |
| log_text += output | |
| # Display logs in small code block | |
| log_placeholder.code(log_text[-1000:], language="bash") | |
| rc = process.poll() | |
| if rc == 0: | |
| st.sidebar.success("🎉 Simulation completed!") | |
| st.cache_data.clear() # Clear streamlit caches to load fresh runs/data | |
| st.rerun() | |
| else: | |
| st.sidebar.error(f"❌ Execution failed (Code: {rc})") | |
| st.sidebar.markdown("---") | |
| st.sidebar.info("Built with streamlit to support continuous integration drift metrics.") | |
| # ----------------- MAIN PANEL ----------------- | |
| st.markdown('<div class="main-title">📈 Sri Lankan Commodity Price Forecasting</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="subtitle">MLOps Automated Daily Scraper & XGBoost Recursive Forecast serving Hugging Face Spaces</div>', unsafe_allow_html=True) | |
| # ----------------- TABS CREATION ----------------- | |
| tab_forecast, tab_history, tab_mlops = st.tabs([ | |
| "🔮 Forecast & Economic Indicators", | |
| "📊 Historical Trends & Analytics", | |
| "🛡️ MLOps Control Room & Model Registry" | |
| ]) | |
| # ----------------- FETCH PREDICTIONS ----------------- | |
| forecast_prices = [] | |
| recent_lag_prices = [] | |
| forecast_error_msg = None | |
| # Filter historical data for target commodity | |
| df_comm = df_historical[df_historical["Commodity"].str.lower() == commodity_choice.lower()].sort_values("Date") | |
| if not df_comm.empty: | |
| recent_prices = df_comm.tail(7) | |
| if len(recent_prices) == 7: | |
| recent_lag_prices = recent_prices["Price"].tolist() | |
| last_actual_price = recent_lag_prices[-1] | |
| last_actual_date = recent_prices["Date"].max() | |
| else: | |
| # Fallback mocks if dataset has too few points | |
| recent_lag_prices = [220.0] * 7 | |
| last_actual_price = 220.0 | |
| last_actual_date = datetime.now() | |
| forecast_error_msg = f"Insufficient history (need 7 days, found {len(recent_prices)}) to build prediction lag." | |
| else: | |
| recent_lag_prices = [220.0] * 7 | |
| last_actual_price = 220.0 | |
| last_actual_date = datetime.now() | |
| forecast_error_msg = "No historical price series found for this commodity." | |
| # Call FastAPI predicted forecast | |
| if api_live and not forecast_error_msg: | |
| try: | |
| pred_resp = requests.post( | |
| f"{api_url}/predict", | |
| json={ | |
| "commodity": commodity_choice, | |
| "lag_prices": recent_lag_prices | |
| }, | |
| timeout=3 | |
| ) | |
| if pred_resp.status_code == 200: | |
| forecast_prices = pred_resp.json().get("7_day_forecast", []) | |
| else: | |
| forecast_error_msg = f"FastAPI returned error {pred_resp.status_code}: {pred_resp.json().get('detail')}" | |
| except Exception as e: | |
| forecast_error_msg = f"Connection timeout fetching prediction: {e}" | |
| else: | |
| if not forecast_error_msg: | |
| forecast_error_msg = "FastAPI backend server is currently offline." | |
| # ========================================================================= | |
| # TAB 1: FORECAST & ECONOMIC INDICATORS | |
| # ========================================================================= | |
| with tab_forecast: | |
| if forecast_error_msg: | |
| st.warning(f"⚠️ Forecast unavailable: {forecast_error_msg}") | |
| if forecast_prices: | |
| # 1. Metric Cards Row | |
| pct_tomorrow = ((forecast_prices[0] - last_actual_price) / last_actual_price) * 100 | |
| pct_day7 = ((forecast_prices[-1] - last_actual_price) / last_actual_price) * 100 | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown( | |
| render_metric_card("Current Price (Day 0)", f"LKR {last_actual_price:.2f}", 0.0), | |
| unsafe_allow_html=True | |
| ) | |
| with col2: | |
| st.markdown( | |
| render_metric_card("Tomorrow's Prediction", f"LKR {forecast_prices[0]:.2f}", pct_tomorrow), | |
| unsafe_allow_html=True | |
| ) | |
| with col3: | |
| st.markdown( | |
| render_metric_card("Day 7 Forecast", f"LKR {forecast_prices[-1]:.2f}", pct_day7), | |
| unsafe_allow_html=True | |
| ) | |
| # 2. Stitch together continuous plot (Last 14 days Actuals + 7 Days Forecast) | |
| df_act_plot = df_comm.tail(14).copy() | |
| df_act_plot = df_act_plot[["Date", "Price"]].rename(columns={"Price": "Price (LKR)"}) | |
| df_act_plot["Type"] = "Actual" | |
| # Build future dates | |
| future_dates = [last_actual_date + pd.Timedelta(days=i) for i in range(1, 8)] | |
| df_fore_plot = pd.DataFrame({ | |
| "Date": future_dates, | |
| "Price (LKR)": forecast_prices, | |
| "Type": "Forecast" | |
| }) | |
| # Connect transition point (last actual becomes first point of forecast series to bridge line gap) | |
| transition_row = pd.DataFrame({ | |
| "Date": [last_actual_date], | |
| "Price (LKR)": [last_actual_price], | |
| "Type": ["Forecast"] | |
| }) | |
| df_fore_plot = pd.concat([transition_row, df_fore_plot], ignore_index=True) | |
| df_combined = pd.concat([df_act_plot, df_fore_plot], ignore_index=True) | |
| # Plot continuous line using Altair | |
| st.subheader("🔮 Unified 7-Day Recursive Forecast Trend") | |
| line_chart = alt.Chart(df_combined).mark_line(point=True).encode( | |
| x=alt.X('Date:T', title='Date', axis=alt.Axis(format='%b %d', labelAngle=-30)), | |
| y=alt.Y('Price (LKR):Q', title='Price (LKR/kg)', scale=alt.Scale(zero=False)), | |
| color=alt.Color('Type:N', scale=alt.Scale(domain=['Actual', 'Forecast'], range=['#2563eb', '#f59e0b']), title='Status'), | |
| strokeDash=alt.condition( | |
| alt.datum.Type == 'Forecast', | |
| alt.value([6, 4]), # dashed line | |
| alt.value([0]) # solid line | |
| ), | |
| tooltip=['Date:T', 'Price (LKR):Q', 'Type:N'] | |
| ).properties( | |
| height=400, | |
| title=f"7-Day Continuous Pricing Trend for {commodity_choice}" | |
| ).interactive() | |
| st.altair_chart(line_chart, use_container_width=True) | |
| # 3. Macroeconomic Factors | |
| st.markdown("---") | |
| st.subheader("🌐 Correlated Economic Factors Outlook") | |
| st.markdown("Predicted exchange rates and fuel prices computed dynamically for forecast days:") | |
| ec_col1, ec_col2 = st.columns(2) | |
| # Calculate factors for future dates | |
| forecast_dates_str = [d.strftime("%Y-%m-%d") for d in future_dates] | |
| eco_factors = [get_economic_factors(d) for d in future_dates] | |
| exchange_rates = [e[0] for e in eco_factors] | |
| fuel_prices = [e[1] for e in eco_factors] | |
| df_eco = pd.DataFrame({ | |
| "Date": forecast_dates_str, | |
| "USD/LKR Rate": exchange_rates, | |
| "Petrol 92 Price (LKR/L)": fuel_prices | |
| }) | |
| with ec_col1: | |
| st.markdown("##### 💵 USD/LKR Exchange Rate Target") | |
| eco_chart1 = alt.Chart(df_eco).mark_line(color='#10b981', point=True).encode( | |
| x=alt.X('Date:T', title='Forecast Date'), | |
| y=alt.Y('USD/LKR Rate:Q', title='Rate (LKR)', scale=alt.Scale(zero=False)), | |
| tooltip=['Date:T', 'USD/LKR Rate:Q'] | |
| ).properties(height=250) | |
| st.altair_chart(eco_chart1, use_container_width=True) | |
| with ec_col2: | |
| st.markdown("##### ⛽ CPC Petrol 92 Retail Price") | |
| eco_chart2 = alt.Chart(df_eco).mark_line(color='#ef4444', point=True).encode( | |
| x=alt.X('Date:T', title='Forecast Date'), | |
| y=alt.Y('Petrol 92 Price (LKR/L):Q', title='Price (LKR)', scale=alt.Scale(zero=False)), | |
| tooltip=['Date:T', 'Petrol 92 Price (LKR/L):Q'] | |
| ).properties(height=250) | |
| st.altair_chart(eco_chart2, use_container_width=True) | |
| else: | |
| st.info("💡 Start the FastAPI engine using terminal commands to render recursive predictions.") | |
| # ========================================================================= | |
| # TAB 2: HISTORICAL TRENDS & ANALYTICS | |
| # ========================================================================= | |
| with tab_history: | |
| if df_historical.empty: | |
| st.warning("No historical dataset loaded.") | |
| else: | |
| # User controls for dates | |
| min_date = df_comm["Date"].min() | |
| max_date = df_comm["Date"].max() | |
| h_col1, h_col2 = st.columns([3, 1]) | |
| with h_col1: | |
| st.subheader("📅 Filter Historical Timeline") | |
| selected_dates = st.date_input( | |
| "Select Date Range", | |
| value=(min_date, max_date), | |
| min_value=min_date, | |
| max_value=max_date | |
| ) | |
| # Parse filter | |
| if isinstance(selected_dates, tuple) and len(selected_dates) == 2: | |
| start_d, end_d = selected_dates | |
| df_filtered = df_comm[(df_comm["Date"] >= pd.to_datetime(start_d)) & (df_comm["Date"] <= pd.to_datetime(end_d))].copy() | |
| else: | |
| df_filtered = df_comm.copy() | |
| # Calculate moving averages | |
| df_filtered["7-Day MA"] = df_filtered["Price"].rolling(window=7, min_periods=1).mean() | |
| df_filtered["30-Day MA"] = df_filtered["Price"].rolling(window=30, min_periods=1).mean() | |
| # Melt columns for cleaner multi-line charting | |
| df_melted = df_filtered.melt( | |
| id_vars=["Date"], | |
| value_vars=["Price", "7-Day MA", "30-Day MA"], | |
| var_name="Series", | |
| value_name="LKR" | |
| ).dropna() | |
| # Draw historical plot | |
| hist_line = alt.Chart(df_melted).mark_line().encode( | |
| x=alt.X('Date:T', title='Timeline'), | |
| y=alt.Y('LKR:Q', title='Price per kg (LKR)', scale=alt.Scale(zero=False)), | |
| color=alt.Color('Series:N', scale=alt.Scale( | |
| domain=['Price', '7-Day MA', '30-Day MA'], | |
| range=['#2563eb', '#10b981', '#ef4444'] | |
| ), title='Legend'), | |
| tooltip=['Date:T', 'Series:N', 'LKR:Q'] | |
| ).properties( | |
| height=400, | |
| title=f"Historical Price Series with Technical Moving Averages for {commodity_choice}" | |
| ).interactive() | |
| st.altair_chart(hist_line, use_container_width=True) | |
| # Statistical summary cards | |
| st.markdown("---") | |
| st.subheader("📊 Summary Statistics") | |
| if not df_filtered.empty: | |
| prices = df_filtered["Price"] | |
| stat_min = prices.min() | |
| stat_max = prices.max() | |
| stat_avg = prices.mean() | |
| stat_std = prices.std() | |
| s_col1, s_col2, s_col3, s_col4 = st.columns(4) | |
| s_col1.metric("Min Price", f"LKR {stat_min:.2f}") | |
| s_col2.metric("Max Price", f"LKR {stat_max:.2f}") | |
| s_col3.metric("Average Price", f"LKR {stat_avg:.2f}") | |
| s_col4.metric("Volatility (Std Dev)", f"{stat_std:.2f}" if not pd.isna(stat_std) else "0.00") | |
| # Download CSV | |
| csv_data = df_filtered.to_csv(index=False).encode('utf-8') | |
| st.download_button( | |
| label="📥 Download Cleaned Time-Series Data (CSV)", | |
| data=csv_data, | |
| file_name=f"{commodity_choice}_cleaned_prices.csv", | |
| mime="text/csv" | |
| ) | |
| # ========================================================================= | |
| # TAB 3: MLOPS CONTROL ROOM & MODEL REGISTRY | |
| # ========================================================================= | |
| with tab_mlops: | |
| st.subheader("🛡️ Real-Time Concept Drift Monitor") | |
| # 1. Load drift status JSON | |
| if os.path.exists(DRIFT_STATUS_PATH): | |
| try: | |
| import json | |
| with open(DRIFT_STATUS_PATH, "r") as f: | |
| drift_status = json.load(f) | |
| dr_col1, dr_col2, dr_col3 = st.columns(3) | |
| dr_col1.metric("Latest Drift Checked", drift_status.get("timestamp")) | |
| # Support both statistical KS test format and legacy mean-based percentage drift | |
| is_ks_test = "p_value" in drift_status | |
| if is_ks_test: | |
| ks_stat = drift_status.get("drift_score", 0.0) | |
| p_val = drift_status.get("p_value", 1.0) | |
| significance_level = drift_status.get("drift_threshold", 0.05) | |
| dr_col2.metric("KS Statistic", f"{ks_stat:.4f}", help="Kolmogorov-Smirnov distance between incoming 7-day prices and historical baseline.") | |
| dr_col3.metric("p-value (Significance)", f"{p_val:.4f}", help=f"Drift is detected if p-value < significance level ({significance_level}).") | |
| if drift_status.get("drift_detected"): | |
| st.error(f"🚨 **Alert: Concept Drift Detected (p-value: {p_val:.4f} < {significance_level:.2f})**! The Kolmogorov-Smirnov test confirms a statistically significant change in price distributions. Automated retraining was triggered.") | |
| else: | |
| st.success(f"✅ **System Stable**: No significant drift detected (p-value: {p_val:.4f} >= {significance_level:.2f}). Prices follow the baseline distribution.") | |
| else: | |
| drift_score_pct = drift_status.get("drift_score", 0) * 100 | |
| drift_thresh_pct = drift_status.get("drift_threshold", 0.15) * 100 | |
| dr_col2.metric("Current Drift Score", f"{drift_score_pct:.2f}%", help="Deviation of incoming 7-day average from historical mean.") | |
| dr_col3.metric("Drift Alert Threshold", f"{drift_thresh_pct:.2f}%") | |
| if drift_status.get("drift_detected"): | |
| st.error(f"🚨 **Alert: Concept Drift Detected ({drift_score_pct:.2f}% > {drift_thresh_pct:.2f}%)**! The automated retraining pipeline was triggered to adapt to market shocks.") | |
| else: | |
| st.success(f"✅ **System Stable**: Current drift ({drift_score_pct:.2f}%) is within safe thresholds. XGBoost forecast model parameters are calibrated.") | |
| with st.expander("🔍 View Technical Drift Details"): | |
| st.json(drift_status) | |
| except Exception as e: | |
| st.warning(f"Error parsing drift status logs: {e}") | |
| else: | |
| st.warning("⚠️ No drift logs discovered. Click '⚡ Run Retrain Simulation' in the sidebar to perform a monitoring sweep.") | |
| st.markdown("---") | |
| st.subheader("📦 MLflow Local Model Registry Logs") | |
| # 2. Load MLflow experiment metadata | |
| mlflow.set_tracking_uri(f"file:{MLFLOW_DIR}") | |
| try: | |
| experiment = mlflow.get_experiment_by_name("SL_Commodity_Forecasting") | |
| if experiment: | |
| runs_raw = mlflow.search_runs( | |
| experiment_ids=[experiment.experiment_id], | |
| order_by=["start_time DESC"] | |
| ) | |
| # Ensure type safety and runtime verification for the runs data | |
| if isinstance(runs_raw, pd.DataFrame) and not runs_raw.empty: | |
| # Filter runs programmatically since MLflow query parser does not support OR/IN operators | |
| target_runs = ['XGBoost_Candidate', 'LinearRegression_Baseline', 'ARIMA_Baseline'] | |
| if "tags.mlflow.runName" in runs_raw.columns: | |
| runs_raw = runs_raw[runs_raw["tags.mlflow.runName"].isin(target_runs)] | |
| if not runs_raw.empty: | |
| runs_df = runs_raw | |
| st.write(f"Showing recent model training runs logged to experiment: **{experiment.name}**") | |
| # Cleanup DataFrame for visualization | |
| display_cols = [] | |
| rename_dict = {} | |
| for c in runs_df.columns: | |
| if c == "run_id": | |
| display_cols.append(c) | |
| rename_dict[c] = "Run ID" | |
| elif "runName" in c: | |
| display_cols.append(c) | |
| rename_dict[c] = "Model/Run Name" | |
| elif c == "start_time": | |
| display_cols.append(c) | |
| rename_dict[c] = "Timestamp" | |
| elif "metrics.rmse" in c: | |
| display_cols.append(c) | |
| rename_dict[c] = "RMSE (Test)" | |
| elif "metrics.mae" in c: | |
| display_cols.append(c) | |
| rename_dict[c] = "MAE (Test)" | |
| elif "params.learning_rate" in c: | |
| display_cols.append(c) | |
| rename_dict[c] = "Learning Rate" | |
| elif "params.max_depth" in c: | |
| display_cols.append(c) | |
| rename_dict[c] = "Max Depth" | |
| df_runs_styled = runs_df[display_cols].rename(columns=rename_dict).copy() | |
| # Format Timestamp column | |
| if "Timestamp" in df_runs_styled.columns: | |
| df_runs_styled["Timestamp"] = pd.to_datetime(df_runs_styled["Timestamp"]).dt.strftime("%Y-%m-%d %H:%M:%S") | |
| st.dataframe(df_runs_styled, use_container_width=True) | |
| else: | |
| st.info("No runs logged in MLflow registry.") | |
| else: | |
| st.info("No runs logged in MLflow registry.") | |
| else: | |
| st.info("Experiment 'SL_Commodity_Forecasting' has not been created yet. Run model training.") | |
| except Exception as e: | |
| st.warning(f"Unable to read local MLflow tracking files: {e}") | |
| # ----------------- FOOTER ----------------- | |
| st.markdown("---") | |
| st.markdown( | |
| '<div style="text-align: center; color: #9ca3af; font-size: 0.85rem;">' | |
| 'Case Study: Sri Lankan Commodity Price Forecasting • Built using Streamlit, MLflow, and FastAPI serving local models. ' | |
| 'All historical data sourced from CBSL Daily Price Reports.</div>', | |
| unsafe_allow_html=True | |
| ) | |