ruoc188 commited on
Commit
94681ea
·
1 Parent(s): e410b64
Files changed (3) hide show
  1. src/forecasting.py +19 -6
  2. src/model_training.py +16 -3
  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
- end_date = datetime.today()
 
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.today() - timedelta(days=3 * 365)
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
- logging.info(f"Bắt đầu cập nhật dữ liệu lúc {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
 
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
- days_since = (datetime.now() - last_date).days
 
 
 
 
 
 
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: