ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

粮食产量预测怎么做?随机森林与XGBoost实战全解析

粮食产量预测怎么做?随机森林与XGBoost实战全解析 简介这是一篇发表于《河北农业大学学报》的机器学习粮食产量预测学术文献面向农业信息化、数据分析及机器学习方向的研究生、科研人员和从业者可作为相关课题的参考文献与模型选型依据。压缩包内共1个文件为PDF格式大小1.69MB内容完整便于直接阅读与引用。文中以保定市1996—2014年粮食产量和16项影响因素为数据运用皮尔逊相关性分析筛选特征系统对比ARIMA、LSTM与ARIMA-GRNN组合模型的预测效果并给出平均相对误差分别为0.96%、2.20%、0.47%的实证结论。已有635人学习下载对粮食产量预测建模、组合模型构建及学术论文写作均具有实际参考价值。1. 为什么粮食产量预测值得用机器学习重做一次粮食产量预测不是新话题但过去在基层业务里最常见的做法是把温度、降雨、产量数据塞进一个多元线性回归或者直接拿上一年的产量做趋势外推。这套方法在常年景况下够用一旦遇到极端高温、生育期错位、局部干旱这些非线性事件误差就会迅速放大。机器学习的价值在于它可以不预设曲线形状直接从历史数据里学到气温、降水、土壤和最终产量之间的复杂交互把「产量预测」这个任务当成一个有监督回归问题来建模。这篇文章要做的事情很具体从数据整理和特征构造开始到你一定能跑起来的随机森林与 XGBoost 代码再到如何用时间边界正确评估模型最后给出生产环境里常用的可解释性技巧。适合正在做农业信息化、智慧农业或者正面临类似「小而脏」数据集回归任务的工程师。2. 粮食产量预测的数据结构与特征构造2.1 产量预测的数据源与样本组织方式在做任何模型之前需要先把「一条样本」是什么定清楚。产量预测里最常见的样本组织方式是一个多元组区域ID 作物类型 年份。对应的目标变量是这一区域在这一年份的单位面积产量通常用kg/ha或kg/亩。特征是同一时期该区域的气象、土壤、遥感和管理信息。这样的宽表每一行代表一个完整观测模型才能直接消费。数据来源通常有四个类别我遇到过不少项目把大量时间花在前面三类数据的对齐上而不是模型本身特征类别典型字段时间粒度聚合方式气象最高温、最低温、降水量、日照时数日生育期内均值、总和、极值、连续天数土壤有机质、pH、全氮、速效钾多年静态按区域直接匹配遥感NDVI、EVI、LAI旬或月生育期峰值、均值管理措施播种日期、品种类型、灌溉量年/次数值或分类编码关于管理措施要特别提醒一句在公开数据集里它经常缺失但它的解释力甚至高于气象。如果手头没有管理数据模型就是在用气象和土壤间接猜测农业行为精度上限会低不少。做业务项目时优先找当地农技站要播种面积、品种占比、施肥量这类数据哪怕只有两三年也对模型有帮助。2.2 气象时序数据的特征聚合与衍生气象数据是逐日记录不能直接把 365 天的温度都作为特征放进去这样不仅维度爆炸更违反了产量形成的时间规律。通常的做法是先根据作物生育期把时间窗切出来再在每个窗口内做聚合。水稻和小麦的生育期不同但通用的窗口会分为营养生长期、生殖生长期、灌浆成熟期每个窗口单独计算统计量。下面是一段常见的气象特征提取函数假设手头有一个包含日均最高温、最低温和降水的weather表import pandas as pd def extract_weather_features(weather, station_id, start_date, end_date): 按站点聚合一个生育期内的气象特征 station_id: 区域索引 start_date/end_date: 生育期起止日期 df weather.loc[weather[station] station_id].copy() df[date] pd.to_datetime(df[date]) mask (df[date] start_date) (df[date] end_date) df df.loc[mask] features { tmax_mean: df[tmax].mean(), tmax_max: df[tmax].max(), tmin_mean: df[tmin].mean(), tmin_min: df[tmin].min(), precip_sum: df[precip].sum(), precip_days: (df[precip] 0).sum(), heat_days: (df[tmax] 32).sum(), cold_days: (df[tmin] 12).sum() } return features这段代码的核心是先用日期掩膜截出生育期然后计算均值、极值和阈值天数。heat_days和cold_days是产量预测里特别有效的特征水稻在灌浆期遇到连续高温会结实率下降玉米在低温寡照时干物质积累不足。只给模型一个「平均气温」通常掩盖了这些极端事件所以必须在聚合时保留阈值事件信息。如果不想手动写日期可以采用更粗糙的月份窗口比如用 6—8 月代表生长季。但这种方式会引入一个隐患不同年份的物候期不是固定不变的同一个日期窗口做出来的特征在两个年份代表的意义不同。我一般会保留「播种期」字段把窗口起点设在播期之后 N 天而不是固定在公历月份。2.3 特征合并与时间边界的训练集划分特征聚合完成后把所有特征合并进一个宽表并检查重复区、缺失值和单位。这里有一个容易犯的错土壤特征可能多年不变如果不小心按年份复制会在数据里留下重复样本而降水量如果某站某天缺测直接 fillna(0) 会让「无降水」和「缺测」混在一起。建议对气象缺测先用站间插值或最近邻填充再进入特征计算。from sklearn.model_selection import train_test_split # 错误示范随机切分会把同一年不同地块的信息泄漏到验证集 train, test train_test_split(df, test_size0.2, random_state42) # 正确做法按年份切分 train_years df[df[year] 2021] test_years df[df[year] 2022]随机切分在产量预测里是灾难性的。因为气象条件有年际相关性同一年内不同区域的样本会共享同一种天气模式随机切分会让模型「提前看到」当年的整体气候验证集的误差被严重低估。正确的做法是直接按年份划分训练集、验证集、测试集分别放在不同的年份区间。当年份数量少时可以用GroupKFold只对年份分组做交叉验证from sklearn.model_selection import GroupKFold groups df[year] gkf GroupKFold(n_splits5) # 每个fold里一组年份做验证剩余年份做训练 for train_idx, val_idx in gkf.split(df, groupsgroups): train, val df.iloc[train_idx], df.iloc[val_idx] print(sorted(train[year].unique()), -, sorted(val[year].unique()))GroupKFold的关键作用是它保证同一个group年份不会同时出现在训练集和验证集。这里的n_splits5意味着把年份分成 5 组因此至少要有 5 个不同的年份否则会报错。这个划分方式将贯穿后续的调参和评估不只是在最初切一次。3. 随机森林与 XGBoost 回归预测模型的选型与调参3.1 从线性回归到树模型的必要跃迁粮食产量和气象特征之间的关系是非线性的。最典型的例子是温度水稻抽穗期遇到 35°C 以上的高温每多持续 1 天结实率可能下降 2%—5%。在 34°C 以下温度上升带来的影响很小过了 35°C影响突然放大。这种「阈值效应」在线性回归里只能通过人为构造一个分段变量来模拟而决策树天然可以按「是否大于 35°C」做切分不需要提前知道拐点位置。随机森林回归预测模型是决策树的集合通过 bootstrap 抽样和特征随机选择生成多棵互不相同的树最终预测取平均。它的优势是方差低、抗过拟合在样本量只有几百到几千时依然稳定。XGBoost 回归预测模型则是梯度提升算法每一棵树拟合前面所有树的残差因此能逼近更复杂的函数但更容易在噪音数据上过拟合需要更精细地控制学习率和正则项。在实际项目里我会同时训练这两个模型作为基线而不是只押注一个。原因很简单随机森林提供一个稳健的下限XGBoost 提供一个精度潜力更大的上限两相对比可以判断数据中的信号到底有多强。如果两个模型的表现接近说明问题可能卡在特征层面而不是模型能力不足。3.2 关键参数作用与默认值不同库的默认值不完全一样这里以scikit-learn和xgboost两个库的当前版本为准给出需要优先调整的参数参数名所在模型默认值作用产量预测常用设置n_estimatorsRandomForest/XGBoost100/300树的数量太小欠拟合太大耗时随机森林 300XGBoost 以早停确定max_depth两者None/6单棵树最大深度控制模型复杂度随机森林 1020XGBoost 37min_samples_leafRandomForest1叶节点最小样本数越大越平滑25learning_rateXGBoost0.3每棵树的步长越小越稳健0.010.1subsampleXGBoost1.0每棵树随机使用的行比例0.70.9colsample_bytreeXGBoost1.0每棵树随机使用的特征比例0.70.9reg_lambdaXGBoost1.0L2 正则权重越大越保守110这里面的第一原则是先确定learning_rate再定n_estimators。如果learning_rate0.3模型可能在 100 棵树后就收敛如果降到 0.05同样的数据可能需要 500 棵树。产量数据通常特征少、噪声大过高的学习率容易让模型对极端产量年份过拟合。我一般把learning_rate固定为 0.05然后用early_stopping_rounds在验证集上自动找到树的数量。3.3 用分组交叉验证做参数搜索接下来的调参代码不能直接套用普通的GridSearchCV因为它的默认cv是KFold会把同一年的样本切到训练和验证两侧造成时间泄漏。需要把GroupKFold传进去并在fit时指定groups参数import xgboost as xgb from sklearn.model_selection import GridSearchCV, GroupKFold param_grid { learning_rate: [0.01, 0.05, 0.1], max_depth: [3, 5, 7], subsample: [0.7, 0.9], colsample_bytree: [0.7, 0.9] } xgb_model xgb.XGBRegressor( n_estimators500, objectivereg:squarederror, random_state42 ) gkf GroupKFold(n_splits5) grid GridSearchCV( xgb_model, param_grid, cvgkf, scoringneg_root_mean_squared_error, n_jobs-1, verbose1 ) grid.fit(df[feature_cols], df[yield], groupsdf[year]) print(最佳 RMSE , -grid.best_score_) print(最佳参数 , grid.best_params_)这段代码里要注意三个细节。第一scoringneg_root_mean_squared_error必须取负值因为 sklearn 的惯例是分数越大越好。第二cvgkf时fit方法里必须传groups否则GroupKFold不知道如何分组。第三n_jobs-1会让网格搜索并行跑但如果服务器内存有限建议去掉这个参数或者把参数组合数控制在 30 个以内避免内存打爆。随机森林的调参逻辑类似把param_grid换成max_depth、min_samples_leaf、n_estimators其余代码可以复用。不要盲目使用随机搜索先把数据量、特征数和计算时间估算出来再决定用GridSearchCV还是RandomizedSearchCV。4. 用 Python 跑通一个粮食产量预测模型的训练与评估4.1 工程目录与数据约定开始写训练脚本前我习惯先搭一个最小的工程目录避免在数据表格数量多了以后迷失方向yield_forecast/ ├── data/ │ ├── weather.csv │ ├── soil.csv │ └── target.csv ├── src/ │ ├── features.py │ ├── train.py │ └── evaluate.py ├── output/ │ └── predictions.csvdata目录存放原始材料不修改src放聚合特征、训练和评估代码output放模型文件和预测结果。这里的要点是features.py的输出必须是统一的宽表字段名在整个项目里保持一致。比如station、year、tmax_mean_veg、precip_sum_rep这种命名写清楚是哪个生育期比用temp11这种缩写省很多排查时间。农业生产数据往往来自不同单位字段命名五花八门。建议在进入模型之前先统一单位温度统一到摄氏度、降水统一到毫米、产量统一到 kg/ha。这个工作听着基础但我见过不少模型因为产量单位混用亩产 vs 公顷产导致 RMSE 数值被错误放大或缩小到无法解释。4.2 训练脚本两个模型与分组交叉验证下面的train.py片段同时训练随机森林和 XGBoost使用分组交叉验证保证年份不泄漏并保存每一折的预测结果方便后续画图和分析。import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import GroupKFold import xgboost as xgb def train_models(df, feature_cols, target_colyield): X df[feature_cols].copy() y df[target_col].copy() groups df[year].copy() gkf GroupKFold(n_splits5) results {} models {} estimators { random_forest: RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf3, random_state42, n_jobs-1 ), xgboost: xgb.XGBRegressor( learning_rate0.05, max_depth5, subsample0.8, colsample_bytree0.8, n_estimators500, random_state42, objectivereg:squarederror, eval_metricrmse ) } for name, estimator in estimators.items(): preds np.zeros(len(X)) for train_idx, val_idx in gkf.split(X, groupsgroups): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] estimator.fit(X_train, y_train) preds[val_idx] estimator.predict(X_val) results[name _pred] preds models[name] estimator return results, models这段代码的推荐理由是它把「模型对比」和「交叉验证」解耦了。每个模型都在完全相同的 5 折分割上训练得到的预测向量可以直接合并成真实的样本预测值而不是某一折的局部指标。n_estimators500配合learning_rate0.05是 XGBoost 在小规模数据上比较稳妥的起点如果验证集 RMSE 还在随树数量下降可以继续提高如果模型已经发生过拟合则优先调低max_depth或增大的reg_lambda。4.3 评估指标解读与模型保存训练之后输出评估指标时不能只打印一个 RMSE。产量预测的业务方通常关心两个问题误差平均有多大、分布是否偏向某一年。下面的代码同时计算 MAE、RMSE、R²并且在按年份分组后查看误差from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score import math def evaluate(true, pred): rmse math.sqrt(mean_squared_error(true, pred)) mae mean_absolute_error(true, pred) r2 r2_score(true, pred) return {RMSE: rmse, MAE: mae, R2: r2} true_y df[yield].values for name in [random_forest, xgboost]: pred_y results[name _pred] metrics evaluate(true_y, pred_y) print(name, metrics)R² 在产量预测中不一定是越高越好。如果测试年份里包含一个极端干旱年模型只要能猜到「那年多雨」R² 就会很高但真正困难的往往是气候正常的年份误差集中在 50100 kg/ha 的窄区间。因此我会额外按年份计算平均绝对误差并把误差最大的年份单独标出来看。训练完成后用joblib保存模型权重和特征列表方便推理阶段加载import joblib joblib.dump(models[xgboost], output/xgb_model.pkl) joblib.dump(feature_cols, output/feature_cols.pkl)保存模型时一定要把feature_cols也存下来否则测试阶段数据列顺序不一致时XGBoost 会静默按位置推断特征结果将完全失真。5. 推上线前值得尝试的 3 个细节SHAP、残差聚类、特征稳定性5.1 用 SHAP 解释预测结果当业务方问「为什么这一年的产量预测这么低」不要拿「模型算出来的」当答案用 SHAP 给出可解释的具体贡献。SHAP 能对每个样本计算每个特征的正负贡献比 feature importance 更细粒度import shap def explain_sample(model, X_sample, feature_names): explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_sample) return shap_values # 对第一行样本解释 shap_values explain_sample(models[xgboost], X_val.iloc[:1], feature_cols)TreeExplainer对树模型非常高效。在产量预测中SHAP 值能直观回答「为什么这年小麦预测 5600 kg但实际 6200」——比如模型看到灌浆期高温天数多给出了负贡献而实际数据里那年的降水恰好缓冲了高温影响。这种发现通常意味着特征工程需要增加「高温且干旱的交互项」而不是简单加一个温度变量。5.2 残差聚类暴露系统盲区模型在所有样本上的 RMSE 看起来还行但误差可能在某个特征区间里系统性放大。做法很简单把训练集和测试集合并画出预测值 - 真实值对precip_sum的散点或者对年份做分组误差热力图。如果发现某些年份误差方向一致比如连续两年都高估那基本可以断定模型缺失了当年的关键情景比如病虫害、政策轮作面积变动。处理残差聚类发现的偏差时先检查这一步特征是不是在训练集和测试集上的分布发生了漂移。气象特征的时间漂移是正常的但如果模型对降水超过 800mm 的年份误差突然增大可以在特征里加入「降水是否高于历史 90 分位」这类标志列。注意不要据此去删除极端年份否则模型在生产中没有见过极端天气反而更危险。5.3 特征重要性的稳定性检查产量数据往往只有几百条记录特征重要性很可能随交叉验证折数变化。建议把每次 fold 的feature_importances_存下来看它们在不同折里的排名稳定性。下面是一个简化的稳定性输出方法import numpy as np importance_ranks [] for train_idx, val_idx in gkf.split(X, groupsgroups): model xgb.XGBRegressor(learning_rate0.05, max_depth5) model.fit(X.iloc[train_idx], y.iloc[train_idx]) importance_ranks.append(model.feature_importances_) mean_importance np.mean(importance_ranks, axis0) std_importance np.std(importance_ranks, axis0) # 按平均重要性排序并观察标准差 for i, fname in enumerate(feature_cols): print(f{fname}: {mean_importance[i]:.3f} ± {std_importance[i]:.3f})如果一个特征的三折排名在第 1 和第 15 之间跳来跳去那它大概率对预期贡献不稳定不适合作为业务决策依据。相反那些每折都稳定排在前三的特征才是值得继续深挖和收集的方向。特征稳定性检查本质上是帮你识别模型是否在处理真实信号而不是某几年偶然的数据关联。这个环节做完模型才算从「实验室指标好」变成「敢于面对业务复盘」。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进