1. 项目概述:为什么需要一个“通用模板”?
每次数学建模比赛或者接到一个数据分析项目,你是不是也经历过这样的循环?拿到题目和数据,脑子里一片空白,不知道从哪里下手;好不容易理清思路,开始写代码,又在数据预处理、特征工程、模型选择和调参之间反复横跳,代码越写越乱,最后自己都看不懂;等到要写论文或者报告了,又得回头从一堆混乱的脚本里扒拉结果和图表。整个过程耗时耗力,还容易出错。我做了十多年的数据科学项目,带过不少学生打数模,发现新手和老手最大的区别,往往不在于掌握了多少高深的算法,而在于有没有一套清晰、可复用、能保证基础质量的工作流。
这就是“使用 sklearn 进行数学建模的通用模板”的价值所在。它不是一个能解决所有问题的“银弹”,而是一个结构化的脚手架。它的核心目标是帮你把建模过程标准化,从数据加载到模型评估,每一步都井井有条。无论你是面对“2025国赛C题”这样的复杂赛题,还是处理“大学生择业选择”这类实际问题,这个模板都能确保你的基础流程不出错,把宝贵的精力集中在问题分析、特征构造和模型创新这些真正能拉开差距的地方。sklearn(Scikit-learn)作为Python机器学习的事实标准库,其API设计本身就非常一致(fit,transform,predict),这为我们构建模板提供了绝佳的基础。这个模板适合所有使用Python进行数据分析、机器学习建模的初学者和中级从业者,它能帮你快速搭建一个稳健的基线模型,并在此之上进行高效迭代。
2. 模板整体架构与设计哲学
一个健壮的建模流程,绝不能是东一榔头西一棒子的脚本堆砌。我设计的这个通用模板,其核心思想是“管道化”和“模块化”。整个流程被抽象为几个顺序执行的、高内聚低耦合的模块,数据像流水一样经过这些模块,被逐步加工成最终的模型预测结果。
2.1 核心流程模块拆解
整个模板围绕一个主函数或一个Jupyter Notebook的单元格顺序展开,主要包含以下六大模块:
- 环境准备与数据加载:解决“在哪里跑”和“数据从哪里来”的问题。
- 探索性数据分析:解决“数据长什么样”的问题,这是所有后续决策的基础。
- 数据预处理与特征工程:解决“如何把数据喂给模型”的问题,这是影响模型性能的关键。
- 模型训练与验证:解决“选哪个模型、怎么调参”的问题。
- 模型评估与优化:解决“模型好不好、怎么变得更好”的问题。
- 结果输出与持久化:解决“如何交付和复用成果”的问题。
这个流程是单向且迭代的。例如,在特征工程后,我们可能需要对模型进行初步评估,然后根据结果返回去调整特征;或者在模型评估后,返回去调整预处理参数。模板提供了清晰的节点,让你知道当前处于哪个阶段,以及可以回溯到哪个阶段进行调整。
2.2 为什么选择这样的架构?
- 可复现性:每一步操作,从数据清洗到模型参数,都被清晰地记录和封装。三个月后,你甚至你的队友,都能一键复现整个实验。
- 可维护性:当需要修改特征工程方法或尝试新模型时,你只需要在对应的模块内改动,而不会“牵一发而动全身”,把整个代码搞乱。
- 效率提升:避免了每次从头开始写
import、读数据、划分训练测试集的重复劳动。模板帮你做好了这些“脏活累活”,让你能快速进入核心的建模环节。 - 减少错误:标准化的流程减少了因步骤遗漏(比如忘了做特征缩放)而导致的低级错误。
注意:模板的“通用性”体现在流程框架和接口上,而不是具体的算法。你需要根据具体问题(比如是分类、回归还是聚类)填充每个模块的具体内容。例如,2024年高教杯B题可能涉及时间序列预测,那么特征工程模块就需要引入滞后特征、滑动窗口统计等。
3. 环境准备与数据加载模块详解
万事开头难,一个稳定的环境是成功的一半。很多奇怪的报错,追根溯源都是环境依赖冲突。
3.1 环境依赖管理
我强烈建议为每一个数学建模项目或数据分析任务创建独立的虚拟环境。这能保证项目依赖的纯净性。使用conda或venv均可。
# 使用 conda 创建环境(推荐,尤其对Windows用户友好) conda create -n math_modeling_2025 python=3.9 conda activate math_modeling_2025 # 使用 venv 创建环境 python -m venv venv_math_modeling # Windows venv_math_modeling\Scripts\activate # Linux/Mac source venv_math_modeling/bin/activate创建环境后,将核心依赖写入requirements.txt文件:
# requirements.txt scikit-learn>=1.3.0 pandas>=1.5.0 numpy>=1.23.0 matplotlib>=3.6.0 seaborn>=0.12.0 jupyter>=1.0.0 # 可选,用于更高级的统计分析或可视化 scipy>=1.9.0 statsmodels>=0.13.0然后使用pip install -r requirements.txt一键安装。在代码开头,我们集中导入这些库,并做好基础设置。
# 模块1: 环境准备与数据加载 import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn import __version__ as sklearn_version # 设置绘图风格和显示选项(让图表更好看,输出更完整) plt.style.use('seaborn-v0_8-whitegrid') # 使用seaborn的网格风格 sns.set_palette("husl") # 设置颜色盘 pd.set_option('display.max_columns', None) # 显示所有列 pd.set_option('display.float_format', '{:.4f}'.format) # 浮点数显示格式 print(f"Sklearn Version: {sklearn_version}")3.2 数据加载的多种场景处理
数学建模中的数据来源五花八门,可能是CSV、Excel,也可能是数据库或API。模板需要兼容这些情况。
def load_data(file_path, file_type='csv', **kwargs): """ 通用数据加载函数 Args: file_path: 文件路径或数据库连接字符串 file_type: 文件类型,支持 'csv', 'excel', 'sql' **kwargs: 传递给对应pandas读取函数的参数 Returns: pandas DataFrame """ if file_type == 'csv': df = pd.read_csv(file_path, **kwargs) elif file_type == 'excel': df = pd.read_excel(file_path, **kwargs) elif file_type == 'sql': # 假设已创建数据库引擎 `engine` query = kwargs.get('query', 'SELECT * FROM table') df = pd.read_sql(query, con=kwargs.get('con')) else: raise ValueError(f"Unsupported file type: {file_type}") print(f"数据加载成功!形状: {df.shape}") print(f"列名: {df.columns.tolist()}") return df # 使用示例 # df = load_data('2025_problem_c_data.csv') # df = load_data('data.xlsx', file_type='excel', sheet_name='Sheet1')加载数据后,第一时间使用df.head()、df.info()和df.describe()进行快速浏览,了解数据规模、类型和基本统计信息。这是探索性数据分析的开始,但我们在下一个模块会系统化地进行。
4. 探索性数据分析的系统化方法
EDA不是简单的画几个图,而是带着问题去审视数据,为后续的预处理和建模提供决策依据。我习惯将EDA分为三个层次:单变量分析、双变量分析和多变量分析。
4.1 单变量分析:了解每一个“士兵”
目标是了解每个特征的分布、中心趋势、离散程度以及缺失情况。
def univariate_analysis(df, target_col=None): """ 单变量分析 """ print("="*50) print("1. 数据基本信息") print("="*50) print(df.info()) print("\n" + "="*50) print("2. 描述性统计(数值型)") print("="*50) print(df.describe()) print("\n" + "="*50) print("3. 描述性统计(分类型)") print("="*50) categorical_cols = df.select_dtypes(include=['object', 'category']).columns for col in categorical_cols: print(f"\n--- {col} ---") print(df[col].value_counts(dropna=False).head(10)) # 看前10个最常见的值 print(f"唯一值数量: {df[col].nunique()}") print("\n" + "="*50) print("4. 缺失值统计") print("="*50) missing_stats = df.isnull().sum() missing_stats = missing_stats[missing_stats > 0].sort_values(ascending=False) if len(missing_stats) > 0: print(missing_stats) # 可视化缺失值 import missingno as msno # 需要安装 missingno msno.matrix(df) plt.title('Missing Values Matrix') plt.show() else: print("无缺失值。") print("\n" + "="*50) print("5. 数值型特征分布可视化") print("="*50) numeric_cols = df.select_dtypes(include=[np.number]).columns # 为避免图形过多,只绘制部分特征或使用子图 cols_to_plot = numeric_cols[:min(6, len(numeric_cols))] # 最多画6个 fig, axes = plt.subplots(2, 3, figsize=(15, 8)) axes = axes.ravel() for idx, col in enumerate(cols_to_plot): axes[idx].hist(df[col].dropna(), bins=30, edgecolor='black', alpha=0.7) axes[idx].set_title(f'Distribution of {col}') axes[idx].set_xlabel(col) axes[idx].set_ylabel('Frequency') plt.tight_layout() plt.show()4.2 双变量分析:寻找特征与目标的关系
这是建模前最关键的一步,帮助我们初步判断哪些特征可能重要。
def bivariate_analysis(df, target_col): """ 双变量分析:特征与目标变量的关系 Args: df: DataFrame target_col: 目标变量列名 """ if target_col not in df.columns: raise ValueError(f"目标列 {target_col} 不在DataFrame中。") target_type = 'numeric' if pd.api.types.is_numeric_dtype(df[target_col]) else 'categorical' numeric_features = df.select_dtypes(include=[np.number]).columns.drop(target_col, errors='ignore') categorical_features = df.select_dtypes(include=['object', 'category']).columns.drop(target_col, errors='ignore') print("="*50) print(f"目标变量 '{target_col}' 与特征的关系分析") print("="*50) # 情况1: 目标变量是数值型(回归问题) if target_type == 'numeric': print("\n--- 数值型特征与目标的相关性 ---") corr_with_target = df[numeric_features].corrwith(df[target_col]).sort_values(ascending=False) print(corr_with_target) # 绘制相关性最高的几个特征与目标的散点图 top_n = min(4, len(corr_with_target)) top_features = corr_with_target.index[:top_n] fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes = axes.ravel() for idx, feat in enumerate(top_features): axes[idx].scatter(df[feat], df[target_col], alpha=0.5) axes[idx].set_xlabel(feat) axes[idx].set_ylabel(target_col) axes[idx].set_title(f'{feat} vs {target_col}\nCorr: {corr_with_target[feat]:.3f}') plt.tight_layout() plt.show() # 对于分类特征,可以看不同类别下目标变量的分布(箱线图) if len(categorical_features) > 0: cat_to_plot = categorical_features[:min(2, len(categorical_features))] for cat_feat in cat_to_plot: # 如果类别太多,取前N个主要的类别 top_categories = df[cat_feat].value_counts().index[:10] df_plot = df[df[cat_feat].isin(top_categories)] plt.figure(figsize=(10, 6)) sns.boxplot(x=cat_feat, y=target_col, data=df_plot) plt.title(f'Distribution of {target_col} across {cat_feat}') plt.xticks(rotation=45) plt.show() # 情况2: 目标变量是分类型(分类问题) else: print(f"\n目标变量 '{target_col}' 的类别分布:") print(df[target_col].value_counts()) # 对于数值型特征,绘制不同目标类别下的分布(小提琴图或箱线图) if len(numeric_features) > 0: feat_to_plot = numeric_features[:min(4, len(numeric_features))] for feat in feat_to_plot: plt.figure(figsize=(8, 5)) sns.violinplot(x=target_col, y=feat, data=df, inner='quartile') plt.title(f'Distribution of {feat} by {target_col}') plt.show() # 对于分类特征,可以使用交叉表或堆叠柱状图 if len(categorical_features) > 0: cat_to_plot = categorical_features[:min(2, len(categorical_features))] for cat_feat in cat_to_plot: cross_tab = pd.crosstab(df[cat_feat], df[target_col], normalize='index') # 行百分比 cross_tab.plot(kind='bar', stacked=True, figsize=(10, 6)) plt.title(f'Relationship between {cat_feat} and {target_col}') plt.ylabel('Proportion') plt.legend(title=target_col, bbox_to_anchor=(1.05, 1), loc='upper left') plt.tight_layout() plt.show()4.3 多变量分析与洞察记录
在完成单变量和双变量分析后,你需要将发现记录下来,形成一份“数据洞察备忘录”。这个备忘录将直接指导下一步的预处理和特征工程。
你可以创建一个Markdown单元格或文本文件,记录如下内容:
- 数据质量:哪些列有缺失?缺失比例如何?是随机缺失还是系统缺失?(例如,收入字段的缺失可能意味着高收入人群不愿透露)。
- 特征分布:哪些特征是偏态分布?是否存在量纲差异巨大的特征?(这决定了是否需要标准化/归一化)。
- 特征与目标关系:哪些特征与目标变量相关性高?哪些分类特征对目标区分度大?
- 潜在问题:是否存在异常值?是否有高度相关的特征(多重共线性)?分类特征的类别是否过多(高基数)?
- 初步特征工程想法:是否需要创建交互特征、分箱、编码?
这个备忘录是你思考过程的结晶,也是与队友沟通和论文写作的重要素材。很多优秀的数学建模论文,其“问题分析”和“模型假设”部分就源于扎实的EDA。
5. 数据预处理与特征工程管道构建
这是将原始数据转化为模型“可口食物”的关键步骤。sklearn的Pipeline和ColumnTransformer是构建自动化、可复现预处理流程的神器。
5.1 数据清洗与缺失值处理策略
根据EDA的发现,制定清洗策略。切忌无脑填充缺失值,要思考缺失的机制。
from sklearn.impute import SimpleImputer, KNNImputer from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer def get_imputation_strategy(df, col, strategy='median'): """ 根据列的数据类型和业务逻辑,返回合适的填充策略。 实际应用中,这个函数会更复杂,可能包含基于其他列的规则。 """ # 示例:对于数值列,默认用中位数填充;对于分类列,用众数填充。 if pd.api.types.is_numeric_dtype(df[col]): if strategy == 'mean': return SimpleImputer(strategy='mean') elif strategy == 'median': return SimpleImputer(strategy='median') elif strategy == 'knn': return KNNImputer(n_neighbors=5) elif strategy == 'iterative': return IterativeImputer(max_iter=10, random_state=42) else: return SimpleImputer(strategy='constant', fill_value=0) # 默认填0 else: # 分类列 return SimpleImputer(strategy='most_frequent')5.2 特征编码与缩放
模型只能处理数值。对于分类变量,必须编码。不同模型对特征的尺度敏感度不同。
from sklearn.preprocessing import ( StandardScaler, MinMaxScaler, RobustScaler, OneHotEncoder, OrdinalEncoder, LabelEncoder ) from sklearn.compose import ColumnTransformer # 假设我们有以下列定义(根据EDA结果调整) numeric_features = ['age', 'income', 'credit_score'] categorical_features_low_card = ['education', 'marital_status'] # 低基数分类特征 categorical_features_high_card = ['zip_code'] # 高基数分类特征,需要特殊处理 target = 'loan_default' # 构建列转换器 preprocessor = ColumnTransformer( transformers=[ # 数值特征:使用RobustScaler(对异常值不敏感) ('num', RobustScaler(), numeric_features), # 低基数分类特征:使用One-Hot编码 ('cat_low', OneHotEncoder(handle_unknown='ignore', sparse_output=False), categorical_features_low_card), # 高基数分类特征:使用目标编码或频率编码(这里用频率编码示例) # 注意:目标编码需要在Pipeline中小心处理,避免数据泄露。这里先展示频率编码。 ('cat_high', 'passthrough', categorical_features_high_card) # 暂时保留,后续单独处理 ], remainder='drop' # 丢弃未指定的列 ) # 对于高基数特征,我们可以在Pipeline之外先进行转换 # 例如,频率编码 df['zip_code_freq'] = df['zip_code'].map(df['zip_code'].value_counts(normalize=True)) # 然后更新 numeric_features,将 `zip_code_freq` 加入,并从原始特征中移除 `zip_code`5.3 特征构造:从数据中挖掘“金矿”
这是体现建模者水平的地方。好的特征往往比复杂的模型更有效。特征构造需要结合领域知识和数据分析直觉。
- 交互特征:例如,在电商推荐中,“用户活跃度” × “商品热度”。
- 多项式特征:对于回归问题,
PolynomialFeatures可以自动生成特征的高次项和交互项,但要小心维度爆炸。 - 分箱:将连续变量离散化,可以捕捉非线性关系。例如,将年龄分为“青年”、“中年”、“老年”。
- 时间序列特征:如果是时间数据,可以构造滞后项、滑动窗口均值、时序趋势等。
- 文本特征:如果是文本数据,使用TF-IDF、词向量等。
from sklearn.preprocessing import PolynomialFeatures from sklearn.decomposition import PCA # 示例:创建多项式特征(通常只用于数值特征) poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) # interaction_only=True 表示只生成交互项,不生成平方项,防止共线性。 # 示例:使用PCA进行特征降维(在特征很多且相关性强时使用) pca = PCA(n_components=0.95) # 保留95%的方差实操心得:特征工程不是一蹴而就的。我通常采用“贪心法”:
- 先使用基础特征(原始特征+简单清洗)训练一个基线模型。
- 基于模型结果(如特征重要性)和业务理解,构造一批新特征。
- 将新特征加入,重新训练模型,观察性能提升。
- 如果提升显著,保留该特征;否则舍弃。如此迭代。
6. 模型训练、验证与调参的标准化流程
有了干净的特征,就可以开始训练模型了。这一步的核心是避免过拟合和公平评估。
6.1 数据划分与交叉验证
永远不要在训练模型的数据上评估模型,那会得到过于乐观的结果。
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold, KFold # 假设 X 是特征矩阵,y 是目标向量 X = df.drop(columns=[target]) y = df[target] # 基础划分:训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y # 分类问题建议使用分层抽样 ) # 更稳健的评估:交叉验证 # 对于分类问题,使用 StratifiedKFold 保持类别比例 if y.nunique() < 10: # 粗略判断为分类问题 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) else: cv = KFold(n_splits=5, shuffle=True, random_state=42) # 使用交叉验证评估一个模型 from sklearn.ensemble import RandomForestClassifier base_model = RandomForestClassifier(n_estimators=100, random_state=42) cv_scores = cross_val_score(base_model, X_train, y_train, cv=cv, scoring='accuracy') print(f"交叉验证平均准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std()*2:.4f})")6.2 构建完整的建模管道
将预处理和模型训练封装进一个Pipeline,这是最佳实践。
from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC # 定义预处理步骤(使用之前定义的 preprocessor) # 定义多个候选模型 pipelines = { 'rf': Pipeline(steps=[ ('preprocessor', preprocessor), ('classifier', RandomForestClassifier(random_state=42)) ]), 'lr': Pipeline(steps=[ ('preprocessor', preprocessor), ('classifier', LogisticRegression(max_iter=1000, random_state=42)) ]), 'svm': Pipeline(steps=[ ('preprocessor', preprocessor), ('classifier', SVC(probability=True, random_state=42)) # probability=True 用于后续绘制ROC曲线 ]) } # 快速评估多个模型 for name, pipeline in pipelines.items(): cv_scores = cross_val_score(pipeline, X_train, y_train, cv=cv, scoring='accuracy') print(f"{name:3s} - 平均准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std()*2:.4f})")6.3 超参数调优:让模型性能更上一层楼
模型有很多“旋钮”(超参数),需要调整到最佳位置。GridSearchCV或RandomizedSearchCV是自动化调参的工具。
from sklearn.model_selection import GridSearchCV # 以随机森林为例,定义参数网格 param_grid_rf = { 'classifier__n_estimators': [100, 200, 300], 'classifier__max_depth': [10, 20, None], 'classifier__min_samples_split': [2, 5, 10], 'classifier__min_samples_leaf': [1, 2, 4] } # 创建 GridSearchCV 对象 grid_search_rf = GridSearchCV( estimator=pipelines['rf'], param_grid=param_grid_rf, cv=cv, scoring='accuracy', n_jobs=-1, # 使用所有CPU核心 verbose=1 ) # 在训练集上进行网格搜索 print("开始随机森林网格搜索...") grid_search_rf.fit(X_train, y_train) print(f"\n最佳参数: {grid_search_rf.best_params_}") print(f"最佳交叉验证分数: {grid_search_rf.best_score_:.4f}") # 获取最佳模型 best_rf_model = grid_search_rf.best_estimator_注意:
RandomizedSearchCV在参数空间较大时比GridSearchCV更高效,它随机采样参数组合进行尝试。对于大型数据集或复杂模型,建议先用RandomizedSearchCV缩小范围,再用GridSearchCV精细调整。
7. 模型评估与结果分析的全面视角
模型训练好了,怎么知道它好不好?不能只看准确率。尤其是对于类别不平衡的数据(比如欺诈检测,正常交易远多于欺诈交易),准确率可能是骗人的。
7.1 多维度评估指标
根据问题类型选择合适的评估指标。
from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report, mean_absolute_error, mean_squared_error, r2_score ) def evaluate_classification_model(model, X_test, y_test, model_name='Model'): """ 评估分类模型,输出多种指标和图表。 """ y_pred = model.predict(X_test) y_pred_proba = model.predict_proba(X_test)[:, 1] if hasattr(model, 'predict_proba') else None print(f"\n{'='*60}") print(f"评估报告 - {model_name}") print(f"{'='*60}") # 基础指标 accuracy = accuracy_score(y_test, y_pred) precision = precision_score(y_test, y_pred, average='weighted') # 对于多分类,使用加权平均 recall = recall_score(y_test, y_pred, average='weighted') f1 = f1_score(y_test, y_pred, average='weighted') print(f"准确率 (Accuracy): {accuracy:.4f}") print(f"精确率 (Precision): {precision:.4f}") print(f"召回率 (Recall): {recall:.4f}") print(f"F1 分数: {f1:.4f}") if y_pred_proba is not None and len(np.unique(y_test)) == 2: # 二分类问题,计算AUC auc = roc_auc_score(y_test, y_pred_proba) print(f"AUC 分数: {auc:.4f}") # 详细分类报告 print("\n详细分类报告:") print(classification_report(y_test, y_pred, target_names=[f'Class {i}' for i in np.unique(y_test)])) # 混淆矩阵热力图 cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(8,6)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=np.unique(y_test), yticklabels=np.unique(y_test)) plt.title(f'Confusion Matrix - {model_name}') plt.ylabel('True Label') plt.xlabel('Predicted Label') plt.tight_layout() plt.show() # ROC 曲线 (仅二分类) if y_pred_proba is not None and len(np.unique(y_test)) == 2: from sklearn.metrics import roc_curve fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba) plt.figure(figsize=(8,6)) plt.plot(fpr, tpr, label=f'{model_name} (AUC = {auc:.3f})') plt.plot([0, 1], [0, 1], 'k--', label='Random Guess') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title(f'ROC Curve - {model_name}') plt.legend(loc="lower right") plt.grid(True, alpha=0.3) plt.show() return { 'accuracy': accuracy, 'precision': precision, 'recall': recall, 'f1': f1, 'auc': auc if 'auc' in locals() else None } # 使用最佳模型在测试集上评估 test_metrics = evaluate_classification_model(best_rf_model, X_test, y_test, model_name='Optimized Random Forest')对于回归问题,评估函数类似,但指标换为MAE,MSE,RMSE,R²,可视化则使用真实值 vs 预测值的散点图或残差图。
7.2 特征重要性分析
理解模型为什么做出预测,有时比预测本身更重要。这对于数学建模论文中的“模型解释”部分至关重要。
# 对于树模型(如随机森林、XGBoost),可以获取特征重要性 if hasattr(best_rf_model.named_steps['classifier'], 'feature_importances_'): # 获取预处理后的特征名称(对于OneHot编码,名称会扩展) # 注意:ColumnTransformer 转换后的特征名需要手动提取,稍复杂 # 这里提供一个简化示例,假设我们能够获得特征名列表 `feature_names` importances = best_rf_model.named_steps['classifier'].feature_importances_ # 假设我们有一个函数能获取 pipeline 最终的特征名 # feature_names = get_feature_names_from_pipeline(best_rf_model['preprocessor']) # 这里用占位符 feature_names = [f'feature_{i}' for i in range(len(importances))] # 创建重要性 DataFrame feat_imp_df = pd.DataFrame({ 'feature': feature_names, 'importance': importances }).sort_values('importance', ascending=False) # 可视化 top N 特征 top_n = 20 plt.figure(figsize=(10, 6)) sns.barplot(x='importance', y='feature', data=feat_imp_df.head(top_n)) plt.title(f'Top {top_n} Feature Importances (Random Forest)') plt.tight_layout() plt.show() print("\n特征重要性 Top 10:") print(feat_imp_df.head(10))实操心得:模型评估后,如果效果不理想,不要急于换更复杂的模型。回头检查:
- 数据问题:EDA是否充分?特征工程是否到位?有没有信息泄露?
- 评估方式问题:指标选对了吗?测试集划分是否合理?交叉验证过程是否正确?
- 简单模型基线:逻辑回归/线性回归这种简单模型的基线分数是多少?你的复杂模型比它好多少?如果好得不多,可能说明特征本身的信息量有限。
8. 模型部署、持久化与报告生成
模型通过测试后,工作还没结束。你需要保存模型,并生成可交付的结果。
8.1 模型持久化
使用joblib(对于sklearn模型通常比pickle更高效)保存训练好的管道。
import joblib import os # 创建保存模型的目录 model_dir = 'saved_models' os.makedirs(model_dir, exist_ok=True) # 保存最佳模型管道 model_path = os.path.join(model_dir, 'best_loan_default_model.pkl') joblib.dump(best_rf_model, model_path) print(f"模型已保存至: {model_path}") # 加载模型(在另一个脚本或环境中) # loaded_model = joblib.load(model_path) # new_predictions = loaded_model.predict(new_data)8.2 结果输出与报告
对于数学建模,最终需要提交论文和可能的结果文件。
def generate_predictions_and_report(model, X_test, y_test, output_dir='results'): """ 生成预测结果和评估报告文件。 """ os.makedirs(output_dir, exist_ok=True) # 1. 在测试集上进行预测 y_pred = model.predict(X_test) y_pred_proba = model.predict_proba(X_test) if hasattr(model, 'predict_proba') else None # 2. 保存预测结果到CSV results_df = X_test.copy() results_df['true_label'] = y_test.values results_df['predicted_label'] = y_pred if y_pred_proba is not None: # 保存每个类别的预测概率 for i in range(y_pred_proba.shape[1]): results_df[f'pred_prob_class_{i}'] = y_pred_proba[:, i] results_path = os.path.join(output_dir, 'test_set_predictions.csv') results_df.to_csv(results_path, index=False) print(f"预测结果已保存至: {results_path}") # 3. 生成文本格式的评估报告 report_path = os.path.join(output_dir, 'model_evaluation_report.txt') with open(report_path, 'w') as f: f.write("="*60 + "\n") f.write("模型评估报告\n") f.write("="*60 + "\n\n") f.write(f"模型类型: {type(model.named_steps['classifier']).__name__}\n") f.write(f"训练数据量: {len(X_train)}\n") f.write(f"测试数据量: {len(X_test)}\n\n") from sklearn.metrics import classification_report report_str = classification_report(y_test, y_pred) f.write("分类报告:\n") f.write(report_str) f.write("\n") f.write("混淆矩阵:\n") cm = confusion_matrix(y_test, y_pred) f.write(np.array2string(cm)) print(f"评估报告已保存至: {report_path}") # 4. 生成关键图表并保存 fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 混淆矩阵热力图 sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0]) axes[0].set_title('Confusion Matrix') axes[0].set_ylabel('True Label') axes[0].set_xlabel('Predicted Label') # 特征重要性图 (如果可用) if hasattr(model.named_steps['classifier'], 'feature_importances_'): importances = model.named_steps['classifier'].feature_importances_ # ... (获取特征名并排序的代码,同上) # 这里简化处理 indices = np.argsort(importances)[-10:] # 取最重要的10个 axes[1].barh(range(len(indices)), importances[indices]) axes[1].set_yticks(range(len(indices))) # axes[1].set_yticklabels([feature_names[i] for i in indices]) # 需要特征名 axes[1].set_xlabel('Feature Importance') axes[1].set_title('Top 10 Feature Importances') else: axes[1].text(0.5, 0.5, 'Feature Importance\nNot Available', ha='center', va='center', fontsize=12) axes[1].set_title('Feature Importance') plt.tight_layout() chart_path = os.path.join(output_dir, 'evaluation_charts.png') plt.savefig(chart_path, dpi=300, bbox_inches='tight') plt.close(fig) # 关闭图形,避免在Notebook中重复显示 print(f"评估图表已保存至: {chart_path}") # 调用函数生成报告 generate_predictions_and_report(best_rf_model, X_test, y_test)这个模板从数据到报告,形成了一个完整的闭环。它最大的价值在于提供了结构,迫使你按科学的步骤思考和工作。在实际的数学建模竞赛中,你可能需要针对赛题特点(如“2026亚太杯数学建模A题”可能涉及优化决策,“2024高教杯数学建模B题”可能涉及评价体系)调整模板中的某些模块,例如增加专门的评价模型模块或优化求解模块。但万变不离其宗,这个以sklearn Pipeline为核心的标准化工作流,能确保你的基础建模部分扎实、高效、可复现,让你有更多时间去攻克问题最核心的难点。记住,好的工具和流程不会限制你的创造力,而是为你腾出思考的空间。