资讯详情

资讯详情

数学建模种植策略可视化:从决策变量到论文级图表

简介这份资源面向参加2024年高教社杯全国大学生数学建模竞赛C题的学生与指导教师聚焦农作物种植策略的可视化实现帮助读者把线性规划、统计分析等建模结果转化为直观图表适合具备一定Python基础、需要完善论文配图与数据探索环节的参赛者。压缩包共31个文件约2MB包含20个png图表、6个py脚本和5个xlsx数据表脚本覆盖数据预处理、描述性统计、异常值分析、价格与占地面积可视化等环节数据表对应赛题附件及清洗整理后的结果png则给出直方图、箱线图、热力图、散点图、QQ图、CDF图等成品图。目前已有1010人学习下载。读者可据此快速复用可视化代码理解从原始附件到清洗数据再到成图的完整链路掌握异常值检测、相关性分析与价格分布呈现等思路为论文图表排版和模型结果展示提供参考。1. 从一份种植策略赛题说起可视化到底在解决什么问题2024 年高教社杯全国大学生数学建模竞赛 C 题农作物的种植策略表面上是线性规划加动态规划的组合题真正动手做过的人会发现卡住大多数队伍的不是模型本身而是把三年、几十个地块、十几种作物的决策变量理清楚之后怎么让人一眼看懂“到底种了什么、为什么这么种”。我见过不少队伍模型跑通了结果论文里只有一张密密麻麻的表格评审看完根本抓不住重点。可视化在这里不是锦上添花它是把抽象决策变量翻译成人类可读结论的唯一通道。这篇笔记面向三类人正在复现这道题、想把结果做成图放进论文的参赛者手里有类似“多地块多周期种植/排产”问题、需要一套可视化模板的从业者以及想搞清楚“建模结果怎么落地成决策看板”的工程师。我会从数据组织讲到具体绘图代码再到参数怎么调、图怎么不翻车全部按能直接抄作业的粒度写。核心结论先放这里种植策略的可视化重点不是画得好看而是把“地块×年份×作物”这个三维决策张量降维成两三张能讲故事的图。2. 先把决策变量理清楚地块、年份、作物三轴怎么组织2.1 为什么可视化之前必须先做数据整形很多人一上来就plt.bar()结果发现 x 轴挤成一团、图例盖住半张图。根因是原始决策变量通常是嵌套字典或者多层索引的 DataFrame直接丢给 matplotlib 必然翻车。常见做法是先把求解结果整理成一张“长表”long format每行是一条记录字段固定为地块编号、年份、作物名称、种植面积、季节第一季/第二季、是否重茬。这张长表是后面所有图的唯一数据源改图不用改模型改模型不用改图。我一般会定义一个build_decision_table()函数把求解器输出的变量值映射回这张表。这样做的好处是无论你用 PuLP、Gurobi 还是自己写的启发式只要最后吐出这张表可视化代码一行都不用动。下面这段代码演示从“变量名→长表”的转换逻辑变量名格式假设为x_地块_年份_作物_季节这是这类赛题里最常见的命名约定。import pandas as pd import re def build_decision_table(var_values, threshold1e-6): var_values: dict, key 为变量名, value 为求解得到的面积 threshold: 小于该值视为未种植, 避免浮点噪声 返回: 长表 DataFrame records [] # 变量名形如 x_A1_2024_小麦_1 pattern re.compile(rx_(?Pplot[^_])_(?Pyear\d{4})_(?Pcrop[^_])_(?Pseason\d)) for name, val in var_values.items(): m pattern.match(name) if not m or val threshold: continue records.append({ plot: m.group(plot), year: int(m.group(year)), crop: m.group(crop), season: int(m.group(season)), area: round(val, 2), }) df pd.DataFrame(records) # 统一排序, 保证绘图时顺序稳定 df df.sort_values([year, plot, season, crop]).reset_index(dropTrue) return df逻辑说明正则里的[^_]是关键它保证作物名里即使带下划线也不会误切但前提是你的变量命名用下划线分隔且作物名本身不含下划线——如果含就把分隔符换成|之类不会出现在名字里的字符。threshold参数建议设成 1e-6 而不是 0因为求解器返回的“零”经常是 1e-9 这种浮点噪声不滤掉会在图上出现高度几乎为零的柱子看起来像 bug。area保留两位小数是为了和题目里“面积精确到亩”的要求对齐如果你的模型允许连续面积这里可以保留更多位。2.2 三轴降维的三种基本图型与适用场景长表有了接下来是选图。地块、年份、作物三个维度一张二维图最多干净地表达两个维度第三个维度必须靠颜色、分面或者动画。我的经验是分三种情况第一种看“每年总种植结构”用堆叠柱状图x 轴年份堆叠段作物适合回答“三年种植比例有没有大调整”。第二种看“每个地块的轮作是否合理”用热力图行是地块、列是年份颜色深浅表示该地块该年的种植强度或作物类别编号适合检查重茬约束有没有被违反。第三种看“某一种作物在各地块的分布”用分面柱状图每个子图一个年份x 轴地块y 轴面积适合回答“小麦是不是集中在某几个地块”。选错图型的典型翻车是用一张堆叠柱状图同时表达地块和年份结果 x 轴标签斜着排还重叠。记住一个原则——分类维度超过 8 个就不要硬塞进一张图的坐标轴改用分面。2.3 用 pandas 做透视把长表变成绘图友好的宽表matplotlib 和 seaborn 对宽表更友好所以中间加一步pivot_table很划算。下面这段把长表转成“年份×作物”的面积汇总以及“地块×年份”的种植强度矩阵。def pivot_by_year_crop(df): # 按年份和作物汇总面积, 缺失填 0 wide df.pivot_table( indexyear, columnscrop, valuesarea, aggfuncsum, fill_value0 ) return wide def pivot_by_plot_year(df): # 地块×年份的种植强度, 用面积总和代表 wide df.pivot_table( indexplot, columnsyear, valuesarea, aggfuncsum, fill_value0 ) return wide参数说明aggfuncsum是因为同一地块同一年同一作物可能分两季种面积要累加如果你要区分季节就把columns改成[crop, season]形成多级列。fill_value0必须加否则热力图会出现空白格看起来像数据缺失实际是“没种”语义完全不同。透视之后建议打印一下wide.shape如果列数超过 15图例会爆炸这时候要考虑把低频作物合并成“其他”。3. 把结果画成能进论文的图四类核心图表的代码实现3.1 年度种植结构堆叠柱状图这张图是论文里出现频率最高的回答“每年种了什么、各占多少”。关键是颜色要固定——同一种作物在三年里必须是同一个颜色否则读者没法横向对比。做法是预先定义一个crop_color字典而不是让 matplotlib 自动分配。import matplotlib.pyplot as plt import matplotlib # 固定中文字体, 避免方块 matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False def plot_structure(wide, crop_color, save_pathstructure.png): fig, ax plt.subplots(figsize(8, 5)) bottom None for crop in wide.columns: vals wide[crop].values ax.bar( wide.index.astype(str), vals, bottombottom, labelcrop, colorcrop_color.get(crop, #999999), edgecolorwhite, linewidth0.5 ) bottom vals if bottom is None else bottom vals ax.set_xlabel(年份) ax.set_ylabel(种植面积亩) ax.legend(locupper right, ncol2, fontsize9) ax.set_title(各年度作物种植结构) fig.tight_layout() fig.savefig(save_path, dpi300) plt.close(fig)逻辑说明bottom变量是堆叠的核心第一层 bottom 为 None之后每层累加。edgecolorwhite加 0.5 线宽是为了让相邻色块有分隔不然深色作物挨在一起会糊成一片。dpi300是论文投稿的底线低于这个数印刷会糊。ncol2让图例分两列作物超过 6 种时单列图例会占掉半张图。注意wide.index.astype(str)年份如果是整数matplotlib 会当成连续轴柱子之间会有奇怪的空隙转成字符串才是分类轴。3.2 地块-年份轮作热力图这张图用来验证轮作约束比如“同一地块不能连续两年种同一种作物”。做法是把作物映射成整数编号画热力图颜色代表作物类别这样一眼就能看出某地块是不是连续两年同一个颜色。import seaborn as sns import numpy as np def plot_rotation_heatmap(df, save_pathrotation.png): # 作物到编号的映射, 保证颜色稳定 crops sorted(df[crop].unique()) crop2id {c: i for i, c in enumerate(crops)} df df.copy() df[crop_id] df[crop].map(crop2id) # 同一地块同一年可能两季, 取面积最大的那季作为代表 idx df.groupby([plot, year])[area].idxmax() rep df.loc[idx] mat rep.pivot(indexplot, columnsyear, valuescrop_id) fig, ax plt.subplots(figsize(6, 8)) sns.heatmap( mat, cmaptab20, cbarFalse, linewidths0.5, linecolorwhite, annotrep.pivot(indexplot, columnsyear, valuescrop).values, fmt, axax ) ax.set_xlabel(年份) ax.set_ylabel(地块) ax.set_title(地块-年份轮作分布) fig.tight_layout() fig.savefig(save_path, dpi300) plt.close(fig)参数说明idxmax()取面积最大的季节作为代表是因为热力图一格只能放一个值如果你的模型两季同等重要就得画两张热力图分别对应第一季和第二季别硬塞。cmaptab20适合分类数据最多 20 类超过就换tab20b。annot里直接标作物名而不是编号读者不用回头查图例这是论文图的一个实用技巧。cbarFalse是因为分类颜色条没有意义留着反而误导。3.3 单作物地块分布分面图当你想论证“某作物为什么集中在这几个地块”分面柱状图最直观。每个年份一个子图x 轴地块y 轴该作物面积。def plot_crop_facet(df, crop_name, save_pathfacet.png): sub df[df[crop] crop_name] years sorted(sub[year].unique()) fig, axes plt.subplots(1, len(years), figsize(4 * len(years), 4), shareyTrue) if len(years) 1: axes [axes] for ax, y in zip(axes, years): d sub[sub[year] y].groupby(plot)[area].sum() ax.bar(d.index, d.values, color#4C72B0) ax.set_title(f{y} 年) ax.set_xlabel(地块) ax.tick_params(axisx, rotation45) axes[0].set_ylabel(面积亩) fig.suptitle(f{crop_name} 地块分布) fig.tight_layout() fig.savefig(save_path, dpi300) plt.close(fig)逻辑说明shareyTrue让三个子图共用 y 轴这样柱子高度可以直接横向比较否则每个子图自动缩放视觉上会骗人。rotation45是地块编号较长时的必备不转就会重叠。fig.suptitle和子图 title 分工总标题说作物子标题说年份。如果地块超过 20 个x 轴还是会挤这时候改成横向条形图barh更合适。3.4 关键指标折线图把约束满足情况画出来论文里除了“种了什么”还要证明“约束都满足了”。把每年的总产量、总成本、重茬违规次数画成折线比文字描述有说服力。def plot_kpi(df, kpi_dict, save_pathkpi.png): kpi_dict: {年份: {profit: ..., violation: ...}} years sorted(kpi_dict.keys()) profit [kpi_dict[y][profit] for y in years] violation [kpi_dict[y][violation] for y in years] fig, ax1 plt.subplots(figsize(7, 4)) ax1.plot(years, profit, o-, color#2E86AB, label总收益) ax1.set_xlabel(年份) ax1.set_ylabel(总收益元, color#2E86AB) ax2 ax1.twinx() ax2.bar(years, violation, alpha0.3, color#D1495B, label重茬违规) ax2.set_ylabel(违规次数, color#D1495B) fig.tight_layout() fig.savefig(save_path, dpi300) plt.close(fig)参数说明双 y 轴twinx适合量纲差很大的两个指标但要注意左轴折线、右轴柱状颜色对应否则读者会看混。alpha0.3让柱子半透明不遮挡折线。违规次数理想情况应该全是 0如果某年非零这张图就是最直接的证据比在正文里写“经检验约束满足”强得多。4. 避坑与排查可视化环节最容易翻车的五个地方4.1 中文字体显示成方块现象图上所有中文变成一个个小方块英文数字正常。原因matplotlib 默认字体不含中文字形。解决在绘图前设置rcParams[font.sans-serif]值填系统里确实存在的中文字体名比如SimHei、Microsoft YaHei。如果换机器跑字体名可能不存在稳妥做法是加一段检测逻辑找不到就回退到英文标签。另外axes.unicode_minusFalse必须一起设否则负号也会变方块。4.2 堆叠柱状图总量对不上现象堆叠图看起来每年总量差不多但和模型输出的总种植面积对不上。原因透视时用了fill_value0但没排除“面积为负”的异常值或者同一记录被重复计入。解决画图前先assert (df[area] 0).all()再assert abs(df[area].sum() - expected_total) 1e-3。这两个断言花不了几行代码但能省掉论文送审前发现数据对不上的恐慌。4.3 热力图颜色把相邻类别混在一起现象两个不同作物在热力图上颜色几乎一样看不出轮作。原因用了连续色图如viridis去表示分类变量。解决分类数据必须用定性色图tab10、tab20、Set3都行类别数超过色图容量时要么合并小类要么手动指定颜色列表。判断标准很简单——如果图例需要渐变条那就不该用于分类。4.4 图例遮挡数据现象图例放在右上角正好盖住最高的那根柱子。原因默认位置没考虑数据分布。解决先画图看数据集中在哪图例就放对面。或者用bbox_to_anchor把图例挪到图外比如ax.legend(bbox_to_anchor(1.02, 1), locupper left)配合fig.tight_layout()自动留白。论文图宁可图例在图外也不要遮挡数据。4.5 保存的图分辨率不够现象屏幕上看着清楚插入论文 PDF 后模糊。原因默认 dpi 是 100印刷需要 300。解决savefig时显式写dpi300并且用fig.tight_layout()而不是plt.tight_layout()前者作用于当前 figure多图场景下不会串。另外保存格式优先 PNG无损或 PDF矢量别用 JPGJPG 的压缩伪影在色块边缘特别明显。5. 进阶技巧让可视化从“能看”变成“能讲”前面四章解决的是“画出来”这一章解决“画得让人信”。第一个技巧是给图加注释而不是加文字说明。比如在堆叠柱状图的某一年上方画一个箭头标注“该年因轮作约束减少了小麦面积”读者视线不用在正文和图之间来回跳。matplotlib 的ax.annotate()配合xytext偏移就能做到偏移量建议用数据坐标和轴坐标混合避免缩放后错位。第二个技巧是用同一套颜色贯穿全文。定义一个crop_color字典所有图都从这里取色读者看第二张图时不需要重新认颜色。这个字典最好在代码最开头定义并且加注释说明每种作物的颜色选择理由比如小麦用金黄、玉米用深黄色系相近但明度拉开。如果作物超过 10 种考虑用“主色纹理”区分而不是继续加颜色。第三个技巧是把敏感性分析画成带状图。种植策略题通常要求做敏感性分析与其列一堆表格不如画一条基准折线加一条阴影带表示参数波动范围。ax.fill_between()一行就能实现alpha0.2让阴影不抢主线。读者一眼就能看出“哪些年份的决策对参数最敏感”这比“经敏感性分析方案稳健”这种空话有说服力得多。最后一个习惯每张图保存时用有意义的文件名比如structure_year_crop.png、rotation_plot_year.png而不是fig1.png、fig2.png。论文改到第五版的时候你会感谢自己当初没偷这个懒。我自己的教训是曾经因为图名全是output1到output8返修时改错了一张图把轮作热力图放进了产量分析的位置被导师一眼看穿。从那以后图名即内容成了我的硬规矩。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →