Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- 1. IMPORT THƯ VIỆN ---
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import joblib
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
# Import các script tiện ích của bạn từ thư mục 'src'
|
| 9 |
+
try:
|
| 10 |
+
from src import benchmark_utils
|
| 11 |
+
from src import diagnostic_plots as diag
|
| 12 |
+
except ImportError:
|
| 13 |
+
st.error("Lỗi: Không tìm thấy file 'src/benchmark_utils.py' hoặc 'src/diagnostic_plots.py'. "
|
| 14 |
+
"Hãy đảm bảo chúng tồn tại trong thư mục 'src/'.")
|
| 15 |
+
st.stop()
|
| 16 |
+
|
| 17 |
+
# --- 2. CẤU HÌNH TRANG WEB ---
|
| 18 |
+
st.set_page_config(
|
| 19 |
+
page_title="Saigon Temperature Forecast",
|
| 20 |
+
page_icon="🌦️",
|
| 21 |
+
layout="wide"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# --- 3. CÁC HÀM TẢI DỮ LIỆU & MÔ HÌNH (VỚI CACHING) ---
|
| 25 |
+
# Mục 1 & 2 trong checklist: Tải mọi thứ nặng bằng cache
|
| 26 |
+
|
| 27 |
+
@st.cache_data
|
| 28 |
+
def load_feature_data(file_path="data/final_dataset_tree.csv"):
|
| 29 |
+
"""Tải dữ liệu features và targets, chuyển đổi index thành datetime."""
|
| 30 |
+
try:
|
| 31 |
+
df = pd.read_csv(file_path)
|
| 32 |
+
|
| 33 |
+
# --- TÙY CHỈNH QUAN TRỌNG ---
|
| 34 |
+
# Đảm bảo 'datetime' là tên cột ngày tháng trong file CSV của bạn
|
| 35 |
+
DATE_COLUMN = 'datetime'
|
| 36 |
+
|
| 37 |
+
if DATE_COLUMN not in df.columns:
|
| 38 |
+
st.error(f"Lỗi: Không tìm thấy cột ngày tháng '{DATE_COLUMN}' trong 'final_dataset_tree.csv'. "
|
| 39 |
+
f"Vui lòng cập nhật biến DATE_COLUMN trong 'app.py'.")
|
| 40 |
+
return pd.DataFrame()
|
| 41 |
+
|
| 42 |
+
df[DATE_COLUMN] = pd.to_datetime(df[DATE_COLUMN])
|
| 43 |
+
df = df.set_index(DATE_COLUMN)
|
| 44 |
+
df = df.sort_index()
|
| 45 |
+
return df
|
| 46 |
+
except FileNotFoundError:
|
| 47 |
+
st.error(f"LỖI: Không tìm thấy file data chính tại: {file_path}")
|
| 48 |
+
return pd.DataFrame()
|
| 49 |
+
|
| 50 |
+
@st.cache_resource
|
| 51 |
+
def load_champion_models():
|
| 52 |
+
"""Tải 5 mô hình chuyên gia (specialist models) từ checklist."""
|
| 53 |
+
models = []
|
| 54 |
+
try:
|
| 55 |
+
for i in range(1, 6):
|
| 56 |
+
file_path = f"models/champion_stacking_day{i}.pkl"
|
| 57 |
+
model = joblib.load(file_path)
|
| 58 |
+
models.append(model)
|
| 59 |
+
return models
|
| 60 |
+
except FileNotFoundError as e:
|
| 61 |
+
st.error(f"LỖI: Không tìm thấy file mô hình. Đã kiểm tra: {e.filename}. "
|
| 62 |
+
"Hãy đảm bảo 5 file .pkl nằm trong thư mục 'models/'.")
|
| 63 |
+
return []
|
| 64 |
+
|
| 65 |
+
@st.cache_data
|
| 66 |
+
def load_performance_data(file_path="data/final_5_day_results_df.csv"):
|
| 67 |
+
"""Tải dữ liệu hiệu suất đã tính toán trước cho Tab 3."""
|
| 68 |
+
try:
|
| 69 |
+
df = pd.read_csv(file_path)
|
| 70 |
+
return df
|
| 71 |
+
except FileNotFoundError:
|
| 72 |
+
st.error(f"LỖI: Không tìm thấy file hiệu suất tại: {file_path}")
|
| 73 |
+
return pd.DataFrame()
|
| 74 |
+
|
| 75 |
+
# --- 4. KHỞI TẠO DỮ LIỆU & TÁCH TEST SET ---
|
| 76 |
+
|
| 77 |
+
# Tải tất cả dữ liệu và mô hình
|
| 78 |
+
all_data_df = load_feature_data()
|
| 79 |
+
models = load_champion_models()
|
| 80 |
+
perf_df = load_performance_data()
|
| 81 |
+
|
| 82 |
+
# --- TÙY CHỈNH QUAN TRỌNG ---
|
| 83 |
+
# Giả định tên các cột target (thực tế) trong file CSV của bạn
|
| 84 |
+
# Checklist không nói rõ, nên tôi giả định tên là 't+1', 't+2', v.v.
|
| 85 |
+
TARGET_COLS = [f't+{i}' for i in range(1, 6)]
|
| 86 |
+
# Giả định tên cột nhiệt độ của ngày HIỆN TẠI (dùng để vẽ lịch sử)
|
| 87 |
+
CURRENT_TEMP_COL = 'temp'
|
| 88 |
+
|
| 89 |
+
# Tách test set (dựa trên ngày trong checklist)
|
| 90 |
+
TEST_START_DATE = "2024-02-20"
|
| 91 |
+
TEST_END_DATE = "2025-09-26"
|
| 92 |
+
|
| 93 |
+
X_test, y_test, test_df = pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 94 |
+
|
| 95 |
+
if not all_data_df.empty:
|
| 96 |
+
try:
|
| 97 |
+
test_df = all_data_df.loc[TEST_START_DATE:TEST_END_DATE].copy()
|
| 98 |
+
|
| 99 |
+
# Giả định: 157 features là TẤT CẢ các cột KHÔNG PHẢI là target
|
| 100 |
+
feature_cols = [col for col in test_df.columns if col not in TARGET_COLS]
|
| 101 |
+
|
| 102 |
+
# Tách X_test (features) và y_test (thực tế)
|
| 103 |
+
X_test = test_df[feature_cols]
|
| 104 |
+
y_test = test_df[TARGET_COLS]
|
| 105 |
+
|
| 106 |
+
# Đổi tên cột y_test cho dễ hiểu (dùng trong Tab 3)
|
| 107 |
+
y_test.columns = [f'Day {i}' for i in range(1, 6)]
|
| 108 |
+
except KeyError:
|
| 109 |
+
st.error(f"Lỗi: Không tìm thấy cột target (ví dụ: '{TARGET_COLS[0]}') hoặc cột "
|
| 110 |
+
f"'{CURRENT_TEMP_COL}' trong file CSV. Vui lòng cập nhật 'app.py'.")
|
| 111 |
+
except Exception as e:
|
| 112 |
+
st.error(f"Lỗi khi xử lý test set: {e}")
|
| 113 |
+
else:
|
| 114 |
+
st.error("Không thể tải dữ liệu chính, ứng dụng không thể tiếp tục.")
|
| 115 |
+
st.stop()
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# --- 5. GIAO DIỆN SIDEBAR (THANH ĐIỀU HƯỚNG) ---
|
| 119 |
+
|
| 120 |
+
st.sidebar.title("Navigation")
|
| 121 |
+
app_section = st.sidebar.radio(
|
| 122 |
+
"Choose a section:",
|
| 123 |
+
("Project Overview & Methodology", "Live 5-Day Forecast", "Model Performance & Diagnostics")
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Date input chỉ hiển thị khi ở tab "Live Forecast"
|
| 127 |
+
selected_date = None
|
| 128 |
+
if app_section == "Live 5-Day Forecast":
|
| 129 |
+
st.sidebar.header("Forecast Input")
|
| 130 |
+
|
| 131 |
+
if not X_test.empty:
|
| 132 |
+
min_date = X_test.index.min()
|
| 133 |
+
max_date = X_test.index.max()
|
| 134 |
+
|
| 135 |
+
selected_date = st.sidebar.date_input(
|
| 136 |
+
"Select a date from the test set:",
|
| 137 |
+
value=min_date,
|
| 138 |
+
min_value=min_date,
|
| 139 |
+
max_value=max_date,
|
| 140 |
+
format="YYYY-MM-DD"
|
| 141 |
+
)
|
| 142 |
+
else:
|
| 143 |
+
st.sidebar.error("Test data could not be loaded.")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# --- 6. GIAO DIỆN CHÍNH (MAIN PANEL) ---
|
| 147 |
+
|
| 148 |
+
if app_section == "Project Overview & Methodology":
|
| 149 |
+
# --- MỤC 3 TRONG CHECKLIST ---
|
| 150 |
+
st.title("Saigon Temperature Forecasting Application 🌦️")
|
| 151 |
+
|
| 152 |
+
st.subheader("Project Summary")
|
| 153 |
+
st.markdown("""
|
| 154 |
+
Mục tiêu của dự án này là dự đoán nhiệt độ trung bình hàng ngày cho TP. Hồ Chí Minh trong 5 ngày tới.
|
| 155 |
+
|
| 156 |
+
* **Dữ liệu:** Dữ liệu thời tiết lịch sử 10 năm từ Visual Crossing.
|
| 157 |
+
* **Mô hình:** Chúng tôi sử dụng 5 mô hình 'chuyên gia' (specialist models) - mỗi mô hình được tối ưu để dự đoán một ngày cụ thể trong tương lai (T+1 đến T+5).
|
| 158 |
+
""")
|
| 159 |
+
|
| 160 |
+
st.subheader("Our 'Two-Stream' Strategy")
|
| 161 |
+
st.markdown("""
|
| 162 |
+
Để tối ưu hóa hiệu suất, chúng tôi đã áp dụng chiến lược "Hai luồng" (Two-Stream):
|
| 163 |
+
1. **Luồng 1 (Linear Models):** Các mô hình tuyến tính (như Linear Regression) được huấn luyện trên một bộ features đã được tinh gọn (sử dụng VIF) để tránh đa cộng tuyến.
|
| 164 |
+
2. **Luồng 2 (Tree-based Models):** Các mô hình phức tạp hơn (như Random Forest, Gradient Boosting) được huấn luyện trên một bộ features toàn diện (157 features) để nắm bắt các mối quan hệ phi tuyến.
|
| 165 |
+
|
| 166 |
+
Mô hình chiến thắng (Champion Model) của chúng tôi là một mô hình **Stacking** từ Luồng 2, cho thấy hiệu suất vượt trội.
|
| 167 |
+
""")
|
| 168 |
+
|
| 169 |
+
st.subheader("Final Model Leaderboard")
|
| 170 |
+
st.markdown("Bảng xếp hạng các mô hình dựa trên điểm RMSE trung bình (càng thấp càng tốt).")
|
| 171 |
+
|
| 172 |
+
# Gọi hàm từ benchmark_utils.py
|
| 173 |
+
leaderboard_df = benchmark_utils.load_leaderboard()
|
| 174 |
+
|
| 175 |
+
if not leaderboard_df.empty:
|
| 176 |
+
# Hiển thị 10 mô hình hàng đầu
|
| 177 |
+
st.dataframe(leaderboard_df.head(10), use_container_width=True)
|
| 178 |
+
else:
|
| 179 |
+
st.warning("Không thể tải dữ liệu leaderboard.")
|
| 180 |
+
|
| 181 |
+
# --------------------------------------------------------------------
|
| 182 |
+
|
| 183 |
+
elif app_section == "Live 5-Day Forecast":
|
| 184 |
+
# --- MỤC 4 TRONG CHECKLIST ---
|
| 185 |
+
st.title("Live 5-Day Forecast")
|
| 186 |
+
|
| 187 |
+
if selected_date and not X_test.empty and models:
|
| 188 |
+
st.header(f"Dự báo cho 5 ngày tới từ: {selected_date.strftime('%Y-%m-%d')}")
|
| 189 |
+
|
| 190 |
+
# 1. Lấy Input Features
|
| 191 |
+
selected_date_ts = pd.Timestamp(selected_date)
|
| 192 |
+
input_features = X_test.loc[[selected_date_ts]]
|
| 193 |
+
|
| 194 |
+
if input_features.empty:
|
| 195 |
+
st.error("Không tìm thấy dữ liệu cho ngày đã chọn.")
|
| 196 |
+
else:
|
| 197 |
+
# 2. Tạo dự đoán
|
| 198 |
+
predictions = []
|
| 199 |
+
for i in range(5):
|
| 200 |
+
model = models[i] # Lấy mô hình T+i
|
| 201 |
+
pred = model.predict(input_features)[0]
|
| 202 |
+
predictions.append(pred)
|
| 203 |
+
|
| 204 |
+
# 3. Hiển thị dự đoán (dùng st.metric)
|
| 205 |
+
forecast_dates = pd.date_range(start=selected_date, periods=6, freq='D')[1:]
|
| 206 |
+
cols = st.columns(5)
|
| 207 |
+
|
| 208 |
+
# Lấy giá trị thực tế để so sánh
|
| 209 |
+
actual_values = y_test.loc[selected_date_ts].values
|
| 210 |
+
|
| 211 |
+
for i in range(5):
|
| 212 |
+
with cols[i]:
|
| 213 |
+
st.metric(
|
| 214 |
+
label=f"Forecast for {forecast_dates[i].strftime('%b %d')}",
|
| 215 |
+
value=f"{predictions[i]:.1f}°C",
|
| 216 |
+
delta=f"Actual: {actual_values[i]:.1f}°C",
|
| 217 |
+
delta_color="off" # Màu xám trung tính
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
# 4. Biểu đồ (Optimal Suggestion)
|
| 221 |
+
st.subheader("Historical Context & Forecast")
|
| 222 |
+
|
| 223 |
+
# Lấy 14 ngày lịch sử
|
| 224 |
+
history_start = selected_date_ts - pd.Timedelta(days=14)
|
| 225 |
+
history_end = selected_date_ts
|
| 226 |
+
|
| 227 |
+
# Lấy dữ liệu 'temp' thực tế từ dataframe gốc
|
| 228 |
+
history_df = all_data_df.loc[history_start:history_end][CURRENT_TEMP_COL]
|
| 229 |
+
|
| 230 |
+
# Tạo dataframe cho dự báo
|
| 231 |
+
forecast_df = pd.DataFrame({
|
| 232 |
+
'Date': forecast_dates,
|
| 233 |
+
'Forecast': predictions
|
| 234 |
+
}).set_index('Date')
|
| 235 |
+
|
| 236 |
+
fig = go.Figure()
|
| 237 |
+
|
| 238 |
+
fig.add_trace(go.Scatter(
|
| 239 |
+
x=history_df.index, y=history_df,
|
| 240 |
+
mode='lines+markers', name='Past 14 Days (Actual)',
|
| 241 |
+
line=dict(color='blue')
|
| 242 |
+
))
|
| 243 |
+
fig.add_trace(go.Scatter(
|
| 244 |
+
x=forecast_df.index, y=forecast_df['Forecast'],
|
| 245 |
+
mode='lines+markers', name='5-Day Forecast',
|
| 246 |
+
line=dict(color='red', dash='dot')
|
| 247 |
+
))
|
| 248 |
+
|
| 249 |
+
fig.update_layout(
|
| 250 |
+
title="Forecast vs. Historical Context",
|
| 251 |
+
xaxis_title="Date", yaxis_title="Temperature (°C)",
|
| 252 |
+
template="plotly_white", legend=dict(x=0.01, y=0.99)
|
| 253 |
+
)
|
| 254 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 255 |
+
else:
|
| 256 |
+
st.warning("Vui lòng đợi... Đang tải dữ liệu hoặc mô hình.")
|
| 257 |
+
|
| 258 |
+
# --------------------------------------------------------------------
|
| 259 |
+
|
| 260 |
+
elif app_section == "Model Performance & Diagnostics":
|
| 261 |
+
# --- MỤC 5 TRONG CHECKLIST ---
|
| 262 |
+
st.title("Model Performance & Diagnostics")
|
| 263 |
+
|
| 264 |
+
if not perf_df.empty and not y_test.empty:
|
| 265 |
+
st.subheader("Performance Degradation over 5 Days")
|
| 266 |
+
st.markdown("Hiệu suất mô hình thay đổi như thế nào khi dự báo xa hơn.")
|
| 267 |
+
|
| 268 |
+
# 1. Biểu đồ suy giảm hiệu suất (RMSE & R2)
|
| 269 |
+
|
| 270 |
+
# --- TÙY CHỈNH ---
|
| 271 |
+
# Đảm bảo 'RMSE' và 'R2' là tên cột chính xác trong file 'final_5_day_results_df.csv'
|
| 272 |
+
RMSE_COL_NAME = 'RMSE'
|
| 273 |
+
R2_COL_NAME = 'R2'
|
| 274 |
+
|
| 275 |
+
col1, col2 = st.columns(2)
|
| 276 |
+
with col1:
|
| 277 |
+
fig_rmse = diag.plot_performance_degradation(
|
| 278 |
+
perf_df,
|
| 279 |
+
metric_column=RMSE_COL_NAME,
|
| 280 |
+
metric_name='RMSE (Temperature °C)',
|
| 281 |
+
color='blue'
|
| 282 |
+
)
|
| 283 |
+
st.plotly_chart(fig_rmse, use_container_width=True)
|
| 284 |
+
with col2:
|
| 285 |
+
fig_r2 = diag.plot_performance_degradation(
|
| 286 |
+
perf_df,
|
| 287 |
+
metric_column=R2_COL_NAME,
|
| 288 |
+
metric_name='R-squared (R²)',
|
| 289 |
+
color='green'
|
| 290 |
+
)
|
| 291 |
+
st.plotly_chart(fig_r2, use_container_width=True)
|
| 292 |
+
|
| 293 |
+
# 2. Biểu đồ Dự báo vs. Thực tế
|
| 294 |
+
st.subheader("Forecast vs. Actual Comparison (on entire test set)")
|
| 295 |
+
|
| 296 |
+
# Hàm này chạy dự đoán trên *toàn bộ* X_test (hàng ngàn dòng)
|
| 297 |
+
# Nó sẽ rất chậm nếu không có cache
|
| 298 |
+
@st.cache_data
|
| 299 |
+
def get_full_test_predictions(_models, _X_test):
|
| 300 |
+
"""Chạy dự đoán trên toàn bộ test set và cache lại."""
|
| 301 |
+
all_preds = {}
|
| 302 |
+
for i in range(5):
|
| 303 |
+
model = _models[i]
|
| 304 |
+
preds = model.predict(_X_test)
|
| 305 |
+
all_preds[f'Day {i+1}'] = preds
|
| 306 |
+
return pd.DataFrame(all_preds, index=_X_test.index)
|
| 307 |
+
|
| 308 |
+
with st.spinner("Running predictions on entire test set... (This is cached for next time)"):
|
| 309 |
+
y_pred_test = get_full_test_predictions(models, X_test)
|
| 310 |
+
|
| 311 |
+
col1, col2 = st.columns(2)
|
| 312 |
+
with col1:
|
| 313 |
+
fig_d1 = diag.plot_forecast_vs_actual(
|
| 314 |
+
y_true=y_test['Day 1'],
|
| 315 |
+
y_pred=y_pred_test['Day 1'],
|
| 316 |
+
day_ahead_title="Day 1 Forecast"
|
| 317 |
+
)
|
| 318 |
+
st.plotly_chart(fig_d1, use_container_width=True)
|
| 319 |
+
with col2:
|
| 320 |
+
fig_d5 = diag.plot_forecast_vs_actual(
|
| 321 |
+
y_true=y_test['Day 5'],
|
| 322 |
+
y_pred=y_pred_test['Day 5'],
|
| 323 |
+
day_ahead_title="Day 5 Forecast"
|
| 324 |
+
)
|
| 325 |
+
st.plotly_chart(fig_d5, use_container_width=True)
|
| 326 |
+
|
| 327 |
+
# 3. Mục Tùy chọn: Deep Dive Expander
|
| 328 |
+
with st.expander("Champion Model Diagnostics (Deep Dive)"):
|
| 329 |
+
st.markdown("Phân tích chi tiết phần dư (lỗi = thực tế - dự báo) cho dự báo Day 1.")
|
| 330 |
+
|
| 331 |
+
y_true_d1 = y_test['Day 1']
|
| 332 |
+
y_pred_d1 = y_pred_test['Day 1']
|
| 333 |
+
dates_d1 = y_test.index
|
| 334 |
+
|
| 335 |
+
fig_res_time = diag.plot_residuals_vs_time(
|
| 336 |
+
y_true_d1, y_pred_d1, dates_d1, "Day 1"
|
| 337 |
+
)
|
| 338 |
+
st.plotly_chart(fig_res_time, use_container_width=True)
|
| 339 |
+
|
| 340 |
+
fig_res_dist = diag.plot_residuals_distribution(
|
| 341 |
+
y_true_d1, y_pred_d1, "Day 1"
|
| 342 |
+
)
|
| 343 |
+
st.plotly_chart(fig_res_dist, use_container_width=True)
|
| 344 |
+
st.markdown("Một mô hình tốt sẽ có phần dư (lỗi) phân phối chuẩn (hình chuông) "
|
| 345 |
+
"quanh giá trị 0 và không có xu hướng (pattern) nào theo thời gian.")
|
| 346 |
+
|
| 347 |
+
else:
|
| 348 |
+
st.warning("Đang tải dữ liệu hiệu suất...")
|