
简介一项基于随机森林模型的水稻产量预测Python项目面向计算机、数据科学与大数据技术、人工智能等专业的课程大作业、课程设计与毕业设计场景。包内提供可运行的Python主程序、包含历史种植数据及实际与预测结果对比的两份CSV数据集配合项目配置文件可帮助读者快速复现从数据清洗、特征构建到模型训练与结果评估的完整预测流程。压缩包共8个文件涵盖Python脚本、CSV数据表、XML配置及Git忽略文件等类型整体仅54KB轻量且结构清晰适合逐模块精读。目前已有203人学习下载可作为项目实战练习、初期立项演示或学习随机森林应用的参考样本。通过对比实际与预测数据还可以直观地评估模型精度理解随机森林在农业预测中的适用场景。代码已经过测试运行成功数据集与脚本分离便于替换为其他农作物数据进行迁移学习能显著减少从零搭建环境与调试代码的时间。1. 水稻产量预测的随机森林方案这份源码解决什么问题、适合谁水稻产量预测在农学里是个老问题在机器学习里是个典型的表格回归任务几十年的气象记录、土壤化验结果和田间管理台账就能构成一份可用数据。随机森林模型在这个场景有天然优势——特征数量不多、样本量不大、噪声很高恰好是随机森林而不是神经网络的舒适区。这份 python 源码要做的就是从气象、土壤和田间管理数据出发训练一个能输出连续产量数值的随机森林回归模型并给出评估指标、特征重要性和后续预测能力。适合两类人一类是农学或遥感背景的研究生和从业者手里有数据但缺工程化建模流程另一类是 python 学习者想拿一个真实项目把 pandas、sklearn、joblib 串起来。下面按我实际做过的方案把这个项目从原理到踩坑完整讲一遍。2. 随机森林凭什么适合做产量预测回归原理与模型选型2.1 从决策树到随机森林取平均值如何压住田间的噪声单棵决策树在做回归时每次分裂都要从所有特征里挑一个最优切分点把样本分成两个子节点直到满足停止条件。叶节点里存的是该区域样本的目标均值新样本落进哪个叶节点输出就是那个均值。这个机制对产量数据的拟合能力很强但问题也随之而来同一品种、同一施肥方案在不同田块之间产量波动 30% 是常态单棵树很容易把这种田间噪声当成规律学进去训练集 R² 很高拿到新年份数据就露馅。随机森林解决这个问题的思路是“群殴”用 bagging 从 N 个样本里做有放回抽样每棵树只见过大约 63.2% 的样本树之间的差异被保留下来每次分裂时再随机选出部分特征参与竞争进一步降低树之间的相关性。最终预测值等于所有树输出结果的平均回归场景下这个平均对噪声有天然的抵消作用对真实信号又能形成一致投票。换句话说产量预测这种“信号弱、噪声大”的数据形态平均机制正好对症这比单纯加深一棵树要稳得多。2.2 为什么不是线性回归、SVR 或 XGBoost产量数据形态决定选型很多人拿到产量数据第一反应是先跑线性回归我也这么干过但大多数情况下效果不好。产量与气象条件的关系不是线性的温度对产量基本是倒 U 型曲线低温影响分蘖、高温影响灌浆降水过多或过少都有害用线性模型表达这种两头都坏的关系非常吃力除非你手动构造二次项和交互项工作量不小。SVR 在小样本回归里常被推荐但核函数的选择有玄学成分RBF 核的 C 和 gamma 对产量数据非常敏感同样一份数据换一组参数结果差很多调试成本高。那为什么不用 XGBoost 或 LightGBM这两个模型在表格竞赛里表现更强但前提是样本量要够常见的产量预测项目往往只有一两百个样本GBDT 类模型在默认参数下容易过拟合需要配合早停和大量调参。随机森林的默认参数下表现相对稳定对异常值和缺失值容忍度高训练完成后还能直接输出特征重要性适合做一个“先用起来”的项目基底。对比维度随机森林XGBoost / LightGBM线性回归小样本稳定性较好需要精细调参简单但欠拟合非线性关系能学能学需要手工构造特征超参数敏感度较低较高低特征重要性内置内置只有系数不够直观训练开销可并行串行迭代最低2.3 产量预测的特征体系气象、土壤、田管三类变量怎么取舍产量预测的特征一般分三类。气象类最常用包括生育期积温GDD、总降水量、降水分布、平均日照时数、极端高温天数这些数据从气象站点或遥感数据里都能拿到。土壤类包括有机质、全氮、速效磷、速效钾和 pH来自土壤化验或者当地土肥站的图件。田间管理类包括移栽期、施肥折纯量、灌溉次数和品种信息这部分数据质量差异很大很多时候要靠台账补录。一个关键原则是特征必须在预测时点之前可得。如果目标是收获前一个月做产量预估就不能用收获期才测的千粒重、穗粒数这类指标。特征数量控制在 15 到 30 个之间样本量只有几百时特征超过 50 个容易出现维度灾难树的分裂被无关变量干扰特征重要性也会失真。宁可少而精也不要为了“多学一点”硬塞高维类别变量。3. 跑通随机森林产量预测 python 源码数据清洗、训练、评估与模型保存3.1 运行环境与项目文件结构sklearn 库安装与脚本拆分先把环境搭好。推荐 python 3.8 以上版本用 pip 安装依赖。如果你还没有安装 sklearn 库下面的命令一行搞定。注意 pandas 和 numpy 的版本不需要刻意追求最新能跑通就可以。pip install pandas numpy scikit-learn matplotlib joblibscikit-learn 1.x 系列对 RandomForestRegressor 的接口变化不大老项目迁移很平滑。我一般把项目拆成四个文件不要把所有逻辑塞进一个脚本里这样后面做参数实验和年份回测时能省大量时间。yield_forecast/ ├── data/ │ └── rice_yield.csv ├── features.py ├── train_model.py ├── evaluate_model.py └── predict.pyfeatures.py 负责读数据和构建特征train_model.py 负责训练和调参evaluate_model.py 输出评估指标和特征重要性图predict.py 加载模型对新数据做预测。这套拆法是我做类似项目踩了不少坑之后沉淀下来的一年后回看当年的数据只要改 data 目录下的 CSV 就能重新跑一遍不会因为代码揉成一团而被迫重写。3.2 数据清洗与特征构建缺失值、产量异常值与积温派生特征获取到的原始数据通常不会很干净常见的坑包括产量列缺失、产量值明显超出当地历史记录、气象字段部分年份缺数据。先处理结构和缺失值。import pandas as pd import numpy as np df pd.read_csv(data/rice_yield.csv, encodinggbk) # 产量必须有真实标签缺失的直接剔除 df df.dropna(subset[yield]) # 产量异常值过滤超过当地历史最高记录 150% 或为负数属于录入错误 hist_max 900 # kg/亩按实际区域调整 df df[(df[yield] 0) (df[yield] hist_max * 1.5)] # 气象缺失值用前后年份同旬均值插值避免直接删样本丢信息 for col in [tmax, tmin, precip]: df[col] df.groupby(site)[col].transform(lambda s: s.interpolate(limit_directionboth))产量缺失必须删行因为它是监督学习的标签不能用插值补。气象列可以插值因为站点和年份之间有相关性前后年份同旬的均值比全表均值可信得多。产量过滤阈值要按区域调整黑龙江和海南的单产水平差很多用固定阈值会误删有效数据。接下来构建积温特征。积温是水稻产量预测里最常被忽视但很有用的特征它把生育期内的高温累积效应量化出来。def calc_gdd(tmax, tmin, base10): 计算生育期内大于 base 摄氏度的有效积温base 取 10 是水稻常见基准。 tmean (tmax tmin) / 2.0 return np.maximum(0, tmean - base).sum() # 按站点和年份分组计算生育期积温 gdd df.groupby([site, year]).apply( lambda g: calc_gdd(g[tmax].values, g[tmin].values) ).reset_index(namegdd_total) df df.merge(gdd, on[site, year], howleft)这个函数的逻辑是逐日计算日平均气温减去基准温度 10 度低于 10 度的部分按 0 处理最后求和。积温越高代表生育期热量越充足但超过某一阈值后光合产物更多消耗在呼吸上产量反而下降所以模型需要靠 max_depth 和特征分裂自己学出这个倒 U 关系。3.3 训练随机森林回归模型按年份切分与初始参数设置产量预测是时间序列性质的数据训练集和测试集不能随机切分否则同一年份的气象条件会被同时分到两边模型等于偷看了测试答案。这里强制按年份划分。from sklearn.ensemble import RandomForestRegressor feature_cols [gdd_total, precip_total, sunshine_hours, extreme_high_temp_days, soil_organic, soil_ph, fertilizer_amount, irrigation_times] X df[feature_cols].values y df[yield].values # 按年份切分2005-2020 年做训练2021-2022 年做测试绝不随机切 train_mask (df[year] 2005) (df[year] 2020) test_mask df[year] 2020 X_train, X_test X[train_mask], X[test_mask] y_train, y_test y[train_mask], y[test_mask] rf RandomForestRegressor( n_estimators500, max_depth15, min_samples_leaf3, max_features0.33, random_state42, n_jobs-1, ) rf.fit(X_train, y_train)参数说明分几个角度n_estimators 设 500树太少偏差大超过 1000 收益递减还拖慢训练max_depth 设 15是防止树无限生长过拟合的关键产量数据特征不多深度太浅学不到交互太深把每个样本都背下来min_samples_leaf 设 3让叶节点至少有三个样本避免极端年份的单条记录主导预测max_features 取 0.33即每次分裂看 8 个特征中的约三分之一这是回归场景的常见做法random_state 固定 42 是为了别人复现你结果时完全一致n_jobs-1 表示用满所有 CPU 核几百棵树并行训练很快。3.4 评估、特征重要性与模型导出joblib 让模型可以重复使用训练只是开始得知道模型到底能不能用。产量预测关注三个指标R² 看解释力RMSE 看实际误差多少公斤MAE 看平均偏了多少。from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error pred rf.predict(X_test) r2 r2_score(y_test, pred) rmse mean_squared_error(y_test, pred) ** 0.5 mae mean_absolute_error(y_test, pred) print(fR²{r2:.3f}, RMSE{rmse:.1f} kg/亩, MAE{mae:.1f} kg/亩)RMSE 单独看没有意义要跟产量均值比。假设当地平均亩产 550 公斤RMSE 60 公斤意味着平均偏差超过 10%这个精度在产量预报里属于能用的水平如果 RMSE 只有 15 公斤而 R² 高达 0.98那基本可以断定数据里混进了收获后实测信息后面避坑章节会展开讲。注意新版 sklearn 里 mean_squared_error 的 squared 参数已经在调整直接用** 0.5最稳妥老版本也不会报错。特征重要性和模型导出放一起import joblib importance pd.Series(rf.feature_importances_, indexfeature_cols).sort_values(ascendingFalse) print(importance.head(10)) joblib.dump(rf, models/rf_yield.pkl) joblib.dump(feature_cols, models/feature_cols.pkl)特征重要性要结合业务判断积温和降水排在前面说明模型学到的是合理气象规律如果某个地块编码排第一反而要警觉。joblib 保存模型后predict.py 里加载时要注意特征顺序必须与训练时完全一致joblib 只存模型不存特征列名所以这里把 feature_cols 也一起 dump 了这是很多第一次上手跑源码的人最容易忽略的点加载模型后先检查列名再预测。4. 随机森林产量预测调参核心参数范围、网格搜索与时间序列切分4.1 四个必调参数的影响范围先手动试出量级再上搜索随机森林回归的默认参数不是不能用但会偏保守想要更好的测试集表现四个参数是绕不开的。n_estimators 决定树的数量。我的经验是从 300 起步观察 OOB 分数袋外样本评分是否随树增多而上升如果加到 500 后分数几乎不动就没有必要再加了纯耗算力。max_depth 是最容易过拟合的旋钮。产量数据特征一般在 20 个左右深度 8 到 16 之间是比较合理的区间。不限制深度的后果是训练 R² 经常能看到 0.95 以上测试掉到 0.5 上下差距拉开就说明模型在背样本而不是学规律。min_samples_leaf 控制叶节点的最少样本数。产量数据噪声大我建议从 3 开始调大它会让测试更稳但也会牺牲对极端年份的敏感度具体看你更怕哪种错误。max_features 在回归任务里常见取特征数的三分之一。如果特征之间高度相关可以往 0.2 调减少树之间的相关性。参数建议范围产量数据上的表现n_estimators300 - 1000增加后 OOB 分数先升后平max_depth8 - 16越大训练越贴近样本测试先升后降min_samples_leaf3 - 5越大越抗噪声但对极端年份不敏感max_features0.2 - 0.4越小树相关性越低避免重复切分同一特征4.2 用 GridSearchCV 搜索参数组合交叉验证折数怎么选不翻车手动试参数容易陷入局部最优交给网格搜索更系统。但产量数据的交叉验证折数有讲究直接用 KFold 会把同一年份的数据同时分进训练和测试造成信息泄漏。正确做法是按时间序列切分或者按年份分组。from sklearn.model_selection import GridSearchCV, TimeSeriesSplit param_grid { n_estimators: [300, 500, 800], max_depth: [8, 12, 16], min_samples_leaf: [3, 5], max_features: [0.25, 0.33, 0.4], } # 时间序列顺序切分3 折每折用前两年预测后一年 tscv TimeSeriesSplit(n_splits3) gs GridSearchCV( RandomForestRegressor(random_state42, n_jobs-1), param_grid, cvtscv, scoringr2, n_jobs-1, verbose1, ) gs.fit(X, y) print(gs.best_params_)TimeSeriesSplit 的训练集永远是测试集的过去这跟产量预测的真实应用场景完全一致。这里用 n_splits3 而不是更大的折数是因为年份少折数太多会导致早期训练集样本不足结果不稳定。搜出来的最佳参数组合要用独立年份的数据再验证一次不能把网格搜索分数当成最终成绩。4.3 时间序列切分随机切分是产量预测翻车的重灾区做量化交易策略回测的人都有血泪经验回测和实盘不一致多半是切分时偷看了未来。产量预测一模一样。如果随机切分训练集和测试集同一年份的气象数据、土壤数据会被拆开模型可以通过年份和站点的隐性编码把测试样本“记住”测试 R² 虚高。真实业务里你要预测的是未来年份模型根本没有见过那年的天气。保险的做法是显式按年份排序df df.sort_values(year).reset_index(dropTrue) train_mask df[year] 2021 test_mask df[year] 2021 X_train, X_test X[train_mask], X[test_mask] y_train, y_test y[train_mask], y[test_mask]如果只有三四年的数据按年份硬切会导致训练集太小模型根本学不到东西这时不要硬上改用第 6 章讲的滑动窗口回测来估计误差。至少要保证测试集里包含完整的独立年份而不是从中间切一刀。5. 水稻产量预测避坑指南特征泄露、外推失效与小样本陷阱5.1 特征泄露测试集混入收获后信息R² 虚高到 0.98现象训练完模型测试集 R² 0.98RMSE 不到 10 公斤换算成相对误差只有 2%。拿去实测完全不对甚至不如直接用往年平均产量当预测值。原因数据表里混进了收获后才能测得的指标比如“穗粒数”“千粒重”“实际收割面积”或者把“当年实际化肥用量”当成特征但预测时点在施肥前根本无法获知。随机森林不会甄别特征时间任何数字都能拿来分裂只要特征与产量有强相关它就会利用到极致。解决建模前先画一张特征时点表把每个字段标到时间轴上分成播种前可得、生育中期可得、收获后可得三类只保留前两类。穗粒数、千粒重这类字段直接删掉实际化肥用量换成“计划施肥量”。这张表应该出现在项目文档第一页而不是模型训练完之后。5.2 外推失效训练年份太平顺极端年份预测集体失灵现象测试年份出现连续 15 天最高温超过 38 度模型预测的产量几乎全落在平均值附近该降的地方不降该升的地方不升。原因随机森林本质上是分段常数模型只能内插不能外推。极端年份的气象组合高温加少雨加强辐射在历史训练数据里从未出现树的所有叶节点里都没有对应样本最后输出只能回归到邻近样本的平均值。这是随机森林这类算法与线性模型或深度学习相比最大的短板。解决把历史极端年份单独拉出来做压力测试。报告结果时分两行正常年份 RMSE 和极端年份 RMSE让使用者知道模型在什么样的情况下会失效。气象特征尽量做成生育期内逐旬聚合而不是整个生育期取平均这样极端天气的影响会体现在部分时段的异常上模型至少有机会学到部分响应。5.3 样本量不足60 个样本、20 个特征随机森林照样过拟合现象训练 R² 0.93测试 R² 只有 0.2 左右测试曲线跟随机猜测差不多。原因决策树的容量太大样本量只有 60 时每棵树可以把每个样本都背下来。随机森林的 bagging 过程需要样本多样性60 个样本采出来的子集差异有限防不了过拟合。解决调高 min_samples_leaf 到 5 以上max_depth 收到 8 以内特征数删到 10 以下。同时加一个线性回归 baseline如果随机森林打不过线性回归不要再纠结模型先补数据。产量预测项目里 300 个样本是分水岭低于这个数任何复杂模型的提升都是幻觉。5.4 特征重要性被 one-hot 变量带偏品种编号排第一气象变量退居二线现象feature_importances_ 排名前十里有六七个是 one-hot 之后的品种、地块、行政村编码积温、降水量这种真气象变量排到十名开外。原因随机森林的 feature_importances_ 偏向取值更多的变量。分类变量 one-hot 之后每个类别一列类别多的变量被切分的次数显著上升重要性虚高。地块编码这类唯一性很强的变量树可以直接靠它做记忆式分裂。解决高基数类别变量不要裸着进模型。要么把品种聚合为“籼稻、粳稻、杂交稻”这种粗分类要么只保留出现次数超过 20 的类别做 one-hot地块编号这类唯一标识直接删掉。更稳的错误评估方式是使用 permutation_importance它通过打乱某个特征后观察分数下降多少来判断重要性不受切分次数偏向影响。6. 让产量预测更可信滑动窗口回测与极端年份单独评估单个测试年份的 R² 多少带点运气成分一次切分的结果不能说明模型稳定。我现在做产量预测都会补一步滑动窗口回测从最早可用的年份起每年都用“过去 N 年训练、预测当年”把所有年份的预测误差排在一起才能看清模型到底在哪些年份靠谱、哪些年份崩盘。results {} train_start 2005 for test_year in range(2018, 2023): train_mask (df[year] train_start) (df[year] test_year) test_mask df[year] test_year rf_tmp RandomForestRegressor( n_estimators500, max_depth15, min_samples_leaf3, max_features0.33, random_state42, n_jobs-1 ) rf_tmp.fit(X[train_mask], y[train_mask]) pred rf_tmp.predict(X[test_mask]) r2 r2_score(y[test_mask], pred) rmse mean_squared_error(y[test_mask], pred) ** 0.5 results[test_year] {R²: round(r2, 3), RMSE: round(rmse, 1)}把 results 打印成表格找出 RMSE 最差的一两年单独查那一年的气象记录。如果最差年份恰好是极端气候年说明模型外推能力确实有边界业务上就要标注“该年预测仅供参考请结合人工研判”。这套流程跟量化交易里按时间轴滚动回测的思路一致核心就一句话样本外验证必须模拟真实的时间推进。我现在接手这类项目第一件事不是跑模型而是画特征时点图把每个字段标到时间轴上确认它在该预测时点之前能拿到然后才谈建模和调参。先做这一步后面省的返工时间远多于建模本身的时间。希望帮到你。本文还有配套的精品资源点击获取