简介:2024年全国大学生数学建模竞赛C题完整代码与数据资源,面向参赛学生及数学建模爱好者。资源围绕赛题全流程,提供数据预处理、相关性分析、聚类分析、线性规划与遗传算法等多个模块的Python脚本,搭配题解结果表格与可视化图表,可快速了解从数据清洗到模型优化的完整实现路径。压缩包共51个文件,以py源码、xlsx结果表、csv数据和png图表为主,另含项目说明文档,整体体积仅2.4MB,轻量易用。所有脚本按问题编号组织,便于对照赛题逐问练习;结果表格和图表可直接用于论文配图参考。资源内附原始数据与中间结果,方便复现完整分析过程。目前已有5960人学习,适合希望复盘赛题解法、参考代码框架或进行同类问题迁移训练的读者。
1. 2024数学建模国赛C题:不只是“种地”的优化题
2024年数学建模国赛C题《农作物的种植策略》,表面上是替一个华北村庄规划3700多亩地种什么,实质上是一道带约束的非线性优化问题。题目给了2024—2030年共7年的地块、作物、单价、产量、成本数据,要求你回答每一块地每个季度种什么、种多少、怎么轮作,才能让总利润最大化。这道题非常适合三类人:参加国赛的建模队(尤其是第一次做优化大题的同学)、想强化Python数据分析能力的程序员、以及想入门运筹优化但不想啃理论教材的从业者。原因是它数据量适中,区分度全在数据处理和约束建模上,不需要高深算法,但很考验把Excel转成决策变量的基本功。全网搜“2024数学建模国赛C题 代码”能找到很多现成方案,但能一次跑通并写到论文里的并不多,本文就把完整路径和翻车点一并讲清楚。
2. 读懂C题数据:从Excel到可计算的DataFrame
2.1 数据文件结构:地块、作物与2024-2030年窗口
C题附件1给的是35块地的面积、土壤类型、灌溉条件,附件2给的是2024—2030年每块地每种作物的亩产量、单价、种植成本、预期销量。文件里还有几个容易看漏的表:地块-作物适宜性表、轮作约束表、豆类占比表。拿到数据第一件事不是画图,而是确认三个主键:地块编号、作物编号、年份。凡是后面不知道在算什么、合并之后行数对不上的代码,多半是这三个主键没对齐。
附件2里有一列标着“滞后”含义的字段,表示上一年市场价格或产量对今年的影响,很多队伍直接当今年数据用,导致预测结果系统性偏移。正确做法是单独拆出来做时间特征,不能混进当期输入。另外,单价和成本的单位也不一样,单价是“元/斤”,成本是“元/亩”,后面建模时单位换算错误是丢分重灾区,建议在读数阶段就把单位统一注释在列名里。
2.2 用pandas读入并清洗:第一步不是建模而是对齐
import pandas as pd # 读入三张核心表,注意sheet_name按实际文件指定 fields = pd.read_excel('附件1.xlsx', sheet_name='地块信息') crops = pd.read_excel('附件2.xlsx', sheet_name='作物数据') suit = pd.read_excel('附件1.xlsx', sheet_name='适宜性') # 统一列名,避免中文列名在后续处理里反复报错 fields.columns = ['id', 'area', 'soil', 'irrigation'] crops.columns = ['id', 'crop', 'year', 'yield', 'price', 'cost', 'lagged_price'] # 把滞后价格单独拆成特征,不参与当期计算 crops['prev_price'] = crops['lagged_price'] crops.drop(columns=['lagged_price'], inplace=True) # 检查缺失值最粗暴但有效的方式:看每列的na数量 print(crops.isna().sum()) print(crops.dtypes)这段代码做的核心事情只有三件:统一列名、拆出滞后特征、检查缺失值和类型。pandas的read_excel在读多sheet时用sheet_name参数定位,实测比openpyxl手工遍历快一个数量级;isna().sum()能一眼看到哪些列有空洞,比画缺失值热力图直接。我一般会在这一步顺便用crops.dtypes检查年份是不是被读成了字符串,这问题在Excel导出时极常见,一旦year是object类型,后面一切按时间筛选的代码都会安静地返回空结果,而且不报错,属于最难排查的隐性bug。
另外要注意,附件里如果出现“—”或“/”这种占位符,pandas会把它读成NaN,但有时会读成字符串,导致该列变成object类型。处理办法是在read_excel时加参数na_values=['—', '/', ''],把常见占位符统一映射成NaN,再决定填充还是删除。填充缺失值不要无脑用0填充,产量和价格这种数值列如果填0,后面目标函数会出现负无穷大的异常区间,建议用同地块同作物的中位数填充。
2.3 作物分类与地块聚类:把2000行数据压缩成决策变量
C题里作物数量多但性质接近,小麦、玉米、大豆属于大田作物,马铃薯、蔬菜属于高价值作物,如果不做合并,决策变量会膨胀到35块地乘以20多种作物再乘以28个季度,求解器直接卡死或算到天亮。常见做法是先按“作物大类”合并:粮食、经济作物、蔬菜、豆类,并把同类作物的价格取加权平均,产量取面积加权。
# 简化示例:按大类聚合 def classify(crop): if crop in ['小麦', '玉米']: return '粮食' elif crop in ['大豆', '豌豆']: return '豆类' elif crop in ['白菜', '萝卜']: return '蔬菜' else: return '经济' crops['category'] = crops['crop'].apply(classify) category_df = crops.groupby(['id', 'year', 'category']).agg({ 'yield': 'mean', 'price': 'mean', 'cost': 'sum' }).reset_index()这里聚合的逻辑是:同一地块、同一年、同一大类作物的产量和价格取平均,成本取加总,因为成本是按面积算的。这样决策变量从几千个降到几百个,线性规划能在一分钟内求出基准解。注意groupby之后的reset_index不是可选项,很多新手做完聚合后索引还是MultiIndex,后面合并时莫名其妙多出一列,这就是索引没重置的典型症状。
地块聚类这步可选,但推荐做。35块地如果每块地单独建模,约束矩阵会非常稀疏;按土壤类型和灌溉条件聚成3—4类,每类用同一套种植策略,结果更符合实际生产习惯,论文里也好解释。聚类用KMeans即可,特征就选面积、土壤类型编码、是否灌溉三个维度。做聚类前要标准化,否则面积这个量纲(几百到上千)会吞掉土壤类型(0/1)的影响。
3. 建模思路落地:从产量预测到种植策略
3.1 目标函数怎么定:利润最大化还是风险最小化
这一问目标函数选利润最大化是最稳妥的。题目读起来是规划问题,不是投资组合问题,把总销售收入减去总种植成本作为目标即可。但这里有个容易被忽略的点:附件2里给的成本是“每亩成本”,不是“每斤成本”,如果不乘以地块面积,目标函数量纲就错了。目标函数的标准写法是:
利润 = Σ(地块面积 × 亩产量 × 单价) - Σ(地块面积 × 每亩成本)
如果题目还给“预期销量”限制,就不能让产量无限大,得加销量上限约束。我一般会把销量上限当成硬约束放进A_ub矩阵,而不是在目标函数里做惩罚项,前者求解稳定,后者调权重的过程太玄学,评审也不好看。另外,C题如果涉及多年规划,目标函数要加上“年际贴现”吗?不需要,题目没有提资金时间价值,加了反而画蛇添足。
3.2 约束条件的数学化:面积、轮作与豆类占比
C题约束分三类:总面积约束、单地块不重茬约束、豆类占比约束。先把它们列成数学式,再翻译成矩阵。
总面积约束:所有地块种植面积之和等于总耕地面积,这是等式约束。实际操作中,我建议放宽成“不超过总耕地面积”,因为题目有时允许休耕,休耕地不种任何作物面积算0,这时等式约束会把问题无解化。
轮作约束:同一地块相邻两年不能种同一类作物,这是二元变量的互斥约束。写成数学式就是x[f,c,y] + x[f,c,y+1] ≤ 1,对每个地块f、每个作物类别c、每个年份y成立。翻译成矩阵时,每个约束是一行,只有对应两个变量的位置填1,其他填0。
豆类占比:豆类种植面积 ≥ 总面积的5%,这是不等式约束。第三个最容易漏,2024年题目明确写了“至少5%的豆类”,不少队伍只做了前两个约束就拿出去跑,结果最优方案里豆类面积是0,被评审一眼看穿。
import numpy as np from scipy.optimize import linprog # 假设决策变量按 (地块, 作物类别, 年份) 顺序展开 n_vars = n_fields * n_categories * n_years x = np.zeros(n_vars) # 利润系数向量:面积 * 产量 * 单价 - 面积 * 成本 c = np.zeros(n_vars) for f in range(n_fields): for cat in range(n_categories): for y in range(n_years): idx = (f * n_categories + cat) * n_years + y area = field_area[f] * area_ratio[f, cat, y] c[idx] = -(area * yield_data[f, cat, y] * price_data[f, cat, y] - area * cost_data[f, cat, y]) # 不等式约束 A x <= b A_ub = [] b_ub = [] # 每块地面积上限 for f in range(n_fields): row = np.zeros(n_vars) row[f * n_categories * n_years : (f+1) * n_categories * n_years] = 1 A_ub.append(row) b_ub.append(field_area[f]) # 豆类面积 >= 5% 总面积,等价于 -豆类面积 <= -0.05 * 总面积 row = np.zeros(n_vars) for f in range(n_fields): for y in range(n_years): idx = (f * n_categories + cat_legume) * n_years + y row[idx] = -field_area[f] A_ub.append(row) b_ub.append(-0.05 * total_area) result = linprog(c, A_ub=np.array(A_ub), b_ub=np.array(b_ub), bounds=(0, 1), method='highs') print('最优利润:', -result.fun)这里最关键的是系数向量c的构造顺序,必须和决策变量展开顺序完全一致,否则所有约束张冠李戴。我建议先把c打印出来看前20个数是否合理正负,再跑求解器。linprog默认求最小值,所以利润系数取负号;bounds=(0,1)表示面积占比在0和1之间,这是连续松弛解,先看基准量级,后面再上整数约束。
3.3 产量预测:用滞后价格与土壤特征建模
C题里2024—2030年的产量和价格是直接给出的,但如果你想在论文里展示“预测能力”,可以用前几年的数据做验证。有的年份题目会给历史参考数据,这时用滞后价格、地块土壤类型、灌溉条件做特征,训练一个轻量级回归模型预测亩产量。
from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split # 构造特征:上一年价格、土壤类型编码、灌溉条件、年份 features = crops[['prev_price', 'soil_code', 'irrigation', 'year']].values target = crops['yield'].values X_train, X_test, y_train, y_test = train_test_split( features, target, test_size=0.2, random_state=42 ) model = RandomForestRegressor(n_estimators=200, max_depth=6, random_state=42) model.fit(X_train, y_train) print('R2:', model.score(X_test, y_test))随机森林在这个数据集上R2一般在0.85以上,已经足够支撑论文里的“产量预测模块”。注意max_depth不要超过8,否则过拟合,测试集R2反而下降。soil_code是你在预处理阶段把“黏土”“沙土”等文本编码成0/1/2得到的,这一步在建模前做。
4. 求解与调参:用scipy和遗传算法逼近最优解
4.1 为什么线性规划不够:非线性成本与动态轮作
基准解能跑通了,但C题真正拿分的是后两问:如果产量受天气影响波动,如果价格随市场变化,怎样做种植决策才稳健。这时的目标函数不再线性,比如产量下降10%时收入不是简单线性下降,因为成本和产量之间有边际关系;轮作约束跨年份联动,决策变量互相耦合,整个问题变成了混合整数非线性规划。
还有一个更隐蔽的问题:线性规划解出来的是“静态最优”,意思是它不考虑年份之间的序列关系。但题目问的是“7年策略”,今年种了小麦明年就不能种小麦,这个决策会锁死后年的选择空间。这种时间耦合只能靠沿着年份逐层递推,或者用全局搜索算法。所以后两问建议直接换遗传算法或差分进化,不要死磕线性化。
4.2 改用遗传算法:用scipy.optimize.differential_evolution
import numpy as np from scipy.optimize import differential_evolution # 决策变量按 (地块, 作物类别, 年份) 展开,还是同样的顺序 n_vars = n_fields * n_categories * n_years def objective(x): x = x.reshape(n_fields, n_categories, n_years) profit = 0.0 # 地块面积约束,用惩罚函数处理 for f in range(n_fields): area_used = x[f].sum() * field_area[f] if area_used > field_area[f] * 1.001: return 1e6 + (area_used - field_area[f]) * 1e4 # 轮作约束:同地块相邻两年不能同一类 for f in range(n_fields): for cat in range(n_categories): for y in range(n_years - 1): if x[f, cat, y] > 0.5 and x[f, cat, y+1] > 0.5: return 1e6 + 1e5 * (x[f, cat, y] + x[f, cat, y+1]) # 计算正常利润 for f in range(n_fields): for cat in range(n_categories): for y in range(n_years): area = x[f, cat, y] * field_area[f] profit += (area * yield_data[f, cat, y] * price_data[f, cat, y] - area * cost_data[f, cat, y]) return -profit bounds = [(0, 1)] * n_vars result = differential_evolution(objective, bounds, seed=42, maxiter=500, popsize=50, tol=1e-6) print('最优利润:', -result.fun) print('迭代代数:', result.nit)differential_evolution是差分进化算法,本质是对连续空间的全局搜索,不需要求梯度,非常适合目标函数带惩罚、导数不存在的场景。这里用惩罚函数把面积约束和轮作约束写进目标函数,凡是违反约束的方案直接返回一个很大的数,求解器会自动避开。这里轮作约束用了0.5作为阈值,因为连续变量不一定恰好等于0或1,面积占比超过一半就算“种植”,这个阈值是按实际意义定的,不是随便拍脑袋。
popsize=50表示每个维度上生成50个初始个体,经验值是维度小于50时popsize设30—60即可。设太大收敛慢,设太小容易陷局部最优。maxiter=500是最大迭代代数,不是必达值,如果提前收敛会自动停止。这里要特别提醒:差分进化是随机算法,seed必须固定,否则同一份代码每次跑出不同结果,评审复现不了,论文直接被打回。
4.3 参数设置的玄学:种群、变异率与收敛判断
遗传算法和差分进化的参数没有“标准答案”,但有几条实测规律值得记下来。
第一条,变异率mutation默认0.5,表示每代有50%的个体发生扰动。太小会早熟,群体很快收敛到局部最优;太大则搜索像随机游走,目标函数值一直上下跳动不收敛。如果你发现result.nit很大但目标函数值还在变,先把mutation调小到0.3试试。
第二条,收敛判断看result.nit和result.fun的变化。如果连续20代目标函数变化小于tol(比如1e-6),基本可以认为收敛了。我习惯在目标函数里加一个计数器,每计算一次就记录下来,事后画一张“代数—目标值”曲线,放进论文附录,评委看了会觉得你很懂算法行为。
第三条,决策变量维度是你最大的敌人。每多一个变量,搜索空间指数膨胀。所以预处理阶段的作物分类、地块聚类做的越狠,求解越轻松。我做过测试,28个作物不合并的版本跑差分进化,500代大概要2小时;合并成4大类后,同样的参数5分钟就收敛了。时间差距就是预处理省的。
这里顺便说一句,差分进化的思路和量化交易策略里用遗传算法寻优很相似,都是在一组连续参数空间里找稳健解,只不过目标函数从夏普比率换成了种植利润。如果你写过类似代码,迁移过来只需要换目标函数,算法框架完全复用。
5. C题踩坑记录:5个导致丢分的常见问题
5.1 地块编号错位导致结果比真实偏低很多
现象:求解出来的最优利润只有参考答案的一半,而且很多地块显示“未种植”。
原因:读入地块面积后,pandas的索引没有重置,后面合并时按索引对齐,导致第5块地的面积被放到了第6块地的行上。这种错位不会报错,但每一步计算都是错的。
解决:每次merge后立刻执行reset_index(drop=True),并在合并前用assert set(fields['id']) == set(crops['id'])检查地块编号集合是否完全一致。我吃了这个亏之后,所有数据处理脚本第一行就写断言。
5.2 “滞后价格”被当成当期价格
现象:产量最优但利润明显偏高,而且价格序列看起来像一个“回音”状,后面几年的价格总在重复前几年。
原因:附件里的滞后价格是上一年的价格,直接乘到今年产量里等于把未来信息泄漏到当前决策,这在时间序列里是大忌。
解决:把滞后列单独拆出来做时间特征,或者用crops['prev_price'] = crops.groupby('crop')['price'].shift(1)自己重新生成真正的上一年价格,这样可以确认原表里的lagged_price到底是不是shift之后的。这里可以再往下查:如果shift后和原lagged_price对不上,说明原表的滞后定义可能不是简单shift,要去看题目的说明,别默默忽略掉。
5.3 豆类5%约束漏写
现象:最优方案里豆类种植面积为0,但题目明确要求“至少5%的豆类面积”。
原因:只写了面积约束和轮作约束,忘了豆类占比。
解决:在一开始列约束清单时把三类约束先写成表格:总面积、轮作、豆类占比,建模前逐项打勾。宁可多写一个约束也不漏一个。类似的逻辑也适用于“连作间隔”,有的题目要求同一地块同一作物要隔2年才能再种,这可以加一个更长的互斥约束,漏掉的话结果看似合理但实际违规。
5.4 求解器卡死或超长时间不返回
现象:differential_evolution跑了半小时还在迭代,日志里目标函数值几乎不动,CPU占用100%。
原因:决策变量维度过大,比如把每个作物都单独建变量而不是合并类别;或者popsize设成了500。维度高了之后,差分进化的交叉和变异都要在整个高维空间里计算,每次目标函数评估都是一次全量利润计算,慢是必然的。
解决:先做作物分类压缩维度,把popsize降到30—60,并给maxiter设上限防止空跑。另外可以在目标函数里加一个缓存装饰器,同样的决策向量只计算一次利润,实测能省30%—40%时间。
5.5 目标函数符号搞反导致结果负值
现象:最后打印出的“最大利润”是负数,而且怎么调都不对。
原因:linprog求最小值,目标函数系数忘了取负号;或者成本在目标函数里被加而不是减。还有一个常见原因:单价是“元/斤”,产量是“斤/亩”,面积是“亩”,三者相乘得到“元”,结果却拿成本(元/亩)直接和它相减,量纲没问题,但如果价格是元/公斤,产量是斤/亩,就会差一个2倍系数。
解决:用一行注释大声写出量纲:利润 = 面积 × 产量 × 单价 - 面积 × 成本,并打印前几个变量的系数做抽查。我习惯在建模前先把单位统一成“斤、亩、元”三件套,所有数据在预处理阶段就完成换算。
6. 验证你的结果:从代码跑通到论文可信
6.1 用历史数据做回测:比纯理论推演更有说服力
C题给的是2024—2030年预测数据,不保证附历史数据。常见做法是把2024年数据当作“伪历史”,用2023年滞后价格预测2024年产量,再和2024年给定值比较,算MAPE。这个动作在论文里叫基准回测,评审看到你做了这一步,会认为你的模型不是拍脑袋。
from sklearn.linear_model import LinearRegression X = crops[['prev_price']].values y = crops['yield'].values model = LinearRegression().fit(X, y) pred = model.predict(X) mape = np.mean(np.abs((y - pred) / y)) * 100 print(f'MAPE: {mape:.2f}%')如果MAPE超过20%,说明光用上一年价格预测产量不够,需要加入土壤类型或季节特征。这段代码虽然简单,但它证明了你验证过预测环节的可用性。论文里写“R2=0.87,MAPE=12.3%”比写“模型效果良好”有说服力得多,这是参考华为杯数学建模优秀论文排版时的最深体会。
6.2 敏感性分析与蒙特卡洛:评审最爱的“稳健性”
把单价、产量按±10%、±20%扰动,各跑50次优化,统计利润均值和标准差。利润均值变化不大、标准差较小的方案就是稳健方案。这个分析不需要额外库,numpy正态分布扰动加一个循环即可。但有两个细节必须注意:扰动必须基于同一个随机种子,否则每轮扰动覆盖不同范围,统计结果不可比;每次优化也要固定seed,保证差分进化在同样的初始种群下搜索。
# 蒙特卡洛简例:扰动单价后重跑优化 base_prices = price_data.copy() profits = [] for i in range(50): noise = np.random.normal(1.0, 0.1, size=price_data.shape) price_data_perturbed = base_prices * noise # 重新求解,记录利润 profits.append(solve(price_data_perturbed)) print('利润均值:', np.mean(profits)) print('利润标准差:', np.std(profits))如果标准差超过均值的15%,说明你的种植策略对价格很敏感,要在论文里写一段对策,比如选择价格波动小的作物提高面积占比。这个分析做完,稳健性这块就抠不倒了。
6.3 图表表达:把决策变量画成热力图和柱状图
最后呈现给评审的图比代码重要。地块-作物-年份的种植方案,用热力图(地块为行、年份为列、颜色为作物类别)最直观,柱状图适合展示各作物面积占比。图注写到“图4 2024年各作物种植面积占比”而不是“结果图”,这是很多队伍丢印象分的地方。热力图用matplotlib的imshow或seaborn的heatmap都能画,但要注意地块编号顺序必须和建模时一致,否则图上的空间关系是错的。
import matplotlib.pyplot as plt import seaborn as sns # heatmap_data 形状为 (地块数, 年份数),值为作物类别编号 sns.heatmap(heatmap_data, cmap='tab20', cbar_kws={'label': '作物类别'}) plt.xlabel('年份') plt.ylabel('地块编号') plt.title('2024-2030年种植结构热力图') plt.savefig('cropping_heatmap.png', dpi=300, bbox_inches='tight')dpi设300以上,保存png而不是jpg,jpg压缩会糊掉色块边界,评审放大看会以为你数据有问题。字体用默认的DejaVu Sans就行,不要为了好看装中文字体,很多评委电脑上没装,打开你的图显示一堆方框,印象分直接归零。
我参赛时最亏的一笔就是没做敏感性分析,答辩时被评审一句“你这个方案在价格波动20%的情况下还成立吗”直接问住。后来每次做优化题,我都先把稳健性验证写好再回头调模型。代码跑通只是开始,能经得起追问的模型才是好模型,希望帮到你。
本文还有配套的精品资源,点击获取