☰
2024数学建模国赛C题复现:Python代码与数据清洗优化求解全流程
2026/10/12 8:01:22 网站建设 项目流程

简介:这份资源面向备战数学建模竞赛的高校学生与建模爱好者,聚焦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_name="2023种植情况") # 第一步:看字段和类型 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, index=land_info["地块编号"], columns=crops) 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(lower=0) # 检查面积异常 merged = plant_2023.merge(land_info, on="地块编号", how="left") abnormal = merged[merged["种植面积"] > merged["面积"]] print("面积异常记录:", len(abnormal)) # 如果异常记录少,逐条查;如果多,说明单位或编号有问题 # 价格填充:按作物分组用中位数填 plant_2023["价格"] = plant_2023.groupby("作物名称")["价格"].transform( lambda x: x.fillna(x.median()) )

clip(lower=0)把负销量截断为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( f"x_{land}_{crop}", lowBound=0, upBound=area_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(f"sales_{crop}", lowBound=0) # 线性化min:sales <= 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是示例值,实际要用题目附件里的数据。upBound=area_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(msg=1, timeLimit=300)) 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自带的求解器,msg=1打印求解日志,timeLimit=300限制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的变量设了lowBound=0,理论上不会负,但如果用了lpSum做减法,可能产生极小负值。解决方法是在输出时过滤,或者用round取整。

4.3 现象:总产量远大于预期销售量,但目标函数没惩罚

原因是目标函数只写了收入,没写超出部分的处理。如果题目要求超出部分降价,必须在目标函数里体现,否则模型会无限扩大种植。解决方法是加一个超出变量,乘以折扣价。

4.4 现象:轮作约束导致2024年无解

原因是2023年种植情况太单一,所有地块都种了豆类,2024年又不能种豆类,其他作物又不适宜。解决方法是把轮作从硬约束改成软约束,加惩罚项,允许少量违反。

4.5 现象:求解时间超过10分钟还没结果

原因是变量太多,或者约束太复杂。解决方法是先用小规模数据测试,确认逻辑无误后再跑全量;或者换OR-Tools的CP-SAT求解器,设置max_time_in_seconds=300。

5. 结果校验与论文呈现:让评委一眼看懂你的代码

5.1 用可视化校验结果合理性

跑出结果后别急着写论文,先画几张图看看。我一般画三张:各地块种植面积堆叠图、各作物总产量与预期销售量对比图、逐年利润变化图。堆叠图能看出地块是否被充分利用,对比图能看出哪些作物过剩或不足,利润图能看出趋势是否合理。

import matplotlib.pyplot as plt # 各地块种植面积堆叠图 pivot = result_df.pivot_table( index="地块", columns="作物", values="面积", aggfunc="sum" ).fillna(0) pivot.plot(kind="bar", stacked=True, figsize=(12, 6)) plt.title("各地块种植面积分布") plt.ylabel("面积(亩)") plt.tight_layout() plt.savefig("land_crop.png", dpi=150) # 产量与预期销售量对比 prod = result_df.groupby("作物")["面积"].sum() * pd.Series(yield_per_mu) compare = pd.DataFrame({ "产量": prod, "预期销售量": pd.Series(expected_sales) }).fillna(0) compare.plot(kind="bar", figsize=(10, 5)) plt.title("产量与预期销售量对比") plt.tight_layout() plt.savefig("prod_vs_sales.png", dpi=150)

pivot_table把结果转成地块×作物的矩阵,stacked=True画堆叠图。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)

这个脚本检查三件事:面积有没有超、产量有没有过剩、有没有作物被完全忽略。面积超限说明约束没写对,产量过剩说明目标函数需要加惩罚,作物没种说明约束太紧或变量没生成。每次跑完优化先跑这个,能省很多回头检查的时间。我自己的习惯是,不管多赶,结果出来先跑一遍这个脚本,确认没大问题再写论文。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询