
简介这份资源面向参加2024年高教社杯全国大学生数学建模竞赛C题的学生与指导教师聚焦农作物种植策略的可视化实现。内容围绕赛题数据预处理与结果呈现展开涵盖整体数据探索、价格与占地面积分析、描述性统计、异常值检测等环节并配套柱状图、热力图、散点图、箱线图、QQ图、CDF曲线等多种图表帮助读者直观理解模型输出与数据趋势。资源包共31个文件包含20个png图表、6个py脚本和5个xlsx数据表压缩包约2MB脚本可直接运行复现数据表便于二次分析。目前已有1010人学习下载适合需要快速搭建可视化框架、对照赛题查漏补缺的参赛者也能为农业种植优化类问题提供可复用的分析思路与图表模板。1. 农作物的种植策略从赛题到可视化一条能跑通的工程路径每年数学建模竞赛前后总有一批同学卡在同一个地方模型建完了求解器也跑出了结果但要把结果讲清楚、把方案说明白却不知道怎么下手。2024 年高教社杯全国大学生数学建模竞赛 C 题「农作物的种植策略」就是典型——它本质是一个带约束的多年期种植规划问题涉及地块、作物、季节、产量、价格、成本等多层变量最终要输出的是可执行的种植方案和可视化图表。很多人第一反应是直接上求解器结果数据没理清、约束写错、图表画出来自己都看不懂。这篇笔记不讲竞赛技巧只讲一条从数据整理到可视化落地的完整路径怎么把题目里的表格变成结构化数据怎么设计决策变量和约束怎么用 Python 把结果画成能直接放进论文的图。适合正在做这道题、或者以后遇到类似资源分配与规划问题的同学。2. 先把题目翻译成数据结构地块、作物与年份的三维表2.1 为什么不能直接拿 Excel 去建模题目给的原始数据通常是几张分散的表格地块面积与类型、作物属性与种植季、历年产量与价格、成本与售价。如果直接把这些表丢进求解器最大的问题是索引对不上——同一个作物在不同地块、不同年份、不同季节下的表现不一样而 Excel 的二维结构很难表达这种三维关系。常见做法是先把数据整理成「地块-作物-年份」的三维结构或者用长表long format把每个组合展开成一行再用分组字段去筛选。这样后面写约束时只需要按字段过滤不用反复查表。我一般会先用 pandas 把原始表读进来统一列名补上缺失的年份和季节字段再生成一个完整的组合表。这一步看起来笨但后面能省掉大量调试时间。2.2 用 pandas 构建种植决策的基础表假设原始数据里有一张地块表land.csv包含地块编号、面积、类型一张作物表crop.csv包含作物名称、适宜季节、亩产量、种植成本、销售价格。先读进来做标准化import pandas as pd import numpy as np # 读取原始数据假设文件在同一目录下 land pd.read_csv(land.csv) # 列地块编号, 面积, 地块类型 crop pd.read_csv(crop.csv) # 列作物名称, 适宜季节, 亩产量, 种植成本, 销售价格 # 统一列名去掉空格避免后续引用出错 land.columns [c.strip() for c in land.columns] crop.columns [c.strip() for c in crop.columns] # 生成年份列表假设规划期为 2024-2030 共 7 年 years list(range(2024, 2031)) # 构建完整的三维组合表每个地块 × 每个作物 × 每个年份 records [] for _, l in land.iterrows(): for _, c in crop.iterrows(): for y in years: records.append({ 地块编号: l[地块编号], 面积: l[面积], 地块类型: l[地块类型], 作物名称: c[作物名称], 适宜季节: c[适宜季节], 亩产量: c[亩产量], 种植成本: c[种植成本], 销售价格: c[销售价格], 年份: y }) df pd.DataFrame(records) print(df.shape) # 输出行数确认组合数量是否合理这段代码的逻辑很直接把三个维度的信息笛卡尔积展开每一行代表「某地块在某年种某作物」的一个候选决策。参数方面years的范围要根据题目实际规划期调整如果题目区分夏秋两季还需要在crop表里保留季节字段并在组合时按季节再拆一层。注意亩产量、种植成本、销售价格这些字段如果题目给的是区间或分年份变化需要提前处理成按年份匹配的列否则后面算收益会全部用同一个值结果失真。2.3 数据校验三个必须检查的字段组合表生成后别急着往下走先做三件事第一检查地块面积总和是否和题目给的总面积一致。如果不一致说明地块表有遗漏或重复。第二检查每个作物的适宜季节是否和地块类型匹配比如水田不能种旱地作物这个约束要在数据层面先标出来而不是等到写约束时再判断。第三检查价格和成本字段有没有空值或零值零值会导致收益计算全部为零空值会让求解器直接报错。# 校验1地块面积总和 print(地块面积总和:, land[面积].sum()) # 校验2标记不匹配的种植组合示例水田只能种水稻 df[是否可种] df.apply( lambda r: 0 if (r[地块类型] 水田 and r[作物名称] ! 水稻) else 1, axis1 ) # 校验3检查空值和零值 print(空值统计:\n, df[[亩产量,种植成本,销售价格]].isnull().sum()) print(零值统计:\n, (df[[亩产量,种植成本,销售价格]] 0).sum())这三步做完数据基本就干净了。后面建模时只需要在约束里引用是否可种字段就能避免不合理的种植组合进入求解空间。3. 决策变量与约束怎么写从线性规划到可求解模型3.1 决策变量的两种常见设计这道题的核心决策是「某地块在某年某季种多少亩某作物」。最直观的变量设计是连续变量x[i,j,t]表示地块 i 在年份 t 种植作物 j 的面积。如果题目要求必须整块地种植或者有最小种植面积限制就需要引入 0-1 变量y[i,j,t]来表示是否种植再用大 M 法把面积和 0-1 变量关联起来。常见做法是先按连续变量求解看结果是否满足实际可操作性如果题目明确要求「一块地只能种一种作物」或者「种植面积必须是整数亩」再切换到混合整数规划。我一般会先用连续变量跑一版因为求解速度快能快速验证约束逻辑对不对。等模型结构稳定了再根据题目要求决定是否加整数约束。3.2 用 PuLP 写一个可运行的最小模型下面是一个简化版模型只考虑面积约束、轮作约束和收益最大化。实际题目还会有更多约束但结构是一样的import pulp # 假设 df 已经过滤掉不可种植的组合 df_valid df[df[是否可种] 1].copy() # 创建问题实例最大化总收益 prob pulp.LpProblem(CropPlanning, pulp.LpMaximize) # 决策变量x[(地块, 作物, 年份)] 表示种植面积连续变量下界为0 x {} for _, row in df_valid.iterrows(): key (row[地块编号], row[作物名称], row[年份]) x[key] pulp.LpVariable(fx_{key[0]}_{key[1]}_{key[2]}, lowBound0) # 目标函数总收益 面积 × 亩产量 × 销售价格 - 面积 × 种植成本 prob pulp.lpSum([ x[key] * (row[亩产量] * row[销售价格] - row[种植成本]) for _, row in df_valid.iterrows() for key in [ (row[地块编号], row[作物名称], row[年份]) ] ]) # 约束1每个地块每年种植总面积不超过地块面积 for land_id in land[地块编号]: for year in years: prob pulp.lpSum([ x[(land_id, crop_name, year)] for crop_name in crop[作物名称] if (land_id, crop_name, year) in x ]) land[land[地块编号] land_id][面积].values[0] # 约束2轮作约束示例——同一地块连续两年不能种同一种作物 for land_id in land[地块编号]: for crop_name in crop[作物名称]: for i in range(len(years)-1): y1, y2 years[i], years[i1] if (land_id, crop_name, y1) in x and (land_id, crop_name, y2) in x: prob x[(land_id, crop_name, y1)] x[(land_id, crop_name, y2)] land[land[地块编号] land_id][面积].values[0] # 求解 prob.solve(pulp.PULP_CBC_CMD(msg0)) print(求解状态:, pulp.LpStatus[prob.status]) print(总收益:, pulp.value(prob.objective))这段代码的关键点有三个目标函数里把亩产量、价格、成本都乘进去了注意单位要统一如果题目给的是每斤价格和每亩产量收益就是面积 × 亩产量 × 价格约束1保证不超种约束2是轮作约束的简化写法实际题目可能要求「同一作物不能连续两年在同一地块种植」那就要用 0-1 变量或者更复杂的逻辑约束。参数方面lowBound0保证面积非负如果题目要求最小种植面积还要加lowBoundmin_area或者用 0-1 变量控制。3.3 约束排查求解器返回不可行时看什么如果prob.status返回Infeasible不要急着改模型先按这个顺序查第一检查地块面积约束是不是写成了「小于等于」但实际数据里面积是零第二检查轮作约束是不是把同一年的变量也限制进去了导致无解第三检查是否有作物在所有地块都不可种但目标函数里还在引用它。常见做法是先把约束一条条注释掉跑一次看哪条导致不可行再针对性修改。4. 结果可视化把种植方案画成能直接用的图4.1 从求解结果到 DataFrame求解完成后x变量里只有非零值才有意义。先把结果提取成表格# 提取非零种植方案 results [] for key, var in x.items(): val var.varValue if val and val 1e-6: # 过滤掉浮点误差导致的极小值 land_id, crop_name, year key results.append({ 地块编号: land_id, 作物名称: crop_name, 年份: year, 种植面积: round(val, 2) }) result_df pd.DataFrame(results) print(result_df.head(10))这里用1e-6作为阈值是因为求解器返回的浮点数可能有微小误差直接判断0会带出大量接近零的值。参数round(val, 2)是为了让结果更可读实际计算时不要提前四舍五入。4.2 用堆叠柱状图展示各地块逐年种植结构最直观的图是堆叠柱状图横轴是年份纵轴是种植面积不同颜色代表不同作物。这样一眼能看出轮作是否合理、各地块是否充分利用。import matplotlib.pyplot as plt import matplotlib # 设置中文字体避免乱码 matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False # 按年份和作物汇总种植面积 pivot result_df.pivot_table( index年份, columns作物名称, values种植面积, aggfuncsum, fill_value0 ) # 绘制堆叠柱状图 pivot.plot(kindbar, stackedTrue, figsize(10, 6), colormaptab20) plt.title(逐年种植结构按作物汇总) plt.xlabel(年份) plt.ylabel(种植面积亩) plt.legend(title作物名称, bbox_to_anchor(1.05, 1), locupper left) plt.tight_layout() plt.savefig(crop_structure.png, dpi300) plt.show()这张图的关键参数是stackedTrue和fill_value0前者保证堆叠效果后者避免缺失组合导致报错。如果作物种类太多图例会挤在一起可以只保留面积占比前 8 的作物其余归为「其他」。4.3 用热力图看地块-作物匹配关系堆叠柱状图看的是总量热力图看的是匹配关系。横轴是作物纵轴是地块颜色深浅代表种植面积能快速发现哪些地块常年闲置、哪些作物被过度集中种植。import seaborn as sns # 按地块和作物汇总种植面积所有年份合计 heat_data result_df.pivot_table( index地块编号, columns作物名称, values种植面积, aggfuncsum, fill_value0 ) plt.figure(figsize(12, 8)) sns.heatmap(heat_data, cmapYlGnBu, annotTrue, fmt.0f, linewidths0.5) plt.title(地块-作物种植面积热力图多年合计) plt.xlabel(作物名称) plt.ylabel(地块编号) plt.tight_layout() plt.savefig(heatmap.png, dpi300) plt.show()热力图的annotTrue会把数值标在格子里适合地块和作物数量都不多的情况。如果数量超过 20建议关掉标注只靠颜色区分。fmt.0f表示不保留小数避免格子太挤。5. 避坑与常见问题那些让结果翻车的细节5.1 单位不统一导致收益算错现象模型跑出来总收益特别大或者特别小和手工估算差一个数量级。原因题目里亩产量可能是「斤/亩」价格是「元/斤」但成本是「元/亩」如果直接相乘单位就乱了。解决在数据整理阶段统一单位把所有金额统一成「元」面积统一成「亩」产量统一成「斤」或「吨」并在代码注释里写清楚。5.2 轮作约束写成了「同年不能种两种」现象求解器返回不可行或者结果里同一地块同一年只种了一种作物。原因轮作约束的本意是「不同年份之间不能连续种同一作物」但写代码时不小心把同一年也限制进去了。解决检查约束里的年份索引确保只比较相邻年份不要在同一年内做互斥。5.3 浮点误差让结果里出现大量极小值现象提取结果时发现很多面积是0.000001这种值图表上看起来像有种植但实际可以忽略。原因线性规划求解器返回的是浮点数理论上为零的变量可能因为数值误差变成极小正数。解决提取结果时加阈值过滤比如val 1e-6或者在求解后对结果做一次四舍五入再判断。5.4 中文字体导致图表乱码现象matplotlib画出来的图里中文全是方框。原因默认字体不支持中文。解决在绘图前设置matplotlib.rcParams[font.sans-serif] [SimHei]如果系统没有 SimHei可以换成Microsoft YaHei或WenQuanYi Micro Hei。注意axes.unicode_minus也要设为False否则负号也会乱码。5.5 约束太紧导致无解却以为是代码错现象模型一直返回Infeasible但检查代码没发现语法错误。原因约束之间互相矛盾比如「每个地块每年必须种满」和「同一作物不能连续种」同时存在时可能某些地块在特定年份无作物可种。解决先把「必须种满」改成「不超过面积」跑一版看是否有解如果有解再逐步加回严格约束定位是哪条导致矛盾。6. 进阶技巧用敏感性分析验证方案的稳健性模型跑通、图表画完之后真正让方案有说服力的是敏感性分析。题目里的价格、产量、成本往往是一个确定值但实际中这些参数会波动。我一般会做两件事第一把销售价格上下浮动 10%看总收益和种植结构变化大不大第二把某个作物的亩产量降低 20%看模型会不会自动减少该作物的种植面积。如果结构变化很小说明方案稳健如果变化剧烈说明模型对某个参数过于敏感需要在论文里说明风险。# 敏感性分析示例价格上下浮动10% base_prices crop.set_index(作物名称)[销售价格].to_dict() sensitivity_results [] for factor in [0.9, 1.0, 1.1]: # 修改价格 for crop_name in base_prices: crop.loc[crop[作物名称] crop_name, 销售价格] base_prices[crop_name] * factor # 重新生成 df_valid 并求解此处省略重复的建模代码实际使用时封装成函数 # ... sensitivity_results.append({ 价格系数: factor, 总收益: pulp.value(prob.objective) }) print(pd.DataFrame(sensitivity_results))这段代码的关键是把建模过程封装成函数每次修改参数后重新生成组合表、重新求解。参数factor取 0.9、1.0、1.1 分别代表价格下降 10%、不变、上涨 10%。如果时间允许还可以对亩产量和种植成本做同样的分析最后用折线图把总收益随参数变化的趋势画出来放在论文里比单纯给一个最优解更有说服力。我自己的习惯是不管题目要不要敏感性分析都会至少跑一版价格浮动因为这一步能暴露很多建模时没注意到的假设。有一次我帮人看模型发现总收益对某个作物的价格极其敏感一问才知道那个作物的价格是题目里唯一没有给区间的后来在论文里专门加了一段说明反而成了加分项。希望帮到你。本文还有配套的精品资源点击获取