Python时间序列实战:ARIMA/SARIMA降雨量预测建模
简介这套基于时间序列分析的降雨量预测Python项目是面向高校课程设计、毕业设计以及数据分析入门者的完整源码包重点解决历史降雨数据的建模与未来趋势预测问题适合农业气象、水资源管理等场景的初级开发者与科研爱好者。项目技术路线覆盖数据导入、缺失值与异常值清洗、平稳性检验、差分处理并集成ARIMA、SARIMA等经典模型帮助读者理解季节性因素对降雨规律的影响掌握从数据预处理到模型评估的完整流程同时提供可视化模块支持查看历史数据曲线和预测结果对比。压缩包约42.49MB内含完整工程源码、依赖库说明与详细系统文档文档清楚说明了安装步骤、运行方式和各项功能便于快速复现项目、二次开发及课程答辩。已有153人学习对希望将时间序列方法落地到气象、农业领域的开发者具有实际参考价值。1. 降雨量预测不是玄学这个 Python 时间序列项目到底能干什么把降雨预测做成一个能跑的 Python 项目是气象、农业类课程设计里的常见选题。这份资源说白了就是一套完整源码加文档历史降雨数据导入、缺失值和异常值处理、时间序列分析、ARIMA/SARIMA 建模、可视化界面全给配齐了。它的核心价值在于你不用从零去搭环境而是直接拿一套能运行的流程把 CSV 里的气象数据清洗成模型能吃的时间序列再通过 ACF/PACF 与 AIC 信息准则定阶输出未来一段时间的降雨量和置信区间。适合三类人做课程设计的学生、想快速验证时间序列建模流程的从业者以及需要给农业或气象场景做个降雨预测雏形的开发。下面按我实际拆这个项目时的顺序把环境配置、数据清洗、建模、可视化和踩坑一次讲清楚。2. 环境与数据准备把历史降雨数据喂进模型之前先过三关2.1 依赖安装与版本选型pandas、statsmodels 与 Python 版本怎么配这个项目的基础技术栈是 pandas 和 numpy 做数据处理statsmodels 做时间序列建模matplotlib 做可视化。建议先建一个干净的虚拟环境避免把系统 Python 搞乱。我一般用 conda 创建conda create -n rain python3.9 conda activate rain pip install pandas numpy statsmodels matplotlib这里有个关键点statsmodels 对 pandas 和 numpy 的版本比较敏感尤其是 0.14 之后的版本对 pandas 2.x 的支持有调整。如果后面出现导入报错优先检查是不是版本组合的问题。常见做法是固定一套组合比如 pandas 1.5.3、statsmodels 0.13.5、numpy 1.23.5虽然不是新的但在时间序列分析这个场景里稳定得多。装完依赖后下一步不是急着加载模型而是先确认数据文件长什么样。项目文档里通常会给出示例 CSV 的字段说明一般至少包含日期列和降雨量列。我拿到手的版本是两列date和rainfall前者是日粒度后者是毫米单位的降雨量。2.2 数据导入与清洗缺失值、异常值的常规处理时间序列建模最怕的是数据里有洞或者有极度离谱的异常值。下面这段是项目里数据预处理模块的常见写法我拆下来后略做了精简import pandas as pd import numpy as np # 读取数据指定日期列自动解析 df pd.read_csv(rainfall_data.csv, parse_dates[date]) # 把非数字的 rainfall 转成 NaN比如 -9999 这种占位符 df[rainfall] pd.to_numeric(df[rainfall], errorscoerce) # 将日期设为索引并排序保证时间顺序 df df.set_index(date).sort_index() # 统计缺失值 print(df.isnull().sum()) # 按时间线性插值保留时间轴完整 df[rainfall] df[rainfall].interpolate(methodtime) # 用 IQR 方法识别极端异常值并压回到合理边界 q1 df[rainfall].quantile(0.25) q3 df[rainfall].quantile(0.75) iqr q3 - q1 lower q1 - 1.5 * iqr upper q3 1.5 * iqr df.loc[df[rainfall] upper, rainfall] upper这段代码的逻辑很直白errorscoerce会把 CSV 里的缺失标识、文本干扰项统一转成NaN避免后面建模直接报错。interpolate(methodtime)是按时间间隔做线性插值比methodlinear更适合日粒度不均匀的数据比如某天没记录但相邻两天有值。IQR 那一段处理的是异常值比如一次台风带来的极端降雨量如果你的场景需要保留极端值可以把 1.5 调成 3或者干脆不改只是把超过上界的值截断防止单点把模型参数拉偏。2.3 数据重采样与平稳性ADF 检验不能跳过清洗完之后还要确认数据的频率是固定的。很多原始数据不是每天都有的比如某几周没有记录这种情况下直接建模SARIMA 会抱怨索引不是规则频率。所以要先重采样再检验平稳性from statsmodels.tsa.stattools import adfuller import matplotlib.pyplot as plt # 按天聚合缺失的天数会变成 NaN daily df[rainfall].resample(D).sum() # 看了缺失情况后再插值一次这次把重采样产生的洞补上 daily daily.interpolate(methodtime) # ADF 检验原序列平稳性 result adfuller(daily.dropna(), autolagAIC) print(ADF 统计量:, result[0]) print(p-value:, result[1]) # 如果 p 0.05做一阶差分后再看 if result[1] 0.05: diff daily.diff().dropna() result_diff adfuller(diff, autolagAIC) print(差分后 p-value:, result_diff[1])resample(D)里的D是日频率如果用M就是按月聚合月底值W是每周。这一步的作用是把时间轴补成规则日历序列后面 SARIMA 才能正常工作。ADF 检验是判断平稳性的核心p 值小于 0.05 说明序列平稳可以直接建模否则需要差分。这里有一个常见认知误区——ADF 检验不是跑一次就完事的差分后还要再检验一次直到 p 值通过。如果多次差分都没通过先别急着继续差大概率是数据本身的结构有问题后面避坑章节会细说。3. 建模核心ARIMA 和 SARIMA 的完整建模流程3.1 看图定阶ACF 与 PACF 怎么看拖尾和截尾数据平稳后第一步是画自相关函数 ACF 和偏自相关函数 PACF 图这是 ARIMA 定阶的传统办法。项目源码里通常有一段类似下面的画图逻辑from statsmodels.graphics.tsaplots import plot_acf, plot_pacf fig, axes plt.subplots(1, 2, figsize(12, 3)) # lags 取 24观察半个周期的相关性 plot_acf(daily.dropna(), lags24, axaxes[0]) plot_pacf(daily.dropna(), lags24, axaxes[1]) plt.tight_layout() plt.show()看图定阶的口诀是ACF 拖尾、PACF 截尾用 AR 模型p 取 PACF 截尾的阶数ACF 截尾、PACF 拖尾用 MA 模型q 取 ACF 截尾的阶数两者都拖尾用 ARMA。lags参数控制横轴相关性的阶数一般取数据长度的 1/3 到 1/2 就够不用画太多。但说实话纯靠肉眼判断在真实气象数据上很容易翻车。我拆这个项目时发现源码里虽然保留了画 ACF/PACF 的过程真正选阶时还是靠 AIC 信息准则做兜底。也就是先画出图当参考再用一组候选参数分别拟合挑 AIC 最小的那个组合。这个思路在后面进阶章节会展开。3.2 SARIMA 拟合order 与 seasonal_order 参数逐项解释降雨数据大多有年周期性比如夏季多雨、冬季少雨所以纯 ARIMA 不够要上 SARIMA。statsmodels 里的实现类名是SARIMAX它其实是带季节项的扩展模型。核心拟合代码如下from statsmodels.tsa.statespace.sarimax import SARIMAX # 假设前面判定的 d1p2, q2 是候选阶数 model SARIMAX( daily.dropna(), order(2, 1, 2), seasonal_order(1, 1, 1, 12), enforce_stationarityFalse, enforce_invertibilityFalse, ) res model.fit(dispFalse) print(res.summary()) # 保存模型后面预测直接复用 import joblib joblib.dump(res, sarima_model.pkl)参数逐项解释order(p,d,q)是非季节部分的三个超参p 是自回归阶数d 是差分阶数q 是移动平均阶数seasonal_order(P,D,Q,m)是季节部分P、D、Q 含义类似m 是季节周期长度。对降雨这种月周期明显的场景m12表示以 12 个月为周期。如果数据是季度粒度m4。这里有两个容易忽略的设置enforce_stationarityFalse和enforce_invertibilityFalse。默认情况下 statsmodels 会强制参数落在平稳和可逆区间内但实际拟合时如果候选参数组合离边界很近强制约束反而会导致收敛失败或者报ConvergenceWarning。设成False可以放宽约束让优化器更容易找到结果代价是偶尔会出现不平稳的拟合所以最后要看模型摘要里的参数是否合理。dispFalse是关掉优化器日志不然控制台会被刷屏。3.3 预测与评估RMSE 与置信区间怎么算模型拟合完接下来要回答两个问题预测结果在测试集上准不准未来一段时间的雨量到底是多少。项目里的预测模块大致是这么写的import numpy as np from sklearn.metrics import mean_squared_error, mean_absolute_error # 把数据切成训练集和测试集留最近 30 天做验证 train daily[:-30] test daily[-30:] # 重新用训练集拟合 model SARIMAX(train, order(2, 1, 2), seasonal_order(1, 1, 1, 12), enforce_stationarityFalse, enforce_invertibilityFalse) res model.fit(dispFalse) # 预测未来 30 步 fc res.get_forecast(steps30) pred_mean fc.predicted_mean # 点预测值 pred_ci fc.conf_int(alpha0.05) # 95% 置信区间 print(RMSE:, np.sqrt(mean_squared_error(test, pred_mean))) print(MAE:, mean_absolute_error(test, pred_mean)) # 保存预测结果到 CSV out pd.DataFrame({ date: pred_mean.index, predicted_rainfall: pred_mean.values, lower_bound: pred_ci.iloc[:, 0].values, upper_bound: pred_ci.iloc[:, 1].values, }) out.to_csv(forecast_result.csv, indexFalse)这里最重要的一个习惯是切出测试集后必须用train重新拟合而不是用全量数据拟合出的那个res去预测最近 30 天。如果直接用全量模型预测训练集的尾部看着 RMSE 漂亮得不得了实际是数据泄露后面线上预测会翻车具体放到避坑章节展开。get_forecast(steps30)返回的是一个PredictionResults对象.predicted_mean是点预测序列.conf_int(alpha0.05)返回两列 DataFrame分别是置信下界和上界。RMSE 和 MAE 是评价预测误差的指标RMSE 对大误差更敏感MAE 更直观。对降雨这类偏态数据RMSE 往往比 MAE 大不少不用惊讶正常现象。4. 可视化与结果输出把模型结果做成能汇报的界面4.1 用 matplotlib 画历史与预测曲线课程设计或者项目汇报最核心的就是这张预测图。matplotlib 画预测曲线的标准套路是把历史实际值、预测点值和置信区间画在同一张图上import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(train.index, train, labeltrain, color#333333) plt.plot(test.index, test, labelactual, color#666666) plt.plot(pred_mean.index, pred_mean, labelforecast, color#d7191c) plt.fill_between( pred_ci.index, pred_ci.iloc[:, 0], pred_ci.iloc[:, 1], color#d7191c, alpha0.2, ) plt.title(Rainfall Forecast with SARIMA) plt.xlabel(Date) plt.ylabel(Rainfall (mm)) plt.legend() plt.grid(True) plt.tight_layout() plt.savefig(forecast_plot.png, dpi120) plt.show()fill_between是画置信区间阴影的常用函数第一个参数是横轴索引后面两个参数分别是下界和上界alpha0.2控制透明度太浓会盖住曲线。savefig的dpi120保证保存的图片在报告里放大后不糊。另外建议把train、test和预测曲线用不同颜色区分汇报时不用废话别人就能看懂哪段是历史、哪段是未来。如果资源自带的可视化界面用的是 Tkinter 或 Flask本质上也就是在内部调用 matplotlib 生成图片再把图片丢到界面上展示。核心的图表生成逻辑就是我上面这段。4.2 预测结果导出与后续使用思路预测曲线只是给人看的程序之间交互需要数据文件。前面提到把预测结果写入 CSV这是最通用的做法。如果是做系统集成我一般会顺手再把结果写进 SQLite 表import sqlite3 conn sqlite3.connect(rainfall.db) out.to_sql(forecast, conn, if_existsreplace, indexFalse) conn.close()to_sql的if_existsreplace表示每次重新预测就覆盖旧表避免重复数据累积。这样做的好处是后端只需要查这张表就能给前端返回预测结果不需要重新跑模型。资源的系统文档里如果提到数据库技术大概率就是类似的设计。另外项目里通常会把多个候选模型做对比比如 ARIMA、SARIMA、Prophet 都跑一遍然后比较 RMSE。Prophet 的接入也不复杂把数据列改名成ds和y即可但那套 API 的调参风格和 statsmodels 差异大如果资源没有自带不建议混在一起用。5. 避坑记录这个项目我翻过车的四个地方5.1 版本冲突statsmodels 与 pandas 打架现象按照文档装完依赖一行代码没写导入 statsmodels 直接报ImportError或者AttributeError: module pandas has no attribute Panel。原因statsmodels 某些版本对 pandas 的内部 API 有依赖pandas 2.x 移除了一些旧接口后低版本 statsmodels 就崩了。这类问题在时间序列包里尤其常见因为 statsmodels 的数据结构高度依赖 pandas。解决不要追新版本直接锁组合。我这次用的是 Python 3.9 pandas 1.5.3 numpy 1.23.5 statsmodels 0.13.5装完再也没出现过导入错误。如果你已经被报错卡住先卸载重装pip uninstall statsmodels pandas numpy然后按上面版本装。注意顺序numpy 要先装否则 statsmodels 编译可能找不到依赖。5.2 删缺失值把时间轴删断了现象清洗阶段直接用df.dropna()删掉有缺失的行结果后面resample(D)倒是没问题但 SARIMA 模型预测的日期索引和真实日期对不上对不上后画图时两条曲线错位评估指标直接爆表。原因dropna()删掉的不仅是缺失值还有对应的日期索引导致时间序列变成不规则间隔。SARIMA 内部默认按连续时间索引计算滞后阶数索引一断模型把相隔一个月的数据当成相邻两天处理结果自然荒谬。解决正确的做法是先resample(D)补全日历再用interpolate(methodtime)填充绝不在建模前直接删行。如果某段连续缺失太多比如超过整段数据的 20%插值也没意义那就需要裁剪这段区间但要确保新的开始和结束索引仍是规则频率。5.3 过度差分为了过 ADF 把数据差到没谱现象ADF 检验 p 值一直大于 0.05于是一次差分不够两次差分还不够第三次差下去p 值终于过了但模型预测结果方差极大画出来的预测曲线像噪点。原因降雨数据通常不只有趋势项还有很强的季节性。单纯做差分虽然能消除部分不平稳但过度差分会把低频的季节信号也差掉导致序列只剩高频噪声模型学到的是随机波动而不是规律。解决先画seasonal_decompose分解趋势、季节、残差三部分看看不平稳到底是趋势引起的还是季节性引起的。如果主要是季节性就应该用 SARIMA 的季节差分D而不是拼命增加非季节差分d。还有个偷懒但有效的工具pmdarima库里的ndiffs()和nsdiffs()可以直接告诉你差分的应当阶数省得自己试错。5.4 数据泄露回测好看实战翻车现象全量数据拟合后预测最近 30 天测试集上 RMSE 只有个位数模型看起来完美但真正往后预测下一周结果偏得离谱。原因这是典型的数据泄露。全量数据已经包含了测试集那段历史模型在训练时见过这些真实值预测它们当然准。真正做预测时未来那段是模型没见过的性能自然会显著下降。解决必须做滚动窗口回测。也就是只使用截止到某个时间点的历史数据训练然后预测下一天或下一周将预测值和真实值比对再把这个时间点向后移动重复整个过程。我在这个项目里写的回测框架大概是这样的predictions [] for end_idx in range(train_size, len(daily) - horizon): train_window daily.iloc[:end_idx] test_window daily.iloc[end_idx:end_idx horizon] model SARIMAX(train_window, order(2,1,2), seasonal_order(1,1,1,12)) tmp_res model.fit(dispFalse) pred tmp_res.get_forecast(stepshorizon).predicted_mean predictions.append(pred.iloc[-1])这样每一步都只用历史数据模拟真实的预测环境评估出来的误差才有参考价值。网格搜索定阶也应该在同样的窗口上做否则选出来的参数依然可能过拟合。6. 进阶用网格搜索自动定阶把 SARIMA 从手动调参里解放出来ACF/PACF 看图定阶看着专业实际效率很低尤其在课程设计里要对比多组参数时。更稳的做法是写一个简单的网格搜索遍历候选的 p、d、q 组合用 AIC 自动选最优解。SARIMA 因为多了四个季节参数组合爆炸得更厉害所以非季节部分用网格搜索季节参数根据周期固定通常就够用了。import itertools import warnings from statsmodels.tsa.statespace.sarimax import SARIMAX warnings.filterwarnings(ignore) def select_order(train, m12): best_aic float(inf) best_order None p_range range(0, 3) # p: 0~2 d_range range(0, 2) # d: 0~1 q_range range(0, 3) # q: 0~2 for p, d, q in itertools.product(p_range, d_range, q_range): try: model SARIMAX( train, order(p, d, q), seasonal_order(1, 1, 1, m), enforce_stationarityFalse, enforce_invertibilityFalse, ) res model.fit(dispFalse) if res.aic best_aic: best_aic res.aic best_order (p, d, q) except Exception: continue return best_order, best_aic best_order, best_aic select_order(train) print(最优阶数:, best_order, AIC:, best_aic)代码逻辑用itertools.product生成所有 (p, d, q) 组合逐个拟合保留 AIC 最小的组合。p_range、d_range、q_range可以根据数据量调整日数据的 p、q 取 0~2 已经足够再大容易过拟合。每组拟合包在try-except里跳过不收敛的组合因为 SARIMA 有些参数组确实会收敛失败直接报错会中断整个搜索。季节部分(1, 1, 1, 12)固定如果数据是季度粒度就把m改成 4。确定最优阶数后用选出来的参数重新在完整训练集上拟合再用joblib.save保存模型后续预测不重复跑搜索。从那以后我每次拿到时间序列数据都强制走一遍这个流程先seasonal_decompose看结构和周期再跑 ADF 决定 d最后网格搜索定 p、q再也不手推参数了。这套习惯帮我至少省掉了一半的调参时间希望帮到你。本文还有配套的精品资源点击获取