fix tiep
Browse files- src/forecasting.py +19 -6
- src/model_training.py +16 -3
- src/monitoring.py +20 -1
src/forecasting.py
CHANGED
|
@@ -9,6 +9,7 @@ import requests
|
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
from datetime import datetime, timedelta
|
|
|
|
| 12 |
from io import StringIO
|
| 13 |
from sklearn.base import BaseEstimator, TransformerMixin
|
| 14 |
from sklearn.metrics import mean_squared_error
|
|
@@ -31,9 +32,20 @@ PIPE_1 = BASE_DIR / "pipelines" / "preprocessing_pipeline.pkl"
|
|
| 31 |
# PIPE_2 = r"pipelines/featureSelection_pipeline.pkl"
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# --- Lấy dữ liệu mới nhất ---
|
| 35 |
def fetch_latest_weather_data(location="Hanoi", days=35):
|
| 36 |
-
|
|
|
|
| 37 |
start_date = end_date - timedelta(days=days)
|
| 38 |
|
| 39 |
base_url = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline"
|
|
@@ -69,6 +81,7 @@ def update_three_year_data(new_data: pd.DataFrame, data_path=DATA_PATH):
|
|
| 69 |
- Giữ lại đúng 3 năm gần nhất (tính từ hôm nay)
|
| 70 |
"""
|
| 71 |
data_path = Path(data_path)
|
|
|
|
| 72 |
|
| 73 |
# Đảm bảo có cột datetime
|
| 74 |
if "datetime" not in new_data.columns:
|
|
@@ -94,7 +107,7 @@ def update_three_year_data(new_data: pd.DataFrame, data_path=DATA_PATH):
|
|
| 94 |
combined.drop_duplicates(subset=["datetime"], keep="last", inplace=True)
|
| 95 |
|
| 96 |
# Giới hạn dữ liệu trong 3 năm gần nhất
|
| 97 |
-
three_years_ago = datetime.
|
| 98 |
combined = combined[combined["datetime"] >= three_years_ago]
|
| 99 |
|
| 100 |
# sắp xếp và lưu lại
|
|
@@ -103,8 +116,6 @@ def update_three_year_data(new_data: pd.DataFrame, data_path=DATA_PATH):
|
|
| 103 |
combined.to_csv(data_path, index=False)
|
| 104 |
|
| 105 |
# kiểm tra
|
| 106 |
-
# print(f"Dữ liệu đã được cập nhật, còn lại {len(combined)} dòng (~3 năm).")
|
| 107 |
-
# print(f"Khoảng thời gian: {combined['datetime'].min().date()} → {combined['datetime'].max().date()}")
|
| 108 |
logging.info(f"Dữ liệu đã được cập nhật, còn lại {len(combined)} dòng (~3 năm).")
|
| 109 |
logging.info(f"Khoảng thời gian: {combined['datetime'].min().date()} → {combined['datetime'].max().date()}")
|
| 110 |
|
|
@@ -156,6 +167,7 @@ def log_rmse_daily(pred_path, actual_path):
|
|
| 156 |
So sánh dự đoán trong file realtime_predictions với dữ liệu thật trong current3weeks.
|
| 157 |
Tính RMSE cho 5 ngày tiếp theo CHỈ KHI đủ dữ liệu cho cả 5 ngày.
|
| 158 |
"""
|
|
|
|
| 159 |
|
| 160 |
# --- Đọc dữ liệu ---
|
| 161 |
pred_path = Path(pred_path)
|
|
@@ -220,7 +232,7 @@ def log_rmse_daily(pred_path, actual_path):
|
|
| 220 |
"base_date": base_date_str,
|
| 221 |
"end_date": forecast_dates[-1].strftime("%Y-%m-%d"),
|
| 222 |
"rmse": rmse_value,
|
| 223 |
-
"logged_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 224 |
}
|
| 225 |
|
| 226 |
all_logs.append(log_entry)
|
|
@@ -241,7 +253,8 @@ def log_rmse_daily(pred_path, actual_path):
|
|
| 241 |
|
| 242 |
# --- Task tự động hàng ngày ---
|
| 243 |
def daily_update():
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
# 1. Lấy dữ liệu mới
|
| 247 |
new_data = fetch_latest_weather_data()
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
import numpy as np
|
| 11 |
from datetime import datetime, timedelta
|
| 12 |
+
from zoneinfo import ZoneInfo
|
| 13 |
from io import StringIO
|
| 14 |
from sklearn.base import BaseEstimator, TransformerMixin
|
| 15 |
from sklearn.metrics import mean_squared_error
|
|
|
|
| 32 |
# PIPE_2 = r"pipelines/featureSelection_pipeline.pkl"
|
| 33 |
|
| 34 |
|
| 35 |
+
# --- Hàm lấy múi giờ ---
|
| 36 |
+
def get_timezone():
|
| 37 |
+
"""Lấy múi giờ từ biến môi trường TZ, mặc định là Asia/Ho_Chi_Minh."""
|
| 38 |
+
tz_string = os.getenv("TZ", "Asia/Ho_Chi_Minh")
|
| 39 |
+
try:
|
| 40 |
+
return ZoneInfo(tz_string)
|
| 41 |
+
except Exception:
|
| 42 |
+
return ZoneInfo("Asia/Ho_Chi_Minh")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
# --- Lấy dữ liệu mới nhất ---
|
| 46 |
def fetch_latest_weather_data(location="Hanoi", days=35):
|
| 47 |
+
tz = get_timezone()
|
| 48 |
+
end_date = datetime.now(tz)
|
| 49 |
start_date = end_date - timedelta(days=days)
|
| 50 |
|
| 51 |
base_url = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline"
|
|
|
|
| 81 |
- Giữ lại đúng 3 năm gần nhất (tính từ hôm nay)
|
| 82 |
"""
|
| 83 |
data_path = Path(data_path)
|
| 84 |
+
tz = get_timezone()
|
| 85 |
|
| 86 |
# Đảm bảo có cột datetime
|
| 87 |
if "datetime" not in new_data.columns:
|
|
|
|
| 107 |
combined.drop_duplicates(subset=["datetime"], keep="last", inplace=True)
|
| 108 |
|
| 109 |
# Giới hạn dữ liệu trong 3 năm gần nhất
|
| 110 |
+
three_years_ago = datetime.now(tz) - timedelta(days=3 * 365)
|
| 111 |
combined = combined[combined["datetime"] >= three_years_ago]
|
| 112 |
|
| 113 |
# sắp xếp và lưu lại
|
|
|
|
| 116 |
combined.to_csv(data_path, index=False)
|
| 117 |
|
| 118 |
# kiểm tra
|
|
|
|
|
|
|
| 119 |
logging.info(f"Dữ liệu đã được cập nhật, còn lại {len(combined)} dòng (~3 năm).")
|
| 120 |
logging.info(f"Khoảng thời gian: {combined['datetime'].min().date()} → {combined['datetime'].max().date()}")
|
| 121 |
|
|
|
|
| 167 |
So sánh dự đoán trong file realtime_predictions với dữ liệu thật trong current3weeks.
|
| 168 |
Tính RMSE cho 5 ngày tiếp theo CHỈ KHI đủ dữ liệu cho cả 5 ngày.
|
| 169 |
"""
|
| 170 |
+
tz = get_timezone()
|
| 171 |
|
| 172 |
# --- Đọc dữ liệu ---
|
| 173 |
pred_path = Path(pred_path)
|
|
|
|
| 232 |
"base_date": base_date_str,
|
| 233 |
"end_date": forecast_dates[-1].strftime("%Y-%m-%d"),
|
| 234 |
"rmse": rmse_value,
|
| 235 |
+
"logged_at": datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
|
| 236 |
}
|
| 237 |
|
| 238 |
all_logs.append(log_entry)
|
|
|
|
| 253 |
|
| 254 |
# --- Task tự động hàng ngày ---
|
| 255 |
def daily_update():
|
| 256 |
+
tz = get_timezone()
|
| 257 |
+
logging.info(f"Bắt đầu cập nhật dữ liệu lúc {datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')}")
|
| 258 |
|
| 259 |
# 1. Lấy dữ liệu mới
|
| 260 |
new_data = fetch_latest_weather_data()
|
src/model_training.py
CHANGED
|
@@ -22,6 +22,7 @@ import subprocess
|
|
| 22 |
from clearml import Logger, Task
|
| 23 |
from sklearn.model_selection import TimeSeriesSplit
|
| 24 |
from datetime import datetime
|
|
|
|
| 25 |
from lightgbm import LGBMRegressor
|
| 26 |
from sklearn.metrics import mean_squared_error
|
| 27 |
|
|
@@ -38,6 +39,15 @@ DATA_PATH = BASE_DIR / "data" / "latest_3_year.csv"
|
|
| 38 |
CURRENT_MODEL_PATH = BASE_DIR / "models" / "Current_model.pkl"
|
| 39 |
TEMP_NEW_MODEL_PATH = BASE_DIR / "models" / "Update_model.pkl"
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# --- Lớp Wrapper cho Multi-Output Prediction ---
|
| 42 |
class CustomMultiOutputRegressor:
|
| 43 |
"""
|
|
@@ -125,9 +135,10 @@ def train_model(X_train, y_train, n_trials=50, random_state=42):
|
|
| 125 |
"""
|
| 126 |
Tinh chỉnh và huấn luyện 5 mô hình LightGBM riêng biệt cho 5 ngày dự báo.
|
| 127 |
"""
|
|
|
|
| 128 |
task = Task.current_task()
|
| 129 |
if not task:
|
| 130 |
-
task = Task.init(project_name="Hanoi Temperature Forecast", task_name=f"Training_{datetime.now():%Y%m%d_%H%M}")
|
| 131 |
logger = task.get_logger()
|
| 132 |
|
| 133 |
best_models_per_target, best_pipelines_per_target, best_params_per_target = {}, {}, {}
|
|
@@ -249,13 +260,14 @@ def save_artifacts(
|
|
| 249 |
models: dict, pipelines: dict, best_params: dict, metrics: dict,
|
| 250 |
final_model_object: CustomMultiOutputRegressor, task: Task, output_path: str
|
| 251 |
):
|
|
|
|
| 252 |
output_path = Path(output_path)
|
| 253 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 254 |
|
| 255 |
artifacts_to_save = {
|
| 256 |
"models": models, "pipelines": pipelines, "best_params": best_params,
|
| 257 |
"metrics": metrics, "final_multi_model": final_model_object,
|
| 258 |
-
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 259 |
}
|
| 260 |
joblib.dump(artifacts_to_save, output_path)
|
| 261 |
logging.info(f"Đã lưu toàn bộ artifacts vào: {output_path}")
|
|
@@ -271,7 +283,7 @@ def save_artifacts(
|
|
| 271 |
task.flush()
|
| 272 |
logging.info("Upload artifact lên ClearML đã hoàn tất.")
|
| 273 |
|
| 274 |
-
retrain_log_path = output_path.parent / "retrain_log.pkl"
|
| 275 |
try:
|
| 276 |
history = joblib.load(retrain_log_path) if retrain_log_path.exists() else []
|
| 277 |
log_entry = {
|
|
@@ -279,6 +291,7 @@ def save_artifacts(
|
|
| 279 |
"best_params_summary": {k: v['boosting_type'] for k, v in best_params.items()}
|
| 280 |
}
|
| 281 |
history.append(log_entry)
|
|
|
|
| 282 |
joblib.dump(history, retrain_log_path)
|
| 283 |
logging.info(f"Đã cập nhật lịch sử huấn luyện tại: {retrain_log_path}")
|
| 284 |
except Exception as e:
|
|
|
|
| 22 |
from clearml import Logger, Task
|
| 23 |
from sklearn.model_selection import TimeSeriesSplit
|
| 24 |
from datetime import datetime
|
| 25 |
+
from zoneinfo import ZoneInfo
|
| 26 |
from lightgbm import LGBMRegressor
|
| 27 |
from sklearn.metrics import mean_squared_error
|
| 28 |
|
|
|
|
| 39 |
CURRENT_MODEL_PATH = BASE_DIR / "models" / "Current_model.pkl"
|
| 40 |
TEMP_NEW_MODEL_PATH = BASE_DIR / "models" / "Update_model.pkl"
|
| 41 |
|
| 42 |
+
# --- Hàm lấy múi giờ ---
|
| 43 |
+
def get_timezone():
|
| 44 |
+
"""Lấy múi giờ từ biến môi trường TZ, mặc định là Asia/Ho_Chi_Minh."""
|
| 45 |
+
tz_string = os.getenv("TZ", "Asia/Ho_Chi_Minh")
|
| 46 |
+
try:
|
| 47 |
+
return ZoneInfo(tz_string)
|
| 48 |
+
except Exception:
|
| 49 |
+
return ZoneInfo("Asia/Ho_Chi_Minh")
|
| 50 |
+
|
| 51 |
# --- Lớp Wrapper cho Multi-Output Prediction ---
|
| 52 |
class CustomMultiOutputRegressor:
|
| 53 |
"""
|
|
|
|
| 135 |
"""
|
| 136 |
Tinh chỉnh và huấn luyện 5 mô hình LightGBM riêng biệt cho 5 ngày dự báo.
|
| 137 |
"""
|
| 138 |
+
tz = get_timezone()
|
| 139 |
task = Task.current_task()
|
| 140 |
if not task:
|
| 141 |
+
task = Task.init(project_name="Hanoi Temperature Forecast", task_name=f"Training_{datetime.now(tz):%Y%m%d_%H%M}")
|
| 142 |
logger = task.get_logger()
|
| 143 |
|
| 144 |
best_models_per_target, best_pipelines_per_target, best_params_per_target = {}, {}, {}
|
|
|
|
| 260 |
models: dict, pipelines: dict, best_params: dict, metrics: dict,
|
| 261 |
final_model_object: CustomMultiOutputRegressor, task: Task, output_path: str
|
| 262 |
):
|
| 263 |
+
tz = get_timezone()
|
| 264 |
output_path = Path(output_path)
|
| 265 |
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 266 |
|
| 267 |
artifacts_to_save = {
|
| 268 |
"models": models, "pipelines": pipelines, "best_params": best_params,
|
| 269 |
"metrics": metrics, "final_multi_model": final_model_object,
|
| 270 |
+
"timestamp": datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")
|
| 271 |
}
|
| 272 |
joblib.dump(artifacts_to_save, output_path)
|
| 273 |
logging.info(f"Đã lưu toàn bộ artifacts vào: {output_path}")
|
|
|
|
| 283 |
task.flush()
|
| 284 |
logging.info("Upload artifact lên ClearML đã hoàn tất.")
|
| 285 |
|
| 286 |
+
retrain_log_path = output_path.parent.parent / "logs" / "retrain_log.pkl"
|
| 287 |
try:
|
| 288 |
history = joblib.load(retrain_log_path) if retrain_log_path.exists() else []
|
| 289 |
log_entry = {
|
|
|
|
| 291 |
"best_params_summary": {k: v['boosting_type'] for k, v in best_params.items()}
|
| 292 |
}
|
| 293 |
history.append(log_entry)
|
| 294 |
+
retrain_log_path.parent.mkdir(parents=True, exist_ok=True)
|
| 295 |
joblib.dump(history, retrain_log_path)
|
| 296 |
logging.info(f"Đã cập nhật lịch sử huấn luyện tại: {retrain_log_path}")
|
| 297 |
except Exception as e:
|
src/monitoring.py
CHANGED
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
| 7 |
sys.path.append(str(Path(__file__).parent.parent))
|
| 8 |
import pandas as pd
|
| 9 |
from datetime import datetime, timedelta
|
|
|
|
| 10 |
from src.model_training import retrain_pipeline
|
| 11 |
|
| 12 |
# --- Cấu hình logging ---
|
|
@@ -23,6 +24,16 @@ RMSE_THRESHOLD = 2.5 # Ngưỡng cảnh báo drift về hiệu năng
|
|
| 23 |
RETRAIN_INTERVAL_DAYS = 90 # Tự động retrain sau 90 ngày
|
| 24 |
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
def check_rmse_drift(log_path=LOG_PATH, threshold=RMSE_THRESHOLD):
|
| 27 |
"""Kiểm tra xem RMSE trung bình 5 ngày gần nhất có vượt ngưỡng không."""
|
| 28 |
log_path = Path(log_path)
|
|
@@ -69,7 +80,9 @@ def check_rmse_drift(log_path=LOG_PATH, threshold=RMSE_THRESHOLD):
|
|
| 69 |
|
| 70 |
def check_retrain_interval(log_path=RETRAIN_LOG_PATH, interval_days=RETRAIN_INTERVAL_DAYS):
|
| 71 |
"""Kiểm tra xem đã quá 90 ngày kể từ lần retrain gần nhất chưa"""
|
|
|
|
| 72 |
log_path = Path(log_path)
|
|
|
|
| 73 |
if not log_path.exists():
|
| 74 |
logging.info("Chưa có lịch sử huấn luyện lại. Bỏ qua kiểm tra khoảng thời gian.")
|
| 75 |
return False
|
|
@@ -85,7 +98,13 @@ def check_retrain_interval(log_path=RETRAIN_LOG_PATH, interval_days=RETRAIN_INTE
|
|
| 85 |
|
| 86 |
last_date_str = records[-1]["timestamp"]
|
| 87 |
last_date = datetime.strptime(last_date_str, "%Y-%m-%d %H:%M:%S")
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
logging.info(f"Lần huấn luyện lại gần nhất: {last_date_str} ({days_since} ngày trước).")
|
| 91 |
if days_since > interval_days:
|
|
|
|
| 7 |
sys.path.append(str(Path(__file__).parent.parent))
|
| 8 |
import pandas as pd
|
| 9 |
from datetime import datetime, timedelta
|
| 10 |
+
from zoneinfo import ZoneInfo
|
| 11 |
from src.model_training import retrain_pipeline
|
| 12 |
|
| 13 |
# --- Cấu hình logging ---
|
|
|
|
| 24 |
RETRAIN_INTERVAL_DAYS = 90 # Tự động retrain sau 90 ngày
|
| 25 |
|
| 26 |
|
| 27 |
+
# --- Hàm lấy múi giờ ---
|
| 28 |
+
def get_timezone():
|
| 29 |
+
"""Lấy múi giờ từ biến môi trường TZ, mặc định là Asia/Ho_Chi_Minh."""
|
| 30 |
+
tz_string = os.getenv("TZ", "Asia/Ho_Chi_Minh")
|
| 31 |
+
try:
|
| 32 |
+
return ZoneInfo(tz_string)
|
| 33 |
+
except Exception:
|
| 34 |
+
return ZoneInfo("Asia/Ho_Chi_Minh")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def check_rmse_drift(log_path=LOG_PATH, threshold=RMSE_THRESHOLD):
|
| 38 |
"""Kiểm tra xem RMSE trung bình 5 ngày gần nhất có vượt ngưỡng không."""
|
| 39 |
log_path = Path(log_path)
|
|
|
|
| 80 |
|
| 81 |
def check_retrain_interval(log_path=RETRAIN_LOG_PATH, interval_days=RETRAIN_INTERVAL_DAYS):
|
| 82 |
"""Kiểm tra xem đã quá 90 ngày kể từ lần retrain gần nhất chưa"""
|
| 83 |
+
tz = get_timezone()
|
| 84 |
log_path = Path(log_path)
|
| 85 |
+
|
| 86 |
if not log_path.exists():
|
| 87 |
logging.info("Chưa có lịch sử huấn luyện lại. Bỏ qua kiểm tra khoảng thời gian.")
|
| 88 |
return False
|
|
|
|
| 98 |
|
| 99 |
last_date_str = records[-1]["timestamp"]
|
| 100 |
last_date = datetime.strptime(last_date_str, "%Y-%m-%d %H:%M:%S")
|
| 101 |
+
|
| 102 |
+
# Chuyển đổi last_date sang múi giờ hiện tại nếu cần
|
| 103 |
+
if last_date.tzinfo is None:
|
| 104 |
+
last_date = last_date.replace(tzinfo=tz)
|
| 105 |
+
|
| 106 |
+
current_time = datetime.now(tz)
|
| 107 |
+
days_since = (current_time - last_date).days
|
| 108 |
|
| 109 |
logging.info(f"Lần huấn luyện lại gần nhất: {last_date_str} ({days_since} ngày trước).")
|
| 110 |
if days_since > interval_days:
|