简介:昆明理工大学数学建模实验2报告以PDF格式提供,聚焦线性规划在广告投放决策中的应用。该实验基于一家广告公司的有限预算,在电视(白天/最佳时间)、无线电广播和杂志之间分配投放次数,需要满足总费用不超过800千元、女性顾客至少2000千名、电视白天至少3次且最佳时间至少2次、广播与杂志投放5至10次等约束,目标是最大化招徕顾客总数。报告完整展示从问题背景、符号说明、模型建立到MATLAB求解的流程,包含实验目的、环境、源程序、运行结果截图与结果分析,调用linprog函数设置目标系数、约束矩阵和边界,求得最优方案为白天电视3次、最佳时间电视2次、无线电广播10次、杂志广告10次,最大顾客数达10000千人。包体为单个PDF文件、158KB,内容紧凑,适合数学建模课程学生、线性规划初学者及MATLAB用户参考,既能理解建模思路与编程实现,也可作为实验报告范例。目前已有350人学习下载。
1. 数学建模+实验2.pdf:PDF里装的不只是论文,而是一条完整建模实验链路
拿到《数学建模+实验2.pdf》这份材料时,很多人的第一反应是找代码模板或现成答案。但真正决定一份实验报告分数的,是你能不能把题目拆成一个可执行的建模闭环:问题理解、数据读取、预处理、模型选择、参数调整、结果验证,最后用规范的论文格式呈现。这份PDF看起来是一次作业,实际是把数模竞赛中最完整的二次实验环节打包给你。它适合三类人:第一次参加数模竞赛的新手、正被实验课报告卡住的学生,以及想建立一套“建模SOP”来减少返工的熟手。
2. 建模实验第一步:把题目拆成能动手的实验方案
2.1 数模实验里的“实验”,和物理实验不是一回事
物理实验是验证一个已知的物理定律,实验步骤和标准结论往往写在教材里。数学建模里的“实验”完全不同:题目只给你一个问题描述和一批数据,没有人告诉你该用哪个模型,更没有标准答案可供比对。
我带的组里有个普遍误区:拿到题目就开跑代码。结果通常是要么花了几个小时写出一个跑不通的脚本,要么模型能出数但不知道下一步该干什么。实验2这类作业的设计意图,恰恰是让你完整走一遍“假设—建模—求解—验证—汇报”的链路,而不是只交一个数字。
所以在写第一行代码之前,我会强迫自己先回答三个问题:题目到底在问什么?数据能支撑什么样的答案?最终交付物是什么形态?这三个答案直接决定了你接下来三天的工作量。
2.2 用问题拆解表把题目转成变量、数据和交付物
把题目变成可执行任务,我用的工具是一张“问题拆解表”。不需要任何软件,一张纸或者一个表格就行。核心是把题面里的每一句要求,映射成变量、数据来源和输出产物。
以某高校课程里一个典型的实验2题目为例:题目给出某区域商品价格与销量的观测数据,要求分析价格对销量的影响,并给出量化结论。用拆解表处理后,问题变成这样:
| 题面语句 | 可量化变量 | 数据要求 | 交付物 |
|---|---|---|---|
| 价格对销量的影响 | 自变量:价格;因变量:销量 | 需要至少一组连续观测记录 | 回归方程、系数显著性、解释段 |
| 给出量化结论 | 弹性系数或边际效应 | 样本量不小于30,无严重缺失 | 结论段:价格每变动1单位销量变化多少 |
| 实验报告形式交付 | 所有计算可复现 | 代码、数据、说明齐全 | PDF报告+代码附件 |
这张表的价值在于:它把“分析价格对销量的影响”这种开放问题,变成了“跑回归、看系数、写结论”三个明确动作。学生照着表推进,至少不会出现模型做完了却不知道要写什么的尴尬。
2.3 方法选型:回归、规划、仿真为什么不能混着用
问题拆解完之后,下一步是选方法。很多新手最大的困惑是:看到数据里有几十个变量,就想着“用神经网络总没错”。但实验2的评分重心通常不在模型多高级,而在方法选得是否合理、假设是否说清楚。
我一般按题目类型做快速判断:题目问“是多少、什么关系”时,走回归或分类;问“怎么安排最优”时,走线性规划或整数规划;问“系统在什么条件下会怎样演变”时,才考虑仿真。把这三类方向混着用是最常见的翻车原因——见过有组把单纯的最优库存问题硬套回归模型,结果只能强行解释,结论没一个站得住。
方法选型不需要一次到位,但至少要选出方向,并在实验方案里写清楚理由。后续求解遇到困难再降级,比如从多项式回归退到线性回归,都比来回换方向要省时间。
2.4 实验方案模板:花30分钟写方案省3小时返工
方案模板我已经固定用了一年多,六个部分,每部分两三句话即可:
- 问题重述:用自己的话把题面转述一遍,确认理解正确;
- 假设清单:列出你为了让模型成立所做的假设,比如“销量不随时间周期波动”;
- 数据字典:说明每个字段的含义、单位、取值范围;
- 方法流程:按“读取—清洗—建模—验证—输出”写清楚每一步用什么方法;
- 预期输出:明确最终要得到哪些数字、图表和结论;
- 风险点:提前标出可能出问题的地方,比如数据缺失严重、多重共线性等。
计划本身用Markdown或Word都行,关键是写完后要能让你在两天后看着它继续干活,而不是靠记忆。实践下来,花30分钟写这个方案,通常能省下至少3小时的返工时间,这比任何建模技巧都值钱。
3. 数据准备与清洗:实验2里一半的翻车发生在这里
3.1 读入数据后的第一件事:核对结构而不是马上建模
数据读入是区分新手和老手的第一道分水岭。新手读入CSV后直接丢进模型;老手会在建模前先回答:数据有多少行、多少列,各列类型是什么,缺失值有多少,量纲差异大不大。这一步花了10分钟,但能避免后面两小时对着报错发呆。
我一般用一段固定的检查脚本起手:
import pandas as pd df = pd.read_csv("data.csv", encoding="gbk") print("数据维度:", df.shape) print("字段类型:\n", df.dtypes) print("缺失值统计:\n", df.isnull().sum())这段代码前三行分别解决三个问题:数据维度告诉你样本量够不够;字段类型帮你发现有没有把数值列读成字符串、有没有日期列被当成普通对象;缺失值统计直接决定了下一步清洗策略。encoding参数的坑最隐蔽,很多课程数据是用Excel导出的,编码常常是gbk而不是utf-8。如果读入时报错或出现乱码,优先换encoding参数比改数据文件更快,常见做法是依次试“utf-8”“gbk”“latin1”。
3.2 缺失值与异常值处理:先定边界,再写代码
缺失值处理没有固定规则,但有可用的经验边界。我常用的判断标准是:缺失率低于5%且缺失模式随机,直接删除对应行;缺失率在5%到30%之间,优先用中位数或众数填充;超过30%,要考虑这个特征是否还值得保留。
删除和填充的区别在于信息损失。随机删除少量样本影响很小,但强行填充大量缺失会让后续统计检验失真。填充代码如下:
# 数值列用中位数填充,避免被少数极端值带偏 num_cols = df.select_dtypes(include="number").columns df[num_cols] = df[num_cols].apply(lambda x: x.fillna(x.median())) # 类别列用众数填充 cat_cols = df.select_dtypes(include="object").columns df[cat_cols] = df[cat_cols].apply(lambda x: x.fillna(x.mode()[0] if len(x.mode()) > 0 else "unknown"))数值列用中位数而不是均值,是因为均值对异常值敏感,一个极端大数会把均值拉高,进而污染填充值。类别列则用众数,表示“大多数样本是什么就补什么”。之后要做异常值检查,常用的IQR规则是:
Q1, Q3 = df["sales"].quantile([0.25, 0.75]) iqr = Q3 - Q1 lower, upper = Q1 - 1.5 * iqr, Q3 + 1.5 * iqr df = df[(df["sales"] >= lower) & (df["sales"] <= upper)]1.5倍IQR是一个经验值,不是数学定理。如果业务上确认某类波动是正常的,比如节假日销量暴增,就应该把倍数放宽到2或3,而不是机械地删掉真实信号。实际处理时要先看业务含义,再动手删数据。
3.3 归一化与特征构造:哪些处理必须做,哪些纯属玄学
归一化是另一个容易走极端的环节。线性回归、逻辑回归、以及带正则项的模型,对特征量纲非常敏感;而决策树、随机森林这类树模型对量纲不敏感,做不做归一化影响不大。先想清楚自己用哪类模型,再决定要不要归一化。
from sklearn.preprocessing import MinMaxScaler, StandardScaler # MinMaxScaler 把数据压到 [0,1],适合分布无明显长尾的变量 minmax_scaler = MinMaxScaler() x_minmax = minmax_scaler.fit_transform(x) # StandardScaler 把数据变为均值0、方差1,适合后续配合正则化 std_scaler = StandardScaler() x_std = std_scaler.fit_transform(x)两者选择的逻辑不复杂:如果特征分布接近均匀或正态,用StandardScaler更稳;如果特征取值范围固定、不希望引入负数,用MinMaxScaler更合适。注意fit_transform只能用在训练集上,测试集的变换要用训练集拟合好的scaler去transform,一旦混用就会造成信息泄漏,这是血泪经验。
特征构造则容易陷入“越多越好”的误区。盲目加平方项、交互项,大概率得到一个训练集完美、测试集垮掉的模型。我一般只做从业务含义出发的特征,比如价格本身就带有“价格弹性”的业务含义,构造价格对数项是有理论依据的;而把两个无关变量强行相乘,就是玄学。
4. 最小二乘回归的建模求解:从模型设定到参数调优
4.1 模型设定:为什么先跑线性模型再用正则加约束
很多实验2的题目落到最后就是一个回归问题。面对回归,我坚持“先用最简单模型跑通,再逐步加复杂度”的原则。原因有二:第一,线性模型的可解释性强,每个系数都能直接对应题目里的业务变量,方便在报告里写分析;第二,简单模型出问题容易排查,至少能判断是数据问题还是方法问题。
模型设定阶段要明确三件事:谁是因变量、谁是自变量、打算验证什么关系。比如研究价格对销量的影响,因变量是销量,自变量是价格及构造特征;假设关系是线性的就直接用普通最小二乘,拿结果判断系数是否显著再决定是否升级。
对实验报告而言,一个系数符号与经济意义一致的线性回归,远比一个拟合优度更高但完全无法解释的黑匣子模型得分高。这也是数模竞赛与机器学习比赛的关键区别:竞赛看重“可解释地解决问题”,而不是单纯压测试集误差。
4.2 用Python完成拟合与交叉验证:命令、参数与输出解读
在线性模型家族里,我常用Ridge(岭回归)做默认选择。它比普通最小二乘多了一个L2正则项,能在特征之间存在相关性时稳定系数估计。下面是完整流程:
import numpy as np from sklearn.linear_model import Ridge from sklearn.model_selection import cross_val_score, KFold from sklearn.preprocessing import StandardScaler # 先归一化,再做回归 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 初始化模型与5折交叉验证 model = Ridge(alpha=1.0) kfold = KFold(n_splits=5, shuffle=True, random_state=42) # 用负均方误差打分,cross_val_score的约定是“越大越好” scores = cross_val_score(model, X_scaled, y, cv=kfold, scoring="neg_mean_squared_error") rmse_scores = np.sqrt(-scores) print("RMSE均值:", rmse_scores.mean()) print("RMSE标准差:", rmse_scores.std())三个参数需要重点理解:alpha是正则强度,越大系数越被压缩,默认1.0通常是个相对稳妥的起点;shuffle=True表示在切分前打乱样本顺序,这能避免数据原本按某种顺序排列造成的有偏划分;random_state=42固定了随机种子,保证每次运行结果一致,这是可复现的基础——没有它,同一份代码跑两遍得到两个数字,报告里都没法写。
cross_val_score用neg_mean_squared_error作为打分,取负号后变成正数就是均方误差。RMSE的均值反映模型整体预测能力,标准差反映稳定性。如果均值很低但标准差很大,说明模型在不同数据子集上表现波动明显,这往往比均值稍高但标准差不大的模型更不可靠。
4.3 残差分析与误差指标:怎么判断模型到底合不合格
拟合完模型后,新手看R²,老手看残差图。R²只回答问题“模型解释了总变异的多少”,但它不能暴露非线性、异方差等结构性问题。残差分析能补齐这块短板:
import matplotlib.pyplot as plt model.fit(X_scaled, y_train) pred = model.predict(X_scaled_test) resid = y_test - pred plt.scatter(pred, resid, alpha=0.6) plt.axhline(y=0, color="red", linestyle="--") plt.xlabel("预测值") plt.ylabel("残差") plt.title("残差图")残差图有几种典型形态:残差随机地分布在零线上下,说明模型基本可靠;残差随预测值增大而发散成漏斗状,提示存在异方差;残差呈现弯曲的弧形,说明漏掉了一个非线性项。看到后两种形态,直接升阶或加交互项往往比强行调alpha更有效。
误差指标的判断要结合业务场景,用下面的表可以快速定位重点:
| 指标 | 计算公式(口头说法) | 关注什么 |
|---|---|---|
| RMSE | 误差平方和取平均后开根号 | 大误差被放大,适合对“特别离谱”的预测敏感的场景 |
| MAE | 误差绝对值的平均 | 平滑稳定,适合对异常值不那么敏感的场景 |
| R² | 1 - 残差平方和 / 总平方和 | 模型对总体变异的解释比例 |
三个指标一起看,能避免单看R²带来的误判。实操建议是:报告里列出RMSE和R²两个值,再用残差图佐证模型形态,这就足够应付绝大多数实验2的评分标准了。
5. 建模实验常见坑与排查:五个高频扣分点逐个拆解
5.1 回归跑出来R²是负的,问题常出在数据检查
现象:模型训练完,R²输出是个负数,或者各个指标全是NaN。
原因:最常见的是数据里有缺失值没有处理,线性回归的内部求解器遇到NaN会直接失败或给出无意义结果。其次是异常值没有被清洗,几万个正常数据里混着几个极端值,足以把回归线拉歪。
解决:回到第3章的起手脚本,先看df.isnull().sum()确认缺失情况,再跑一次异常值IQR过滤。做完这两步再重新拟合,90%以上的负R²问题都会消失。如果清洗后仍为负,才考虑是不是模型形式选错。
5.2 时间序列被随机切分,模型提前“偷看”了未来
现象:交叉验证时RMSE很低,但把模型放到实际场景里预测,效果明显差一个量级。
原因:数据是时间序列,交叉验证却用了shuffle=True随机切分。随机打乱后,训练集里混进了“未来”样本,测试集里混进了“过去”样本,模型在测试时等于参考答案在书上,自然分数漂亮。这是实验报告里最隐蔽的数据泄漏形式。
解决:时间型数据改用TimeSeriesSplit切分,或者手动按时间顺序前80%训练、后20%测试。交叉验证只保证在“随机且同分布”的前提下有效,时间序列天然不满足这个前提。这一点在报告的方法说明里提一句,能让评分人看到你做了功课。
5.3 特征量纲差100倍,正则项等于白加
现象:Ridge回归加了alpha之后,系数依旧忽大忽小不稳定,换个随机种子结果就变了。
原因:特征没有归一化。当某个特征的数值范围是0到1,另一个特征的范围是0到10000时,正则项会不均匀地压缩系数,导致小量纲特征被过度惩罚、大量纲特征几乎不受限制。这时加正则和没加差不多。
解决:在拟合前对特征做StandardScaler标准化,再喂给Ridge。标准化之后所有特征同处一个尺度,正则项的惩罚才会真正作用于所有变量。这也是第4章流程里“先Scaler再fit”的原因,顺序错一步,结果就不对。
5.4 训练集很香测试集翻车,过拟合信号被忽略
现象:训练集RMSE只有0.05,测试集RMSE是0.8,交叉验证的五折分数参差不齐。
原因:特征太多、样本太少,或者构造了无业务含义的高次项。模型把训练集的噪声当成规律学会了,换个数据集立刻暴露。
解决:先砍特征,把多项式阶数降回线性,再看交叉验证的标准差。标准差大说明模型不稳定,优先调大alpha增强正则,而不是增加模型复杂度。如果砍完之后训练误差和测试误差都在可接受范围内,这个模型才具备基本可用性。更严格的验证可以再做一次“留一法”,代价是计算时间变长,但实验报告里写出来非常加分。
5.5 图表的编号和正文引用对不上,报告直接被打回
现象:正文写“结果见表2”,评审翻到表2发现是另一张表;图3的标题写的是图2的内容。
原因:手工编号最容易出错。写报告时插入了新图,后面的编号忘记整体刷新,或者删掉一章后引用没有同步更新。这个问题和建模水平无关,但造成的印象分损失比模型效果差还严重。
解决:用带交叉引用的编辑器写报告,比如Markdown配合自动编号插件,正文引用和图表编号绑定在一起,改插图为不自动错号。交付前的最后一小时,不要做任何建模调参,只做一件事:从头到尾顺一遍所有图表编号与正文引用,逐一核对。养成这个习惯能救回不少分。
6. 从能跑通到能拿分:报告结构与复现自检技巧
6.1 六段式报告结构:每一段写什么、写多长
实验2的报告不一定是有厚厚封面的论文,但结构必须完整。我用的六段式结构可以适配大多数课程实验:
| 段落 | 写什么 | 篇幅占比建议 |
|---|---|---|
| 问题重述 | 用自己的话复述题目,确认不偏题 | 10% |
| 假设与符号说明 | 列假设、定义符号,展示建模素养 | 5% |
| 数据与预处理 | 数据来源、清洗过程、缺失处理 | 15% |
| 模型建立与求解 | 模型公式、求解方法、参数设置 | 35% |
| 验证与误差分析 | 交叉验证结果、残差图、误差指标 | 25% |
| 结论与改进方向 | 结论、局限、可改进的地方 | 10% |
评分人最看重的是“模型建立与求解”和“验证与误差分析”两块。公式写明白、参数写清楚、指标和残差图放出来,报告的基本盘就稳了。前两段别写太多,见过一半篇幅写背景介绍的,最后没时间去讲模型,只能得不偿失。
6.2 交付前的复现自检:三个月后还能一键跑通
交付前我会强制自己做一次“复现自检”:把代码、数据、说明文件放进同一个命名规范的目录里,清掉代码中所有绝对路径记录环境版本与随机种子。隔一天在干净目录里重新跑一遍,跑不通就修到能跑为止。这比临时检查代码要可靠得多,因为你自己写的时候知道依赖关系,等过一天再跑才会暴露出“缺了某一步预处理”的问题。
我现在的习惯是把这份复现自检当作实验报告的一个组成部分。吃过太多亏之后才明白,数模实验2真正考核的并不是你会不会调某个库的参数,而是你能否让自己的模型、代码和结论经得起别人按你的步骤再走一遍。希望帮到你。
本文还有配套的精品资源,点击获取