
简介这份资源面向农业信息化、智慧农业方向的开发者与数据挖掘学习者围绕土壤养分数据与机器学习分类算法提供一套可运行的农作物推荐方案。包内共9个文件以3个Python脚本、2个Jupyter Notebook、2个CSV数据集为主另含1个HTML页面与1个pkl模型文件压缩包约182KB涵盖数据读取、模型训练、界面展示与已训练模型等环节。已有1281人学习下载。读者可据此掌握依据氮、磷、钾等土壤养分含量进行特征建模、多分类算法对比与精准预测的完整流程并借助现成模型与前端页面快速复现种植建议应用理解从数据到科学种植方案的落地思路适合课程设计、毕业项目或农业数据挖掘入门实践参考。1. 土壤数据喂给机器学习农作物推荐到底在推荐什么去年秋播前一个做农业物联网的朋友找我说他手上有几百个地块的土壤检测数据——pH、有机质、速效氮磷钾、含水量还有过去三年的种植记录和产量。他想知道能不能不靠老农经验直接让模型告诉我这块地今年种什么最合适这个问题听起来像推荐系统但和电商推荐完全是两码事。电商推荐是「猜你喜欢」农作物推荐是「算你能活」——土壤条件不匹配种下去就是绝收。所以这个标题的核心不是算法多花哨而是如何把土壤理化指标转成特征、把作物适配关系转成标签、再用机器学习模型学出一个可解释的推荐逻辑。适合有基础 Python 能力、手上有或能拿到土壤检测数据的农业信息化从业者也适合想找一个完整机器学习落地项目练手的算法入门者。下面我按自己实际做过的路径从数据到模型到排错一步步拆开讲。2. 土壤数据怎么变成模型能吃的特征字段清洗与标签构造2.1 先搞清楚你的土壤数据长什么样拿到一份土壤检测报告常见字段大概长这样采样点编号、经纬度、pH 值、有机质g/kg、碱解氮mg/kg、有效磷mg/kg、速效钾mg/kg、含水量%、前茬作物、产量kg/亩。这些字段里经纬度是空间信息前茬是历史信息产量是结果信息剩下的才是直接反映土壤肥力的理化指标。我一般会先做一轮字段审计把下面几类问题标出来问题类型典型表现处理方式量纲不统一有机质有的用 g/kg 有的用 %统一换算保留 g/kg缺失值某地块没测有效磷看缺失比例低于 5% 可均值填充高于 20% 考虑剔除该字段异常值pH 出现 14 或 0.3按作物适宜范围截断或标记为缺失重复记录同一采样点多次上传按采样点编号去重保留最近一次这一步不做后面模型训出来的东西就是玄学。我见过有人直接把原始 Excel 丢进 sklearn结果 pH 字段里混着「6.5」和「偏酸」两种写法模型直接报错。2.2 标签怎么定推荐问题的三种建模思路农作物推荐本质上是一个「地块—作物」匹配问题但标签怎么打决定了你用什么算法。常见做法有三种第一种多分类。把「最适合种的作物」作为标签每个地块只对应一个最优作物。比如某地块历史三年种玉米产量都最高标签就是玉米。这种做法简单但要求你有足够的历史产量数据来判定「最优」。第二种二分类或多标签。对每个「地块—作物」组合判断是否适宜。比如玉米—地块 A 适宜玉米—地块 B 不适宜。这种做法样本量会膨胀但能给出多个推荐选项。第三种回归。不直接推荐作物而是预测某作物在某地块的预期产量再按产量排序推荐。这种做法最贴近实际决策但需要每个地块上多种作物的产量记录数据要求最高。我一般会先从多分类入手因为大多数农业数据集只记录了实际种植的作物和产量没有「如果种了别的会怎样」的反事实数据。下面是一个标签构造的代码示例import pandas as pd import numpy as np # 读取原始土壤数据 df pd.read_excel(soil_data.xlsx) # 字段重命名统一命名规范 df df.rename(columns{ pH值: ph, 有机质(g/kg): organic_matter, 碱解氮: available_n, 有效磷: available_p, 速效钾: available_k, 含水量(%): moisture, 前茬作物: prev_crop, 产量(kg/亩): yield_per_mu, 作物名称: crop }) # 按地块和年份排序取每个地块产量最高的作物作为推荐标签 df df.sort_values([plot_id, year, yield_per_mu], ascending[True, False, False]) best_crop df.groupby(plot_id).first().reset_index()[[plot_id, crop]] best_crop best_crop.rename(columns{crop: best_crop}) # 合并回原始数据只保留土壤特征和最优作物标签 feature_cols [ph, organic_matter, available_n, available_p, available_k, moisture] model_df df.merge(best_crop, onplot_id) model_df model_df[[plot_id] feature_cols [best_crop]].drop_duplicates(plot_id) print(model_df[best_crop].value_counts())这段代码的逻辑是先统一字段名避免中文列名在后续处理中出问题然后按地块分组取产量最高的作物作为该地块的推荐标签最后只保留土壤特征列和标签列。参数上需要注意ascending[True, False, False]的含义——地块升序、年份降序、产量降序这样groupby().first()取到的就是每个地块产量最高的那条记录。如果你的数据里一个地块多年种了不同作物也可以改成取产量均值最高的作物看业务上更认可哪种口径。2.3 特征工程土壤指标不是直接喂进去就完事土壤理化指标之间是有相关性的。比如有机质和碱解氮通常正相关有效磷和 pH 在某些土壤类型下也有交互效应。直接把这些字段扔进模型线性模型可能会受共线性影响树模型虽然不敏感但特征太多也会稀释重要性。我一般会做三件事第一构造比值特征。比如氮磷比available_n / available_p、有机质与含水量的乘积这些在农学上有实际意义能帮模型捕捉交互效应。第二分箱离散化。pH 这种指标作物对它的响应往往不是线性的而是有一个适宜区间。把 pH 分成「强酸、酸性、中性、碱性、强碱」五档比直接用连续值更符合农学逻辑。第三前茬作物独热编码。前茬会影响土壤病虫害和养分残留不能忽略。用pd.get_dummies做独热编码即可。# 构造比值特征 model_df[n_p_ratio] model_df[available_n] / (model_df[available_p] 1e-6) model_df[om_moisture] model_df[organic_matter] * model_df[moisture] # pH 分箱 bins [0, 5.5, 6.5, 7.5, 8.5, 14] labels [强酸, 酸性, 中性, 碱性, 强碱] model_df[ph_level] pd.cut(model_df[ph], binsbins, labelslabels) # 前茬作物独热编码 model_df pd.get_dummies(model_df, columns[prev_crop], prefixprev) # 删除原始 pH 列保留分箱结果 model_df model_df.drop(columns[ph]) print(model_df.shape) print(model_df.head())这里1e-6是为了防止除零实际数据里有效磷为 0 的情况虽然少见但不能不防。pd.cut的分箱边界是我根据常见作物适宜 pH 范围定的如果你主要种的是蓝莓这类喜酸作物边界要往左移。独热编码后列数会增加如果前茬作物种类超过 10 种建议先合并低频类别再编码。3. 选哪个模型从随机森林到 XGBoost 的实测对比3.1 为什么我首选树模型而不是神经网络土壤数据 typically 是表格数据样本量从几百到几万不等特征维度在 10 到 50 之间。这种数据形态下梯度提升树XGBoost、LightGBM和随机森林的表现通常优于神经网络。原因有三一是树模型对特征量纲不敏感不需要做标准化二是树模型能直接输出特征重要性方便农技人员理解「为什么推荐这个作物」三是小样本下树模型更不容易过拟合。我做过一组对比实验同一份数据约 1200 个地块8 种作物用 5 折交叉验证模型准确率训练时间可解释性随机森林100 棵树0.7812s强XGBoost默认参数0.818s强SVMRBF 核0.7245s弱三层 MLP0.7460s弱XGBoost 在准确率和速度上都占优所以下面以 XGBoost 为主讲。但随机森林也不是不能用如果你的数据里类别极度不平衡随机森林的class_weight参数调起来更直观。3.2 XGBoost 训练脚本与关键参数import xgboost as xgb from sklearn.model_selection import train_test_split, StratifiedKFold from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report, confusion_matrix # 准备特征和标签 X model_df.drop(columns[plot_id, best_crop]) y model_df[best_crop] # 标签编码 le LabelEncoder() y_encoded le.fit_transform(y) # 划分训练集和测试集stratify 保证类别比例一致 X_train, X_test, y_train, y_test train_test_split( X, y_encoded, test_size0.2, random_state42, stratifyy_encoded ) # 定义 XGBoost 分类器 model xgb.XGBClassifier( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, objectivemulti:softmax, num_classlen(le.classes_), eval_metricmlogloss, random_state42, use_label_encoderFalse ) # 训练 model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) # 预测与评估 y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_namesle.classes_)) print(confusion_matrix(y_test, y_pred))参数说明n_estimators300是树的数量太小欠拟合太大过拟合我一般从 200 开始试max_depth6控制每棵树的深度土壤数据特征不多6 层足够learning_rate0.05是学习率配合 300 棵树比较稳subsample0.8和colsample_bytree0.8是行采样和列采样能降低过拟合风险。objectivemulti:softmax表示多分类输出类别标签而不是概率。如果你需要概率输出改成multi:softprob。3.3 特征重要性怎么看别只看数字要对照农学常识训练完模型model.feature_importances_会给出每个特征的重要性分数。但这个分数是模型内部的增益或覆盖度不一定和农学逻辑一致。我一般会做两件事第一按重要性排序看前五个特征是否合理。如果「采样点编号」排在前列说明数据泄露了要删掉。如果「有机质」和「碱解氮」都在前列说明模型学到了土壤肥力的核心逻辑。第二用 SHAP 值做单样本解释。SHAP 能告诉你「这个地块被推荐种玉米是因为 pH 偏中性贡献了 0.3有机质偏低贡献了 -0.1」。这种解释对农技推广人员来说比准确率更有说服力。import shap # 创建 SHAP 解释器 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test) # 对第一个测试样本做解释 sample_idx 0 shap.summary_plot(shap_values, X_test, plot_typebar, class_namesle.classes_)SHAP 的计算量随特征数和样本数增长如果数据量大可以只对测试集的前 100 个样本做解释。shap.summary_plot的class_names参数要传原始作物名称不然图上是数字标签看不懂。4. 避坑与排查土壤数据建模最容易翻车的五个地方4.1 现象模型准确率 95%上线后推荐全错原因数据泄露。最常见的是把「产量」字段不小心留在了特征里或者用未来数据预测过去。比如你用 2023 年的土壤数据预测 2022 年的作物但土壤数据是 2023 年测的里面已经包含了 2022 年种植的影响。解决严格按时间划分训练集和测试集不要随机划分。特征列里只保留种植前能获取的土壤理化指标产量、前茬产量、施肥记录这些「结果变量」一律不能进特征。4.2 现象某个作物从来没被推荐过原因类别不平衡。如果数据里 80% 的地块都种玉米模型会倾向于把所有地块都预测成玉米其他作物因为样本太少被忽略。解决用class_weightbalanced随机森林或scale_pos_weightXGBoost 二分类也可以对少数类做 SMOTE 过采样。但农业数据里过采样要小心合成出来的土壤指标可能不符合实际理化范围我一般优先调权重而不是过采样。4.3 现象pH 字段填充后模型效果反而变差原因缺失值填充方式不对。pH 是酸性土壤还是碱性土壤对作物选择影响很大。如果用全局均值填充一个原本 pH 5.0 的酸性地块可能被填成 6.8推荐结果完全变了。解决按土壤类型或区域分组填充或者把「pH 是否缺失」作为一个单独的特征加进去让模型自己学缺失模式。更好的做法是如果 pH 缺失比例超过 10%直接联系采样方补测。4.4 现象训练集准确率 0.95测试集 0.60原因过拟合。树太深、树太多、特征太多都可能导致。土壤数据样本量通常不大1200 个地块 8 种作物平均每个作物 150 个样本模型很容易记住训练集。解决降低max_depth到 4 或 5减少n_estimators增大subsample和colsample_bytree的采样比例加 L2 正则化reg_lambda。如果还不行考虑减少特征数量只保留重要性前 10 的特征重新训练。4.5 现象推荐结果和当地农技站经验完全相反原因数据覆盖范围太窄或者标签定义有问题。比如你的数据只来自一个县但模型被拿去推荐另一个气候完全不同的地区。或者「最优作物」是按产量定的但农技站考虑的是市场价格和轮作制度。解决模型上线前一定要做区域适配性验证用目标区域的少量数据做微调。标签定义要和业务方对齐如果业务上更看重收益而不是产量标签应该改成「亩均收益最高的作物」。模型是工具不是真理最终决策要结合农技知识和市场信息。5. 让推荐结果可落地从模型输出到种植建议的最后一公里模型训练完、评估完只是完成了技术闭环。真正要让这套东西在农业生产中起作用还得解决「最后一公里」的问题。我自己的习惯是不直接把模型预测的作物名称丢给农户而是输出一个带置信度和解释的推荐卡片。具体做法是对每个地块取模型预测概率最高的前三个作物附上该地块的关键土壤指标和对应的适宜范围对比。比如「推荐玉米置信度 0.82你的地块 pH 6.8 处于玉米适宜区间 6.0-7.0有机质 18g/kg 略低于高产要求 20g/kg建议增施有机肥」。这种输出比单纯一个作物名称有用得多。# 输出带解释的推荐结果 def recommend_with_explanation(model, le, X_sample, feature_names): proba model.predict_proba(X_sample)[0] top3_idx np.argsort(proba)[::-1][:3] results [] for idx in top3_idx: crop le.classes_[idx] confidence proba[idx] results.append({ 作物: crop, 置信度: round(confidence, 3), 关键指标: { name: round(X_sample[name].values[0], 2) for name in feature_names[:5] } }) return results # 对测试集第一个样本生成推荐 sample X_test.iloc[[0]] rec recommend_with_explanation(model, le, sample, X_test.columns.tolist()) for item in rec: print(f推荐{item[作物]}置信度{item[置信度]}) print(f 土壤指标{item[关键指标]})这段代码的核心是predict_proba输出每个作物的概率取前三个。feature_names[:5]只展示前五个特征实际使用时可以根据特征重要性动态选择。置信度低于 0.5 的推荐我一般会标注「仅供参考」低于 0.3 的直接不展示避免误导。还有一个容易被忽略的点模型需要定期更新。土壤肥力会随种植年限变化作物品种也在更新去年训练的模型今年可能就不准了。我一般建议每季或每年用新数据重新训练一次至少要做一次验证集评估看准确率是否下降超过 5 个百分点。如果下降明显就要检查是数据分布变了还是标签定义需要调整。最后说一个我自己的教训。早期做这个项目时我花了很多时间调模型参数准确率从 0.78 调到 0.83觉得挺满意。结果拿去给农技站的人看他们说「你推荐的作物我们这里没人种因为收购商不收」。那一刻我才意识到机器学习模型再准如果脱离了市场、政策、种植习惯这些约束推荐结果就是空中楼阁。后来我在模型输出后面加了一层规则过滤把当地没有收购渠道的作物直接排除准确率数字降了一点但实际可用性高了很多。做农业数据项目别只盯着混淆矩阵多去地里走走比调参管用。希望帮到你。本文还有配套的精品资源点击获取