Spaces:
Sleeping
Sleeping
| # --- 1. IMPORT LIBRARIES --- | |
| import streamlit as st | |
| import pandas as pd | |
| import joblib | |
| import plotly.graph_objects as go | |
| from datetime import datetime | |
| from typing import List | |
| import numpy as np | |
| # Import your utility scripts from the 'src' directory | |
| try: | |
| from src import benchmark_utils | |
| from src import diagnostic_plots as diag | |
| except ImportError: | |
| st.error("Error: Could not find 'src/benchmark_utils.py' or 'src/diagnostic_plots.py'. " | |
| "Please ensure they exist in the 'src/' directory.") | |
| st.stop() | |
| # --- 2. PAGE CONFIGURATION --- | |
| st.set_page_config( | |
| page_title="Saigon Temperature Forecast", | |
| page_icon="🌦️", | |
| layout="wide" | |
| ) | |
| # --- START OF NEW THEME SECTION (ĐÃ CẬP NHẬT) --- | |
| def load_css(): | |
| """Tải CSS tùy chỉnh để tạo giao diện 'thời tiết' với ĐỘ TƯƠNG PHẢN CAO.""" | |
| st.markdown(""" | |
| <style> | |
| /* ===== FONT CHUNG ===== */ | |
| .stApp, .stSidebar { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| } | |
| /* ===== NỀN CHÍNH (MAIN BACKGROUND) ===== */ | |
| [data-testid="stAppViewContainer"] { | |
| background-image: linear-gradient(to bottom, #B0E0E6, #F0F8FF); | |
| background-attachment: fixed; | |
| background-size: cover; | |
| } | |
| /* ===== 1. THANH CHỌN TAB (st.tabs) ===== */ | |
| /* Tab không được chọn */ | |
| button[data-baseweb="tab"][aria-selected="false"] { | |
| background-color: rgba(255, 255, 255, 0.7) !important; /* Nền mờ */ | |
| color: #0E2A47 !important; /* Chữ đậm */ | |
| border-top-left-radius: 8px; | |
| border-top-right-radius: 8px; | |
| padding: 12px 16px !important; /* <<< THÊM PADDING */ | |
| } | |
| /* Tab ĐANG ĐƯỢC CHỌN */ | |
| button[data-baseweb="tab"][aria-selected="true"] { | |
| background-color: #FFFFFF !important; /* Nền TRẮNG ĐỤC */ | |
| color: #004080 !important; /* Chữ MÀU XANH ĐẬM */ | |
| font-weight: 700 !important; | |
| border-top-left-radius: 8px; | |
| border-top-right-radius: 8px; | |
| border-bottom: 3px solid #004080 !important; /* Viền xanh đậm */ | |
| padding: 12px 16px !important; /* <<< THÊM PADDING */ | |
| } | |
| /* ===== 2. THẺ DỰ BÁO (METRIC CARDS) ===== */ | |
| div[data-testid="stMetric"] { | |
| background-color: rgba(255, 255, 255, 0.95) !important; /* Nền trắng (đục hơn) */ | |
| border: 1px solid #B0C4DE; /* Thêm viền (xanh nhạt) */ | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important; /* Đổ bóng đậm hơn */ | |
| backdrop-filter: blur(5px); | |
| transition: transform 0.2s ease; | |
| } | |
| div[data-testid="stMetric"]:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important; | |
| } | |
| /* Tiêu đề thẻ (Forecast for...) - đã có tương phản tốt */ | |
| div[data-testid="stMetricLabel"] p { | |
| font-size: 1.1rem !important; | |
| font-weight: 600 !important; | |
| color: #333333; /* Xám đậm */ | |
| } | |
| /* Giá trị nhiệt độ - đã có tương phản tốt */ | |
| div[data-testid="stMetricValue"] { | |
| font-size: 2.8rem !important; | |
| font-weight: 700 !important; | |
| color: #004080; /* Xanh navy đậm */ | |
| } | |
| /* Giá trị "Actual" (delta) - đã có tương phản tốt */ | |
| div[data-testid="stMetricDelta"] { | |
| font-size: 1rem !important; | |
| font-weight: 600 !important; | |
| color: #555555; /* Xám vừa */ | |
| } | |
| /* ===== 3. TIÊU ĐỀ (HEADINGS) ===== */ | |
| h1, h2, h3 { | |
| color: #004080 !important; /* Dùng chung màu XANH ĐẬM NHẤT */ | |
| text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.15) !important; /* Thêm đổ bóng ĐEN (thay vì trắng) */ | |
| } | |
| /* ===== 4. BẢNG (DATAFRAME) ===== */ | |
| .stDataFrame { | |
| background-color: #FFFFFF; /* Nền TRẮNG ĐỤC */ | |
| border: 1px solid #CCCCCC !important; /* Viền xám nhạt */ | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| /* Tiêu đề của bảng */ | |
| [data-testid="stDataGridHeader"] { | |
| background-color: #F0F8FF; /* Nền header (Alice Blue) */ | |
| color: #004080; /* Chữ xanh đậm */ | |
| } | |
| /* ===== 5. BIỂU ĐỒ (PLOTLY) ===== */ | |
| .plotly-graph-div { | |
| background-color: #FFFFFF; /* Nền TRẮNG ĐỤC */ | |
| border: 1px solid #E0E0E0; /* Viền xám rất nhạt */ | |
| border-radius: 8px; | |
| } | |
| /* ===== 6. VĂN BẢN THÔNG THƯỜNG (PARAGRAPH & MARKDOWN) ===== */ | |
| /* Quy tắc này áp dụng cho văn bản st.markdown và các đoạn văn bản khác */ | |
| .stMarkdown, p, li { | |
| color: #333333 !important; /* Xám đen, tương phản tốt trên nền sáng */ | |
| font-size: 1.05rem; /* Có thể thêm tùy chọn để chữ lớn hơn một chút */ | |
| } | |
| /* SAFE DataFrame Styling */ | |
| [data-testid="stDataFrame"] { | |
| border: 1px solid #CCCCCC !important; | |
| border-radius: 8px !important; | |
| background-color: #FFFFFF !important; | |
| } | |
| /* ===== EXPANDERS (vẫn giữ như cũ) ===== */ | |
| div[data-testid="stExpander"] { | |
| background-color: rgba(255, 255, 255, 0.9) !important; | |
| border-radius: 10px !important; | |
| border: 1px solid rgba(0, 0, 0, 0.1) !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Gọi hàm CSS ngay lập tức | |
| load_css() | |
| # --- END OF NEW THEME SECTION --- | |
| # --- 3. DATA & MODEL LOADING FUNCTIONS (WITH CACHING) --- | |
| # Checklist Items 1 & 2: Cache all heavy operations | |
| def load_hourly_performance_data(file_path="data/hourly_120h_evaluation_results.csv"): | |
| """Loads hourly RMSE/R2 performance data (T+1h to T+120h).""" | |
| try: | |
| df = pd.read_csv(file_path) | |
| # Giả định cột đầu tiên là Horizon (1, 2, 3...) | |
| df['Horizon'] = df.index + 1 | |
| # Giữ nguyên code này ngay cả khi cột Horizon đã có sẵn | |
| return df | |
| except FileNotFoundError: | |
| st.warning(f"Warning: Hourly Performance data not found at: {file_path}. Cannot show degradation plot.") | |
| return pd.DataFrame() | |
| def load_hourly_data(file_path="data/final_hourly_feature_dataset.csv"): | |
| """Loads the Hourly Direct dataset using the provided demo file.""" | |
| try: | |
| # Tải file features hourly từ thư mục 'data/' | |
| df_hourly = pd.read_csv(file_path) | |
| # --- Xử lý Cột Ngày Giờ (CRITICAL CUSTOMIZATION) --- | |
| DATE_COLUMN = 'datetime' | |
| if DATE_COLUMN not in df_hourly.columns: | |
| st.error(f"Error: Date column '{DATE_COLUMN}' not found in hourly data CSV. Please check the column name.") | |
| return pd.DataFrame() | |
| # Chuyển cột 'datetime' sang định dạng datetime và đặt làm index | |
| df_hourly[DATE_COLUMN] = pd.to_datetime(df_hourly[DATE_COLUMN]) | |
| df_hourly = df_hourly.set_index(DATE_COLUMN) | |
| df_hourly = df_hourly.sort_index() | |
| return df_hourly | |
| except FileNotFoundError: | |
| st.error(f"ERROR: Hourly data file not found at: {file_path}. Please check the path and file name.") | |
| return pd.DataFrame() | |
| except Exception as e: | |
| st.error(f"An unexpected error occurred while loading hourly data: {e}") | |
| return pd.DataFrame() | |
| def load_24_hourly_models(): | |
| """Tải 24 mô hình LGBM chuyên biệt (T+1h đến T+24h) cho biểu đồ.""" | |
| hourly_models = {} | |
| # Số lượng mô hình bạn muốn tải (chúng ta giả định có 24 file) | |
| num_horizons = 24 | |
| try: | |
| for h in range(1, num_horizons + 1): | |
| # Giả định file model T+1h đến T+10h là file bạn gửi, các file khác nằm trong 'models/' | |
| if h <= 10: | |
| file_path = f"models/lgbm_model_target_temp_next_{h}h.pkl" # Sử dụng tên file bạn gửi | |
| else: | |
| file_path = f"models/lgbm_model_target_temp_next_{h}h.pkl" # Giả định path | |
| model = joblib.load(file_path) | |
| hourly_models[h] = model | |
| if len(hourly_models) < num_horizons: | |
| st.warning(f"Warning: Only {len(hourly_models)} hourly models loaded. Graph will be incomplete.") | |
| return hourly_models | |
| except FileNotFoundError as e: | |
| st.error(f"ERROR: Missing hourly model file: {e.filename}. Cannot generate full hourly graph.") | |
| return {} | |
| def load_feature_data(file_path="data/final_dataset_tree.csv"): | |
| """Loads features and targets, converts index to datetime.""" | |
| try: | |
| df = pd.read_csv(file_path) | |
| # --- CRITICAL CUSTOMIZATION --- | |
| # Ensure 'datetime' is your date column in the CSV | |
| DATE_COLUMN = 'datetime' | |
| if DATE_COLUMN not in df.columns: | |
| st.error(f"Error: Date column '{DATE_COLUMN}' not found in 'final_dataset_tree.csv'. " | |
| f"Please update the DATE_COLUMN variable in 'app.py'.") | |
| return pd.DataFrame() | |
| df[DATE_COLUMN] = pd.to_datetime(df[DATE_COLUMN]) | |
| df = df.set_index(DATE_COLUMN) | |
| df = df.sort_index() | |
| return df | |
| except FileNotFoundError: | |
| st.error(f"ERROR: Main data file not found at: {file_path}") | |
| return pd.DataFrame() | |
| def load_champion_models(): | |
| """Loads the 5 specialist models from the checklist.""" | |
| models = [] | |
| try: | |
| for i in range(1, 6): | |
| file_path = f"models/champion_stacking_day{i}.pkl" | |
| model = joblib.load(file_path) | |
| models.append(model) | |
| return models | |
| except FileNotFoundError as e: | |
| st.error(f"ERROR: Model file not found. Checked: {e.filename}. " | |
| "Ensure the 5 .pkl files are in the 'models/' directory.") | |
| return [] | |
| """Loads pre-calculated performance data for Tab 3.""" | |
| try: | |
| df = pd.read_csv(file_path) | |
| return df | |
| except FileNotFoundError: | |
| st.error(f"ERROR: Performance file not found at: {file_path}") | |
| return pd.DataFrame() | |
| # --- 4. INITIALIZE DATA & SPLIT TEST SET --- | |
| # Load all data and models | |
| all_data_df = load_feature_data() | |
| models = load_champion_models() | |
| perf_df = load_performance_data() | |
| # --- CRITICAL CUSTOMIZATION --- | |
| TARGET_COLS = ['temp_next_1_day', 'temp_next_2_day', 'temp_next_3_day', 'temp_next_4_day', 'temp_next_5_day'] | |
| CURRENT_TEMP_COL = 'temp' | |
| # Split test set (based on checklist dates) | |
| TEST_START_DATE = "2024-02-18" | |
| TEST_END_DATE = "2025-09-26" | |
| X_test, y_test, test_df = pd.DataFrame(), pd.DataFrame(), pd.DataFrame() | |
| if not all_data_df.empty: | |
| try: | |
| test_df = all_data_df.loc[TEST_START_DATE:TEST_END_DATE].copy() | |
| # Assumption: 157 features are ALL columns that are NOT targets | |
| feature_cols = [col for col in all_data_df.columns if col not in TARGET_COLS] | |
| # Split X_test (features) and y_test (actuals) | |
| # Logic fix: X_test must be derived from test_df | |
| X_test = test_df[feature_cols] | |
| y_test = test_df[TARGET_COLS] | |
| # Rename y_test columns for clarity (used in Tab 3) | |
| y_test.columns = [f'Day {i}' for i in range(1, 6)] | |
| except KeyError: | |
| st.error(f"Error: Target columns (e.g., '{TARGET_COLS[0]}') or " | |
| f"'{CURRENT_TEMP_COL}' column not found in CSV. Please update 'app.py'.") | |
| except Exception as e: | |
| st.error(f"Error processing test set: {e}") | |
| else: | |
| st.error("Could not load main data, application cannot continue.") | |
| st.stop() | |
| # --- CRITICAL CUSTOMIZATION (Hourly Targets) --- | |
| HOURLY_TARGET_COLS = ['target_temp_next_24h', 'target_temp_next_48h', 'target_temp_next_72h', | |
| 'target_temp_next_96h', 'target_temp_next_120h'] | |
| # Load models và data mới | |
| hourly_data_df = load_hourly_data(file_path="data/final_hourly_feature_dataset.csv") # Dùng tên file features chính xác | |
| hourly_perf_df = load_hourly_performance_data(file_path="data/hourly_120h_evaluation_results.csv") # File hiệu suất | |
| hourly_models_24h = load_24_hourly_models() # Dùng 24 mô hình LGBM | |
| # Tạo input features cho Hourly | |
| if not hourly_data_df.empty: | |
| HOURLY_FEATURE_COLS = [col for col in hourly_data_df.columns if col not in HOURLY_TARGET_COLS] | |
| # Lấy test set | |
| X_test_hourly = hourly_data_df.loc[TEST_START_DATE:TEST_END_DATE][HOURLY_FEATURE_COLS].copy() | |
| # FIX LỖI 1 (Model Prediction Dtypes): Loại bỏ các cột object (sunrise/sunset) | |
| columns_to_drop_objects = ['sunrise', 'sunset'] | |
| X_test_hourly = X_test_hourly.drop(columns=columns_to_drop_objects, errors='ignore') | |
| HOURLY_FEATURE_COLS = X_test_hourly.columns.tolist() # Cập nhật lại feature list sau khi drop | |
| else: | |
| X_test_hourly = pd.DataFrame() | |
| # --- Định nghĩa Hàm Dự đoán 24h Thực tế (Giữ nguyên logic bên trong) --- | |
| def predict_next_24_hours(input_features: pd.DataFrame, models: dict) -> List[float]: | |
| # ... (Code hàm này giữ nguyên như lần trước) | |
| predictions = [] | |
| num_horizons = len(models) | |
| if input_features.empty or not models: | |
| # Nếu thiếu model, tạo 24 giá trị giả lập dựa trên nhiệt độ hiện tại (temp) | |
| last_temp = input_features['temp'].iloc[-1] if not input_features.empty else 28.0 | |
| # Dùng np đã được import | |
| np.random.seed(42) | |
| return [last_temp + 1.5 * np.sin(2 * np.pi * (h + 10) / 24) + np.random.normal(0, 0.5) | |
| for h in range(num_horizons)] | |
| # Chạy mô hình Direct Hourly | |
| for h in range(1, num_horizons + 1): | |
| try: | |
| model = models[h] | |
| pred = model.predict(input_features)[0] | |
| predictions.append(pred) | |
| except: | |
| predictions.append(float('nan')) | |
| return predictions | |
| # --- 5. GIAO DIỆN SIDEBAR (ĐÃ XÓA) --- | |
| # Toàn bộ phần sidebar đã bị xóa | |
| # --- 6. GIAO DIỆN CHÍNH (MAIN PANEL) --- | |
| # Tạo các tab ngang thay vì radio button | |
| tab1, tab2, tab3, tab4 = st.tabs([ | |
| "📑 Project Overview & Methodology", | |
| "🌦️ Live 5-Day Forecast", | |
| "📊 Model Performance & Diagnostics", | |
| "⏱️ Hourly Prediction" # TAB MỚI | |
| ]) | |
| # --- TAB 1: Project Overview --- | |
| with tab1: | |
| # --- MỤC 3 TRONG CHECKLIST --- | |
| st.title("Saigon Temperature Forecasting Application 🌦️") | |
| # --- NÂNG CẤP: Thêm hình ảnh --- | |
| st.image("https://image.vietnam.travel/sites/default/files/2023-03/shutterstock_626352947_0.jpg?v=1762135399", | |
| caption="Ho Chi Minh City. Credit: Vietnam Tourism", use_container_width=True) | |
| # Bạn có thể thay thế URL trên bằng URL của riêng bạn, hoặc | |
| st.subheader("Project Summary") | |
| st.markdown(""" | |
| The goal of this project is to forecast the average daily temperature for Ho Chi Minh City for the next 5 days. | |
| * **Data:** 10 years of historical weather data from Visual Crossing. | |
| * **Model:** We use 5 'specialist' models - each model is optimized to predict a specific future day (T+1 to T+5). | |
| """) | |
| # --- NÂNG CẤP: Thêm emoji --- | |
| st.subheader("🚀 Our 'Two-Stream' Strategy") | |
| st.markdown(""" | |
| To optimize performance, we applied a "Two-Stream" strategy: | |
| 1. **Stream 1 (Linear Models):** Linear models (like Linear Regression) were trained on a feature set pruned using VIF to avoid multicollinearity. | |
| 2. **Stream 2 (Tree-based Models):** More complex models (like Random Forest, Gradient Boosting) were trained on a comprehensive set of 156 features to capture non-linear relationships. | |
| Our Champion Model is a **Stacking** model from Stream 2, which demonstrated superior performance. | |
| """) | |
| # --- NÂNG CẤP: Thêm emoji --- | |
| st.subheader("🏆 Final Model Leaderboard") | |
| st.markdown("Model leaderboard ranked by average RMSE score (lower is better).") | |
| # Gọi hàm từ benchmark_utils.py | |
| leaderboard_df = benchmark_utils.load_leaderboard() | |
| if not leaderboard_df.empty: | |
| # Lấy 10 mô hình hàng đầu và reset index (bỏ index cũ) | |
| top_10_df = leaderboard_df.head(10).reset_index(drop=True) | |
| # Đặt index mới bắt đầu từ 1 | |
| top_10_df.index = range(1, len(top_10_df) + 1) | |
| # Hiển thị Dataframe đã sửa | |
| st.dataframe(top_10_df, use_container_width=True) | |
| else: | |
| st.warning("Could not load leaderboard data.") | |
| # -------------------------------------------------------------------- | |
| # --- TAB 2: Live Forecast --- | |
| with tab2: | |
| # --- MỤC 4 TRONG CHECKLIST --- | |
| st.title("Live 5-Day Forecast") | |
| # --- ĐÃ DI CHUYỂN LOGIC DATE INPUT VÀO ĐÂY --- | |
| st.subheader("Forecast Input") | |
| selected_date = None | |
| if not X_test.empty: | |
| min_date = X_test.index.min() | |
| max_date = X_test.index.max() | |
| selected_date = st.date_input( # Đã xóa st.sidebar. | |
| "Select a date from the test set:", | |
| value=min_date, | |
| min_value=min_date, | |
| max_value=max_date, | |
| format="YYYY-MM-DD" | |
| ) | |
| else: | |
| st.error("Test data could not be loaded.") # Đã xóa st.sidebar. | |
| st.divider() # Thêm đường kẻ ngang | |
| # Biến 'selected_date' GHI Giờ đã được định nghĩa ở trên | |
| if selected_date and not X_test.empty and models: | |
| st.header(f"5-Day Forecast from: {selected_date.strftime('%Y-%m-%d')}") | |
| # 1. Lấy Input Features | |
| selected_date_ts = pd.Timestamp(selected_date) | |
| # Sửa lỗi logic: input_features phải được lấy từ X_test | |
| if selected_date_ts in X_test.index: | |
| input_features = X_test.loc[[selected_date_ts]] | |
| else: | |
| st.error("Data not found for the selected date in X_test.") | |
| input_features = pd.DataFrame() # Tạo dataframe rỗng để tránh lỗi sau | |
| if input_features.empty: | |
| st.error("Data not found for the selected date.") | |
| else: | |
| # 2. Tạo dự đoán | |
| predictions = [] | |
| for i in range(5): | |
| model = models[i] # Lấy mô hình T+i | |
| pred = model.predict(input_features)[0] | |
| predictions.append(pred) | |
| # 3. Hiển thị dự đoán (dùng st.metric) | |
| forecast_dates = pd.date_range(start=selected_date, periods=6, freq='D')[1:] | |
| cols = st.columns(5) | |
| # Lấy giá trị thực tế để so sánh | |
| actual_values = [] | |
| if selected_date_ts in all_data_df.index: | |
| actual_row = all_data_df.loc[selected_date_ts] | |
| for col_name in TARGET_COLS: | |
| actual_values.append(actual_row[col_name]) | |
| else: | |
| actual_values = [float('nan')] * 5 | |
| is_partial_forecast = any(pd.isna(v) for v in actual_values) | |
| for i in range(5): | |
| with cols[i]: | |
| actual_val = actual_values[i] | |
| delta_text = f"Actual: {actual_val:.1f}°C" if pd.notna(actual_val) else "Actual: --" | |
| st.metric( | |
| label=f"Forecast for {forecast_dates[i].strftime('%b %d')}", | |
| value=f"{predictions[i]:.1f}°C", | |
| delta=delta_text, | |
| delta_color="off" | |
| ) | |
| # --- NÂNG CẤP: Thêm "Why" Insights --- | |
| st.subheader("Forecast Insights (Why?)") | |
| # Lấy 2 features từ input_features (đã được xác nhận tồn tại) | |
| temp_lag_1 = input_features['temp_lag_1'].iloc[0] | |
| precip_today = input_features['precip'].iloc[0] | |
| # Hiển thị insight dựa trên giá trị | |
| if temp_lag_1 > 30: # Giả định 30°C là "rất nóng" | |
| st.info(f"💡 Insight: Yesterday was very hot ({temp_lag_1:.1f}°C). The model is using this strong 'persistence' signal for tomorrow's forecast.") | |
| elif temp_lag_1 < 25: # Giả định 25°C là "mát mẻ" | |
| st.info(f"💡 Insight: Yesterday was cool ({temp_lag_1:.1f}°C). This is likely pulling the initial forecast down.") | |
| if precip_today > 10: # Giả định 10mm là "ngày mưa" | |
| st.info(f"💡 Insight: The selected day had {precip_today:.1f}mm of rain. This humidity and cloud cover is factored into the forecast.") | |
| elif 'temp_lag_1' not in locals() or (temp_lag_1 >= 25 and temp_lag_1 <= 30): | |
| st.info("💡 Insight: Weather conditions appear stable. The forecast is primarily driven by seasonal trends and recent temperature history.") | |
| # --- KẾT THÚC NÂNG CẤP --- | |
| # --- NÂNG CẤP MỚI: Thêm "Feature Inspector" --- | |
| st.markdown("---") # Thêm đường kẻ ngang | |
| with st.expander("🔍 Feature Inspector: What the Model Saw on this Day"): | |
| if not input_features.empty: | |
| # Chúng ta sẽ hiển thị các tính năng trong các cột có tổ chức | |
| col1, col2, col3 = st.columns(3) | |
| # --- Cột 1: Core Weather & Persistence --- | |
| with col1: | |
| st.subheader("Core Conditions") | |
| st.metric(label="Today's Avg Temp (temp)", value=f"{input_features['temp'].iloc[0]:.1f}°C") | |
| st.metric(label="Today's 'Feels Like' (feelslike)", value=f"{input_features['feelslike'].iloc[0]:.1f}°C") | |
| st.metric(label="Humidity", value=f"{input_features['humidity'].iloc[0]:.1f}%") | |
| st.metric(label="Cloud Cover", value=f"{input_features['cloudcover'].iloc[0]:.1f}%") | |
| st.metric(label="Precipitation", value=f"{input_features['precip'].iloc[0]:.1f} mm") | |
| # --- Cột 2: Recent History (Lags & Rolling Windows) --- | |
| with col2: | |
| st.subheader("Recent History") | |
| st.metric(label="Temp Yesterday (temp_lag_1)", value=f"{input_features['temp_lag_1'].iloc[0]:.1f}°C") | |
| st.metric(label="7-Day Avg Temp (temp_roll_7d_mean)", value=f"{input_features['temp_roll_7d_mean'].iloc[0]:.1f}°C") | |
| # --- GIỮ NGUYÊN LỖI THEO YÊU CẦU --- | |
| # Code này sẽ gây lỗi KeyError nếu 'precip_roll_7d_sum' không tồn tại | |
| st.metric(label="7-Day Total Rainfall (precip_roll_7d_sum)", value=f"{input_features['precip_roll_7d_sum'].iloc[0]:.1f} mm") | |
| st.metric(label="14-Day Temp Volatility (temp_roll_14d_std)", value=f"{input_features['temp_roll_14d_std'].iloc[0]:.2f}°C") | |
| # --- Cột 3: Seasonal & Atmospheric Context --- | |
| with col3: | |
| st.subheader("Seasonal Context") | |
| st.metric(label="Day of Year", value=f"{input_features['day_of_year'].iloc[0]}") | |
| st.metric(label="Sea Level Pressure", value=f"{input_features['sealevelpressure'].iloc[0]:.1f} hPa") | |
| st.metric(label="Wind Speed", value=f"{input_features['windspeed'].iloc[0]:.1f} km/h") | |
| st.metric(label="Wind Direction", value=f"{input_features['winddir'].iloc[0]:.0f}°") | |
| else: | |
| st.warning("No feature data available for the selected date.") | |
| # --- KẾT THÚC NÂNG CẤP "Feature Inspector" --- | |
| # --- BIỂU ĐỒ DỮ LIỆU TRAINING --- | |
| st.subheader("Training Set Overview") | |
| with st.expander("Show plot of all training data (before 2024-02-18)"): | |
| train_end_date = pd.Timestamp(TEST_START_DATE) - pd.Timedelta(days=1) | |
| train_df = all_data_df.loc[:train_end_date][CURRENT_TEMP_COL] | |
| fig_train = go.Figure() | |
| fig_train.add_trace(go.Scatter( | |
| x=train_df.index, y=train_df, | |
| mode='lines', name='Training Data (Actual)', | |
| line=dict(color='#005aa7', width=1) | |
| )) | |
| fig_train.update_layout( | |
| title="Actual Temperature - Full Training Set", | |
| xaxis_title="Date", yaxis_title="Temperature (°C)", | |
| template="plotly_white", | |
| xaxis_rangeslider_visible=True, # Thêm slider | |
| yaxis_fixedrange=True # Khóa trục Y | |
| ) | |
| st.plotly_chart(fig_train, use_container_width=True) | |
| # 4. Biểu đồ Context | |
| st.subheader("Historical Context & Forecast") | |
| history_start = selected_date_ts - pd.Timedelta(days=14) | |
| history_end = selected_date_ts | |
| history_df = all_data_df.loc[history_start:history_end][CURRENT_TEMP_COL] | |
| forecast_df = pd.DataFrame({ | |
| 'Date': forecast_dates, | |
| 'Forecast': predictions | |
| }).set_index('Date') | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=history_df.index, y=history_df, | |
| mode='lines+markers', name='Past 14 Days (Actual)', | |
| line=dict(color='blue') | |
| )) | |
| fig.add_trace(go.Scatter( | |
| x=forecast_df.index, y=forecast_df['Forecast'], | |
| mode='lines+markers', name='5-Day Forecast', | |
| line=dict(color='red', dash='dot') | |
| )) | |
| fig.update_layout( | |
| title="Forecast vs. Historical Context", | |
| xaxis_title="Date", yaxis_title="Temperature (°C)", | |
| template="plotly_white", legend=dict(x=0.01, y=0.99) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # --- NÂNG CẤP: Biểu đồ thông minh hơn --- | |
| st.subheader("5-Day Forecast vs. Actual Comparison") | |
| fig_comp = go.Figure() | |
| # 1. Luôn thêm đường Dự báo | |
| fig_comp.add_trace(go.Scatter( | |
| x=forecast_dates, y=predictions, | |
| mode='lines+markers', name='5-Day Forecast', | |
| line=dict(color='red', dash='dot') | |
| )) | |
| # 2. Chỉ thêm đường Thực tế nếu có đủ 5 ngày dữ liệu | |
| if not is_partial_forecast: | |
| fig_comp.add_trace(go.Scatter( | |
| x=forecast_dates, y=actual_values, | |
| mode='lines+markers', name='5-Day Actual', | |
| line=dict(color='blue') | |
| )) | |
| fig_comp.update_layout(title="5-Day Forecast vs. Actual Values") | |
| else: | |
| # Nếu không, chỉ hiển thị dự báo | |
| fig_comp.update_layout(title="5-Day Forecast (Actual data not yet available)") | |
| # Luôn hiển thị biểu đồ | |
| fig_comp.update_layout( | |
| xaxis_title="Date", yaxis_title="Temperature (°C)", | |
| template="plotly_white", legend=dict(x=0.01, y=0.99) | |
| ) | |
| st.plotly_chart(fig_comp, use_container_width=True) | |
| # --- KẾT THÚC NÂNG CẤP --- | |
| else: | |
| # Điều chỉnh lại cảnh báo này | |
| if not selected_date: | |
| st.warning("Test data could not be loaded.") | |
| else: | |
| st.warning("Please wait... Loading data or models.") | |
| # -------------------------------------------------------------------- | |
| # --- TAB 3: Model Performance --- | |
| with tab3: | |
| # --- MỤC 5 TRONG CHECKLIST --- | |
| st.title("Model Performance & Diagnostics") | |
| if not perf_df.empty and not y_test.empty: | |
| st.subheader("Performance Degradation over 5 Days") | |
| st.markdown("How model performance changes as the forecast horizon increases.") | |
| MODEL_NAME = 'Champion (Stacking)' | |
| champion_perf_df = perf_df[perf_df['Model'] == MODEL_NAME].copy() | |
| # 1. Biểu đồ suy giảm hiệu suất (RMSE & R2) | |
| RMSE_COL_NAME = 'RMSE (Absolute Error)' | |
| R2_COL_NAME = 'R-squared' | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| fig_rmse = diag.plot_performance_degradation( | |
| champion_perf_df, | |
| metric_column=RMSE_COL_NAME, | |
| metric_name='RMSE (Temperature °C)', | |
| color='blue' | |
| ) | |
| st.plotly_chart(fig_rmse, use_container_width=True) | |
| with col2: | |
| fig_r2 = diag.plot_performance_degradation( | |
| champion_perf_df, | |
| metric_column=R2_COL_NAME, | |
| metric_name='R-squared (R²)', | |
| color='green' | |
| ) | |
| st.plotly_chart(fig_r2, use_container_width=True) | |
| # --- NÂNG CẤP: Biểu đồ tương tác với Slider --- | |
| st.subheader("Interactive Forecast vs. Actual Comparison") | |
| # 1. Thêm slider | |
| selected_horizon = st.slider( | |
| "Select Forecast Horizon (Day) to inspect:", | |
| 1, 5, 1 | |
| ) | |
| # 2. Lấy dữ liệu dự đoán (đã được cache) | |
| def get_full_test_predictions(_models, _X_test): | |
| """Run predictions on the entire test set and cache the results.""" | |
| all_preds = {} | |
| for i in range(5): | |
| model = _models[i] | |
| preds = model.predict(_X_test) | |
| all_preds[f'Day {i+1}'] = preds | |
| return pd.DataFrame(all_preds, index=_X_test.index) | |
| with st.spinner("Running predictions on entire test set... (This is cached for next time)"): | |
| y_pred_test = get_full_test_predictions(models, X_test) | |
| # 3. Chọn dữ liệu dựa trên slider | |
| y_true_selected = y_test[f'Day {selected_horizon}'] | |
| y_pred_selected = y_pred_test[f'Day {selected_horizon}'] | |
| # 4. Vẽ 1 biểu đồ duy nhất | |
| fig_interactive = diag.plot_forecast_vs_actual( | |
| y_true=y_true_selected, | |
| y_pred=y_pred_selected, | |
| day_ahead_title=f"Day {selected_horizon} Forecast" | |
| ) | |
| st.plotly_chart(fig_interactive, use_container_width=True) | |
| # --- KẾT THÚC NÂNG CẤP --- | |
| # 3. Mục Tùy chọn: Deep Dive Expander | |
| with st.expander("Champion Model Diagnostics (Deep Dive)"): | |
| st.markdown("Detailed analysis of residuals (error = actual - predicted) for the Day 1 forecast.") | |
| y_true_d1 = y_test['Day 1'] | |
| y_pred_d1 = y_pred_test['Day 1'] | |
| dates_d1 = y_test.index | |
| fig_res_time = diag.plot_residuals_vs_time( | |
| y_true_d1, y_pred_d1, dates_d1, "Day 1" | |
| ) | |
| st.plotly_chart(fig_res_time, use_container_width=True) | |
| fig_res_dist = diag.plot_residuals_distribution( | |
| y_true_d1, y_pred_d1, "Day 1" | |
| ) | |
| st.plotly_chart(fig_res_dist, use_container_width=True) | |
| st.markdown("A good model will have residuals (errors) normally distributed (bell curve) " | |
| "around 0 and show no pattern over time.") | |
| else: | |
| st.warning("Loading performance data...") | |
| # --- TAB 4: Hourly Prediction --- | |
| with tab4: | |
| st.title("Hourly Prediction (Next 24 Hours)") | |
| st.subheader("Forecast Start Time") | |
| if not X_test_hourly.empty: | |
| min_ts = X_test_hourly.index.min() | |
| max_ts = X_test_hourly.index.max() | |
| # 1. Date Selection | |
| selected_date = st.date_input( | |
| "Select the date:", | |
| value=max_ts.date(), # Mặc định chọn ngày cuối cùng | |
| min_value=min_ts.date(), | |
| max_value=max_ts.date(), | |
| format="YYYY-MM-DD", | |
| key="hourly_date_input" # Thêm key duy nhất | |
| ) | |
| # 2. Hour Selection (Chỉ show các giờ có sẵn trong ngày đã chọn) | |
| available_hours_in_day = X_test_hourly[X_test_hourly.index.date == selected_date].index.hour.unique().sort_values() | |
| if available_hours_in_day.empty: | |
| st.warning(f"No hourly data found for {selected_date}. Please select a different date.") | |
| st.stop() | |
| # Chọn giờ: Mặc định chọn giờ muộn nhất trong ngày (latest known hour) | |
| default_hour = available_hours_in_day.max() | |
| default_hour_index = available_hours_in_day.get_loc(default_hour) | |
| selected_hour = st.selectbox( | |
| "Select the latest known hour:", | |
| options=available_hours_in_day.tolist(), | |
| index=default_hour_index, | |
| format_func=lambda x: f"{x:02d}:00:00" | |
| ) | |
| # Kết hợp ngày và giờ thành Timestamp duy nhất | |
| latest_time_for_day = pd.to_datetime(f"{selected_date} {selected_hour:02d}:00:00") | |
| # Lấy Input Features cho timestamp đã chọn | |
| input_features_hourly = X_test_hourly.loc[[latest_time_for_day]] | |
| st.info(f"The model runs based on data up to the latest known hour: **{latest_time_for_day.strftime('%Y-%m-%d %H:%M:%S')}**") | |
| st.divider() | |
| # 1. Chạy Dự đoán Hourly (cho biểu đồ T+1h đến T+24h) | |
| predictions_24h = predict_next_24_hours(input_features_hourly, hourly_models_24h) | |
| # --- TÍNH TOÁN METRIC T+24h --- | |
| t_plus_24h_metric_value = predictions_24h[23] if len(predictions_24h) >= 24 else (predictions_24h[-1] if predictions_24h else float('nan')) | |
| # 2. Hiển thị Dự đoán T+24h (Tức là giờ đó ngày mai) | |
| st.subheader(f"Summary Forecast for Next Day (Starting {latest_time_for_day.strftime('%H:%M')})") | |
| forecast_start_ts = latest_time_for_day + pd.Timedelta(hours=1) | |
| # Tính các giá trị cho T+2h và T+3h | |
| t_plus_2h_value = predictions_24h[1] if len(predictions_24h) >= 2 else float('nan') | |
| t_plus_3h_value = predictions_24h[2] if len(predictions_24h) >= 3 else float('nan') | |
| # Các giá trị Max/Mean (sử dụng np đã được import) | |
| avg_temp = np.nanmean(predictions_24h) | |
| max_temp = np.nanmax(predictions_24h) | |
| # Tạo 5 cột mới để hiển thị các metric (T+2h, T+3h, T+24h, Average, Max) | |
| col_t2, col_t3, col_t24, col_avg, col_max = st.columns(5) | |
| # Tính Timestamp cho các dự báo điểm (T+2h và T+3h) | |
| forecast_t2_ts = forecast_start_ts + pd.Timedelta(hours=1) | |
| forecast_t3_ts = forecast_start_ts + pd.Timedelta(hours=2) | |
| forecast_t24_ts = forecast_start_ts + pd.Timedelta(hours=23) | |
| # --- 1. Metric T+2h --- | |
| with col_t2: | |
| st.metric( | |
| label=f"Forecast @ {forecast_t2_ts.strftime('%H:%M')} (T+2H)", | |
| value=f"{t_plus_2h_value:.1f}°C" | |
| ) | |
| # --- 2. Metric T+3h --- | |
| with col_t3: | |
| st.metric( | |
| label=f"Forecast @ {forecast_t3_ts.strftime('%H:%M')} (T+3H)", | |
| value=f"{t_plus_3h_value:.1f}°C" | |
| ) | |
| # --- 3. Metric T+24h (Giữ lại để đối chiếu) --- | |
| with col_t24: | |
| st.metric( | |
| label=f"Forecast @ {forecast_t24_ts.strftime('%H:%M')} (T+24H)", | |
| value=f"{t_plus_24h_metric_value:.1f}°C" | |
| ) | |
| # --- 4. Metric Average --- | |
| with col_avg: | |
| st.metric(label="Next 24h Average Temp", value=f"{avg_temp:.1f}°C") | |
| # --- 5. Metric Max (Sử dụng bố cục ngang) --- | |
| with col_max: | |
| st.metric(label="Next 24h Max Temp", value=f"{np.nanmax(predictions_24h):.1f}°C", | |
| delta="Peak Heat") | |
| # --- BẮT ĐẦU THAY THẾ BIỂU ĐỒ TAB 4 --- | |
| # 5.1 Graph: Bối cảnh Lịch sử & Dự báo | |
| st.subheader("Historical Context & Forecast (Hourly)") | |
| # Lấy 24 giờ lịch sử | |
| history_start_ts = latest_time_for_day - pd.Timedelta(hours=23) # Lùi 23 giờ để có 24 điểm | |
| history_end_ts = latest_time_for_day | |
| # Lấy 'temp' (actual) từ dataframe GỐC theo giờ | |
| history_df_hourly = hourly_data_df.loc[history_start_ts:history_end_ts]['temp'] | |
| # Tạo dataframe cho 24h dự báo | |
| forecast_hourly_index = pd.date_range(start=forecast_start_ts, periods=len(predictions_24h), freq='H') | |
| forecast_df_hourly = pd.DataFrame({ | |
| 'Time': forecast_hourly_index, | |
| 'Forecast': predictions_24h | |
| }).set_index('Time') | |
| # Vẽ biểu đồ | |
| fig_hist_hourly = go.Figure() | |
| fig_hist_hourly.add_trace(go.Scatter( | |
| x=history_df_hourly.index, y=history_df_hourly, | |
| mode='lines+markers', name='Past 24 Hours (Actual)', | |
| line=dict(color='blue') | |
| )) | |
| fig_hist_hourly.add_trace(go.Scatter( | |
| x=forecast_df_hourly.index, y=forecast_df_hourly['Forecast'], | |
| mode='lines+markers', name='Next 24 Hours (Forecast)', | |
| line=dict(color='red', dash='dot') | |
| )) | |
| fig_hist_hourly.update_layout( | |
| title="Hourly Forecast vs. Historical Context", | |
| xaxis_title="Time", yaxis_title="Temperature (°C)", | |
| template="plotly_white", legend=dict(x=0.01, y=0.99) | |
| ) | |
| st.plotly_chart(fig_hist_hourly, use_container_width=True) | |
| # 5.2 Graph: So sánh Dự báo vs Thực tế | |
| st.subheader("24-Hour Forecast vs. Actual Comparison") | |
| # Lấy 'temp' (actual) cho 24 giờ TỚI | |
| try: | |
| future_actuals_df = hourly_data_df.loc[forecast_hourly_index]['temp'] | |
| actual_values_24h = future_actuals_df.values | |
| except KeyError: | |
| # Xảy ra nếu forecast_hourly_index vượt ra ngoài dữ liệu | |
| actual_values_24h = [float('nan')] * len(predictions_24h) | |
| # Kiểm tra xem có bất kỳ giá trị NaN nào không | |
| is_partial_hourly_forecast = any(pd.isna(v) for v in actual_values_24h) or (len(actual_values_24h) < len(predictions_24h)) | |
| fig_comp_hourly = go.Figure() | |
| # 1. Luôn thêm đường Dự báo | |
| fig_comp_hourly.add_trace(go.Scatter( | |
| x=forecast_hourly_index, y=predictions_24h, | |
| mode='lines+markers', name='24-Hour Forecast', | |
| line=dict(color='red', dash='dot') | |
| )) | |
| # 2. Chỉ thêm đường Thực tế (màu xanh) nếu có đủ dữ liệu | |
| if not is_partial_hourly_forecast: | |
| fig_comp_hourly.add_trace(go.Scatter( | |
| x=forecast_hourly_index, y=actual_values_24h, | |
| mode='lines+markers', name='24-Hour Actual', | |
| line=dict(color='blue') | |
| )) | |
| fig_comp_hourly.update_layout(title="24-Hour Forecast vs. Actual Values") | |
| else: | |
| # Nếu không, chỉ hiển thị dự báo | |
| fig_comp_hourly.update_layout(title="24-Hour Forecast (Actual data not yet available)") | |
| # Luôn hiển thị biểu đồ | |
| fig_comp_hourly.update_layout( | |
| xaxis_title="Time", yaxis_title="Temperature (°C)", | |
| template="plotly_white", legend=dict(x=0.01, y=0.99) | |
| ) | |
| st.plotly_chart(fig_comp_hourly, use_container_width=True) | |
| # --- KẾT THÚC THAY THẾ BIỂU ĐỒ TAB 4 --- | |
| # --- NEW GRAPH 1: RMSE Degradation Plot (Reliability) --- | |
| st.subheader("Model Reliability: Error Degradation") | |
| if not hourly_perf_df.empty: | |
| # SỬ DỤNG DỮ LIỆU HIỆU SUẤT THEO GIỜ (120H) | |
| # Chỉ lấy 24 giờ đầu tiên nếu bạn muốn tập trung vào 24h forecast | |
| # Nếu muốn hiển thị 120h, hãy bỏ .head(24) | |
| df_plot = hourly_perf_df.head(24) | |
| # Giả định các cột là 'Horizon' và 'RMSE' | |
| fig_rmse_hourly = go.Figure() | |
| fig_rmse_hourly.add_trace(go.Scatter( | |
| x=df_plot['Horizon'], | |
| y=df_plot['RMSE'], | |
| mode='lines+markers', | |
| name='RMSE', | |
| line=dict(color='#005aa7') | |
| )) | |
| fig_rmse_hourly.update_layout( | |
| title="RMSE Degradation: Forecast Error vs. Hour Ahead (T+1h to T+24h)", | |
| xaxis_title="Forecast Horizon (Hours)", | |
| yaxis_title="RMSE (°C)", | |
| template="plotly_white", | |
| yaxis_range=[0, df_plot['RMSE'].max() * 1.1 if not df_plot['RMSE'].empty else 1], | |
| height=400 # Chiều cao cố định để cân đối với biểu đồ khác | |
| ) | |
| st.plotly_chart(fig_rmse_hourly, use_container_width=True) | |
| else: | |
| st.warning("Could not load Hourly RMSE Degradation data from hourly_120h_evaluation_results.csv.") | |
| # 6. Hiển thị Features Dùng để Dự đoán (Giữ nguyên) | |
| st.markdown("---") | |
| with st.expander("🔍 Feature Inspector: Hourly Inputs for the Forecast"): | |
| if not input_features_hourly.empty: | |
| important_hourly_features = [ | |
| 'temp', 'humidity', 'windspeed', 'cloudcover', | |
| 'temp_lag_1h', 'humidity_lag_24h', 'temp_diff_24h', | |
| 'temp_roll_24h_mean', 'humidity_roll_24h_mean', | |
| 'hour_sin', 'day_of_year_sin' | |
| ] | |
| col_h1, col_h2, col_h3 = st.columns(3) | |
| for i, feature in enumerate(important_hourly_features): | |
| if feature in input_features_hourly.columns: | |
| value = input_features_hourly[feature].iloc[0] | |
| label = feature.replace('_', ' ').title() | |
| target_col = [col_h1, col_h2, col_h3][i % 3] | |
| with target_col: | |
| st.metric(label=label, value=f"{value:.2f}") | |
| else: | |
| st.warning("No hourly feature data available for the selected hour.") | |
| else: | |
| st.warning("Please wait... Loading hourly data or models.") |