数学建模竞赛C题实战:从数据清洗到优化建模的完整Python实现

发布时间:2026/8/26 7:38:16
数学建模竞赛C题实战:从数据清洗到优化建模的完整Python实现 1. 从“保姆级”到“实战级”一份数学建模竞赛的深度通关指南又到了一年一度的五一数学建模竞赛季对于很多初次参赛或者经验尚浅的同学来说C题往往像一座横亘在眼前的大山题目描述里那些复杂的现实问题、海量的数据和陌生的专业术语让人望而生畏。网上流传的“保姆级”教程很多但大多停留在“给代码、给数据”的层面真正能讲清楚“为什么这么做”、“遇到问题怎么办”的少之又少。今天我想从一个多次参与建模指导的“老手”角度和大家聊聊如何真正吃透一道建模赛题特别是像五一赛C题这种综合性强的题目。我们不仅要拿到“鱼”代码和数据更要学会“渔”分析思路和问题解决能力把“保姆级”的依赖升级为“实战级”的自信。五一数学建模竞赛的C题历来以贴近现实、数据驱动、模型综合而著称。它很少考察单一、经典的数学模型而是倾向于将一个真实的、略显“ messy ”杂乱的问题抛给你需要你从数据清洗开始到问题分析、模型构建、求解验证完成一个完整的数据分析或决策优化流程。这恰恰是数学建模的核心价值——用数学工具解决实际问题。因此面对C题我们的目标不是寻找一个“标准答案”而是构建一套“自洽的、有说服力的解决方案”。接下来我将以一次虚构但典型的C题为例融合了近年赛题特点拆解从破题到成文的完整逻辑链并附上可复现的Python代码框架和数据处理要点。2. 典型C题结构拆解你的战场地图是什么样在拿到题目具体描述前我们可以先预判一下C题的常见结构。这能帮助我们在阅读题目时快速抓取关键信息分配精力。一个典型的C题通常包含以下几个部分2.1 问题背景与数据描述这部分会讲述一个现实故事比如“城市共享单车的调度优化”、“新能源汽车充电桩的布局规划”、“疫情后旅游景区的客流预测”等。背景描述中会隐含问题的边界、约束条件和优化目标。同时组委会会提供一批数据可能是CSV、Excel或TXT格式。数据往往具有以下特征不完全干净存在缺失值、异常值、格式不一致如日期格式混乱等问题。多源异构可能包含数值型数据如温度、销量、类别型数据如天气类型、区域编号、时间序列数据按小时或天的记录甚至文本描述。规模适中通常不是“大数据”但足以考验你的数据处理能力行数可能在几千到几十万条。2.2 具体任务问题这是题目的核心一般会分解为3-4个子问题问题之间通常有递进关系。问题一数据预处理与描述性分析。要求你对提供的数据进行清洗、转换和初步分析绘制关键图表揭示数据的基本规律和特征。这是整个建模的基础也是评阅时第一眼会看的部分。问题二基础模型建立与分析。针对某个具体方面建立一个相对基础的数学模型。例如建立一个预测模型如线性回归、时间序列或一个分类模型如逻辑回归、决策树并进行简单的分析和解释。问题三综合/优化模型建立与求解。这是题目的难点和重点。通常需要你建立一个更复杂的模型如多目标优化模型、动态规划模型、复杂的机器学习模型如XGBoost、LSTM或仿真模型。你需要设计或利用算法如遗传算法、模拟退火进行求解并对结果进行深入讨论。问题四模型应用、扩展或敏感性分析。基于前面建立的模型进行情景分析“如果…会怎样”、提出决策建议或讨论模型的稳健性参数变化对结果的影响。2.3 提交要求明确要求提交论文通常为PDF和可能的数据处理结果、源代码。论文的规范性、逻辑性和可读性其重要性不亚于模型本身。理解了这个结构我们在读题时就能像看地图一样知道每个部分对应战场的哪个区域该如何部署兵力时间和脑力。3. 实战推演以“共享单车调度优化”为例假设今年C题是关于“城市共享单车时空分布优化”。题目提供了过去一个月内城市各站点每小时的借车、还车记录以及站点位置信息、天气数据等。3.1 问题一数据预处理与探索性分析EDA这是你的第一步也是建立信心的关键。代码不一定要炫技但一定要稳健、清晰。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from datetime import datetime # 1. 数据读取与初窥 df_transaction pd.read_csv(bike_transactions.csv) # 交易记录 df_station pd.read_csv(station_info.csv) # 站点信息 df_weather pd.read_csv(weather_data.csv) # 天气数据 print(交易数据形状:, df_transaction.shape) print(交易数据列信息:\n, df_transaction.info()) print(交易数据前5行:\n, df_transaction.head()) # 2. 数据清洗 # 处理缺失值 print(缺失值统计:\n, df_transaction.isnull().sum()) # 对于关键字段如站点ID、时间的缺失行考虑删除 df_transaction_clean df_transaction.dropna(subset[station_id, timestamp]) # 对于数值型字段如骑行时长的缺失可用中位数或均值填充但要谨慎并说明理由 # df_transaction_clean[duration].fillna(df_transaction_clean[duration].median(), inplaceTrue) # 处理时间字段 df_transaction_clean[timestamp] pd.to_datetime(df_transaction_clean[timestamp]) df_transaction_clean[hour] df_transaction_clean[timestamp].dt.hour df_transaction_clean[day_of_week] df_transaction_clean[timestamp].dt.dayofweek # 周一0 df_transaction_clean[is_weekend] df_transaction_clean[day_of_week].apply(lambda x: 1 if x 5 else 0) # 处理异常值例如骑行时间超过24小时或为负数的记录 df_transaction_clean df_transaction_clean[(df_transaction_clean[duration] 0) (df_transaction_clean[duration] 24*60)] # 3. 数据合并如果需要 # 将天气数据按日期小时合并到交易数据中 df_weather[timestamp] pd.to_datetime(df_weather[date] df_weather[hour].astype(str) :00:00) df_merged pd.merge(df_transaction_clean, df_weather[[timestamp, temperature, weather_type]], ontimestamp, howleft) # 再将站点信息合并进来 df_merged pd.merge(df_merged, df_station, onstation_id, howleft) # 4. 探索性数据分析EDA # 绘制核心变量的分布 fig, axes plt.subplots(2, 2, figsize(14, 10)) # 每日总借车量趋势 daily_rentals df_merged.groupby(df_merged[timestamp].dt.date)[rentals].sum() axes[0, 0].plot(daily_rentals.index, daily_rentals.values) axes[0, 0].set_title(Daily Total Rentals Trend) axes[0, 0].set_xlabel(Date) axes[0, 0].set_ylabel(Rentals) axes[0, 0].tick_params(axisx, rotation45) # 一天内各小时平均借车量小时级规律 hourly_pattern df_merged.groupby(hour)[rentals].mean() axes[0, 1].bar(hourly_pattern.index, hourly_pattern.values) axes[0, 1].set_title(Average Hourly Rentals) axes[0, 1].set_xlabel(Hour of Day) axes[0, 1].set_ylabel(Average Rentals) # 不同天气下的平均借车量 weather_effect df_merged.groupby(weather_type)[rentals].mean().sort_values(ascendingFalse) axes[1, 0].bar(weather_effect.index, weather_effect.values) axes[1, 0].set_title(Average Rentals by Weather Type) axes[1, 0].set_xlabel(Weather Type) axes[1, 0].set_ylabel(Average Rentals) axes[1, 0].tick_params(axisx, rotation45) # 站点借还车量Top10 station_flow df_merged.groupby(station_id).agg({rentals:sum, returns:sum}).sum(axis1).sort_values(ascendingFalse).head(10) axes[1, 1].bar(station_flow.index.astype(str), station_flow.values) axes[1, 1].set_title(Top 10 Stations by Total Flow (RentalsReturns)) axes[1, 1].set_xlabel(Station ID) axes[1, 1].set_ylabel(Total Flow) axes[1, 1].tick_params(axisx, rotation45) plt.tight_layout() plt.savefig(eda_plots.png, dpi300) # 保存图片用于论文 plt.show() # 输出关键统计量 print(数据清洗后形状:, df_merged.shape) print(描述性统计:\n, df_merged[[rentals, returns, duration, temperature]].describe())注意EDA的核心是“探索”和“提问”。每画一张图都要问自己这个图表说明了什么规律对后续建模有什么启示例如从小时规律图中发现早晚高峰那么预测模型就必须能捕捉这种周期性。把这些观察和思考写在论文里。3.2 问题二建立单车需求预测模型问题二可能要求预测未来一天各站点每小时的借车需求量。这是一个经典的时间序列预测问题但结合了空间站点属性。一个稳妥的策略是对每个重要站点或站点聚类分别建立预测模型。为什么选择分别建模因为不同站点的需求模式差异巨大居民区、商业区、交通枢纽。混合建模容易丢失个性特征。模型选型对于初学者可以从SARIMA季节性自回归移动平均模型开始因为它能很好地处理时间序列的趋势和季节性。对于更复杂的模式可以尝试ProphetFacebook开源或轻量级的机器学习模型如LightGBM将小时、星期几、天气等作为特征。# 以单个站点为例使用Prophet进行预测需先安装 fbprophet from prophet import Prophet # 准备某个站点例如 station_id 1001的历史需求数据 station_data df_merged[df_merged[station_id] 1001][[timestamp, rentals]].copy() station_data station_data.rename(columns{timestamp: ds, rentals: y}) # 按小时聚合如果数据已经是小时级则无需此步 station_data station_data.resample(H, onds).sum().reset_index() # 划分训练集例如前28天和测试集最后1天 train station_data.iloc[:-24] # 假设最后24小时是测试集 test station_data.iloc[-24:] # 创建并拟合Prophet模型 model Prophet( yearly_seasonalityFalse, # 数据只有一个月不考虑年季节 weekly_seasonalityTrue, daily_seasonalityTrue, # 小时数据考虑天内的季节模式 seasonality_modemultiplicative # 根据数据特点选择加法或乘法 ) # 可以添加额外的回归因子如天气需要对齐时间 # future model.make_future_dataframe(periods24, freqH) # future pd.merge(future, weather_data, onds, howleft) # model.add_regressor(temperature) model.fit(train) # 预测未来24小时 future model.make_future_dataframe(periods24, freqH) forecast model.predict(future) # 可视化预测结果 fig model.plot(forecast) plt.title(fDemand Forecast for Station 1001) plt.show() # 评估预测精度在测试集上 from sklearn.metrics import mean_absolute_error, mean_squared_error predicted forecast.iloc[-24:][yhat].values actual test[y].values mae mean_absolute_error(actual, predicted) rmse np.sqrt(mean_squared_error(actual, predicted)) print(fStation 1001 - MAE: {mae:.2f}, RMSE: {rmse:.2f})实操心得Prophet对缺失值和异常值有一定鲁棒性且自带可视化能快速给出不错的结果非常适合竞赛时间紧的场景。但要注意对于几百个站点都跑一遍Prophet可能较慢此时可以考虑对站点进行聚类如K-Means对每一类站点训练一个共享模型或使用更高效的全局模型如DeepAR、TFT时序预测Transformer但这些模型实现更复杂。在论文中清晰说明你的选型理由和权衡过程。3.3 问题三构建单车调度优化模型这是最体现建模功力的部分。问题可能定义为如何在每天开始前调度卡车将单车从富余站点运往短缺站点以最小化总调度成本或距离并满足各站点的预测需求同时考虑卡车容量、行驶时间等约束。这本质上是一个带时间窗的车辆路径问题VRPTW或多商品网络流问题的变体。我们可以将其建模为一个混合整数线性规划MILP问题。模型抽象决策变量x_{ijk}二进制变量表示卡车k是否从站点i行驶到站点j。y_{ik}整数变量表示卡车k在站点i装载或卸载的单车数量正为装负为卸。目标函数最小化所有卡车的总行驶距离或时间。约束条件每个站点除车场的净流出量等于其需求预测需求量减去初始库存。卡车容量限制任意时刻卡车上的单车数量不能超过其容量。流量平衡卡车进入一个站点必须离开该站点除起点和终点。时间窗约束如果有时效要求站点服务必须在某个时间范围内完成。二进制变量和整数变量的定义域约束。求解策略对于中小规模问题几十个站点几辆车可以使用优化求解器如PuLP调用CBC、GLPK或ortools在有限时间内求精确解或优质可行解。对于大规模问题则需要设计启发式算法如遗传算法GA、模拟退火SA或大规模邻域搜索LNS。# 使用 ortools 求解一个简化版VRP的示例框架 from ortools.constraint_solver import routing_enums_pb2 from ortools.constraint_solver import pywrapcp def create_data_model(): 创建问题数据 data {} # 假设有5个站点0是车场距离矩阵 data[distance_matrix] [ [0, 10, 15, 20, 25], [10, 0, 35, 25, 30], [15, 35, 0, 30, 20], [20, 25, 30, 0, 15], [25, 30, 20, 15, 0] ] # 每个站点的需求正表示需要送车负表示需要收车 data[demands] [0, 5, -3, 7, -4] # 车场需求为0 data[vehicle_capacities] [10, 10] # 两辆车容量均为10 data[num_vehicles] 2 data[depot] 0 # 车场索引 return data def solve_vrp_with_capacity(): data create_data_model() manager pywrapcp.RoutingIndexManager(len(data[distance_matrix]), data[num_vehicles], data[depot]) routing pywrapcp.RoutingModel(manager) # 定义距离回调函数 def distance_callback(from_index, to_index): from_node manager.IndexToNode(from_index) to_node manager.IndexToNode(to_index) return data[distance_matrix][from_node][to_node] transit_callback_index routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) # 添加容量约束 def demand_callback(from_index): from_node manager.IndexToNode(from_index) return data[demands][from_node] demand_callback_index routing.RegisterUnaryTransitCallback(demand_callback) routing.AddDimensionWithVehicleCapacity( demand_callback_index, 0, # null capacity slack data[vehicle_capacities], # vehicle maximum capacities True, # start cumul to zero Capacity ) # 设置搜索参数 search_parameters pywrapcp.DefaultRoutingSearchParameters() search_parameters.first_solution_strategy ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) search_parameters.local_search_metaheuristic ( routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) search_parameters.time_limit.seconds 30 # 求解 solution routing.SolveWithParameters(search_parameters) # 打印结果 if solution: total_distance 0 total_load 0 for vehicle_id in range(data[num_vehicles]): index routing.Start(vehicle_id) plan_output fRoute for vehicle {vehicle_id}:\n route_distance 0 route_load 0 while not routing.IsEnd(index): node_index manager.IndexToNode(index) route_load data[demands][node_index] plan_output f {node_index} Load({route_load}) - previous_index index index solution.Value(routing.NextVar(index)) route_distance routing.GetArcCostForVehicle( previous_index, index, vehicle_id) plan_output f {manager.IndexToNode(index)} Load({route_load})\n plan_output fDistance of the route: {route_distance}m\n plan_output fLoad of the route: {route_load}\n print(plan_output) total_distance route_distance total_load route_load print(fTotal distance of all routes: {total_distance}m) print(fTotal load of all routes: {total_load}) else: print(No solution found!) if __name__ __main__: solve_vrp_with_capacity()踩坑实录直接套用经典VRP模型到调度问题最容易忽略的是时间维度。调度是在一天内分时段发生的而经典VRP通常假设所有任务同时开始。我们的问题更接近“多时段车辆路径问题”或需要结合网络流模型。一个更合理的简化是将一天划分为几个时段如早高峰前、午间、晚高峰前在每个时段内求解一个静态的VRP并将上一个时段末的站点库存状态作为下一个时段的初始状态。在论文中必须清晰地阐述你对时间维度的处理方式这是模型合理性的关键。3.4 问题四策略建议与敏感性分析基于问题三的优化结果你可以提出具体的调度方案如每辆卡车的具体路径和各站点的装卸货量。此外敏感性分析能极大提升论文的深度。敏感性分析可以做些什么需求预测误差的影响将预测的需求上下浮动10%重新运行优化模型观察总调度成本的变化。这能检验你的调度方案对预测误差的鲁棒性。关键参数分析改变卡车容量、单次调度成本等参数分析它们对最优解的影响。例如你可能会发现当卡车容量增加到某个值后总成本下降不再明显这可以为采购决策提供依据。不同优化目标的对比除了最小化总距离尝试最小化最大单辆车行驶距离均衡负载或最小化用户平均等待时间需引入排队模型对比不同目标下的调度方案差异。# 一个简单的敏感性分析示例分析需求波动对总成本的影响 def sensitivity_analysis_demand(base_demands, fluctuation_range0.1, steps5): 分析需求在[1-fluctuation_range, 1fluctuation_range]范围内波动时最优成本的变化 results [] for scale in np.linspace(1-fluctuation_range, 1fluctuation_range, steps): scaled_demands [int(d * scale) for d in base_demands] # 这里需要调用你的优化求解函数传入 scaled_demands # total_cost solve_optimization_model(scaled_demands, other_params...) # 为了示例我们用一个简单的模拟成本代替 simulated_cost sum(abs(d) for d in scaled_demands) * 1.5 # 模拟成本与总需求绝对值成正比 results.append((scale, simulated_cost)) print(fDemand Scale: {scale:.2f}, Simulated Total Cost: {simulated_cost:.2f}) # 绘制敏感性分析图 scales, costs zip(*results) plt.figure(figsize(8,5)) plt.plot(scales, costs, bo-, linewidth2) plt.xlabel(Demand Scale Factor) plt.ylabel(Total Scheduling Cost (Simulated)) plt.title(Sensitivity Analysis: Impact of Demand Fluctuation on Cost) plt.grid(True, alpha0.3) plt.savefig(sensitivity_demand.png, dpi300) plt.show() return results # 假设 base_demands 是各站点的净需求列表来自问题二预测 base_demands [0, 5, -3, 7, -4] sensitivity_analysis_demand(base_demands)在论文中展示这些分析结果并给出管理启示“我们的模型在需求预测误差±10%范围内表现稳定总成本波动小于5%说明方案具有较好的鲁棒性。建议运营方优先考虑增加核心区域站点的车辆容量因为敏感性分析显示这是降低总成本最有效的边际改进方向。”4. 论文写作与代码整合你的最终武器模型建得好还要论文讲得好。数学建模竞赛是“建模”竞赛更是“论文”竞赛。4.1 论文结构骨架摘要重中之重用一段话概括整个工作。模板针对XX问题我们首先进行了数据预处理方法发现了XX规律进而建立了XX预测模型模型名精度达到XX在此基础上构建了以XX为目标的优化模型模型名采用XX算法求解得到了XX调度方案最后进行了敏感性分析发现XX。本文的特色在于XX。问题重述用自己的话简要复述问题不要照抄题目。模型假设与符号说明列出关键假设如“假设单车需求预测误差服从正态分布”并用表格清晰说明所有符号。数据分析与预处理展示EDA的核心图表和结论。模型建立与求解按问题顺序分别详细描述每个模型的数学形式目标函数、约束条件、求解方法和结果。公式要清晰编号。模型检验与敏感性分析展示模型评估指标如预测模型的MAE、RMSE和敏感性分析结果。模型评价与推广客观评价模型的优点和局限性并提出改进方向或推广到其他场景的可能性。参考文献规范引用。附录可以放核心代码关键部分非全部。4.2 代码整合与可复现性将你的所有代码组织在一个清晰的目录中例如/project_五一建模C题 /data # 存放原始数据和清洗后的数据 /src # 存放源代码 /1_eda_preprocessing.py /2_demand_forecasting.py /3_optimization_model.py /4_sensitivity_analysis.py /utils.py # 公共函数 /output # 存放生成的图表、结果文件 /paper # 存放论文LaTeX或Word源文件 README.md # 项目说明写明环境依赖Python 3.8, pandas, scikit-learn, ortools等和运行步骤在论文中提及关键算法时可以说明“详见附录代码文件src/3_optimization_model.py第XX-XX行”。这极大提升了工作的可信度和可复现性。5. 常见“天坑”与临场应对策略结合多年经验和网络热议点以下是新手最容易翻车的地方5.1 数据预处理不当Garbage in, garbage out坑盲目用均值填充所有缺失值不处理异常值导致模型被极端值带偏。应对仔细分析缺失模式随机缺失系统缺失。对于时间序列考虑用前向填充或插值对于类别变量用众数或单独设为“未知”类别。对于异常值结合业务判断骑行时间24小时可能真是长途用户也可能是错误记录采用箱线图或3σ原则识别并决定是修正、剔除还是保留。5.2 模型复杂度过高或过低坑一上来就搞深度学习LSTM预测结果数据量不够训练过度拟合或者只用简单线性回归无法捕捉复杂关系。应对遵循“奥卡姆剃刀”原则从简单模型开始。先试SARIMA/Prophet如果效果不佳再考虑LightGBM等树模型。在论文中展示模型对比过程哪怕是一个简单的对比表格说明你选择最终模型的理由。5.3 忽略模型的可解释性坑用了复杂的集成模型或神经网络预测效果不错但说不清为什么导致论文在“模型分析”部分空洞无物。应对即使使用“黑箱”模型也要尽力解释。对于树模型如LightGBM可以输出特征重要性图对于预测结果可以做残差分析检查是否还存在规律。在优化模型中可以分析影子价格对偶变量来理解约束的紧致程度。5.4 论文变成代码说明书坑通篇在讲“我们用了pandas的read_csv函数”“我们调用了sklearn的RandomForest”而没有讲清楚背后的数学原理和业务逻辑。应对论文的核心是“模型”不是“代码”。代码是工具论文要阐述的是你用工具构建的“数学结构”。多写公式多画示意图如调度网络图多进行逻辑推导。5.5 时间管理失控坑前两天都在纠结数据清洗和第一个简单模型最后一天通宵赶优化模型和论文结果漏洞百出。应对严格制定时间表。Day1完成选题、数据预处理、EDA和问题一。Day2完成问题二和问题三的核心建模与求解写出论文草稿。Day3完成问题四、所有分析、模型检验、全文润色、排版和检查。留出最后几个小时作为缓冲。数学建模竞赛是一场智力的马拉松更是项目管理和科学沟通的综合演练。面对C题稳住心态把握“从数据出发用模型说话以论文呈现”的主线将上述思路和代码作为你的脚手架结合具体题目灵活变通你一定能交出一份远超“保姆级”教程的、属于自己的精彩答卷。记住清晰的逻辑和完整的闭环比任何一个高深的模型都更重要。