2024数学建模国赛C题复现:Python代码与数据清洗优化求解全流程

发布时间:2026/10/12 8:01:37
2024数学建模国赛C题复现:Python代码与数据清洗优化求解全流程
简介这份资源面向备战数学建模竞赛的高校学生与建模爱好者聚焦2024年全国大学生数学建模竞赛C题提供完整的代码实现与配套数据可用于赛题复盘、算法学习与论文写作参考。压缩包共51个文件约2.4MB以17个Python脚本为核心覆盖线性规划、遗传算法、聚类分析、相关性分析与异常值检测等建模方法并配有9个xlsx与3个csv数据文件、4个xml工程配置及13张png可视化图表另含README说明文档便于快速理解整体思路与运行流程。资源按问题一至问题三分模块组织脚本命名清晰数据与结果文件对应完整适合边调试边对照学习。目前已有5961人学习下载读者可借此掌握从数据预处理、模型构建到结果可视化的完整链路并参考其中的排错与优化思路提升建模实战能力。1. 2024数学建模国赛C题代码和数据到底该怎么复现2024年数学建模国赛C题考完后后台被问得最多的一句话是“有没有能直接跑的代码和数据”我特别理解这种心情——三天三夜熬下来论文交了但心里清楚自己的代码是拼凑的想找一份靠谱的复现方案对照一下。C题历来是国赛里数据量最大、工程味最重的一道2024年也不例外题目围绕农作物种植策略展开涉及大量地块、作物、年份的排列组合本质是一个带约束的大规模优化问题。很多人卡在第一步数据怎么读、怎么清洗、怎么把业务约束翻译成代码里的约束条件。这篇文章不讲空话我把自己复现这类题目的完整路径拆开从数据读取到模型求解到结果校验每一步都给可抄的代码和参数说明。适合两类人一是刚打完比赛想复盘的学生二是准备2025年比赛想提前练手的队伍。代码用Python写求解器用PuLP和OR-Tools两条路线都讲数据格式按国赛常见的Excel附件来。2. 数据读取与清洗从Excel附件到可计算的结构2.1 先搞清楚C题的数据长什么样2024年C题的数据一般以两个Excel文件给出一个是地块与作物的基础信息表另一个是2023年的种植与销售数据。基础信息表里通常包含地块编号、地块类型平旱地、梯田、水浇地等、地块面积、是否适合种植某类作物。种植数据表里则是地块、作物、季节、面积、产量、销售量、价格这些字段。很多人拿到数据直接pd.read_excel然后就开始建模结果跑到一半发现地块类型对不上、面积单位不统一、有些作物在某些地块上根本不能种。我一般会先做三件事看字段类型、看缺失值、看业务约束的隐含条件。import pandas as pd import numpy as np # 读取两个核心表注意sheet_name按实际文件改 land_info pd.read_excel(2024C_附件1.xlsx, sheet_name地块信息) plant_2023 pd.read_excel(2024C_附件2.xlsx, sheet_name2023种植情况) # 第一步看字段和类型 print(land_info.dtypes) print(plant_2023.dtypes) # 第二步看缺失和唯一值 print(land_info.isnull().sum()) print(plant_2023.isnull().sum()) print(地块类型, land_info[地块类型].unique()) print(作物名称, plant_2023[作物名称].unique()) # 第三步统一面积单位国赛常见的是亩但有时混用公顷 # 这里假设原始单位是亩统一转成亩 land_info[面积] land_info[面积].astype(float) plant_2023[种植面积] plant_2023[种植面积].astype(float)这段代码的逻辑是先摸清数据底细再动手。dtypes能看出哪些字段被读成了object比如面积里混了“约”字就会变成字符串。isnull().sum()定位缺失C题里常见的是某些地块某年没种东西销售量为空这种空值不能直接填0要区分“没种”和“种了但没卖”。unique()看地块类型和作物名称是为了后面建约束时知道有哪些类别。参数上sheet_name一定要按实际文件改国赛附件经常多个sheet读错了后面全错。面积单位统一这步别省我见过有队伍把亩和公顷混着算最后总产量差了15倍论文里数据对不上直接降档。2.2 把业务约束翻译成数据表C题的核心约束通常包括地块类型决定可种作物、同一地块同一季节只能种一种作物、种植面积不能超过地块面积、轮作要求比如豆类不能连作、销售量不能超过产量。这些约束在代码里不能靠if-else硬写要整理成结构化的表。我一般会建三个辅助表可种植矩阵、地块面积字典、作物分类表。# 建可种植矩阵行是地块列是作物值1表示可种 land_types land_info[地块类型].unique() crops plant_2023[作物名称].unique() # 根据题目给的适宜种植规则手动填这里给示例 suitable { 平旱地: [小麦, 玉米, 谷子], 梯田: [小麦, 玉米, 豆类], 水浇地: [水稻, 小麦, 蔬菜], } # 实际要按题目附件里的适宜性表来别自己编 plant_matrix pd.DataFrame(0, indexland_info[地块编号], columnscrops) for _, row in land_info.iterrows(): for crop in suitable.get(row[地块类型], []): if crop in crops: plant_matrix.loc[row[地块编号], crop] 1 # 地块面积字典 area_dict dict(zip(land_info[地块编号], land_info[面积])) # 作物分类粮食、蔬菜、豆类等用于轮作约束 crop_type { 小麦: 粮食, 玉米: 粮食, 谷子: 粮食, 水稻: 粮食, 蔬菜: 蔬菜, 豆类: 豆类 }这段的关键是把题目里的文字规则变成0/1矩阵。suitable字典里的内容必须严格按题目附件来我这里是示例实际比赛时题目会给一张“地块类型-作物适宜性”表照着填就行。plant_matrix的索引是地块编号列是作物后面建优化模型时直接用它做约束如果plant_matrix.loc[地块, 作物] 0那这个地块就不能种这个作物。area_dict用于面积约束。crop_type用于轮作约束比如“豆类作物不能连续两年种在同一地块”就需要知道哪些作物属于豆类。这一步做完数据就从Excel变成了模型能吃的格式。2.3 缺失值和异常值的处理边界C题数据里最常见的异常有三种一是销售量为负二是种植面积大于地块面积三是价格突然为0。销售量为负一般是退货或数据录入错误我一般直接按0处理因为负销量在业务上没意义。种植面积大于地块面积说明数据有误要么是面积单位问题要么是地块编号对错了这种要回去查原始表不能硬改。价格为0可能是赠送或数据缺失如果该作物当年有销量但价格为0我会用该作物其他年份的均价填充如果完全没销量就保留0因为没种就没价格。# 处理销售量负值 plant_2023[销售量] plant_2023[销售量].clip(lower0) # 检查面积异常 merged plant_2023.merge(land_info, on地块编号, howleft) abnormal merged[merged[种植面积] merged[面积]] print(面积异常记录, len(abnormal)) # 如果异常记录少逐条查如果多说明单位或编号有问题 # 价格填充按作物分组用中位数填 plant_2023[价格] plant_2023.groupby(作物名称)[价格].transform( lambda x: x.fillna(x.median()) )clip(lower0)把负销量截断为0简单但有效。面积异常检查用merge后比较abnormal里如果有记录先看是不是单位问题比如地块面积是亩而种植面积是公顷那就统一单位如果单位没错那就是数据错误这种地块在建模时要么剔除要么按面积上限修正。价格填充用分组中位数而不是全局均值因为不同作物价格差异大全局均值会把蔬菜价格拉到粮食水平。这一步做完数据基本干净了可以进入建模。3. 优化模型构建从目标函数到约束条件3.1 目标函数怎么定利润最大还是产量最大C题的目标通常是“最大化收益”或“最大化利润”但题目里往往还隐含了“满足国家粮食安全要求”这类软约束。我一般把目标函数写成总销售收入减去总种植成本再根据题目要求加惩罚项。2024年C题的一个特点是考虑了“预期销售量”和“实际产量”的差异如果产量超过预期销售量多出的部分可能降价出售或滞销。这个逻辑必须在目标函数里体现否则模型会盲目扩大种植面积。import pulp # 假设有n个地块m种作物t年 # 决策变量x[i][j][k]表示第k年在地块i种作物j的面积 # 这里简化为单年多年只需加一层循环 prob pulp.LpProblem(C题_种植优化, pulp.LpMaximize) # 参数预期销售量、价格、成本 expected_sales plant_2023.groupby(作物名称)[销售量].sum().to_dict() price plant_2023.groupby(作物名称)[价格].mean().to_dict() cost_per_mu {小麦: 500, 玉米: 600, 水稻: 800, 蔬菜: 1200, 豆类: 400} # 决策变量 x {} for land in land_info[地块编号]: for crop in crops: if plant_matrix.loc[land, crop] 1: x[(land, crop)] pulp.LpVariable( fx_{land}_{crop}, lowBound0, upBoundarea_dict[land] ) # 辅助变量实际销售量 min(产量, 预期销售量) 超出部分*折扣 # 产量 面积 * 亩产量 yield_per_mu {小麦: 400, 玉米: 500, 水稻: 600, 蔬菜: 2000, 豆类: 300} sales {} for crop in crops: total_prod pulp.lpSum( x[(land, crop)] * yield_per_mu.get(crop, 0) for land in land_info[地块编号] if (land, crop) in x ) sales[crop] pulp.LpVariable(fsales_{crop}, lowBound0) # 线性化minsales total_prod, sales expected_sales prob sales[crop] total_prod prob sales[crop] expected_sales.get(crop, 0) # 目标函数收入 - 成本 revenue pulp.lpSum(sales[crop] * price.get(crop, 0) for crop in crops) total_cost pulp.lpSum( x[(land, crop)] * cost_per_mu.get(crop, 0) for (land, crop) in x ) prob revenue - total_cost这段代码的核心是把“销售量取产量和预期销售量的较小值”这个非线性关系线性化了。sales[crop] total_prod和sales[crop] expected_sales两个约束同时作用求解器会自动让sales取较小的那个。yield_per_mu和cost_per_mu是示例值实际要用题目附件里的数据。upBoundarea_dict[land]保证单个地块上种的面积不超过地块总面积。目标函数里没有加超出部分的折扣销售如果题目要求可以再加一个变量表示超出量乘以折扣价。参数上price用均值如果题目给了逐年价格要按年分别设。3.2 约束条件的代码实现面积、轮作、适宜性约束是C题最容易翻车的地方。我一般把约束分成硬约束和软约束。硬约束必须满足比如面积不能超、不适宜的地块不能种。软约束尽量满足比如轮作要求如果满足不了就加惩罚。代码里用prob 逐条加。# 硬约束1每个地块种植总面积不超过地块面积 for land in land_info[地块编号]: prob pulp.lpSum( x[(land, crop)] for crop in crops if (land, crop) in x ) area_dict[land] # 硬约束2不适宜的地块不能种已经在变量生成时排除了这里不用重复 # 硬约束3同一地块同一季节只能种一种作物如果题目有季节划分 # 假设每个地块一年只种一季那上面面积约束已经隐含了 # 如果分春夏秋需要按季节加约束 # 软约束轮作豆类不能连作 # 需要知道2023年种了什么 plant_2023_dict plant_2023.set_index([地块编号, 作物名称])[种植面积].to_dict() for land in land_info[地块编号]: for crop in crops: if crop_type.get(crop) 豆类 and (land, crop) in x: # 如果2023年该地块种了豆类2024年不能种 if (land, crop) in plant_2023_dict: prob x[(land, crop)] 0面积约束用lpSum把所有作物的面积加起来小于等于地块面积。轮作约束这里写的是“如果2023年种了豆类2024年不能种”实际题目可能要求“豆类三年内不能连作”那就需要记录前三年的种植情况逻辑类似。注意plant_2023_dict的键是地块和作物的组合如果2023年某地块没种豆类那这个键不存在约束就不加。软约束如果导致无解可以改成加惩罚项到目标函数比如prob -1000 * x[(land, crop)]让求解器尽量不违反。3.3 求解器选择PuLP和OR-Tools的差别PuLP适合线性模型语法简单但求解速度一般。OR-Tools的CP-SAT求解器对整数规划更快但语法稍复杂。C题如果变量规模在几千个以内PuLP够用如果地块和作物组合上万建议用OR-Tools。我一般先用PuLP写一版验证逻辑再用OR-Tools跑大规模。# PuLP求解 prob.solve(pulp.PULP_CBC_CMD(msg1, timeLimit300)) print(状态, pulp.LpStatus[prob.status]) print(目标值, pulp.value(prob.objective)) # 输出结果 result [] for (land, crop), var in x.items(): if var.varValue 0.01: result.append({ 地块: land, 作物: crop, 面积: var.varValue }) result_df pd.DataFrame(result) print(result_df.head(20))PULP_CBC_CMD是PuLP自带的求解器msg1打印求解日志timeLimit300限制5分钟防止跑太久。varValue 0.01过滤掉求解器产生的极小值这些值在业务上等于0。结果输出成DataFrame方便后续写论文。如果状态是Infeasible说明约束冲突要回去检查是不是某个地块被约束死了。OR-Tools的写法类似但变量定义用model.NewNumVar约束用model.Add求解用model.Maximize这里不展开核心逻辑一样。4. 避坑与排查复现C题时最容易翻车的5个地方4.1 现象求解器返回Infeasible但约束看起来没问题原因通常是某个地块的可种植作物集合为空或者面积约束和轮作约束冲突。比如某地块2023年种了豆类2024年题目要求必须轮作但该地块适宜种植的作物里只有豆类那就无解了。解决方法是先检查每个地块的可种植作物数量如果为0或1手动调整约束或放宽轮作要求。# 检查每个地块可种作物数 for land in land_info[地块编号]: count sum(1 for crop in crops if (land, crop) in x) if count 1: print(f地块{land}可种作物数{count}可能冲突)4.2 现象结果里某些作物种植面积为负原因一般是求解器精度问题或者约束写反了。PuLP的变量设了lowBound0理论上不会负但如果用了lpSum做减法可能产生极小负值。解决方法是在输出时过滤或者用round取整。4.3 现象总产量远大于预期销售量但目标函数没惩罚原因是目标函数只写了收入没写超出部分的处理。如果题目要求超出部分降价必须在目标函数里体现否则模型会无限扩大种植。解决方法是加一个超出变量乘以折扣价。4.4 现象轮作约束导致2024年无解原因是2023年种植情况太单一所有地块都种了豆类2024年又不能种豆类其他作物又不适宜。解决方法是把轮作从硬约束改成软约束加惩罚项允许少量违反。4.5 现象求解时间超过10分钟还没结果原因是变量太多或者约束太复杂。解决方法是先用小规模数据测试确认逻辑无误后再跑全量或者换OR-Tools的CP-SAT求解器设置max_time_in_seconds300。5. 结果校验与论文呈现让评委一眼看懂你的代码5.1 用可视化校验结果合理性跑出结果后别急着写论文先画几张图看看。我一般画三张各地块种植面积堆叠图、各作物总产量与预期销售量对比图、逐年利润变化图。堆叠图能看出地块是否被充分利用对比图能看出哪些作物过剩或不足利润图能看出趋势是否合理。import matplotlib.pyplot as plt # 各地块种植面积堆叠图 pivot result_df.pivot_table( index地块, columns作物, values面积, aggfuncsum ).fillna(0) pivot.plot(kindbar, stackedTrue, figsize(12, 6)) plt.title(各地块种植面积分布) plt.ylabel(面积亩) plt.tight_layout() plt.savefig(land_crop.png, dpi150) # 产量与预期销售量对比 prod result_df.groupby(作物)[面积].sum() * pd.Series(yield_per_mu) compare pd.DataFrame({ 产量: prod, 预期销售量: pd.Series(expected_sales) }).fillna(0) compare.plot(kindbar, figsize(10, 5)) plt.title(产量与预期销售量对比) plt.tight_layout() plt.savefig(prod_vs_sales.png, dpi150)pivot_table把结果转成地块×作物的矩阵stackedTrue画堆叠图。prod用面积乘以亩产量算总产量compare对比产量和预期销售量。如果某作物产量远大于预期说明模型需要调整目标函数或加约束。图保存成PNG论文里直接插入。5.2 论文里代码和数据的呈现技巧论文里的代码不要贴全部只贴核心片段比如目标函数和关键约束。数据不要贴原始Excel贴清洗后的统计表。我一般会在附录里放一张“参数设置表”列出所有用到的参数和来源比如亩产量、成本、价格注明是题目附件还是自己估算的。这样评委能快速判断你的模型是否合理。参数来源值说明亩产量附件2按作物取2023年均值种植成本附件1按作物题目给定预期销售量附件2按作物2023年总销量价格附件2按作物2023年均价这张表放在论文正文里比大段文字描述清楚得多。代码片段用等宽字体加注释说明每一步在做什么。最后结果校验部分要写清楚你检查了哪些异常、怎么处理的这能体现工程的严谨性。5.3 一个我常用的结果合理性快速检查脚本最后分享一个我每次跑完优化都会用的检查脚本三秒钟能看出结果有没有大问题。def check_result(result_df, land_info, expected_sales, yield_per_mu): # 检查1面积是否超限 area_used result_df.groupby(地块)[面积].sum() area_limit land_info.set_index(地块编号)[面积] over area_used[area_used area_limit 0.01] print(面积超限地块, over.to_dict()) # 检查2产量是否远大于预期 prod result_df.groupby(作物)[面积].sum() * pd.Series(yield_per_mu) for crop in prod.index: if prod[crop] expected_sales.get(crop, 0) * 1.5: print(f{crop}产量{prod[crop]:.0f}预期{expected_sales.get(crop,0):.0f}过剩) # 检查3是否有作物完全没种 planted set(result_df[作物]) all_crops set(expected_sales.keys()) print(未种植作物, all_crops - planted) check_result(result_df, land_info, expected_sales, yield_per_mu)这个脚本检查三件事面积有没有超、产量有没有过剩、有没有作物被完全忽略。面积超限说明约束没写对产量过剩说明目标函数需要加惩罚作物没种说明约束太紧或变量没生成。每次跑完优化先跑这个能省很多回头检查的时间。我自己的习惯是不管多赶结果出来先跑一遍这个脚本确认没大问题再写论文。希望帮到你。本文还有配套的精品资源点击获取