Harveyntt commited on
Commit
8a9ed0e
·
verified ·
1 Parent(s): 88d3c90

Upload 4 files

Browse files
src/__init__.py ADDED
File without changes
src/benchmark_utils.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ @st.cache_data(hash_funcs={pd.DataFrame: lambda _: None})
5
+ def load_leaderboard(file_path="data/results_df_all_tuned.csv"):
6
+ """
7
+ Tải và xử lý file CSV chứa kết quả leaderboard của các mô hình.
8
+
9
+ Args:
10
+ file_path (str): Đường dẫn đến file CSV leaderboard.
11
+
12
+ Returns:
13
+ pd.DataFrame: DataFrame đã được sắp xếp, sẵn sàng để hiển thị.
14
+ """
15
+ try:
16
+ df = pd.read_csv(file_path)
17
+
18
+ SORT_COLUMN_NAME = 'RMSE (Absolute Error)'
19
+
20
+ if SORT_COLUMN_NAME in df.columns:
21
+ df_sorted = df.sort_values(by=SORT_COLUMN_NAME, ascending=True)
22
+ else:
23
+ st.warning(f"Không tìm thấy cột '{SORT_COLUMN_NAME}' để sắp xếp leaderboard. "
24
+ f"Vui lòng kiểm tra file `src/benchmark_utils.py`.")
25
+ df_sorted = df
26
+
27
+ return df_sorted
28
+
29
+ except FileNotFoundError:
30
+ st.error(f"LỖI: Không tìm thấy file leaderboard tại đường dẫn: {file_path}")
31
+ return pd.DataFrame() # Trả về DataFrame rỗng nếu có lỗi
32
+ except Exception as e:
33
+ st.error(f"Lỗi khi tải leaderboard: {e}")
34
+ return pd.DataFrame()
src/diagnostic_plots.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import plotly.graph_objects as go
3
+ import plotly.express as px
4
+ import streamlit as st # Cần thiết để báo lỗi nếu cột không tồn tại
5
+
6
+ # --- HÀM 1: Biểu đồ suy giảm hiệu suất (Theo Checklist mục 5) ---
7
+
8
+ def plot_performance_degradation(df, metric_column, metric_name, color='blue'):
9
+ """
10
+ Tạo biểu đồ đường (line plot) cho thấy một chỉ số (metric) thay đổi
11
+ như thế nào qua 5 ngày dự báo.
12
+
13
+ Args:
14
+ df (pd.DataFrame): DataFrame được tải từ 'final_5_day_results_df.csv'.
15
+ metric_column (str): Tên cột chính xác trong CSV (ví dụ: 'RMSE (Absolute Error)').
16
+ metric_name (str): Tên hiển thị đẹp cho trục Y (ví dụ: 'RMSE (Temperature °C)').
17
+ color (str): Tên màu cho đường line.
18
+
19
+ Returns:
20
+ plotly.graph_objects.Figure: Một đối tượng biểu đồ Plotly.
21
+ """
22
+
23
+ # --- TÙY CHỈNH QUAN TRỌNG (ĐÃ SỬA) ---
24
+ # Cột chứa "Day 1", "Day 2",... là 'Horizon'
25
+ DAY_AHEAD_COLUMN = 'Horizon'
26
+ # ---------------------------
27
+
28
+ if DAY_AHEAD_COLUMN not in df.columns:
29
+ st.error(f"Lỗi plot: Không tìm thấy cột '{DAY_AHEAD_COLUMN}' trong dữ liệu. "
30
+ f"Vui lòng kiểm tra file `src/diagnostic_plots.py`.")
31
+ return go.Figure()
32
+ if metric_column not in df.columns:
33
+ st.error(f"Lỗi plot: Không tìm thấy cột '{metric_column}' trong dữ liệu. "
34
+ f"Vui lòng kiểm tra file `src/diagnostic_plots.py`.")
35
+ return go.Figure()
36
+
37
+ # --- SỬA LỖI LOGIC: Chuyển "Day 1" thành số 1 ---
38
+ # Tạo một bản copy để tránh cảnh báo
39
+ plot_df = df.copy()
40
+
41
+ # Trích xuất số từ cột 'Horizon' (ví dụ: 'Day 1' -> 1)
42
+ # và tạo cột mới 'day_num'
43
+ plot_df['day_num'] = plot_df[DAY_AHEAD_COLUMN].str.extract(r'(\d+)').astype(int)
44
+ plot_df = plot_df.sort_values(by='day_num')
45
+ # ---------------------------------------------
46
+
47
+ fig = go.Figure()
48
+
49
+ fig.add_trace(go.Scatter(
50
+ x=plot_df['day_num'], # Dùng cột số 'day_num' mới cho trục X
51
+ y=plot_df[metric_column],
52
+ mode='lines+markers',
53
+ name=metric_name,
54
+ line=dict(color=color, width=3),
55
+ marker=dict(size=8)
56
+ ))
57
+
58
+ fig.update_layout(
59
+ title=f"<b>{metric_name} vs. Forecast Horizon</b>",
60
+ xaxis_title="Day Ahead (Horizon)",
61
+ yaxis_title=metric_name,
62
+ title_x=0.5, # Căn giữa tiêu đề
63
+ template="plotly_white",
64
+ xaxis = dict(tickmode = 'linear', tick0 = 1, dtick = 1) # Đảm bảo trục X là 1, 2, 3, 4, 5
65
+ )
66
+
67
+ # Nếu là R2, đặt giới hạn trục y từ 0 đến 1 cho dễ nhìn
68
+ if "R2" in metric_name or "R-squared" in metric_name:
69
+ fig.update_layout(yaxis_range=[0, 1])
70
+
71
+ return fig
72
+
73
+ # --- HÀM 2: Biểu đồ Dự báo vs. Thực tế (Theo Checklist mục 5) ---
74
+
75
+ def plot_forecast_vs_actual(y_true, y_pred, day_ahead_title):
76
+ """
77
+ Tạo biểu đồ phân tán (scatter plot) so sánh giá trị dự báo và giá trị thực tế.
78
+
79
+ Args:
80
+ y_true (array-like): Mảng chứa các giá trị thực tế.
81
+ y_pred (array-like): Mảng chứa các giá trị dự báo.
82
+ day_ahead_title (str): Tiêu đề phụ (ví dụ: "Day 1" hoặc "Day 5").
83
+
84
+ Returns:
85
+ plotly.graph_objects.Figure: Một đối tượng biểu đồ Plotly.
86
+ """
87
+
88
+ # Tạo DataFrame tạm thời để vẽ
89
+ plot_df = pd.DataFrame({
90
+ 'Actual': y_true,
91
+ 'Predicted': y_pred
92
+ })
93
+
94
+ fig = px.scatter(
95
+ plot_df,
96
+ x='Actual',
97
+ y='Predicted',
98
+ title=f"<b>Forecast vs. Actual - {day_ahead_title}</b>",
99
+ opacity=0.7,
100
+ hover_data={'Actual': ':.2f', 'Predicted': ':.2f'}
101
+ )
102
+
103
+ # Thêm đường chéo (y=x) thể hiện dự báo hoàn hảo
104
+ min_val = min(plot_df['Actual'].min(), plot_df['Predicted'].min())
105
+ max_val = max(plot_df['Actual'].max(), plot_df['Predicted'].max())
106
+
107
+ fig.add_trace(go.Scatter(
108
+ x=[min_val, max_val],
109
+ y=[min_val, max_val],
110
+ mode='lines',
111
+ name='Perfect Prediction',
112
+ line=dict(color='red', dash='dash', width=2)
113
+ ))
114
+
115
+ fig.update_layout(
116
+ title_x=0.5,
117
+ xaxis_title="Actual Temperature (°C)",
118
+ yaxis_title="Predicted Temperature (°C)",
119
+ template="plotly_white"
120
+ )
121
+ return fig
122
+
123
+ # --- CÁC HÀM 3 & 4: Biểu đồ "Deep Dive" (Theo Checklist mục 5 - Tùy chọn) ---
124
+
125
+ def plot_residuals_vs_time(y_true, y_pred, dates, day_ahead_title):
126
+ """
127
+ Tạo biểu đồ phân tán của phần dư (residuals) theo thời gian.
128
+
129
+ Args:
130
+ y_true (array-like): Mảng giá trị thực tế.
131
+ y_pred (array-like): Mảng giá trị dự báo.
132
+ dates (array-like): Mảng chứa ngày th��ng tương ứng.
133
+ day_ahead_title (str): Tiêu đề phụ (ví dụ: "Day 1").
134
+
135
+ Returns:
136
+ plotly.graph_objects.Figure: Một đối tượng biểu đồ Plotly.
137
+ """
138
+ residuals = y_true - y_pred
139
+
140
+ plot_df = pd.DataFrame({
141
+ 'Date': dates,
142
+ 'Residual': residuals
143
+ })
144
+
145
+ fig = px.scatter(
146
+ plot_df,
147
+ x='Date',
148
+ y='Residual',
149
+ title=f"<b>Residuals vs. Time - {day_ahead_title}</b>",
150
+ opacity=0.7
151
+ )
152
+
153
+ # Thêm đường y=0 (lỗi bằng 0)
154
+ fig.add_hline(y=0, line=dict(color='red', dash='dash', width=2))
155
+
156
+ fig.update_layout(
157
+ title_x=0.5,
158
+ yaxis_title="Residual (Actual - Predicted)",
159
+ template="plotly_white"
160
+ )
161
+ return fig
162
+
163
+
164
+ def plot_residuals_distribution(y_true, y_pred, day_ahead_title):
165
+ """
166
+ Tạo biểu đồ histogram phân phối của phần dư (residuals).
167
+
168
+ Args:
169
+ y_true (array-like): Mảng giá trị thực tế.
170
+ y_pred (array-like): Mảng giá trị dự báo.
171
+ day_ahead_title (str): Tiêu đề phụ (ví dụ: "Day 1").
172
+
173
+ Returns:
174
+ plotly.graph_objects.Figure: Một đối tượng biểu đồ Plotly.
175
+ """
176
+ residuals = y_true - y_pred
177
+
178
+ fig = px.histogram(
179
+ residuals,
180
+ nbins=50,
181
+ title=f"<b>Residuals Distribution - {day_ahead_title}</b>"
182
+ )
183
+
184
+ fig.update_layout(
185
+ title_x=0.5,
186
+ xaxis_title="Residual (Error)",
187
+ yaxis_title="Count",
188
+ template="plotly_white"
189
+ )
190
+ return fig
src/feature_engineering_live.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+
5
+ def create_live_feature_vector(live_daily_summary: dict, historical_data: pd.DataFrame) -> pd.DataFrame:
6
+ """Create a single-row DataFrame of features suitable for the 5-day models.
7
+
8
+ This is a pragmatic, reduced-feature implementation: it fills a template row
9
+ using the last historical day as a baseline and replaces/engineers the most
10
+ important features from live_daily_summary + recent history.
11
+
12
+ Note: The full project used ~157 features. Implementing all of them here is
13
+ tedious and error-prone; this function focuses on ~25 high-importance
14
+ features commonly used in temperature forecasting. It will also attempt to
15
+ preserve the original columns order (using historical_data.columns) so
16
+ models expecting the same schema are less likely to fail.
17
+ """
18
+ if historical_data is None or historical_data.empty:
19
+ raise ValueError("historical_data must be a non-empty DataFrame")
20
+
21
+ # Use the last historical row as a template (copy to avoid mutation)
22
+ template = historical_data.iloc[-1].copy()
23
+
24
+ # Start with a series having same index as template (so column ordering is preserved)
25
+ today_row = pd.Series(index=historical_data.columns, dtype="float64")
26
+
27
+ # Basic direct mappings (if columns exist)
28
+ mappings = {
29
+ 'temp': ['temp', 'temperature', 'avg_temp'],
30
+ 'feelslike': ['feelslike', 'feels_like'],
31
+ 'humidity': ['humidity'],
32
+ 'precip': ['precip', 'precipitation', 'rain'],
33
+ 'windspeed': ['windspeed', 'wind_speed', 'windspd'],
34
+ 'cloudcover': ['cloudcover', 'clouds', 'cloud_percent']
35
+ }
36
+
37
+ for feature, candidates in mappings.items():
38
+ val = None
39
+ for c in candidates:
40
+ if c in live_daily_summary:
41
+ val = live_daily_summary.get(c)
42
+ break
43
+ # fallback to nested keys in OpenWeather-like structures
44
+ if val is None and 'main' in live_daily_summary and feature in live_daily_summary['main']:
45
+ val = live_daily_summary['main'].get(feature)
46
+ if val is None and feature in live_daily_summary:
47
+ val = live_daily_summary.get(feature)
48
+
49
+ # Put into today_row if a matching column exists
50
+ for col in historical_data.columns:
51
+ if col == feature and val is not None:
52
+ today_row[col] = float(val)
53
+
54
+ # If 'temp' column still missing fill from template or live summary
55
+ if 'temp' in historical_data.columns and pd.isna(today_row.get('temp')):
56
+ if 'temp' in live_daily_summary:
57
+ today_row['temp'] = float(live_daily_summary['temp'])
58
+ else:
59
+ today_row['temp'] = float(template.get('temp', np.nan))
60
+
61
+ # Temporal features
62
+ today_ts = pd.Timestamp.now().normalize()
63
+ if 'year' in historical_data.columns:
64
+ today_row['year'] = today_ts.year
65
+ if 'month' in historical_data.columns:
66
+ today_row['month'] = today_ts.month
67
+ if 'day_of_year' in historical_data.columns:
68
+ today_row['day_of_year'] = today_ts.dayofyear
69
+
70
+ # Lag features (use recent historical days)
71
+ def safe_hist(col, offset=1):
72
+ idx = -offset
73
+ try:
74
+ return float(historical_data[col].iloc[idx])
75
+ except Exception:
76
+ return np.nan
77
+
78
+ if 'temp_lag_1' in historical_data.columns:
79
+ today_row['temp_lag_1'] = safe_hist('temp', 1)
80
+ if 'temp_lag_2' in historical_data.columns:
81
+ today_row['temp_lag_2'] = safe_hist('temp', 2)
82
+ if 'humidity_lag_1' in historical_data.columns:
83
+ today_row['humidity_lag_1'] = safe_hist('humidity', 1)
84
+
85
+ # Rolling windows: combine last N historical days with today's live 'temp' when available
86
+ def rolling_stat(col, window=7, stat='mean'):
87
+ try:
88
+ hist_vals = historical_data[col].dropna().iloc[-(window-1):].astype(float)
89
+ if not np.isnan(today_row.get(col)):
90
+ combined = pd.concat([hist_vals, pd.Series([today_row[col]])], ignore_index=True)
91
+ else:
92
+ combined = hist_vals
93
+ if combined.empty:
94
+ return np.nan
95
+ if stat == 'mean':
96
+ return float(combined.mean())
97
+ if stat == 'std':
98
+ return float(combined.std())
99
+ if stat == 'sum':
100
+ return float(combined.sum())
101
+ return np.nan
102
+ except Exception:
103
+ return np.nan
104
+
105
+ if 'temp_roll_7d_mean' in historical_data.columns:
106
+ today_row['temp_roll_7d_mean'] = rolling_stat('temp', window=7, stat='mean')
107
+ if 'temp_roll_7d_std' in historical_data.columns:
108
+ today_row['temp_roll_7d_std'] = rolling_stat('temp', window=7, stat='std')
109
+ if 'temp_roll_14d_std' in historical_data.columns:
110
+ today_row['temp_roll_14d_std'] = rolling_stat('temp', window=14, stat='std')
111
+
112
+ # If the model expects precip_roll_7d_sum and we can compute it
113
+ if 'precip' in historical_data.columns and 'precip_roll_7d_sum' in historical_data.columns:
114
+ today_row['precip_roll_7d_sum'] = rolling_stat('precip', window=7, stat='sum')
115
+
116
+ # Fill other columns conservatively using the last historical values (template)
117
+ for col in historical_data.columns:
118
+ if pd.isna(today_row.get(col)):
119
+ try:
120
+ today_row[col] = float(template[col]) if pd.notna(template[col]) else np.nan
121
+ except Exception:
122
+ today_row[col] = np.nan
123
+
124
+ # Convert to single-row DataFrame and ensure dtypes
125
+ today_df = pd.DataFrame([today_row])
126
+ today_df.index = [pd.Timestamp.now()]
127
+
128
+ # Reorder columns to match historical_data (already aligned) and return
129
+ today_df = today_df.reindex(columns=historical_data.columns)
130
+ return today_df