如何使用fbprophet结合多回归因子实现未来小时温度预测?
兄弟,我太懂你这个坑了!用Prophet加了一堆自定义回归器训练,在训练测试集上表现贼好,结果要预测未来时直接炸锅,提示回归器缺失——核心原因就是你生成的future数据框里只有ds列,而你加的那些像hourlyLag、movMean这类特征,要么依赖历史温度,要么是基于历史温度计算的统计量,Prophet根本不知道怎么给未来的这些列填值!
我给你拆解下问题,再一步步说怎么解决:
先搞懂为啥报错
model.make_future_dataframe()只会生成一个带ds(时间戳)的空架子,你之前加的13个回归器里,分三类:
- 能直接从
ds算出来的:比如hour、month、dayOfYear——这些未来时间是已知的,好处理 - 依赖历史温度的:比如
hourlyLag(前1小时温度)、dailyLag(前24小时)、movMean(滚动均值)——这些是核心难点,因为未来温度是我们要预测的,没法直接拿到这些值 - 逻辑上有问题的:比如
devFromMean(movMean - Temperature)——这是用目标变量本身算出来的,训练时用真实温度没问题,但预测时温度未知,根本没法算,这类特征直接删了吧!
一步步解决问题
第一步:处理可直接计算的时间类回归器
像hour、month这些,你完全可以在生成future数据框后直接补上,因为未来的时间是确定的:
# 生成未来数据框 future = model.make_future_dataframe(periods=24*365*3, freq='h') # 给未来数据框加时间特征 future['hour'] = future['ds'].dt.hour future['month'] = future['ds'].dt.month future['dayOfYear'] = future['ds'].dt.day # 按你原代码的逻辑,注意dt.day是当月第几天,dt.dayofyear是当年第几天,按需选
这些列补完,时间类的回归器就没问题了。
第二步:处理依赖历史温度的回归器(核心难点)
像hourlyLag、dailyLag、movMean这些,因为未来温度未知,没法直接算,这里给你两个实用思路:
思路1:递归预测(最适合Prophet的方案)
简单说就是走一步看一步:每预测出一个小时的温度,就用这个预测值当“历史数据”,计算下一个小时需要的滞后项,再预测下一个小时。代码大概是这样:
# 1. 先准备好历史最后一段数据(要覆盖最大的滞后窗口,比如weeklyLag需要24*7=168小时) last_history = temp.tail(24*7).copy() # 把历史数据和未来数据框拼起来 full_df = pd.concat([last_history, future], ignore_index=True) # 给full_df加个yhat列存预测值,初始历史部分用真实y full_df['yhat'] = full_df['y'] # 2. 循环遍历每个未来时间点,一步步预测 for i in range(len(last_history), len(full_df)): # 计算当前时间点需要的滞后项 # 前1小时的温度:历史用真实y,已预测的用yhat full_df.loc[i, 'hourlyLag'] = full_df.loc[i-1, 'y'] if i-1 < len(last_history) else full_df.loc[i-1, 'yhat'] # 前24小时的温度 full_df.loc[i, 'dailyLag'] = full_df.loc[i-24, 'y'] if i-24 < len(last_history) else full_df.loc[i-24, 'yhat'] # 前7天的温度 full_df.loc[i, 'weeklyLag'] = full_df.loc[i-24*7, 'y'] if i-24*7 < len(last_history) else full_df.loc[i-24*7, 'yhat'] # 计算滚动均值和标准差:用历史+已预测的温度 window_start = max(0, i-24) past_temps = full_df.loc[window_start:i-1, 'y'].fillna(full_df.loc[window_start:i-1, 'yhat']) full_df.loc[i, 'movMean'] = past_temps.mean() full_df.loc[i, 'mStd'] = past_temps.std() # 上下边界跟着算 full_df.loc[i, 'ub'] = full_df.loc[i, 'movMean'] + 1.6 * full_df.loc[i, 'mStd'] full_df.loc[i, 'lb'] = full_df.loc[i, 'movMean'] - 1.6 * full_df.loc[i, 'mStd'] # 现在所有回归器都齐了,预测当前时间点 current_row = full_df.loc[[i]] pred = model.predict(current_row) # 把预测值存到yhat里,供下一个时间点用 full_df.loc[i, 'yhat'] = pred['yhat'].values[0] # 最后提取未来的预测结果 future_predictions = full_df.loc[len(last_history):, ['ds', 'yhat', 'yhat_lower', 'yhat_upper']]
这个方法虽然麻烦,但能解决滞后项的问题,缺点是如果预测周期很长(比如3年),循环会有点慢,但小时级3年也才26k多步,电脑能扛住。
思路2:砍掉依赖历史的回归器,用Prophet内置季节性
其实Prophet本身已经内置了小时、日、周、年的季节性特征,你手动加的dailyLag、weeklyLag这些,Prophet自己能通过季节性捕捉到。不如把这些依赖历史的回归器删掉,让Prophet自己处理季节性:
# 重新定义模型,用内置季节性 model = Prophet( daily_seasonality=True, # 捕捉小时级季节性 weekly_seasonality=True, yearly_seasonality=True, seasonality_mode='additive' ) # 只加能直接计算的时间回归器(甚至这些都可以不加,Prophet自己会处理) model.add_regressor('hour') model.add_regressor('month') # 训练和预测就简单了 model.fit(train) future = model.make_future_dataframe(periods=24*365*3, freq='h') # 补时间特征 future['hour'] = future['ds'].dt.hour future['month'] = future['ds'].dt.month # 直接预测,不会报错! predictions = model.predict(future)
这个方法最省心,而且Prophet的季节性拟合能力很强,效果不一定比你手动加滞后项差。
第三步:删掉逻辑无效的回归器
像devFromMean、devFromUB、devFromLB这些,都是用“真实温度”减出来的,而真实温度是我们要预测的目标,预测时根本拿不到,这些特征完全没用,直接从模型里删掉!
最后给你个快速试错的修改版
先把依赖历史的回归器从模型里去掉,只留时间类的,验证下预测流程能跑通:
# 重新准备特征(删掉无效的) temp = df[["Temperature"]].apply(kelvinToDegC).copy() # 只留能直接从时间算的特征 temp["hour"] = temp.index.hour temp["dayOfYear"] = temp.index.day temp["month"] = temp.index.month temp = temp.reset_index() temp.rename(columns={"Date": "ds", "Temperature": "y"}, inplace=True) # 重新定义模型 model = Prophet(daily_seasonality=True, weekly_seasonality=True, yearly_seasonality=True) model.add_regressor("hour") model.add_regressor("dayOfYear") model.add_regressor("month") model.fit(train) # 生成未来数据框+补特征 future = model.make_future_dataframe(periods=24*365*3, freq='h') future['hour'] = future['ds'].dt.hour future['dayOfYear'] = future['ds'].dt.day future['month'] = future['ds'].dt.month # 预测,这次不会报错了! predictions = model.predict(future)
这样先跑通流程,再根据需要加递归预测的滞后项就行。
备注:内容来源于stack exchange,提问作者Imtiaz Nabi

