lightbgm.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. from datetime import datetime, timedelta
  5. import lightgbm as lgb
  6. import joblib
  7. import optuna
  8. from sklearn.utils import resample
  9. from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
  10. # =========================
  11. # 1. 基础配置
  12. # =========================
  13. # 数据文件夹路径
  14. input_folder = '/share/home/jinhm/PhD_Project/A_Data/Data/nwp_processed/OL_JIANGMEN'
  15. # 特征和目标变量
  16. features = ['ssrd', 'fdir', 'tcc', 'lcc', 'mcc', 'hcc', '2t', 'sp', 'tcwv']
  17. target = 'power'
  18. # 每个文件读取前 n 行
  19. n = 288
  20. # 是否对训练集做功率分箱重采样
  21. USE_RESAMPLE = True
  22. # 只建议重采样训练集,不建议重采样验证集和测试集
  23. RESAMPLE_VAL = False
  24. # Optuna 搜索次数
  25. N_TRIALS = 30
  26. # 随机种子
  27. RANDOM_STATE = 42
  28. # =========================
  29. # 2. 生成文件名
  30. # =========================
  31. def gen_file_names(start_date, end_date):
  32. """
  33. 根据起止日期生成 00 和 12 起报文件路径。
  34. 文件格式示例:2025061400.csv, 2025061412.csv
  35. """
  36. files = []
  37. date_fmt = "%Y%m%d"
  38. start_dt = datetime.strptime(start_date, date_fmt)
  39. end_dt = datetime.strptime(end_date, date_fmt)
  40. current = start_dt
  41. while current <= end_dt:
  42. file_00 = os.path.join(input_folder, f"{current.strftime(date_fmt)}00.csv")
  43. file_12 = os.path.join(input_folder, f"{current.strftime(date_fmt)}12.csv")
  44. if os.path.exists(file_00):
  45. files.append(file_00)
  46. else:
  47. print(f"文件不存在,跳过: {file_00}")
  48. if os.path.exists(file_12):
  49. files.append(file_12)
  50. else:
  51. print(f"文件不存在,跳过: {file_12}")
  52. current += timedelta(days=1)
  53. return files
  54. # =========================
  55. # 3. 读取数据
  56. # =========================
  57. def load_dataset(start_date, end_date, dataset_name):
  58. """
  59. 读取指定时间范围内的数据。
  60. """
  61. files = gen_file_names(start_date, end_date)
  62. data_list = []
  63. print(f"\n开始读取{dataset_name}...")
  64. print(f"{dataset_name}文件数量: {len(files)}")
  65. for f in files:
  66. try:
  67. df = pd.read_csv(f)
  68. df_short = df.iloc[:n].copy()
  69. required_cols = features + [target]
  70. if all(col in df_short.columns for col in required_cols):
  71. df_short = df_short[required_cols].copy()
  72. # 转成数值型,异常值转为 NaN
  73. for col in required_cols:
  74. df_short[col] = pd.to_numeric(df_short[col], errors='coerce')
  75. # 删除缺失值
  76. df_short = df_short.dropna(subset=required_cols)
  77. if len(df_short) > 0:
  78. data_list.append(df_short)
  79. else:
  80. missing_cols = [col for col in required_cols if col not in df_short.columns]
  81. print(f"文件缺少字段,跳过: {f}, 缺少: {missing_cols}")
  82. except Exception as e:
  83. print(f"Error reading {f}: {e}")
  84. if not data_list:
  85. raise ValueError(f"No {dataset_name} data loaded!")
  86. dataset_df = pd.concat(data_list, axis=0, ignore_index=True)
  87. print(f"{dataset_name}样本数: {len(dataset_df)}")
  88. return dataset_df
  89. # =========================
  90. # 4. 功率分箱重采样
  91. # =========================
  92. def resample_power_bins(df, target_col='power', n_bins=10, random_state=42, total_samples=None):
  93. """
  94. 对大于 0 的功率样本进行分箱重采样,使不同功率区间样本数量更接近。
  95. 零功率样本保留,不参与重采样。
  96. 注意:
  97. 只建议对训练集使用,不建议对验证集和测试集使用。
  98. """
  99. df = df.copy()
  100. # 仅对 power > 0 的样本重采样
  101. mask = df[target_col] > 0
  102. df_pos = df.loc[mask].copy()
  103. df_zero = df.loc[~mask].copy()
  104. # 如果没有正功率样本,直接返回原始打乱数据
  105. if len(df_pos) == 0:
  106. print("警告:没有正功率样本,跳过重采样。")
  107. return df.sample(frac=1, random_state=random_state).reset_index(drop=True)
  108. power_vals = df_pos[target_col]
  109. # 如果所有正功率都一样,无法分箱,直接返回原始打乱数据
  110. if power_vals.min() == power_vals.max():
  111. print("警告:正功率样本取值完全相同,跳过重采样。")
  112. return df.sample(frac=1, random_state=random_state).reset_index(drop=True)
  113. # 分箱
  114. bins = np.linspace(power_vals.min(), power_vals.max(), n_bins + 1)
  115. df_pos.loc[:, 'power_bin'] = pd.cut(power_vals, bins=bins, include_lowest=True)
  116. # 统计每个 bin 的样本数量
  117. bin_counts = df_pos['power_bin'].value_counts().sort_index()
  118. # 以最多的 bin 为基准;也可以指定 total_samples 控制总采样量
  119. if total_samples is None:
  120. max_count = int(bin_counts.max())
  121. else:
  122. max_count = max(int(total_samples // n_bins), 1)
  123. resampled_list = []
  124. for b, group in df_pos.groupby('power_bin', observed=True):
  125. if len(group) == 0:
  126. continue
  127. if len(group) < max_count:
  128. # 少数区间:有放回上采样
  129. group_resampled = resample(
  130. group,
  131. replace=True,
  132. n_samples=max_count,
  133. random_state=random_state
  134. )
  135. else:
  136. # 多数区间:无放回下采样
  137. group_resampled = resample(
  138. group,
  139. replace=False,
  140. n_samples=max_count,
  141. random_state=random_state
  142. )
  143. resampled_list.append(group_resampled)
  144. if not resampled_list:
  145. print("警告:分箱后没有可用样本,跳过重采样。")
  146. return df.drop(columns=['power_bin'], errors='ignore') \
  147. .sample(frac=1, random_state=random_state) \
  148. .reset_index(drop=True)
  149. df_resampled_pos = pd.concat(resampled_list, ignore_index=True)
  150. df_resampled_pos = df_resampled_pos.drop(columns=['power_bin'], errors='ignore')
  151. # 合并零功率样本
  152. df_new = pd.concat([df_zero, df_resampled_pos], ignore_index=True)
  153. # 打乱
  154. df_new = df_new.sample(frac=1, random_state=random_state).reset_index(drop=True)
  155. return df_new
  156. # =========================
  157. # 5. 读取训练、验证、测试集
  158. # =========================
  159. train_df = load_dataset('20250614', '20260228', '训练集')
  160. val_df = load_dataset('20260301', '20260331', '验证集')
  161. test_df = load_dataset('20260401', '20260417', '测试集')
  162. # =========================
  163. # 6. 重采样
  164. # =========================
  165. if USE_RESAMPLE:
  166. print("\n开始对训练集进行功率分箱重采样...")
  167. train_df = resample_power_bins(
  168. train_df,
  169. target_col=target,
  170. n_bins=10,
  171. random_state=RANDOM_STATE
  172. )
  173. print(f"重采样后训练集样本数: {len(train_df)}")
  174. if RESAMPLE_VAL:
  175. print("\n警告:你开启了验证集重采样,这通常不建议。")
  176. val_df = resample_power_bins(
  177. val_df,
  178. target_col=target,
  179. n_bins=10,
  180. random_state=RANDOM_STATE
  181. )
  182. print(f"重采样后验证集样本数: {len(val_df)}")
  183. else:
  184. print(f"验证集保持原始分布,样本数: {len(val_df)}")
  185. else:
  186. print("\n未开启重采样。")
  187. # =========================
  188. # 7. 构造 X 和 y
  189. # =========================
  190. X_train = train_df[features]
  191. y_train = train_df[target]
  192. X_val = val_df[features]
  193. y_val = val_df[target]
  194. X_test = test_df[features]
  195. y_test = test_df[target]
  196. print("\n最终数据规模:")
  197. print(f"X_train: {X_train.shape}, y_train: {y_train.shape}")
  198. print(f"X_val: {X_val.shape}, y_val: {y_val.shape}")
  199. print(f"X_test: {X_test.shape}, y_test: {y_test.shape}")
  200. # =========================
  201. # 8. 评价函数
  202. # =========================
  203. def calc_metrics(y_true, y_pred, name="数据集"):
  204. """
  205. 计算常用回归指标。
  206. """
  207. rmse = np.sqrt(mean_squared_error(y_true, y_pred))
  208. mae = mean_absolute_error(y_true, y_pred)
  209. r2 = r2_score(y_true, y_pred)
  210. print(f"\n{name}评价指标:")
  211. print(f"{name} RMSE: {rmse:.4f}")
  212. print(f"{name} MAE : {mae:.4f}")
  213. print(f"{name} R² : {r2:.4f}")
  214. return {
  215. "rmse": rmse,
  216. "mae": mae,
  217. "r2": r2
  218. }
  219. # =========================
  220. # 9. Optuna 超参数优化
  221. # =========================
  222. def objective(trial):
  223. """
  224. Optuna 目标函数:最小化验证集 RMSE。
  225. """
  226. params = {
  227. "objective": "regression",
  228. "metric": "rmse",
  229. # 超参数搜索范围
  230. "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.2, log=True),
  231. "num_leaves": trial.suggest_int("num_leaves", 16, 128),
  232. "feature_fraction": trial.suggest_float("feature_fraction", 0.6, 1.0),
  233. "bagging_fraction": trial.suggest_float("bagging_fraction", 0.6, 1.0),
  234. "bagging_freq": trial.suggest_int("bagging_freq", 1, 10),
  235. "min_child_samples": trial.suggest_int("min_child_samples", 5, 50),
  236. # 固定参数
  237. "verbose": -1,
  238. "seed": RANDOM_STATE,
  239. "feature_fraction_seed": RANDOM_STATE,
  240. "bagging_seed": RANDOM_STATE,
  241. "data_random_seed": RANDOM_STATE
  242. }
  243. train_set = lgb.Dataset(X_train, y_train)
  244. val_set = lgb.Dataset(X_val, y_val, reference=train_set)
  245. gbm = lgb.train(
  246. params,
  247. train_set,
  248. num_boost_round=1000,
  249. valid_sets=[train_set, val_set],
  250. valid_names=["train", "valid"],
  251. callbacks=[
  252. lgb.early_stopping(stopping_rounds=50, verbose=False),
  253. lgb.log_evaluation(period=0)
  254. ]
  255. )
  256. val_pred = gbm.predict(X_val, num_iteration=gbm.best_iteration)
  257. val_rmse = np.sqrt(mean_squared_error(y_val, val_pred))
  258. return val_rmse
  259. print("\n开始使用 Optuna 进行超参数优化...")
  260. study = optuna.create_study(
  261. direction="minimize",
  262. sampler=optuna.samplers.TPESampler(seed=RANDOM_STATE)
  263. )
  264. study.optimize(objective, n_trials=N_TRIALS)
  265. print("\nOptuna 优化完成。")
  266. print("最优 RMSE:", study.best_value)
  267. print("最优超参数:")
  268. for k, v in study.best_params.items():
  269. print(f" {k}: {v}")
  270. # =========================
  271. # 10. 使用最优参数训练最终模型
  272. # =========================
  273. best_params = study.best_params.copy()
  274. best_params.update({
  275. "objective": "regression",
  276. "metric": "rmse",
  277. "verbose": -1,
  278. "seed": RANDOM_STATE,
  279. "feature_fraction_seed": RANDOM_STATE,
  280. "bagging_seed": RANDOM_STATE,
  281. "data_random_seed": RANDOM_STATE
  282. })
  283. print("\n用最优参数训练最终 LightGBM 模型...")
  284. train_set = lgb.Dataset(X_train, y_train)
  285. val_set = lgb.Dataset(X_val, y_val, reference=train_set)
  286. gbm = lgb.train(
  287. best_params,
  288. train_set,
  289. num_boost_round=1000,
  290. valid_sets=[train_set, val_set],
  291. valid_names=["train", "valid"],
  292. callbacks=[
  293. lgb.early_stopping(stopping_rounds=50, verbose=True),
  294. lgb.log_evaluation(period=100)
  295. ]
  296. )
  297. print("\n训练完毕。")
  298. print(f"最佳迭代轮数 best_iteration: {gbm.best_iteration}")
  299. # =========================
  300. # 11. 模型评估
  301. # =========================
  302. val_pred = gbm.predict(X_val, num_iteration=gbm.best_iteration)
  303. test_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)
  304. val_metrics = calc_metrics(y_val, val_pred, name="验证集")
  305. test_metrics = calc_metrics(y_test, test_pred, name="测试集")
  306. # =========================
  307. # 12. 保存模型
  308. # =========================
  309. model_save_path_txt = './lightgbm_power_model.txt'
  310. model_save_path_pkl = './lightgbm_power_model.pkl'
  311. gbm.save_model(model_save_path_txt)
  312. joblib.dump(gbm, model_save_path_pkl)
  313. print("\n模型已保存:")
  314. print(f"TXT模型: {model_save_path_txt}")
  315. print(f"PKL模型: {model_save_path_pkl}")
  316. # =========================
  317. # 13. 保存预测结果
  318. # =========================
  319. val_result = pd.DataFrame({
  320. "y_true": y_val.values,
  321. "y_pred": val_pred
  322. })
  323. test_result = pd.DataFrame({
  324. "y_true": y_test.values,
  325. "y_pred": test_pred
  326. })
  327. val_result.to_csv("./lightgbm_val_prediction.csv", index=False)
  328. test_result.to_csv("./lightgbm_test_prediction.csv", index=False)
  329. print("\n预测结果已保存:")
  330. print("验证集预测结果: ./lightgbm_val_prediction.csv")
  331. print("测试集预测结果: ./lightgbm_test_prediction.csv")