1.Rbob.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import pandas as pd
  2. import numpy as np
  3. import xgboost as xgb
  4. from xgboost import XGBRegressor
  5. from sklearn.metrics import mean_squared_error, r2_score
  6. import matplotlib.pyplot as plt
  7. from skopt import BayesSearchCV
  8. from sklearn.preprocessing import StandardScaler
  9. from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, TimeSeriesSplit
  10. from Dtool import fill_missing_values, reverse_column
  11. from api import fetch_data_by_indicators
  12. # 使用示例
  13. indicators = ["RBWTICKMc1", "C2406121350446455",'USGGBE02 Index', "Cinjcjc4 index",'injcjc4 index','C2201059138_241106232710','C2406036178','C22411071623523660','C2312081670','REFOC-T-EIA_241114135248','C2304065621_241024124344','REFOC-T-EIA_241114135248','C22503031424010431']
  14. df = fetch_data_by_indicators(indicators)
  15. df = fetch_data_by_indicators(indicators, "data_input/RBOB.xlsx")
  16. # ------------ 全局配置参数 ------------
  17. FILE_PATH = 'data_input/RBOB.xlsx'
  18. NUM_BOOST_ROUND = 1000
  19. RANDOM_STATE = 42
  20. USE_HYPERPARAM_TUNING = False # 若 False 则直接使用 xgb.train
  21. TARGET_COL = '美国RBOB汽油裂解'
  22. TEST_PERIOD = 20
  23. SEARCH_MODE = 'random' # 可选 'grid' / 'bayesian' / 'random'
  24. SHOW_PLOTS = False
  25. ADJUST_FULL_PREDICTIONS = True
  26. DEFAULT_PARAMS = {
  27. 'objective': 'reg:squarederror',
  28. 'learning_rate': 0.1,
  29. 'max_depth': 8,
  30. 'min_child_weight': 3,
  31. 'gamma': 2,
  32. 'subsample': 0.85,
  33. 'colsample_bytree': 0.75,
  34. 'eval_metric': 'rmse',
  35. 'seed': 42,
  36. 'reg_alpha': 0.45,
  37. 'reg_lambda': 1.29,
  38. }
  39. # —— 因子预处理相关配置 ——
  40. FILL_METHODS = {
  41. '美国2年通胀预期': 'rolling_mean_5',
  42. '美国首次申领失业金人数/4WMA': 'interpolate',
  43. '道琼斯旅游与休闲/工业平均指数': 'interpolate',
  44. '美国EIA成品油总库存(预测/供应需求3年季节性)': 'interpolate',
  45. '美国成品车用汽油倒推产量(预测/汽油库存维持上年季节性)/8WMA': 'interpolate',
  46. '美国成品车用汽油炼厂与调和装置净产量/4WMA(预测/上年季节性)超季节性/5年': 'interpolate',
  47. '美国炼厂可用产能(路透)(预测)': 'interpolate',
  48. '美国炼厂CDU装置检修量(新)': 'interpolate',
  49. '美湾单位辛烷值价格(预测/季节性)': 'interpolate',
  50. '美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年': 'interpolate'
  51. }
  52. SHIFT_CONFIG = [
  53. ('美国2年通胀预期', 56, '美国2年通胀预期_提前56天'),
  54. ('美国首次申领失业金人数/4WMA', 100, '美国首次申领失业金人数/4WMA_提前100天'),
  55. ('美国首次申领失业金人数/4WMA', 112, '美国首次申领失业金人数/4WMA_提前112天'),
  56. ('道琼斯旅游与休闲/工业平均指数', 14, '道琼斯旅游与休闲/工业平均指数_提前14天'),
  57. ('美国EIA成品油总库存(预测/供应需求3年季节性)', 15,
  58. '美国EIA成品油总库存(预测/供应需求3年季节性)_提前15天'),
  59. ('美国成品车用汽油炼厂与调和装置净产量/4WMA(预测/上年季节性)超季节性/5年',
  60. 30,
  61. '美国成品车用汽油炼厂与调和装置净产量/4WMA(预测/上年季节性)超季节性/5年_提前30天'),
  62. ('美国炼厂CDU装置检修量(新)', 30, '美国炼厂CDU装置检修量(新)_提前30天'),
  63. ('美国炼厂可用产能(路透)(预测)', 100,
  64. '美国炼厂可用产能(路透)(预测)_提前100天')
  65. ]
  66. REVERSE_CONFIG = [
  67. ('美国首次申领失业金人数/4WMA',
  68. '美国首次申领失业金人数/4WMA_逆序'),
  69. ('美国首次申领失业金人数/4WMA_提前100天',
  70. '美国首次申领失业金人数/4WMA_提前100天_逆序'),
  71. ('美国首次申领失业金人数/4WMA_提前112天',
  72. '美国首次申领失业金人数/4WMA_提前112天_逆序'),
  73. ('美国EIA成品油总库存(预测/供应需求3年季节性)',
  74. '美国EIA成品油总库存(预测/供应需求3年季节性)_逆序'),
  75. ('美国EIA成品油总库存(预测/供应需求3年季节性)_提前15天',
  76. '美国EIA成品油总库存(预测/供应需求3年季节性)_提前15天_逆序'),
  77. ('美国炼厂可用产能(路透)(预测)_提前100天',
  78. '美国炼厂可用产能(路透)(预测)_逆序'),
  79. ('美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年',
  80. '美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年_逆序')
  81. ]
  82. SPECIAL_REVERSE = {
  83. '美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年_逆序_2022-01-01': {
  84. 'base_column': '美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年_逆序',
  85. 'condition_date': pd.Timestamp('2022-01-01')
  86. }
  87. }
  88. # ------------ 数据加载与预处理 ------------
  89. def load_and_preprocess_data(file_path):
  90. df = pd.read_excel(file_path, sheet_name='Sheet1')
  91. print(df)
  92. df.rename(columns={'DataTime': 'Date'}, inplace=True)
  93. df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
  94. df.set_index('Date', inplace=True)
  95. full_range = pd.date_range(df.index.min(),df.index.max(),freq='D')
  96. df_daily = df.reindex(full_range).reset_index()
  97. df_daily.rename(columns={'index': 'Date'}, inplace=True)
  98. df_daily = fill_missing_values(df_daily,FILL_METHODS,return_only_filled=False)
  99. for col, days, new_col in SHIFT_CONFIG:
  100. df_daily[new_col] = df_daily[col].shift(days)
  101. last_idx = df_daily[TARGET_COL].last_valid_index()
  102. last_day = df_daily.loc[last_idx, 'Date']
  103. df_daily = df_daily[(df_daily['Date'] >= '2009-08-01') &(df_daily['Date'] <= last_day +pd.Timedelta(days=30))]
  104. df_daily = df_daily[df_daily['Date'].dt.weekday < 5]
  105. for base, new in REVERSE_CONFIG:
  106. df_daily[new] = reverse_column(df_daily, base)
  107. for col, cfg in SPECIAL_REVERSE.items():
  108. df_daily[col] = np.where(df_daily['Date'] >= cfg['condition_date'],df_daily[cfg['base_column']],np.nan)
  109. df_daily = df_daily[(df_daily['Date'] > last_day)|df_daily[TARGET_COL].notna()]
  110. return df_daily, last_day
  111. # ------------ 划分与特征构建 ------------
  112. def split_and_build_features(df_daily, last_day):
  113. train = df_daily[df_daily['Date'] <= last_day].copy()
  114. test = train.tail(TEST_PERIOD).copy()
  115. train = train.iloc[:-TEST_PERIOD].copy()
  116. future = df_daily[df_daily['Date'] > last_day].copy()
  117. feature_columns = [
  118. '美湾单位辛烷值价格(预测/季节性)',
  119. '美国炼厂CDU装置检修量(新)_提前30天',
  120. '美国EIA成品油总库存(预测/供应需求3年季节性)_提前15天_逆序',
  121. '美国首次申领失业金人数/4WMA_提前100天_逆序',
  122. '美国成品车用汽油倒推产量(预测/汽油库存维持上年季节性)/8WMA',
  123. '美国成品车用汽油炼厂与调和装置净产量/4WMA(预测/上年季节性)超季节性/5年_提前30天',
  124. '美国汽油调和组分RBOB库存(预测/线性外推)超季节性/3年_逆序_2022-01-01'
  125. ]
  126. X_train = train[feature_columns]
  127. y_train = train[TARGET_COL]
  128. X_test = test[feature_columns]
  129. y_test = test[TARGET_COL]
  130. X_future = future[feature_columns]
  131. return X_train, y_train, X_test, y_test, X_future, train, test, future
  132. # ------------ 特征缩放与异常值权重 ------------
  133. def scale_and_weight_features(X_train, X_test, X_future):
  134. scaler = StandardScaler()
  135. X_tr = scaler.fit_transform(X_train)
  136. X_te = scaler.transform(X_test)
  137. X_fu = scaler.transform(X_future)
  138. return scaler, X_tr, X_te, X_fu
  139. def detect_outliers_weights(X,weight_normal=1.0,weight_outlier=0.05,threshold=3):
  140. z = np.abs((X - X.mean()) / X.std())
  141. mask = (z > threshold).any(axis=1)
  142. return np.where(mask, weight_outlier, weight_normal)
  143. # ------------ 模型训练 ------------
  144. def train_model_with_tuning(X_tr, y_tr, X_te, y_te, weights, use_tuning):
  145. if use_tuning:
  146. param_dist = {
  147. 'learning_rate': list(np.arange(0.01, 0.11, 0.01)),
  148. 'max_depth': list(range(4, 11)),
  149. 'min_child_weight': list(range(1, 6)),
  150. 'gamma': list(np.arange(0, 0.6, 0.1)),
  151. 'subsample': list(np.arange(0.5, 1.01, 0.05)),
  152. 'colsample_bytree': list(np.arange(0.5, 1.01, 0.05)),
  153. 'reg_alpha': [0, 0.1, 0.2, 0.3, 0.4, 0.45, 0.5],
  154. 'reg_lambda': list(np.arange(1.0, 1.6, 0.1))
  155. }
  156. xgb_reg = XGBRegressor(objective='reg:squarederror',
  157. eval_metric='rmse',
  158. n_estimators=NUM_BOOST_ROUND,
  159. seed=RANDOM_STATE)
  160. tscv = TimeSeriesSplit(n_splits=3)
  161. if SEARCH_MODE == 'grid':
  162. search = GridSearchCV(xgb_reg,
  163. param_grid=param_dist,
  164. scoring='neg_mean_squared_error',
  165. cv=tscv,
  166. verbose=1,
  167. n_jobs=-1)
  168. elif SEARCH_MODE == 'bayesian':
  169. search = BayesSearchCV(xgb_reg,
  170. search_spaces=param_dist,
  171. n_iter=50,
  172. scoring='neg_mean_squared_error',
  173. cv=tscv,
  174. random_state=RANDOM_STATE,
  175. verbose=1,
  176. n_jobs=-1)
  177. else:
  178. search = RandomizedSearchCV(xgb_reg,
  179. param_distributions=param_dist,
  180. n_iter=50,
  181. scoring='neg_mean_squared_error',
  182. cv=tscv,
  183. random_state=RANDOM_STATE,
  184. verbose=1,
  185. n_jobs=-1)
  186. search.fit(X_tr, y_tr, sample_weight=weights)
  187. best_model = search.best_estimator_
  188. print("调优后的最佳参数:", search.best_params_)
  189. best_model.fit(X_tr, y_tr,
  190. eval_set=[(X_te, y_te)],
  191. early_stopping_rounds=20,
  192. verbose=200)
  193. else:
  194. dtrain = xgb.DMatrix(X_tr, label=y_tr, weight=weights)
  195. dtest = xgb.DMatrix(X_te, label=y_te)
  196. best_model = xgb.train(DEFAULT_PARAMS,
  197. dtrain,
  198. num_boost_round=NUM_BOOST_ROUND,
  199. evals=[(dtrain, 'Train'),
  200. (dtest, 'Test')],
  201. verbose_eval=False)
  202. return best_model
  203. # ------------ 评估与预测 ------------
  204. def evaluate_and_predict(model, scaler, X_tr, y_tr, X_te, y_te, X_fu,
  205. use_tuning):
  206. X_tr_s = scaler.transform(X_tr)
  207. X_te_s = scaler.transform(X_te)
  208. X_fu_s = scaler.transform(X_fu)
  209. if isinstance(model, xgb.Booster):
  210. y_tr_pred = model.predict(xgb.DMatrix(X_tr_s))
  211. y_te_pred = model.predict(xgb.DMatrix(X_te_s))
  212. y_fu_pred = model.predict(xgb.DMatrix(X_fu_s))
  213. else:
  214. y_tr_pred = model.predict(X_tr_s)
  215. y_te_pred = model.predict(X_te_s)
  216. y_fu_pred = model.predict(X_fu_s)
  217. print("Train MSE:", mean_squared_error(y_tr, y_tr_pred),
  218. "Test MSE:", mean_squared_error(y_te, y_te_pred))
  219. if len(y_te) >= 2:
  220. print("Train R2:", r2_score(y_tr, y_tr_pred),
  221. "Test R2:", r2_score(y_te, y_te_pred))
  222. else:
  223. print("Test 样本不足,跳过 R² 计算")
  224. return y_tr_pred, y_te_pred, y_fu_pred
  225. # ------------ 结果后处理(生成日度 & 月度 DataFrame) ------------
  226. def merge_and_prepare_df(train, test, future, y_te_pred, y_fu_pred):
  227. # 合并历史与未来预测
  228. test = test.copy()
  229. future = future.copy()
  230. test['预测值'] = y_te_pred
  231. future['预测值'] = y_fu_pred
  232. hist_actual = pd.concat([
  233. train[train['Date'].dt.year >= 2023][['Date', TARGET_COL]],
  234. test[['Date', TARGET_COL]]
  235. ])
  236. hist_actual.columns = ['Date', '实际值']
  237. future_pred = future[future['Date'] >= '2022-08-01'][['Date', '预测值']].rename(columns={'预测值': TARGET_COL}).copy()
  238. last_val = float(hist_actual.iloc[-1]['实际值'])
  239. future_pred.iloc[0, 1] = last_val
  240. merged = pd.merge(hist_actual, future_pred,on='Date', how='outer').sort_values('Date', ascending=False)
  241. daily_df = merged.copy()
  242. # 月度重采样
  243. monthly_df = daily_df.copy()
  244. monthly_df['Date'] = pd.to_datetime(monthly_df['Date'])
  245. monthly_df.set_index('Date', inplace=True)
  246. monthly_df = monthly_df.resample('ME').mean().reset_index()
  247. # 方向准确率:仅在实际值和预测值都非空时计算
  248. pred_dir = np.sign(monthly_df[TARGET_COL].diff())
  249. true_dir = np.sign(monthly_df['实际值'].diff())
  250. valid = monthly_df[TARGET_COL].notna() & monthly_df['实际值'].notna()
  251. monthly_df['方向准确率'] = np.where(valid & (pred_dir == true_dir), '正确',
  252. np.where(valid & (pred_dir != true_dir), '错误', np.nan))
  253. # 绝对偏差
  254. monthly_df['绝对偏差'] = (monthly_df[TARGET_COL] - monthly_df['实际值']).abs()
  255. # 降序 & 打印
  256. monthly_df = monthly_df.sort_values('Date', ascending=False).reset_index(drop=True)
  257. return daily_df, monthly_df
  258. def generate_and_fill_excel(
  259. daily_df,
  260. monthly_df,
  261. target_name, # 写入的“预测标的”显示名
  262. classification, # 列表页-分类
  263. model_framework, # 列表页-模型框架
  264. creator, # 列表页-创建人
  265. pred_date, # 列表页-预测日期
  266. frequency, # 列表页-预测频度
  267. output_path='update.xlsx'
  268. ):
  269. with pd.ExcelWriter(output_path, engine='xlsxwriter') as writer:
  270. workbook = writer.book
  271. # —— 计算三个汇总值 ——
  272. # 1) 测试值:最新月度的预测值
  273. test_value = monthly_df[TARGET_COL].iloc[0]
  274. # 2) 方向准确率:正确数 / 有效数
  275. total = monthly_df['方向准确率'].notna().sum()
  276. correct = (monthly_df['方向准确率'] == '正确').sum()
  277. direction_accuracy = f"{correct/total:.2%}" if total > 0 else ""
  278. # 3) 平均绝对偏差
  279. absolute_deviation = monthly_df['绝对偏差'].mean()
  280. # ========= 列表页 =========
  281. ws_list = workbook.add_worksheet('列表页')
  282. writer.sheets['列表页'] = ws_list
  283. headers = ['预测标的','分类','模型框架','创建人','预测日期','测试值','预测频度','方向准确率','绝对偏差']
  284. ws_list.write_row(0, 0, headers)
  285. ws_list.write_row(1, 0, [
  286. target_name,
  287. classification,
  288. model_framework,
  289. creator,
  290. pred_date,
  291. test_value,
  292. frequency,
  293. direction_accuracy,
  294. absolute_deviation
  295. ])
  296. # ========= 详情页 =========
  297. detail_df = monthly_df[['Date', '实际值', TARGET_COL, '方向准确率', '绝对偏差']].copy()
  298. detail_df.columns = ['指标日期','实际值','预测值','方向','偏差率']
  299. detail_df.to_excel(writer,sheet_name='详情页',index=False,header=False,startrow=2)
  300. ws_detail = writer.sheets['详情页']
  301. ws_detail.write(0, 0, target_name)
  302. ws_detail.write_row(1, 0, ['指标日期','实际值','预测值','方向','偏差率'])
  303. # ========= 日度数据表 =========
  304. daily_out = daily_df[['Date', '实际值', TARGET_COL]].copy()
  305. daily_out.columns = ['指标日期','实际值','预测值']
  306. daily_out.to_excel(writer,sheet_name='日度数据表',index=False,header=False,startrow=2)
  307. ws_daily = writer.sheets['日度数据表']
  308. ws_daily.write(0, 0, target_name)
  309. ws_daily.write_row(1, 0, ['指标日期','实际值','预测值'])
  310. print(f"已生成并填充 {output_path}")
  311. # ------------ 全量训练与预测 ------------
  312. def train_full_model_and_predict(X_train, y_train, X_test, y_test, X_future):
  313. X_all = pd.concat([X_train, X_test])
  314. y_all = pd.concat([y_train, y_test])
  315. scaler_all = StandardScaler().fit(X_all)
  316. X_all_s = scaler_all.transform(X_all)
  317. X_fu_s = scaler_all.transform(X_future)
  318. model = XGBRegressor(**DEFAULT_PARAMS, n_estimators=NUM_BOOST_ROUND)
  319. model.fit(X_all_s, y_all)
  320. y_fu_full = model.predict(X_fu_s)
  321. return model, y_fu_full, scaler_all
  322. # ------------ 可视化 ------------
  323. def plot_final_predictions(train, y_tr, y_tr_pred, test, y_te, y_te_pred,
  324. future, last_day):
  325. plt.figure(figsize=(15, 6))
  326. plt.plot(train['Date'], y_tr, label='Train True')
  327. plt.plot(train['Date'], y_tr_pred, label='Train Pred')
  328. plt.plot(test['Date'], y_te, label='Test True', alpha=0.7)
  329. plt.plot(test['Date'], y_te_pred, label='Test Pred')
  330. plt.plot(future['Date'], future['预测值'], label='Future Pred')
  331. plt.axvline(test['Date'].iloc[0], color='gray', linestyle='--')
  332. plt.axvline(last_day, color='black', linestyle='--')
  333. plt.legend()
  334. plt.xlabel('Date')
  335. plt.ylabel(TARGET_COL)
  336. plt.title('Prediction Visualization')
  337. plt.grid(True)
  338. plt.show()
  339. # ------------ 主函数 ------------
  340. def main():
  341. df_daily, last_day = load_and_preprocess_data(FILE_PATH)
  342. X_tr, y_tr, X_te, y_te, X_fu, train, test, future = split_and_build_features(df_daily, last_day)
  343. scaler, X_tr_s, X_te_s, X_fu_s = scale_and_weight_features(X_tr, X_te, X_fu)
  344. weights = detect_outliers_weights(X_tr_s)
  345. model = train_model_with_tuning(X_tr_s, y_tr, X_te_s, y_te, weights,USE_HYPERPARAM_TUNING)
  346. y_tr_pred, y_te_pred, y_fu_pred = evaluate_and_predict(model, scaler, X_tr, y_tr, X_te, y_te, X_fu,USE_HYPERPARAM_TUNING)
  347. daily_df, monthly_df = merge_and_prepare_df(train, test, future,y_te_pred, y_fu_pred)
  348. print(monthly_df)
  349. print(daily_df)
  350. generate_and_fill_excel(
  351. daily_df,
  352. monthly_df,
  353. target_name='美国RBOB汽油裂解',
  354. classification='原油',
  355. model_framework='XGBoost',
  356. creator='张立舟',
  357. pred_date='2024/11/11',
  358. frequency='月度',
  359. output_path='update.xlsx'
  360. )
  361. full_model, y_fu_full, scaler_full = train_full_model_and_predict(X_tr, y_tr, X_te, y_te, X_fu)
  362. if ADJUST_FULL_PREDICTIONS:
  363. offset = y_te.iloc[-1] - y_fu_full[0]
  364. y_fu_full += offset
  365. if SHOW_PLOTS:
  366. plot_final_predictions(
  367. train, y_tr, y_tr_pred, test, y_te, y_te_pred,
  368. future.assign(预测值=y_fu_full), last_day)
  369. return daily_df, monthly_df
  370. if __name__ == '__main__':
  371. daily_df, monthly_df = main()