简介:数学建模比赛常用代码(Python版)是一份面向数模参赛者的实用代码合集,系统覆盖从基础语法、数值计算到机器学习与可视化的常用建模工具,适合希望快速搭建算法方案的读者。资源共71个文件,压缩包约3.42MB,以txt算法说明、docx模型文档、py可运行脚本为主,另有dat数据文件与rar补充包,便于按类别查阅。内容包括一维/二维插值、线性与非线性规划、整数与二次规划、动态规划、智能优化算法、TOPSIS、层次分析、灰色预测、主成分分析,以及支持向量机、随机森林、决策树、BP/卷积神经网络等高频赛题模型,基本覆盖数模竞赛常见题型。目前已有1976人浏览学习,说明其在实际备赛中具有较好参考价值。配套代码与文档可帮助理解算法原理并迁移应用到具体赛题,节省编码和调参时间。
1. 数学建模比赛的代码,不是越多越好
三天赛程,真正能跑的代码其实就那么几段。见过太多队伍第一天花在找代码上,第二天发现下载的东西根本跑不通,最后一天熬夜补论文。这份数学建模比赛常用代码Python版,不是算法大全,而是把数据读入、缺失值处理、异常值检测、模型训练、交叉验证、可视化出图这些高频环节封装成可以直接调用的脚本。它的价值在于:赛前花两小时过一遍接口,比赛时改数据路径和几个参数就能出结果。想清楚这点再下载:它解决的是“有代码可用”和“代码能复现”的问题,不是帮你自动得奖。适合参加过比赛但写过一堆重复代码的人,也适合第一次参赛想少走弯路的新手。
2. 先看懂这份代码包的结构:文件职责与调用约定
拿到压缩包先别急着运行,第一步是搞清楚每个文件是干什么的。这份代码包按功能拆模块,不是按算法拆文件。原因是数模比赛换题很频繁,上午还在做分类,下午可能就换成了优化模型,按功能拆目录能让大部分代码跨题复用,改动量最小。
2.1 目录结构与各文件职责
解压后典型的目录结构如下:
math_modeling_code/ ├── config.yaml # 全局配置文件 ├── data/ # 放比赛给的数据 │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后的数据 ├── src/ │ ├── load_data.py # 数据读取模块 │ ├── preprocess.py # 清洗、缺失值、异常值 │ ├── feature_engineering.py # 特征构造 │ ├── models.py # 分类/回归/聚类模型封装 │ ├── evaluate.py # 交叉验证与评估 │ └── visualize.py # 可视化出图 ├── notebooks/ # 探索性分析的notebook └── submissions/ # 最终提交的结果文件各文件的核心职责可以这样理解:
| 文件 | 职责 | 对应比赛环节 |
|---|---|---|
| config.yaml | 统一管理路径和全局参数 | 赛前配置 |
| load_data.py | 读入Excel/CSV,处理编码和sheet | 数据准备 |
| preprocess.py | 缺失值、异常值、归一化 | 数据清洗 |
| feature_engineering.py | 构造新特征、时间戳拆解 | 特征工程 |
| models.py | 封装sklearn常见模型 | 建模 |
| evaluate.py | 交叉验证、网格搜索 | 模型选择 |
| visualize.py | 出图并保存到指定目录 | 论文配图 |
这种结构的好处是:赛场上如果发现数据格式不对,只需要改load_data.py,不需要碰模型代码;如果发现模型效果差,只需要改models.py和evaluate.py,不需要重写数据清洗逻辑。模块边界清晰,排查问题的时候不会上下游一起翻车。
2.2 数据读取模块的接口约定
load_data.py是第一个要看的文件。它把所有读取逻辑收敛到两个函数里,避免每道题都重新写一遍pd.read_csv,还要反复处理编码报错。
# src/load_data.py import pandas as pd from pathlib import Path def load_csv(file_path, sep=',', encoding='utf-8'): """ 读取CSV,自动处理常见编码错误。 file_path: 文件路径 sep: 分隔符,默认逗号 encoding: 默认utf-8,失败时尝试gbk """ try: return pd.read_csv(file_path, sep=sep, encoding=encoding) except UnicodeDecodeError: return pd.read_csv(file_path, sep=sep, encoding='gbk')逻辑说明:优先按utf-8读取,遇到解码错误自动转gbk,这是国产赛题Excel另存为CSV时最常见的坑。参数上,sep要按实际数据调整,有些题目给的是分号或制表符分隔,不改这个参数读进来的就是一整列。
def load_excel(file_path, sheet_name=0): """ 读取Excel,sheet_name默认第一个sheet。 返回DataFrame,统一把空单元格转为NaN。 """ df = pd.read_excel(file_path, sheet_name=sheet_name) return df这里有个约定:所有读取接口返回的都是DataFrame,且空值统一变成NaN。比赛后期如果发现哪一步计算出现奇怪的数值,第一步先检查是不是源头读取时把空字符串当成了有效数据。
2.3 全局参数与特定参数怎么分离
config.yaml的作用是让队友之间不互相踩配置。全局参数放数据路径、随机种子、输出目录,模型相关参数不放在这里,而是放在models.py的模型封装函数里。
# config.yaml data: raw_dir: "data/raw" processed_dir: "data/processed" target_col: "y" # 目标列名,按题目修改 id_col: "id" # 主键列名,预测时用于对齐 experiment: random_seed: 42 # 固定随机种子,保证结果可复现 cv_folds: 5 # 交叉验证折数 test_size: 0.2 # 留出验证集比例固定随机种子这个参数经常被忽略。比赛里同一套代码跑两次结果不一样,八成是没设random_state。配置文件里加一行,所有用到随机的函数都读这个值,能省掉很多不必要的争论。
2.4 为什么用Python而不是MATLAB
在这个代码包的语境下,选Python主要是三个现实原因:第一,数据清洗和缺失值处理用pandas比MATLAB直接得多;第二,机器学习模型这块sklearn的成熟度在比赛场景下够用,随机森林、梯度提升树都是几行代码的事;第三,可视化出图后可以无缝接入论文。MATLAB在规划求解、微分方程数值解上仍然有优势,但这份代码包定位是数据类赛题的高频流程,Python的性价比更高。如果赛题涉及大规模线性规划或偏微分方程,建议再准备专门的MATLAB工具箱脚本,不是说这份包里没写就代表Python不能做,而是场景不匹配,硬写只会浪费时间。
3. 数据预处理脚本的三板斧:读入、清洗、变换
数据预处理是数模比赛里最耗时的一环。评审看不到你清洗数据花了多少工夫,但模型效果差、图表异常,根子基本都在预处理。手里有一套稳定的预处理脚本,能让你把更多时间留给特征工程和论文撰写。
3.1 读取CSV的编码与分隔符问题
比赛提供的CSV文件来源复杂,有些是从Excel另存的,有些是爬虫抓的,编码和分隔符不统一是常态。
# src/preprocess.py 配套的读取示例 import pandas as pd file_path = "data/raw/train.csv" # 读取时先看前几行,确认解析正确 try: df = pd.read_csv(file_path, encoding='utf-8') except UnicodeDecodeError: df = pd.read_csv(file_path, encoding='gbk') # 检查列数是否是预期值 expected_cols = 15 if df.shape[1] != expected_cols: print(f"列数异常:期望{expected_cols}列,实际{df.shape[1]}列")逻辑说明:第一次读取成功后不要直接进入建模,先打印shape和head()看列数是否正常。常见问题是分隔符是分号或制表符,读进来所有列挤成一列,这时候手动指定sep参数比写自动推断靠谱。自动推断看起来方便,但遇到特殊字符容易静默出错。
3.2 缺失值处理的分层策略
缺失值处理没有万能公式,要看数据规模和缺失比例。这套代码里给出三个处理函数,按场景选用:
# src/preprocess.py import numpy as np def drop_missing(df, threshold=0.4): """ 删除缺失比例超过threshold的列。 threshold: 缺失率阈值,默认40%以上直接删列 """ missing_ratio = df.isnull().mean() drop_cols = missing_ratio[missing_ratio > threshold].index return df.drop(columns=drop_cols) def fill_missing_by_value(df, fill_dict): """ 按指定值填充,fill_dict格式为 {列名: 填充值}。 数值型用中位数/均值,类别型用众数。 """ return df.fillna(fill_dict) def fill_missing_by_interpolate(df, method='linear'): """ 对时间序列类数据使用插值填充。 method: 插值方法,linear或quadratic """ return df.interpolate(method=method)参数说明:缺失比例阈值的设置要看样本量。样本量大的时候删除一列影响不大;样本量小的时候,比如只有几百行,宁可保留缺失列用填充策略。中位数比均值抗异常值干扰,类别特征用众数填充,这是默认习惯。插值方法适合时间序列补值,但前几个位置补不了,需要另配向前填充。
3.3 异常值检测与处理
异常值检测有两个方向:一是数据本身录入错误,二是真实存在但会干扰模型拟合的极端值。比赛中一般不做剔除,先标记和处理。
# src/preprocess.py def detect_outliers_iqr(df, col, multiplier=1.5): """ 基于IQR方法检测异常值。 multiplier: IQR倍数,默认1.5为标准区间 """ Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - multiplier * IQR upper = Q3 + multiplier * IQR return df[(df[col] < lower) | (df[col] > upper)].index def detect_outliers_zscore(df, col, threshold=3): """ 基于Z分数检测异常值。 threshold: Z分数阈值,默认超过3倍标准差视为异常 """ z = np.abs((df[col] - df[col].mean()) / df[col].std()) return df[z > threshold].index逻辑说明:IQR方法不依赖均值,对偏态分布更稳健;Z分数方法计算快但容易被极端值本身拉偏均值。比赛时两种方法都跑一遍,把两份异常集合做交集,命中交集的样本才处理,能减少误删。处理方式一般是先看一下这些异常值是录入错误还是真实值,不要一上来就删行。
3.4 归一化与标准化怎么选
归一化和标准化经常被混用,实际上对距离类模型影响很大。代码包里分别封装了两个函数:
# src/preprocess.py from sklearn.preprocessing import StandardScaler, MinMaxScaler def scale_standard(df, cols): """ 标准化:均值为0,标准差为1。保留异常值信息。 cols: 需要缩放的列名列表 """ scaler = StandardScaler() df[cols] = scaler.fit_transform(df[cols]) return df, scaler def scale_minmax(df, cols, feature_range=(0, 1)): """ 归一化:缩放到feature_range范围,默认0-1。 对有限范围的数据更友好,但受异常值影响大。 """ scaler = MinMaxScaler(feature_range=feature_range) df[cols] = scaler.fit_transform(df[cols]) return df, scaler注意:fit_transform返回的是numpy数组,直接赋值回DataFrame会丢列名,代码里用df[cols]的方式保持索引对齐。实际使用中,如果后续要用PCA或聚类这类基于距离的算法,先做标准化收敛更快;如果特征本身物理意义有明确上下界,比如经纬度、百分比,用MinMax归一化更合理。
4. 模型脚本的选择逻辑:分类、回归、聚类的调用姿势
模型脚本这一章是代码包的核心,但也是最容易踩坑的地方。很多队伍拿到数据就直接套随机森林,根本不看任务类型和数据规模。这套代码把模型封装成统一的函数,内部设置好默认参数,同时保留改参入口。
4.1 分类场景:逻辑回归与随机森林
逻辑回归是基线模型,随机森林是主力模型。代码包里默认优先跑逻辑回归,原因是可以快速验证数据预处理是否正确。
# src/models.py from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier def train_logistic(X_train, y_train, C=1.0, penalty='l2'): """ C: 正则化强度的倒数,越小正则越强 penalty: l1或l2,l1可做特征选择 """ model = LogisticRegression(C=C, penalty=penalty, max_iter=1000, random_state=42) model.fit(X_train, y_train) return model def train_rf_classifier(X_train, y_train, n_estimators=200, max_depth=None): """ n_estimators: 树的数量,默认200 max_depth: 树的最大深度,None表示不限制 """ model = RandomForestClassifier( n_estimators=n_estimators, max_depth=max_depth, min_samples_leaf=2, random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) return model参数说明:逻辑回归里max_iter调到1000是因为sklearn默认100在特征多时可能不收敛,比赛数据经常几百个特征,跑出警告说明迭代次数不够。随机森林的min_samples_leaf=2是为了防止叶子节点过细造成过拟合。n_jobs=-1表示用全部CPU核心,比赛时性能提升明显。
4.2 回归场景:线性回归与梯度提升树
回归任务中线性回归作为基线,梯度提升树作为主力。比赛评分通常看误差指标,梯度提升树在非线性和特征交互场景下容错率更高。
# src/models.py from sklearn.linear_model import LinearRegression from sklearn.ensemble import HistGradientBoostingRegressor def train_linear(X_train, y_train): model = LinearRegression() model.fit(X_train, y_train) return model def train_gbr(X_train, y_train, max_iter=200, learning_rate=0.1): """ max_iter: 迭代轮数,树的数量 learning_rate: 学习率,越小越保守但需要更多迭代 """ model = HistGradientBoostingRegressor( max_iter=max_iter, learning_rate=learning_rate, random_state=42 ) model.fit(X_train, y_train) return model逻辑说明:HistGradientBoostingRegressor比普通GradientBoostingRegressor在数据量大时更快,且对缺失值有内置处理,比赛数据预处理没做好时这是个后悔药。learning_rate和max_iter是一对关系,学习率设小就要增大迭代次数,不然欠拟合。
4.3 聚类场景:KMeans与层次聚类
聚类在数模赛题里通常是做数据探索的一部分,比如客户分群、区域划分。KMeans方便,但聚类数K要靠轮廓系数判断。
# src/models.py from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score def kmeans_with_silhouette(X, k_range=(2, 10), random_state=42): """ 对一系列K值计算轮廓系数,返回最佳K和对应的模型。 k_range: K值扫描范围 """ best_k = None best_score = -1 best_model = None for k in range(k_range[0], k_range[1] + 1): model = KMeans(n_clusters=k, random_state=random_state, n_init='auto') labels = model.fit_predict(X) score = silhouette_score(X, labels) if score > best_score: best_score = score best_k = k best_model = model return best_k, best_model, best_scoren_init参数在新版sklearn里默认改成auto,老版本需要显式设置n_init=10,否则会有收敛警告。轮廓系数只对凸簇数据友好,如果数据形状复杂,要结合可视化判断,别只看分数选K。
4.4 交叉验证与网格搜索的固定套路
网格搜索是比赛里调参的基本操作,但很多人直接把整个参数网格丢进去,结果跑了一夜没出结果。
# src/evaluate.py from sklearn.model_selection import GridSearchCV def search_params(model, param_grid, X_train, y_train, cv=5, n_jobs=-1): """ param_grid: 参数网格字典 cv: 交叉验证折数,默认5 n_jobs: 并行计算核心数,-1表示全部 """ grid = GridSearchCV( estimator=model, param_grid=param_grid, cv=cv, scoring='f1' if model._estimator_type == 'classifier' else 'neg_mean_squared_error', n_jobs=n_jobs, verbose=1 ) grid.fit(X_train, y_train) return grid.best_params_, grid.best_score_参数说明:scoring根据模型类型自动切换,分类用F1,回归用负均方误差,这样统一接口不会传错参数。网格搜索前先评估参数组合数量,超过20组就要考虑随机搜索或者缩小范围,否则比赛时间不够用。GridSearchCV在数据量大时内存消耗很高,建议先对训练集做一次subsample测试,确认能跑通再放全量。
5. 用这份代码踩过的坑:现象、原因与解法
代码跑不通不是最痛苦的,最痛苦的是程序没报错、输出一片乱。这一章把高频翻车点按“现象、原因、解决”写清楚,都是实际比赛里能救命的经验。
5.1 读取数据出现乱码或列名错位
现象:pd.read_csv读入后打印df.head(),发现列名是乱码,或者第一行数据变成了列名,所有列往左错了一位。
原因:UTF-8编码的CSV被Excel打开再另存后成了GBK编码,或者表格里第一行本来就是数据,没有表头。列名错位更常见于某些题目直接把变量名放在第二行,而read_csv默认把第一行当表头。
解决:先打开原始文件确认结构,再用load_csv函数读取。遇到没有表头的文件,给read_csv加header=None,然后手动指定列名。我用过的最快方案是写一个临时脚本,一次性打印文件前五行字节内容,确认编码后再定参数。
5.2 缺失值直接删除导致样本量骤减
现象:调用dropna()后训练集从一万行掉到两千行,后面模型怎么训练都过拟合。
原因:数据里多个特征都带缺失,只要任一行任意列有缺失就删行,样本量直接被砍掉大半。这个操作对数据量小的赛题是灾难。
解决:改用列删除,先删缺失率大于40%的特征列,再对剩余特征逐列填充;对于关键特征,不要用均值硬填,先分组后填充,比如按时间分组的中位数填充,效果比全局均值好很多。
5.3 标准化时把训练集和测试集一起fit
现象:模型训练时验证集效果不错,提交后离线测试分数明显偏低。
原因:先对全量数据做了标准化fit_transform,再切分训练集和测试集,导致测试集信息提前泄露到scaler的均值和方差里。竞赛不是不能用全量数据,但做模型评估时必须保证每个fold的scaler只在训练fold上fit。
解决:用ColumnTransformer或Pipeline,把标准化和模型放进同一个Pipeline,交叉验证时每个fold自动重新fit。在比赛里想快速验证,就先切分再fit:
# 正确的顺序:先切分,再fit scaler from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)逻辑说明:fit_transform方法在测试集上不能用,只能用fit后transform,确保测试集不参与缩放参数的估计。这个坑报错概率很低,因为代码能跑,只是结果偏保守或偏乐观。
5.4 网格搜索时间爆炸
现象:GridSearchCV跑了一小时还没结束,队友已经在催论文截图了。
原因:param_grid里参数组数太多,交叉验证又设了10折,数据量几万行,计算量成倍放大。比赛时间有限,这种做法属于方案设计失误。
解决:网格搜索之前先算参数组合数量,超过20组就改用RandomizedSearchCV或手动逐个试。配合n_jobs=-1并行,同时把cv从10降到5。还有一个习惯是先在2万行的子集上跑一版,确认参数范围合理再放全量。
5.5 特征量纲没统一导致聚类结果全偏
现象:聚类后的中心点坐标看着没问题,但画到图上一团糟,明显有几个特征主导了距离计算。
原因:KMeans用的是欧氏距离,量纲大的特征数值天然占优势。比如收入特征取值范围几万,年龄特征只有几十,聚类结果基本只看收入。
解决:聚类前必须做标准化或归一化,这个不是可选项是必须项。解决后重新跑轮廓系数,分数会明显提升。比赛里如果聚完之后还想继续做分析,记得把聚类中心反标准化回原尺度再解释。
6. 赛前最后一小时:把这套代码跑成一条流水线
比赛最后一天最怕临时发现某一步报错。我的做法是把所有环节串成一个pipeline脚本,输入原始数据路径,输出预测结果文件和模型指标。
# 赛前快速验证:data -> result 一条命令跑完 import pandas as pd from src.load_data import load_csv from src.preprocess import drop_missing, fill_missing_by_value, scale_standard from src.models import train_rf_classifier from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score # 1. 读数据 df = load_csv("data/raw/train.csv") # 2. 清洗:删高缺失列,填充剩余缺失 df = drop_missing(df, threshold=0.4) df = df.fillna(df.median()) # 3. 特征与标签分离 X = df.drop(columns=['id', 'y']) y = df['y'] # 4. 切分并标准化 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) X_train, scaler = scale_standard(X_train) X_test[X_train.columns] = scaler.transform(X_test[X_train.columns]) # 5. 训练并评估 model = train_rf_classifier(X_train, y_train, n_estimators=200) pred = model.predict(X_test) print(f"F1: {f1_score(y_test, pred):.4f}")这段脚本每个比赛开始前跑一遍,确认从读入到出指标全链路无报错。比赛过程中如果改了配置或模型,再跑一遍这个脚本做回归验证,能及时发现新代码引入的bug。
文档开赛前花一天时间,把代码包的每个模块跑一次,确保数据清洗、模型训练、出图三块都能单独工作。比赛时问自己三个问题:缺失值处理了吗?特征和标签对齐了吗?预测结果有没有统一格式?三个都确认再提交。
核心的代码逻辑其实在比赛第一天就定了,后面两天更多是把同一个流程迭代熟练。从那以后我每次比赛前都强制走一遍这个流程:先跑通pipeline,再讨论模型调优。希望这份代码包能帮你把踩坑的成本降下来,把时间花在真正拉开差距的地方。
本文还有配套的精品资源,点击获取