简介:2024年高教社杯全国大学生数学建模竞赛C题“农作物的种植策略”配套可视化代码,面向参赛学生和数据分析学习者。它聚焦于将模型结果转化为直观图表,涵盖数据预处理、描述性统计、异常值分析与多维度可视化,使用Python的matplotlib、seaborn等库实现。压缩包共31个文件,包括6个py脚本、20个png图表和5个xlsx数据表,整体仅2MB,结构清晰:脚本完成数据清洗与绘图,Excel工作表保存原始及整理后的数据,PNG则呈现柱状图、热力图、散点图等分析结果。已有1005人学习下载,适合希望快速复现赛题分析流程、参考可视化代码风格或进行二次开发的用户。通过这份资源,读者可以掌握从数据预处理到可视化呈现的完整思路,直接运行代码生成图表,并对照Excel数据理解作物种植策略中的关键影响因素。 2024 年高教社杯的 C 题放出来之后,我身边不少参赛的朋友都在吐槽:数据量大、约束条件细、地块类型绕,最后论文写完了,图却不知道怎么画,“可视化代码”这事看起来简单,真到自己手写要用的时候,就发现到处是坑。
我今天就把自己把 C 题整套数据从清洗到建模、再落到可视化代码的过程完整拆开。这篇内容主要围绕农作物的种植策略展开,重点解决两个问题:怎么把题目的数据约束转化为可计算的方案,以及怎么用 Python 把地块、作物、产量、收益这些关系画得让评委一眼看懂。不管你是刚接触数学建模的小白,还是已经卡在画图阶段的老手,这篇都适合你花十几分钟过一遍。
1. 题目再拆解:C 题到底在问什么
1.1 背景与数据范围
C 题给的是华北某山村 2023 年的实际种植数据,地块分成平旱地、梯田、山坡地和水浇地四类,每一类又细分出具体地块编号。作物种类覆盖小麦、玉米、谷子、高粱、黄豆、黑豆、红豆、绿豆、爬豆、水稻、土豆、红薯、大蒜、白菜、白萝卜、胡萝卜、西红柿、茄子、柿子椒、大葱、黄瓜、四季豆、食用菌、灵芝、羊肚菌等二十多种。
别看题目背景只写了个“种植策略”,它本质上是一道多地块、多作物、多约束的非线性规划问题。你要在满足重茬限制、豆类固氮、食用菌后茬处理、每种作物每年只种一季等一系列条件下,为 2024 到 2030 年制定最优种植方案。
我一开始拿到数据时最头疼的其实是这种状态:问题二给了庞大的网格搜索空间,问题三还要做不确定性分析。很多人写到后面直接放弃可视化,结果论文里全是表格,评委看得费劲,得分自然受影响。
1.2 问题一:最基础也最容易被轻视
问题一要求“假定未来两年气候稳定、市场稳定、亩产量和销售价格保持不变”,这句话直接决定模型复杂度。你不需要预测,不需要回归,只需要做约束下的最优规划。
但注意它的交叠设定——同一块地不能连续种植同一种作物,各种植季之间也不能重茬,豆类作物要参与轮作,食用菌种过之后需要休耕。这意味着你的决策变量不只是“种什么”,还包括“前茬是什么”,是一个带历史状态约束的指标规划,不是简单的单期最大化。
可视化在这个阶段最重要的不是炫技,而是把每块地在两年内的作物安排呈现清楚。我推荐做堆叠柱状图加地块分面,横轴是地块编号,纵轴是种植面积,颜色代表不同作物,再叠加季度轮换信息,一张图就能说完整个方案。
1.3 问题二与问题三:从静态到动态
问题二要在问题一基础上放宽假设,允许未来产量、成本、价格波动,并且某些作物可以(或必须)连续种植,此时整个决策变成多阶段规划。这里的核心难点不再只是约束判断,而是“未来参数怎么定”。
最稳妥的做法是:用 2023 年的数据作为基准,结合题目附带的 2024-2030 预期增长率和波动区间做情景生成,再对每种情景跑规划,最后加权各情景的最优方案。这个过程会产生大量中间数据,如果不靠可视化做中间检查,很容易出现“模型算出负产量、地块面积超限”这种低级错误。
问题三则是引入不确定性的敏感性分析,我通常的做法是把销售价格和亩产量按±5%、±10%、±15% 做扰动,观察作物选择是否发生跳变。这一部分最出效果的可视化是折线带置信区间图,把不同扰动水平下的收益范围和种植面积变化画出来,直接能说明策略的稳健性。
2. 数据处理链路与模型策略
2.1 2023 年基准数据怎么清洗
题目给的 Excel 里最核心的字段是:地块名、地块类型、作物名、种植面积、亩产量(斤/亩)、销售单价(元/斤)、种植成本(元/亩)。这些字段本身很干净,但里面潜伏着三个问题。
第一,题目里小麦、玉米等粮食作物的“亩产量”通常是一季的数据,但如果某块地一年能种两季(比如水浇地冬小麦夏玉米轮作),你需要按季拆开,否则年化产量会翻倍出错。第二,蔬菜类的成本区间波动很大,不同地块间可能给出不同的参考值,直接用平均值会损失地块差异信息。第三,食用菌和灵芝这类特殊作物,数据可能只有个别地块有,做全局约束时容易漏掉“只能种在特定大棚地块”这样的隐含条件。
我的清洗思路是:先把所有表按地块编号纵向拼接,再按“季”横展开,形成一张每一行是“地块×年×季”的长表。后续所有建模、绘图都基于这张长表,不要再用原始宽表直接算。
2.2 从数据到规划:参数计算与约束编码
在正式开始优化前,我习惯先把每亩收益算出来:
[ \text{profit_per_mu} = \text{yield_per_mu} \times \text{price} - \text{cost_per_mu} ]
C 题里题目给了部分作物的销售价格,但没给全,比如部分蔬菜和食用菌,可能需要你根据市场价格自行补充。我自己在复现时,对缺失价格取了同类作物的平均价,并在附录里做了说明,这样模型至少可复现。
约束编码要注意的是“重茬”判断。最简单的方式是构造一个 0-1 变量矩阵,行代表地块,列代表作物,值 1 表示该季种该作物,然后用前一季的矩阵做点乘,强制同位置不能同时为 1。如果地块数量多,直接用 scipy.optimize.linprog 或 pulp 都能解;如果地块多到几千块,建议用 openpyxl 把约束条件导出成 LP 文件,再用 CBC 或 GLPK 求解,避免在 Python 里硬算导致内存爆炸。
2.3 网格搜索与结果导出结构
问题二我采用的是 grid search 加单期规划组合:先对每个地块、每种作物、每个年份跑预测收益,把预测结果填入二维矩阵,再逐期求解。这样每次求解规模小、速度快,而且便于把中间结果实时存成 CSV,用 matplotlib 画分阶段趋势。
结果导出的统一结构我推荐这样定义:
{ "year": 2024, "plot_id": "A1", "plot_type": "平旱地", "season": "第一季", "crop": "小麦", "area": 20.5, "profit_per_mu": 1234.5, "total_profit": 25307.25 }每画一张图,都从这样的字典列表里抽字段,灵活性最高。后续不论做堆叠图、热力图还是桑基图,都不需要再回头改数据结构。
3. 可视化方案设计:从“能看”到“讲得清”
3.1 可视化目标:不是好看,是辅助论证
我在评阅过一些建模论文后发现,评委看图最在意的不是配色,而是能否快速提取信息。所以可视化的第一目标永远是辅助论证。比如你论证“水浇地适合小麦玉米轮作”,就应该画一张水浇地的季相分布图,让评委一眼看到轮作关系,而不是只画柱状图堆叠成品。
我在这道题里把图表分成四层:数据概览层、模型输入层、优化方案层和敏感性分析层。
| 图表类型 | 对应问题 | 展示内容 |
|---|---|---|
| 堆叠柱状图 | 问题一 | 每个地块各作物面积占比 |
| 分面热力地图 | 问题一/二 | 地块类型 × 作物 × 年份的种植分布 |
| 分组柱状图 | 问题二 | 不同情景下的总收益对比 |
| 折线图带置信区间 | 问题三 | 价格/产量扰动下的收益稳健性 |
| 桑基图 | 问题二 | 地块类型到作物再到收益的资金流向 |
这套图表的逻辑是一张图回答一个问题,不堆叠信息。很多团队喜欢一张图塞五六种元素,最后图例占了半个画布,反而说不清问题。
3.2 配色与字体细节
数学建模论文是黑白的,但答辩 PPT 可以是彩色的。我建议在论文中用白底黑线、灰度配色,在答辩中用高区分度的色板。一个容易踩的坑是直接用 matplotlib 默认色板,在论文打印时颜色区分度极低。
推荐用 colorbrewer 的 Set3 或 Tableau 色板,代码里这样指定:
colors = [ "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd" ]字体方面,中文论文必须配中文字体,macOS 用"Arial Unicode MS",Windows 用"SimHei",Linux 服务器最好上传字体文件让 matplotlib 动态加载,不要赌系统自带。
3.3 模块化设计:一份代码出全套图
在实际写的时候,我建议把可视化代码按照“数据读取 — 字段解析 — 绘图函数 — 主循环调用”四层组织,不要把所有图写在同一个循环里。因为 2024-2030 有 7 年,问题一问题二问题三加起来要上百张图,一旦要调整配色,逐图改能改到崩溃。
我的基本组织结构是:
# plot_utils.py def plot_stack_bar(df, output_path): ... def plot_heatmap_by_plot_type(df, output_path): ... def plot_gantt_by_crop(df, output_path): ...然后主脚本统一调用。这样做的好处是:如果后期题目数据勘误,只需重新生成数据 CSV,绘图函数完全不用改动。
4. 核心可视化代码实现
4.1 地块类型与种植面积分布图
这张图解决的是“题目给了哪些地块、各类地块面积多大”的概述问题。数据读取后,先按地块类型聚合面积。
import pandas as pd import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams["font.sans-serif"] = ["SimHei"] matplotlib.rcParams["axes.unicode_minus"] = False df = pd.read_excel("data/2023_data.xlsx", sheet_name="地块信息") type_area = df.groupby("地块类型")["面积"].sum().sort_values() fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.barh(type_area.index, type_area.values, color="#80b1d3") ax.set_xlabel("种植面积(亩)") ax.set_title("2023 年各类地块种植面积概览") for bar, val in zip(bars, type_area.values): ax.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height() / 2, f"{val:.1f}", va="center") plt.tight_layout() plt.savefig("output/plot_area_by_type.png", dpi=300) plt.show()为什么先画这张图?因为 C 题的地块面积差异很大,如果不先做概览,后面画堆叠图时很容易被最大地块的数值拉乱坐标轴,导致小地块信息完全被压扁。提前确认面积分布,再有针对性地决定要不要做分面。
4.2 作物收益率散点图:筛选主栽作物的利器
这张图解决的是“哪些作物值得优先种植”的问题。横轴是亩产量,纵轴是销售单价,点的大小代表种植面积,颜色代表作物类别。看到这张图,你基本能判断模型最后优化出的主力作物应该是哪些。
crop_data = pd.read_excel("data/2023_data.xlsx", sheet_name="作物数据") fig, ax = plt.subplots(figsize=(12, 8)) scatter = ax.scatter( crop_data["亩产量"], crop_data["销售单价"], s=crop_data["种植面积"] * 2, alpha=0.6, c=pd.factorize(crop_data["作物类别"])[0], cmap="Set3" ) ax.set_xlabel("亩产量(斤/亩)") ax.set_ylabel("销售单价(元/斤)") ax.set_title("作物亩产量与销售单价分布(点大小=种植面积)") for idx, row in crop_data.iterrows(): ax.annotate(row["作物名称"], (row["亩产量"], row["销售单价"]), fontsize=9, xytext=(5, 5), textcoords="offset points") plt.tight_layout() plt.savefig("output/plot_crop_scatter.png", dpi=300) plt.show()这张图对问题的帮助非常直观:如果某种作物亩产量高、单价也高、种植面积也不小,那在模型里它一定是最受欢迎的决策选择。反过来说,如果某个作物三项指标都很低,模型大概率在最优解里不会选它。这是你检验求解结果合理性的一张“直觉图”。
4.3 2024-2030 最优方案堆叠柱状图
这是论文里最核心的一张结果图。横轴是年份,纵轴是总种植面积,不同颜色堆叠代表不同作物。为了让每种作物都能被看到,我建议每年只取种植面积占比前 10 的作物,其余归为“其他”,否则 20 多种作物堆在一张图里,图例就能占满半页。
opt_results = pd.read_csv("output/optimal_solution.csv") top_crops = ( opt_results.groupby("作物")["面积"] .sum() .sort_values(ascending=False) .head(10) .index ) opt_results["作物分组"] = opt_results["作物"].where( opt_results["作物"].isin(top_crops), "其他" ) pivot_df = opt_results.pivot_table( index="年份", columns="作物分组", values="面积", aggfunc="sum", fill_value=0 ) pivot_df = pivot_df[top_crops.tolist() + ["其他"]] fig, ax = plt.subplots(figsize=(14, 7)) pivot_df.plot(kind="bar", stacked=True, ax=ax, colormap="Set3", edgecolor="white") ax.set_xlabel("年份") ax.set_ylabel("种植面积(亩)") ax.set_title("2024-2030 年最优种植方案面积构成") ax.legend(ncol=4, fontsize=9) plt.xticks(rotation=0) plt.tight_layout() plt.savefig("output/plot_optimal_stack.png", dpi=300) plt.show()我实际画的时候发现,如果不加edgecolor="white",相邻年份相同作物的色块会连成一片,边界不清晰,打印出来尤其明显。另外,堆叠柱状图的图例顺序要和柱子内部顺序一致,否则读者对不上,这个问题用pivot_df[top_crops.tolist() + ["其他"]]这句代码就能解决。
4.4 地块类型与作物关系热力图
热力图适合展示“哪类地块适合种哪些作物”的二维关系。我用的是 seaborn 的heatmap,数据先做地块类型与作物的交叉表。
import seaborn as sns cross_df = opt_results.pivot_table( index="地块类型", columns="作物", values="面积", aggfunc="sum", fill_value=0 ) # 只保留累计面积前 15 的作物,避免图太宽 top_crops_area = cross_df.sum(axis=0).sort_values(ascending=False).head(15) cross_df = cross_df[top_crops_area.index] fig, ax = plt.subplots(figsize=(14, 6)) sns.heatmap(cross_df, annot=True, fmt=".0f", cmap="YlGnBu", linewidths=0.5, ax=ax) ax.set_title("2024-2030 年地块类型 × 作物累计种植面积热力图") plt.tight_layout() plt.savefig("output/plot_heatmap.png", dpi=300) plt.show()这张图的优势是能一眼看出模型是否出现“山坡地种水稻”这种违反常识的方案。我在复现时曾经因为约束漏写了水稻只能种水浇地,结果模型把水稻分到山坡地,单看数值表格完全没发现,一画热力图立刻暴露问题——这就是可视化作为模型校验工具的价值。
4.5 收益与面积的关系图(气泡图)
气泡图可以把“面积、收益、作物类型”三个维度的信息同时呈现在一张图里。横轴是累计种植面积,纵轴是累计收益,气泡大小代表平均亩收益,颜色代表作物类别。
summary = opt_results.groupby("作物").agg( 面积=("面积", "sum"), 总收益=("收益", "sum"), 平均亩收益=("亩收益", "mean") ).reset_index() fig, ax = plt.subplots(figsize=(12, 8)) bubble = ax.scatter( summary["面积"], summary["总收益"], s=summary["平均亩收益"] * 0.8, alpha=0.6, c=pd.factorize(summary["作物"])[0], cmap="tab20" ) ax.set_xlabel("累计种植面积(亩)") ax.set_ylabel("累计总收益(元)") ax.set_title("作物累计面积与收益气泡图") for idx, row in summary.iterrows(): ax.annotate(row["作物"], (row["面积"], row["总收益"]), fontsize=9, xytext=(4, 4), textcoords="offset points") plt.tight_layout() plt.savefig("output/plot_bubble.png", dpi=300) plt.show()这张图放在问题二的结果分析里特别加分。它能把“少数作物贡献大部分收益”的帕累托特征讲清楚,评委会觉得你不仅算出了方案,还理解了方案背后的经济学结构。
5. 常见问题与排查技巧实录
这一部分都是我自己在复现和代跑代码时真实踩过的坑,比很多官方文档写得直白,建议直接收藏。
5.1 中文乱码与负号显示问题
matplotlib 默认字体不含中文字形,直接画图会出现一个个方框。在 Windows 下用SimHei基本能解决,但要注意负号也会因为字体替换变成方框,必须加matplotlib.rcParams["axes.unicode_minus"] = False。
在 macOS 下,SimHei不存在,推荐用"Arial Unicode MS"或者"PingFang SC"。如果在服务器上跑,什么都不带,最省事的方案是直接用plt.rcParams["font.family"] = "sans-serif",然后下载一个Noto Sans CJK SC的 otf 字体文件,放到 matplotlib 字体目录里并删除缓存。
import matplotlib.font_manager as fm fm.fontManager.addfont("/path/to/NotoSansCJKsc-Regular.otf") plt.rcParams["font.family"] = "Noto Sans CJK SC"5.2 图例重叠与条目不齐
图例重叠在堆叠柱状图里几乎是必现的,特别是作物种类超过 12 种后。我的经验是优先用ax.legend(ncol=4, bbox_to_anchor=(0.5, -0.15), loc="upper center")把图例放到图外下方,而不是放在绘图区内。如果放图外还重叠,就减少堆叠的作物种类,用“其他”来兜底。论文图最好不要出现超过 12 类的图例。
5.3 数据透视表 NaN 值处理
pivot_table默认对缺失的“地块 × 作物”组合会填 NaN,直接画热力图时会出现白色方块,看着像是数据缺失。加fill_value=0把它们变成 0,表示“没种”而不是“没数据”,语义上更准确,图表也更干净。
5.4 导出高分辨率图
论文插图通常要求 300dpi 以上,plt.savefig(..., dpi=300)是底线。同时注意保存格式,期刊或论文模板一般要求 EPS 或 PDF,matplotlib 支持直接存成 PDF,但有些中文字体在 PDF 里会提示“无法嵌入”。这种情况我会用bbox_inches="tight"再加figure.autolayout=True,基本能避免大部分裁边和字体问题。
plt.savefig("output/plot.pdf", dpi=300, bbox_inches="tight")5.5 常见错误速查表
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
FindFont: font family not found | 中文字体未注册 | 用font_manager.addfont指定字体文件 |
ValueError: operands could not be broadcast | 数据表行列不对齐 | 检查pivot_table的 index 和 columns 参数 |
LinAlgError: Singular matrix | 线性规划约束矩阵奇异 | 检查是否有重复约束或全零行 |
KeyError: '面积' | 列名不匹配 | 打印df.columns.tolist()核对 |
| 图形空白但无报错 | 数据是 NaN 或被过滤 | 检查聚合后 DataFrame 是否为空 |
6. 一点个人体会
C 题这套数据量放在数学建模竞赛里不算大,但维度嵌套得很深,地块类型、作物种类、年份、季节、约束条件叠加起来,如果不在每一个阶段画图校验,模型出错几乎是必然的。可视化代码不是论文写好之后才补的装饰品,而是建模过程中帮你发现约束错误、确认数据口径、检验结果合理性的核心工具。
我自己的习惯是:每算完一步就导出 CSV,每导出一个 CSV 就画一张图,每画一张图就问自己一个问题——“这个结果符合常理吗?如果不符合,是模型错了还是数据错了?”这套流程走下来,论文的图表基本是顺手就齐了,根本不需要最后赶工。
如果你今年也在准备国赛或者其他数据类竞赛,不妨试试按我上面的思路把数据表和可视化一起往前推。前期多花二十分钟画中间图,后期可能帮你省下的是整整一天排查逻辑错误的时间。
本文还有配套的精品资源,点击获取