简介:这是一份基于随机森林算法实现的水稻产量预测Python源码项目,面向计算机、数据科学、人工智能等专业学生,可支撑课程设计、毕业设计或初期项目演示。项目包含8个文件,核心main.py为模型训练与预测主程序,两个csv文件分别存放水稻产量历史数据与预测后对比结果,另有xml项目配置、.gitignore及IntelliJ项目结构文件,整体压缩包仅54KB,轻量易部署。目前已有203人学习浏览,代码经运行验证可直接使用。通过该项目可完整了解数据导入、特征处理、随机森林回归建模、产量预测及真实值对比的流程,适合作为机器学习入门实战和课设项目改造的参考。
1. 水稻产量预测的随机森林方案:先看数据划分,再谈模型精度
我第一次拿到“水稻产量预测(随机森林模型python源码)”这套项目包时,没有急着看模型代码,而是先找数据是怎么划分的。表格式农业数据上,随机森林往往是不用调太多参数就能达到可用精度的第一个模型;它不像深度学习动辄要上万条样本,也不像线性回归只能抓住直线关系。你可以用它做收割前估产、下一年种植方案比选,或者把课设做成真正能交差的农业数据项目。这篇文章不绕弯子,直接讲数据怎么整理、Python 源码怎么写、核心参数怎么调,以及哪些坑会让你的产量模型瞬间“翻车”。适合刚入门 Python 的数据分析新手,也适合被农学数据折磨过一段时间的从业者。
2. 随机森林模型为什么适合水稻产量预测:从数据特性说起
2.1 随机森林的基本逻辑:平均多棵决策树的结果
水稻产量数据集有三个绕不开的麻烦:样本量通常只有几百行,地区之间差异大,气象和田间管理之间存在明显交互作用。用线性回归去拟合,得到的是“每多施 1kg 氮肥就增产多少斤”这种固定斜率,实际上施肥超过阈值后增产效果迅速下降;用深度学习去硬刚,样本量又撑不起稍微复杂的网络。随机森林恰好卡在中间:它由多棵决策树组成,训练时对样本行做 bootstrap 抽样,每棵树用一份子样本独立训练,回归预测取所有树的平均值。每一棵树可以学出一个类似“日均温超过 28.5℃且降雨量少于 450mm 时产量明显下降”的阈值规则,整体平均下来,单棵树的过拟合波动就被摊薄了。
技术核心是每棵树在分裂时不会遍历全部特征,而是先随机抽一个特征子集,再在这个子集里选最有区分度的分裂点,对应 scikit-learn 里的max_features。特征子集让树与树之间的相关性降低,平均结果变得更稳定。农业数据里经常出现某一年极端干旱或病虫害暴发,单棵树容易被这种极端年份带偏,集成平均可以防止个别异常年份成为模型主导。这也是随机森林在产量预测项目里最常见的价值——它不做“最精准的插值”,而是做“最不容易跑偏的预测”。
2.2 产量预测需要哪些字段:把能用和不能用的分开
目标列只有一个:单位面积产量。我在项目里习惯把产量列命名为yield,单位统一成 kg/亩。从统计年鉴拿到的数据如果单位是 t/ha,先换算再建模,否则评估阶段还要倒回去解释,容易弄错数量级。特征字段按来源分成四组:
| 分组 | 常见字段 | 预测时能不能拿到 |
|---|---|---|
| 气象 | 生育期降雨量、日均温、高温天数、日照时数 | 历史可查,当年用气象预报替代 |
| 土壤肥料 | 碱解氮、有效磷、速效钾、有机质、pH | 播种前测土即可获得 |
| 田间管理 | 移栽日期、插秧密度、灌溉次数、施肥量 | 决策变量,可设定方案后预测 |
| 品种与区域 | 品种类型、地块编号、海拔、土壤类型 | 已知 |
真正要警惕的是那些“看起来太准”的字段:穗数、每穗粒数、千粒重、理论产量。这些指标本身就跟最终产量有直接因果关系,而且要等水稻成熟期才能数出来。如果带着这些字段建模,测试年份的预测就已经包含答案了。这套随机森林方案要解决的是收割前估产,所以这类字段一律不能进特征列表。
哪些字段该优先补?按我的经验,高温天数(超过35℃的日数)和生殖生长期的降雨量,通常排在特征重要性前面。与其加密土壤采样点,不如先把气象数据补成完整的生育期序列;模型输出的特征重要性也能反过来验证数据采集方向有没有跑偏。
2.3 数据清洗:先给模型一个干净表格
scikit-learn 的RandomForestRegressor不接受 NaN,缺失值必须先处理。气象字段缺失时,我一般用同一农业区划内的多年同期均值补,而不是用整列均值,否则汛期缺测会被平均成旱季水平。土壤字段缺失时,用同一地块最近一次有效测量向前填充,这比插值更符合土壤肥力变化缓慢的实际情况。施肥量缺失时,除了补一个保守的中位数,还要额外生成一列“施肥量是否缺失”的标志位,让模型自己学习缺失状态对产量的影响。
异常值处理不要直接按 3σ 一刀切。产量特别低的地块可能是病虫害绝收,如果建模目标是固定轮作方案,这些样本会拉高预测误差;产量特别高可能是试验田,混进来会让结果过于乐观。我的做法是先加一列is_outlier,用农学常识筛出可疑点,逐条核实后再决定保留还是删除。代码里写df = df[df['yield'] < 1200]很容易,但这个阈值背后要有依据。
清洗完的 DataFrame 要满足三个条件:每一行是一个样本,行粒度是“地块 × 年份”;列名没有空格和特殊符号;类别列是字符串且取值稳定。字段名带空格导致训练和预测列对不上的报错,是新手最容易卡住的地方,后面环境配置再具体说。
3. 手写随机森林 Python 源码:从环境配置到跑通最小模型
3.1 配置 Python 环境:在 VSCode 里建好虚拟环境再装包
拿到源码第一件事不是看代码,而是把 Python 环境准备好。常见做法是用 Python 自带的venv建一个隔离环境,避免系统里的包版本互相干扰。终端里依次执行下面几条命令:
python -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip python -m pip install pandas numpy scikit-learn matplotlib joblibWindows 上激活命令是.venv\Scripts\activate。用python -m venv而不是直接pip install,是为了让依赖只装在这个项目里,之后用pip freeze > requirements.txt就能锁定版本。如果后面要更新包,也不会影响其他 Python 项目。
装完依赖后,在 VSCode 里按Ctrl+Shift+P打开命令面板,输入Python: Select Interpreter,选中.venv目录下的 Python。这一步对应很多 python 环境配置教程里强调的“解释器切换”。如果代码运行时报ModuleNotFoundError: No module named 'sklearn',但pip show scikit-learn又显示已安装,九成是解释器选错了,指向了全局环境而不是虚拟环境。
3.2 读入产量数据并按年份划分训练集和测试集
数据文件最常见的是 CSV,我从年鉴和农事记录整理后会存成下面这种格式:每一行代表某个地块某一年水稻的种植记录,列包含年份、气象、肥料、品种等。读取代码:
import pandas as pd df = pd.read_csv('rice_yield_data.csv', encoding='utf-8-sig') print(df.shape) print(df.head())encoding='utf-8-sig'能自动处理 Excel 另存 CSV 时带出来的 BOM 头;如果文件是 GBK 编码,把参数改成encoding='gbk'。打印df.head()是确认列名没有空格、单位没有混用。
接下来划分训练集和测试集。我不太喜欢在这里直接用train_test_split(random_state=42),因为农业数据有强烈的时间相关性。比如 2021 年和 2022 年的气象背景相似,随机抽取会让同一年份的记录同时出现在训练集和验证集,模型等于提前看到了相近气候条件下的产量,验证分数虚高。我更常做的是按年份切分:
FEATURES = [ 'year', 'rainfall_mm', 'avg_temp_c', 'sunshine_hour', 'nitrogen_kg', 'phosphorus_kg', 'potassium_kg', 'irrigation_times', 'variety_code', 'soil_code' ] train_df = df[df['year'] < 2023].reset_index(drop=True) test_df = df[df['year'] >= 2023].reset_index(drop=True) X_train = train_df[FEATURES] y_train = train_df['yield'] X_test = test_df[FEATURES] y_test = test_df['yield'] print(f'Train: {len(X_train)} rows, Test: {len(X_test)} rows')year既出现在特征列表里,又用来划分数据,看起来有点矛盾,实际上是合理的:模型可以把年份作为连续特征,间接学习新品种推广和栽培技术改进带来的整体产量上升趋势;而划分时按年份切分,确保测试集年份不参与训练,模拟的是“用过去预测未来”的真实场景。如果数据只有两三年年份,测试集就太小,可以把测试年份改成最近一年,其余全部训练。
3.3 训练随机森林模型:核心参数与参数表
数据准备好之后,模型部分其实很简短。以下这段就是随机森林回归的最小可运行代码:
from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor( n_estimators=400, max_depth=12, min_samples_split=4, min_samples_leaf=1, max_features='sqrt', random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(y_pred[:5])逻辑说明:fit(X_train, y_train)会构建 400 棵决策树,每棵树使用 bootstrap 抽样的训练子集,回归时每棵输出叶子节点目标均值,最终预测是所有树的平均。n_jobs=-1让 sklearn 使用全部 CPU 核心,数据量不大时差别不明显,但特征多的时候能省时间。predict返回一个一维数组,长度等于测试集行数。
初次运行不需要过度调参,但要对核心参数建立直觉。下表是我在产量预测项目里常用的一组起点和调整方向:
| 参数 | 起步值 | 说明 |
|---|---|---|
n_estimators | 300~500 | 树太少结果波动大,超过 500 后收益递减,训练时间线性增长 |
max_depth | 10~None | None让树完全生长,容易把小样本噪声记住;产量表一般建议先设 10~15 |
min_samples_split | 4~10 | 决定内部节点继续分裂的最少样本数,越大模型越平滑 |
min_samples_leaf | 1~5 | 叶子最少样本数,设 2~5 可以避免预测值波动太大 |
max_features | 'sqrt' | 每次分裂随机选特征数,回归问题常用'sqrt'或0.5,减小树之间相关性 |
random_state | 42 | 固定随机种子,否则每次运行结果不同,排错时很难判断是代码问题还是随机性 |
这几个参数没有绝对最优,只有最适合当前数据量。先跑通再进入下一章评估,评估会让你回头调整参数,而不是一上来就陷入调参玄学。
4. 模型评估与验证:用 RMSE 和特征重要性判断产量模型能否落地
4.1 评估指标计算:R²、RMSE、MAE 一个都不能少
训练完模型,第一步是看数字,但只看 R² 是最容易误判的。一个面积大、极端年份多的数据集,R² 可能很高,因为方差主要被地区差异解释了;可一旦换成 RMSE,你会发现预测误差仍然很大。所以我在评估代码里同时打印三个指标:
import numpy as np from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score rmse = np.sqrt(mean_squared_error(y_test, y_pred)) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) baseline_rmse = np.sqrt(mean_squared_error(y_test, np.full(len(y_test), y_test.mean()))) print(f'R²={r2:.3f} RMSE={rmse:.1f} kg/亩 MAE={mae:.1f} kg/亩') print(f'基线RMSE={baseline_rmse:.1f} kg/亩')baseline_rmse是“直接预测测试集平均产量”时的误差。如果模型 RMSE 和基线 RMSE 差不多,说明随机森林根本没学到有用信号,只是把答案收敛到了均值附近。如果测试集平均产量是 500kg/亩,RMSE=70kg/亩,意味着平均误差约 14%,对收割前估产已经够用;MAE=50kg/亩则说明一半的地块预测误差在 50kg 以内。不同地区均值差异很大,只看 R² 而脱离产量均值去谈可接受区间,没有业务价值。
模型有没有过拟合,可以通过训练集 RMSE 和测试集 RMSE 的差距来看。如果训练集 RMSE 是 10kg/亩,测试集是 80kg/亩,说明模型把历史样本背下来了,这个方案还不具备落地能力。这时候优先做的事不是加大n_estimators,而是提高min_samples_split和min_samples_leaf,让模型不要学得太细。
4.2 分组验证策略:按年份或按地块分组,避免测试数据泄漏
源码里如果直接写train_test_split(df, test_size=0.2, random_state=42),在农业数据上几乎必然造成信息泄漏。原因很简单:同一地块不同年份之间,土壤本底是一致的;同一区域相邻年份,气象条件高度相似。随机划分会把这种相似样本同时分到训练集和验证集,模型一看“这地块我见过”,分数自然好看,真正面对新地块时立刻现原形。
更稳妥的做法是按地块分组划分。代码里用GroupShuffleSplit可以保证同一个field_id的所有记录只出现在训练集或测试集:
from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next( gss.split(df, df['yield'], groups=df['field_id']) ) train_g = df.iloc[train_idx] test_g = df.iloc[test_idx]groups=df['field_id']是关键,它让分组器按照地块编号切分,而不是按行随机切。没有地块编号时,只能退而求其次按年份切分,也就是 3.2 的做法。按年份切分还有一个好处:模型在训练阶段完全看不到未来年份的气候和品种变化,评估结果更接近真实预测场景。
如果数据覆盖年数够多,可以用TimeSeriesSplit做滚动验证:先用前几年训练、后一年验证,再往前推一年用更多数据训练、再验证下一年。这样做的代价是模型要重新训练多次,但能看出模型在不同气候背景下的稳定性,比单次划分更有说服力。
4.3 特征重要性:找出决定产量预测的前几名
随机森林的一大优势是训练完成后直接给出feature_importances_。这个数组长度为特征个数,总和为 1,数值越大表示该特征在树分裂中带来的误差下降越多。我通常把它转成 Series 排序,再画一张水平条形图:
import matplotlib.pyplot as plt importance = pd.Series( model.feature_importances_, index=FEATURES ).sort_values() plt.figure(figsize=(8, 6)) importance.plot.barh() plt.xlabel('Feature Importance') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)sort_values()让重要性从小排到大,barh画出来最大的特征在最上面,一眼就能看出前三名。输出图之外,我还会把重要性数值落一份 CSV,方便跟农学专家核对:如果降雨量排第一,说明这个地区产量受水分控制为主,预测时要重点关注气象预报;如果year排第一,说明技术进步贡献明显,那么预测未来年份时必须保留这个特征,否则模型会系统性低估产量。
需要提醒的是,特征重要性是模型视角的关联程度,不是因果关系。氮肥施用量可能排很高,但真实原因可能是农户在预估产量较高的地块主动多施肥,产量和施肥量互相影响。这是同步性偏差,随机森林无法区分因果方向。解读重要性报告时,定性为“模型主要依靠哪些信息做判断”,不要直接写进结论说“施肥是增产的决定因素”。
5. 水稻产量预测避坑指南:4 个让随机森林翻车的高发问题
5.1 坑:随机划分训练集,让验证分数“虚高”
现象:代码直接train_test_split(df, test_size=0.2, random_state=0),R² 跑到 0.95,换到下一年的新数据预测时误差翻倍。原因:同一地块或相邻年份的记录同时出现在训练集和验证集,模型把地块身份记下来了,验证阶段相当于看见了类似样本的答案。解决:强制按年份或地块分组切分,同时检查特征列表里有没有“千粒重”“理论产量”这类收成指标。数据泄漏是这类型项目里最隐蔽的问题,一旦指标虚高,后面所有调参都没有意义。
5.2 坑:类别变量手工替换成 1、2、3,给树生造出顺序
现象:把“品种”列手工映射成{'杂交稻':1, '常规稻':2}后,模型能跑通,但特征重要性里“品种”突然排第一。原因:1、2 这种整数编码对树而言隐含了数值大小关系,模型会沿着“小于 1.5 往左走”这种划分方式去寻找伪规律,而水稻品种本身没有顺序。解决:类别品种少时用OneHotEncoder,每个品种一列,随机森林可以决定在哪个品种节点上分裂;类别过多时先把低频品种合并为“其他”,再做 one-hot。我习惯先用df['variety'].value_counts()看类别数量,少于 8 类直接 one-hot,多于 8 类先合并再编码。
5.3 坑:随机森林不能外推,预测未来年份会系统性偏低
现象:训练数据覆盖 2010—2022 年,测试年份是 2023 年,模型预测结果全部落在训练集均值附近,2023 年实际产量大幅上升没有抓住。原因:随机森林是分段常数模型,预测值是训练集叶节点的均值,它只能内插,不能外推。当测试年份的气温、新品种基因潜力超出训练范围时,模型只能找历史区间内最接近的样本做平均,自然跟不上趋势。解决:预测之前先比较训练集和测试集特征范围,X_test.max() - X_train.max()差距较大时,把预测结果标记为参考值,而不是结论值。如果确实需要外推,可以把“年份”作为特征,让模型学一个粗糙的时间趋势,但超出训练范围太多时,模型依旧会被拉回到最近年份的水平,这是随机森林的边界,不是参数问题。
5.4 坑:不固定random_state,同一份代码跑出两个结果
现象:今天跑 R²=0.82,明天跑 R²=0.79,换一台电脑后差距更大;检查代码什么都没改,但就是复现不了。原因:随机森林本身有 bootstrap 抽样和随机特征选择,不固定随机种子,每次训练出的树结构都不同。结果趋势一致,但具体指标会轻微浮动。解决:模型构造里固定random_state=42,数据划分的GroupShuffleSplit和train_test_split也要固定同样的种子;分享源码时把依赖版本用pip freeze > requirements.txt记录下来,scikit-learn 小版本升级也可能改变分裂器的随机行为。这属于“后悔药”型问题,遇到一次就知道要优先锁配置。
6. 落地技巧:网格搜索调参、保存模型并预测新地块
6.1 用 GridSearchCV 自动找参数:把调参从玄学变成流程
手动调参可以先跑通,但产品化落地要靠网格搜索。常见做法是把n_estimators、max_depth、min_samples_split放进候选参数表,让 sklearn 自动组合并交叉验证:
from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [200, 400], 'max_depth': [8, 12, None], 'min_samples_split': [2, 5, 10], 'max_features': ['sqrt', 0.5] } grid = GridSearchCV( RandomForestRegressor(random_state=42), param_grid, scoring='neg_root_mean_squared_error', cv=3, n_jobs=-1 ) grid.fit(X_train, y_train) print(grid.best_params_) best_model = grid.best_estimator_scoring='neg_root_mean_squared_error'表示评分标准是 RMSE 的负值,sklearn 认为数值越大越好,所以负 RMSE 最大化等价于 RMSE 最小化。cv=3在产量数据这种小样本上比较现实,cv=5会让每折训练数据更少,稳定性反而下降。网格搜索结束后,用best_params_里推荐的值重建模型,会比手工试探节省很多时间。
6.2 保存模型并预测新地块
调参完成的模型要落盘,不能每次预测都重新训练。用joblib保存和加载:
import joblib joblib.dump(best_model, 'rice_yield_model.joblib') loaded_model = joblib.load('rice_yield_model.joblib') new_sample = pd.DataFrame([{ 'year': 2024, 'rainfall_mm': 780, 'avg_temp_c': 26.5, 'sunshine_hour': 1250, 'nitrogen_kg': 12, 'phosphorus_kg': 5, 'potassium_kg': 8, 'irrigation_times': 6, 'variety_code': 1, 'soil_code': 2 }]) pred = loaded_model.predict(new_sample)[0] print(f'预测亩产: {pred:.1f} kg/亩')注意new_sample的列名和顺序必须与训练时完全一致。joblib保存的是模型对象,不是特征列表,如果训练代码里改了列名,加载旧模型再预测新数据时会报错。我一般会把FEATURES列表转成 JSON 和模型文件放在同一目录,预测脚本加载模型前先比对特征顺序。这个习惯避免了大半年后想复现模型时完全记不清特征构成的尴尬。
另一个建议是把训练时间、数据集版本、RMSE 三项写进一个model_card.txt,和模型文件放在一起。下次回看时能直接判断这个模型是哪个数据版本出来的,预测若有偏差也能快速定位是数据问题还是模型问题。这套源码方案的边界也很清晰:它适合中等样本、以历史数据为主的产量估测,不适合对极端气候和超长外推做精确判断。先用数据划分解决泄漏,再用 RMSE 判断误差,最后用特征重要性指导采集方向,整个项目才算真正闭环。希望这些踩坑经验帮到你少走两步弯路。
本文还有配套的精品资源,点击获取