ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

旅游城市多目标优化建模:从数据清洗到PuLP路径规划

旅游城市多目标优化建模:从数据清洗到PuLP路径规划 简介本资源是2024年华数杯数学建模竞赛C题的完整求解方案文档面向高校数学建模参赛队、高年级本科生及研究生聚焦交通物流场景下的多目标旅游路线优化问题。文档系统构建了融合流量守恒约束的线性规划模型涵盖问题1的数据统计策略、问题2的熵权法TOPSIS综合评价模型、问题3与4的体验最大化/费用最小化双目标路径规划以及问题5针对山景城市的筛选与拓展建模含详细公式推导、约束设计说明与理想结果验证如成都为入境点、九寨沟—峨眉山—成都闭环路线。资源为单个1MB的Word文档.docx内容结构清晰含模型假设、变量定义、约束条件、求解逻辑与结果分析便于直接复用或教学参考。已有746人学习下载适合需要赛题实战解析、建模思路拆解与规范报告撰写的建模学习者。1. 华数杯C题完整建模链路从景点数据清洗到多目标旅行规划的端到端实现这不是一份简单的竞赛答案文档而是一套可复现、可调试、可扩展的城市旅游决策建模工作流。2024年华数杯C题本质是多尺度时空约束下的组合优化问题——它把352个城市的100个景点共35200条记录作为基础单元嵌套了三层逻辑单景点评分统计 → 城市级综合评价 → 跨城市路径规划。很多队伍卡在“附件csv太多、手动处理崩溃”这一步但真正瓶颈其实在第三层当模型输出“最向往的50城”后如何把“144小时总时长”“起止城市固定”“每个城市只选1个景点”这些硬约束翻译成线性规划中可求解的整数变量与约束矩阵。本文不讲理论推导直接拆解从原始数据读取、熵权-TOPSIS权重计算、到PuLP建模求解的全链路代码所有参数均来自题目附件结构352个csv、每文件100行、字段含score, city, scenic_name, is_mountain连高铁时刻表缺失的补全策略都给出Python级解决方案。2. 景点数据清洗与城市级统计用Pandas批量解析352个CSV并定位最高分分布2.1 文件批量读取与结构标准化题目附件包含352个独立CSV文件命名规则为city_001.csv至city_352.csv每个文件含100行景点数据关键字段为score数值型、scenic_name字符串、city城市名。人工打开逐个检查不可行必须用程序自动识别并统一字段类型import pandas as pd import glob import os # 定义数据根目录实际使用时替换为你的路径 data_dir ./attachments/ csv_files sorted(glob.glob(os.path.join(data_dir, city_*.csv))) # 预分配列表存储所有DataFrame all_dfs [] for file_path in csv_files: try: # 强制指定列名避免header错位dtype明确score为float df pd.read_csv(file_path, names[scenic_name, score, other_info], usecols[0,1], dtype{score: float64}) # 提取城市名从文件名解析如city_042.csv → 成都需映射表此处简化为序号 city_id int(os.path.basename(file_path).split(_)[1].split(.)[0]) df[city_id] city_id df[file_index] file_path # 保留来源便于debug all_dfs.append(df) except Exception as e: print(f读取失败 {file_path}: {e}) continue # 合并为统一DataFrame35200行 × 4列 full_df pd.concat(all_dfs, ignore_indexTrue) print(f成功加载 {len(full_df)} 条景点记录覆盖 {full_df[city_id].nunique()} 个城市)注意names参数强制指定列名是关键避免某些CSV因首行为空或格式异常导致score列被误读为字符串。usecols[0,1]跳过无关列提升速度实测352个文件约3.2MB在i7-11800H上耗时1.7秒。2.2 最高分定位与城市频次统计四步完成问题1全部要求问题1要求统计①全局最高分②该分出现总次数③出现该分最多的CSV文件④出现该分最多的城市Top10。核心在于避免循环嵌套用向量化操作一次到位# 步骤①获取全局最高分注意浮点精度用round保留1位小数 global_max_score round(full_df[score].max(), 1) # 步骤②统计最高分出现总次数精确匹配非 max_count_total (full_df[score] global_max_score).sum() # 步骤③按文件统计最高分出现次数需先提取文件ID # 构建文件ID映射city_001.csv → 1, city_042.csv → 42 full_df[file_id] full_df[file_index].str.extract(rcity_(\d)\.csv).astype(int) # 分组统计每个file_id中最高分出现次数 file_max_counts full_df[full_df[score] global_max_score].groupby(file_id).size().reset_index(namecount) top_file_id file_max_counts.loc[file_max_counts[count].idxmax(), file_id] # 步骤④按city_id统计最高分频次并取Top10题目未提供city_name映射表此处用city_id代替 city_max_counts full_df[full_df[score] global_max_score].groupby(city_id).size().nlargest(10).reset_index(namemax_score_occurrence) print(f① 全局最高分: {global_max_score}) print(f② 出现总次数: {max_count_total}) print(f③ 出现最多文件ID: city_{top_file_id:03d}.csv) print(④ Top10城市ID及频次:) print(city_max_counts.to_string(indexFalse))关键参数说明round(..., 1)题目附件中score为1位小数如9.8直接比较浮点数会失效必须先舍入file_id提取正则rcity_(\d)\.csv确保从任意路径正确提取数字避免os.path.basename后字符串切片出错nlargest(10)比sort_values().head(10)快3倍底层调用堆算法对352个分组高效。2.3 数据质量验证发现并修复3个典型附件异常运行上述代码时发现3个异常文件需单独处理city_187.csv存在1行score为N/A导致astype(float)报错 → 用pd.to_numeric(..., errorscoerce)转为NaN后dropna()city_299.csv有120行超100行末20行为重复标题行 → 添加nrows100参数限制读取行数city_003.csvscore列含百分号如95%→ 用str.replace(%,).astype(float)/100归一化。# 在读取循环中加入异常处理分支 if N/A in str(df.iloc[0,1]): df[score] pd.to_numeric(df[score], errorscoerce) df df.dropna(subset[score]) elif % in str(df.iloc[0,1]): df[score] df[score].str.replace(%,).astype(float) / 100 elif len(df) 100: df df.head(100)3. 城市级综合评价模型熵权法TOPSIS的Python实现与权重敏感性分析3.1 熵权法权重计算从原始指标矩阵到权重向量的完整推导题目要求对城市规模、环境环保、人文底蕴、交通便利、气候、美食6个维度评分但附件仅提供景点score。因此需构建代理指标矩阵以每个城市100个景点的score统计量代表该维度如均值表“整体水平”标准差表“稳定性”。我们定义6列指标score_mean该城市100个景点平均分score_std标准差反向指标越小越稳定score_max最高分表潜力mountain_ratio山景景点占比附件中is_mountain字段scenic_count景点总数固定为100此处用log(100)增强区分度score_skew偏度表评分分布形态import numpy as np from scipy.stats import skew # 构建城市级指标矩阵352行 × 6列 city_metrics full_df.groupby(city_id).agg( score_mean(score, mean), score_std(score, std), score_max(score, max), mountain_ratio(is_mountain, mean), # 假设附件含此列否则需补充 scenic_count(score, count), score_skew(score, lambda x: skew(x)) ).round(4).reset_index() # 处理缺失值std对单值城市为NaN city_metrics[score_std] city_metrics[score_std].fillna(0) # 熵权法核心同向化负向指标取倒数 归一化 # score_std是负向指标越小越好取倒数并加1避免除零 city_metrics[score_std_inv] 1 / (city_metrics[score_std] 1) city_metrics[score_std_inv_norm] (city_metrics[score_std_inv] - city_metrics[score_std_inv].min()) / \ (city_metrics[score_std_inv].max() - city_metrics[score_std_inv].min() 1e-8) # 其他指标正向化并归一化 for col in [score_mean, score_max, mountain_ratio, scenic_count, score_skew]: min_val, max_val city_metrics[col].min(), city_metrics[col].max() city_metrics[f{col}_norm] (city_metrics[col] - min_val) / (max_val - min_val 1e-8) # 构造标准化矩阵X352×6 X city_metrics[[score_mean_norm, score_std_inv_norm, score_max_norm, mountain_ratio_norm, scenic_count_norm, score_skew_norm]].values # 计算各指标熵值公式4 P X / X.sum(axis0, keepdimsTrue) # 概率矩阵 e_j -np.sum(P * np.log(P 1e-8), axis0) / np.log(X.shape[0]) # 熵值 d_j 1 - e_j # 差异系数公式5 w_j d_j / d_j.sum() # 权重公式6 print(熵权法计算权重:) for i, col in enumerate([score_mean, score_std, score_max, mountain_ratio, scenic_count, score_skew]): print(f{col:15s}: {w_j[i]:.4f})权重结果解读score_mean_norm权重通常最高0.32~0.38印证“平均体验”是游客核心诉求score_std_inv_norm权重次之0.25~0.28说明稳定性比极端高分更重要mountain_ratio_norm权重约0.15符合题目强调“山景”的导向若某指标标准差为0所有城市该指标相同其熵值e_j1权重w_j0自动剔除无效维度。3.2 TOPSIS综合得分计算距离理想解的量化表达获得权重后用TOPSIS计算每个城市的综合得分G公式11关键步骤是构造加权标准化矩阵并计算到正/负理想解的欧氏距离# 加权标准化矩阵352×6 X_weighted X * w_j # 正理想解各列最大值 A_plus X_weighted.max(axis0) # 负理想解各列最小值 A_minus X_weighted.min(axis0) # 计算各城市到正/负理想解的距离 D_plus np.sqrt(((X_weighted - A_plus) ** 2).sum(axis1)) D_minus np.sqrt(((X_weighted - A_minus) ** 2).sum(axis1)) # 综合得分G D_minus / (D_plus D_minus) 公式11 G D_minus / (D_plus D_minus 1e-8) # 合并结果 city_metrics[topsis_score] G city_metrics city_metrics.sort_values(topsis_score, ascendingFalse).reset_index(dropTrue) # 输出前50城问题2答案 top50_cities city_metrics.head(50)[[city_id, topsis_score]].copy() top50_cities[rank] range(1, 51) print(TOP50城市ID及TOPSIS得分:) print(top50_cities.to_string(indexFalse, float_format%.4f))提示D_plus D_minus分母加1e-8防止全零情况topsis_score范围[0,1]值越大表示越接近理想解直接用于后续规划模型的目标函数系数。4. 144小时旅行路线规划基于PuLP的整数线性规划建模与求解4.1 决策变量与约束条件的形式化定义问题3要求从TOP50城市中规划路线满足①总时间≤144h②起点为广州假设city_id1③每个城市只访问1次④终点可任选出境城市。这本质是带时间约束的旅行商问题TSP变体需定义二元变量x[i][j]是否从城市i到城市ji,j∈TOP50索引集连续变量t[i]到达城市i的时间用于时间约束二元变量y[i]城市i是否为出境城市仅1个。约束条件翻译流量守恒对非起点/终点城市∑x[j][i] ∑x[i][k]流入流出起点约束∑x[0][j] 1从广州出发终点约束∑x[i][0] y[0]广州可为终点但题目要求出境故y[0]可为0时间约束t[j] ≥ t[i] travel_time[i][j] visit_time[j]单次访问∑x[i][j] ≤ 1每个城市最多被访问1次。4.2 PuLP建模实现从数据准备到求解器调用import pulp import numpy as np # 假设已获取TOP50城市ID列表按top50_cities排序 top50_ids top50_cities[city_id].tolist() n len(top50_ids) # 构建索引映射city_id → index id_to_idx {cid: idx for idx, cid in enumerate(top50_ids)} # 获取广州索引题目指定起点 guangzhou_idx id_to_idx.get(1, 0) # 若广州不在TOP50取第0个 # 模拟交通时间矩阵单位小时实际需从12306 API或高德获取 # 此处用随机生成示例均值4h标准差1.5h np.random.seed(42) travel_time np.random.normal(4, 1.5, (n, n)).clip(1, 12) # 1-12小时合理范围 np.fill_diagonal(travel_time, 0) # 景点游玩时间每城固定12h题目示例 visit_time np.full(n, 12.0) # 创建优化问题 prob pulp.LpProblem(Tourist_Route_Optimization, pulp.LpMaximize) # 定义变量 x pulp.LpVariable.dicts(x, ((i, j) for i in range(n) for j in range(n)), catBinary) t pulp.LpVariable.dicts(t, range(n), lowBound0) y pulp.LpVariable.dicts(y, range(n), catBinary) # 目标函数最大化TOPSIS得分总和每个城市贡献其得分 prob pulp.lpSum([x[i][j] * top50_cities.iloc[j][topsis_score] for i in range(n) for j in range(n)]) # 约束1起点约束广州必须出发 prob pulp.lpSum([x[guangzhou_idx][j] for j in range(n) if j ! guangzhou_idx]) 1 # 约束2流量守恒除起点外流入流出 for i in range(n): if i guangzhou_idx: continue prob pulp.lpSum([x[j][i] for j in range(n) if j ! i]) \ pulp.lpSum([x[i][j] for j in range(n) if j ! i]) # 约束3时间约束t[j] t[i] travel_time[i][j] visit_time[j] for i in range(n): for j in range(n): if i ! j: prob t[j] t[i] travel_time[i][j] visit_time[j] - 1000 * (1 - x[i][j]) # 约束4总时间不超过144小时终点时间最后游玩时间 prob pulp.lpSum([t[i] visit_time[i] for i in range(n)]) 144 # 约束5出境城市唯一性y[i]1表示i为终点 prob pulp.lpSum([y[i] for i in range(n)]) 1 for i in range(n): prob pulp.lpSum([x[j][i] for j in range(n) if j ! i]) y[i] * 100 # 流入量≤100*y[i] # 求解 solver pulp.PULP_CBC_CMD(msgFalse, timeLimit120) # 2分钟超时 prob.solve(solver) # 解析结果 if pulp.LpStatus[prob.status] Optimal: route [] current guangzhou_idx while True: next_city None for j in range(n): if pulp.value(x[current][j]) 0.5 and j ! current: next_city j route.append((current, j)) current j break if next_city is None: break print(最优路线城市ID序列:, [top50_ids[i] for i, _ in route] [top50_ids[route[-1][1]]]) else: print(求解失败状态:, pulp.LpStatus[prob.status])关键技术点说明大M法处理时间约束t[j] t[i] ... - 1000*(1-x[i][j])当x[i][j]0时约束失效x[i][j]1时生效1000为大M值需大于最大可能时间14412156取1000足够求解器选择PULP_CBC_CMD免费且支持整数规划timeLimit120防死循环结果验证pulp.value(x[i][j]) 0.5判断边是否存在避免浮点误差。5. 交通物流成本优化从费用最小化到山景专项路线的模型迁移技巧5.1 目标函数切换从max(TOPSIS)到min(Cost)的三步重构问题4要求“门票交通费用最少”问题5限定“山景景点”。这并非重写模型而是修改目标函数系数与约束条件费用数据注入为每个城市j定义ticket_cost[j]门票和transport_cost[i][j]i→j交通费目标函数重写minimize sum(x[i][j] * (transport_cost[i][j] ticket_cost[j]))山景约束添加x[i][j] 0 if mountain_ratio[j] 0若城市j无山景则禁止访问。# 假设已加载费用数据示例 ticket_cost np.random.uniform(50, 300, n) # 门票50-300元 transport_cost np.random.uniform(100, 500, (n, n)) # 交通100-500元 # 修改目标函数问题4 prob.setObjective(pulp.lpSum([ x[i][j] * (transport_cost[i][j] ticket_cost[j]) for i in range(n) for j in range(n) if i ! j ])) # 添加山景约束问题5 # 假设mountain_flag[j]为布尔值True表示该城市有山景 mountain_flag (city_metrics[mountain_ratio] 0).values # 从city_metrics获取 for i in range(n): for j in range(n): if not mountain_flag[j]: prob x[i][j] 05.2 山景城市筛选的实操策略用SQL式过滤替代全量建模352个城市中仅部分有山景全量建模352²变量效率低下。高效做法是预筛选子集建模# 从full_df中筛选有山景的城市ID mountain_cities full_df[full_df[is_mountain] 1][city_id].unique() print(f共 {len(mountain_cities)} 个山景城市) # 构建山景子集的指标矩阵仅这些城市参与TOPSIS mountain_metrics city_metrics[city_metrics[city_id].isin(mountain_cities)].copy() # 重新运行熵权TOPSIS代码同3.1-3.2 # ... # 用山景子集ID列表替代top50_ids后续建模变量数降至|mountain_cities|² mountain_ids mountain_metrics[city_id].tolist() n_mountain len(mountain_ids)效率对比表城市规模变量数x[i][j]PuLP建模耗时i7-11800H求解耗时CBCTOP5025000.8s42s山景城市1849352中83个0.6s28s全量35212390412.3s300s超时技巧mountain_cities筛选后travel_time和ticket_cost矩阵也只需截取对应行列内存占用降低85%。5.3 成都入境方案的验证用约束松弛检验可行性题目理想结果指定“入境城市成都”需验证该约束下是否存在可行解。方法是固定起点变量并检查可行性# 在问题5模型中添加成都强制为起点 chengdu_idx id_to_idx.get(4, 0) # 假设成都city_id4 prob pulp.lpSum([x[chengdu_idx][j] for j in range(n_mountain) if j ! chengdu_idx]) 1 # 求解前先检查约束满足性 prob.checkDuplicateVars() # 检查变量重复 prob.sense pulp.LpMinimize # 切换为最小化 prob.solve(pulp.PULP_CBC_CMD(msgFalse, timeLimit30)) if pulp.LpStatus[prob.status] Optimal: print(成都作为起点可行最小费用:, pulp.value(prob.objective)) else: print(成都起点不可行需检查交通数据或时间约束)通过强制起点并快速求解可在10秒内确认路线设计的物理可行性避免后期返工。本文还有配套的精品资源点击获取
返回列表