尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

信贷数据预处理实战:从国赛C题看金融风控建模的数据基石

信贷数据预处理实战:从国赛C题看金融风控建模的数据基石 1. 项目背景与核心挑战从“国赛C题”看信贷数据预处理的实战价值每年九月的全国大学生数学建模竞赛国赛对于很多理工科学生而言不亚于一场“学术高考”。其中C题作为数据分析与建模类的经典题型往往聚焦于社会经济、金融管理等现实问题对参赛者的数据处理、模型构建和策略分析能力提出了极高要求。2020年的国赛C题“中小微企业的信贷策略”就是一个典型的金融风控场景下的数据科学实战项目。题目本身提供了一批企业的信贷相关数据要求参赛者通过数据分析和建模制定出合理的信贷策略。而这一切的基石正是数据预处理。很多初次接触这类赛题的同学拿到数据后的第一反应往往是直奔模型尝试各种复杂的算法结果却常常事倍功半模型效果不佳甚至无法运行。其根本原因就在于忽略了数据预处理这个至关重要的环节。金融数据尤其是涉及企业信贷的数据天生就带有“脏、乱、缺”的特性数据格式不统一、存在大量缺失值、包含异常或错误记录、量纲差异巨大。如果不经过系统性的清洗和转换再精妙的模型也只能是“垃圾进垃圾出”。因此一份高质量的“数据预处理代码”其价值远不止于完成赛题要求。它是一套将原始、混乱的业务数据转化为可供模型直接“消化”的、干净、规整的特征数据的标准化流水线。这个过程考验的是对业务逻辑的理解、对数据敏感度的把握以及将数据处理逻辑严谨地转化为代码的能力。今天我们就以2020年国赛C题为背景抛开具体的题目数据深入探讨一套通用、健壮、可复用的中小微企业信贷数据预处理代码框架的设计思路与核心实现。无论你是备战未来的数模竞赛还是从事相关的数据分析工作这套方法论都将为你提供清晰的行动指南。2. 信贷数据预处理的核心流程与设计哲学在动手写代码之前我们必须先建立正确的认知数据预处理不是一系列孤立操作的堆砌而是一个有明确目标、环环相扣的系统工程。其核心目标是构建高质量的特征为后续的信用评分、违约预测等模型服务。基于此我们可以将整个流程抽象为四个阶段我习惯称之为“数据预处理的四重奏”。2.1 第一阶段探索性数据分析——用眼睛“摸清”数据底细在清洗任何数据之前你必须先了解它。EDAExploratory Data Analysis就是你的“侦察兵”。这个阶段不求产出只求洞察。代码化的EDA不仅仅是生成几个图表更要系统性地记录数据的“健康状态”。首先是结构性探查。你需要用代码快速回答数据有多少行样本数、多少列特征数各列的数据类型是什么数值型、字符型、日期型内存占用情况如何这一步通常使用pandas的info()和describe()方法就能完成大半。但我会额外关注字符型字段中是否混入了本应是数值型的数据例如金额字段里出现了“暂无”、“-”等字符这往往是数据采集时的常见问题。其次是内容质量扫描。核心是三大问题的量化缺失值分析计算每个特征的缺失率。并非所有缺失都需要处理但你必须知道它们在哪里。一个高于50%缺失率的特征很可能直接考虑删除。唯一值分析对于分类特征查看其唯一值的个数和分布。如果某个特征如“企业名称”几乎每个样本都不同它作为模型特征的价值可能极低但作为ID标识符又有用。异常值侦测对于数值型特征通过箱线图Boxplot、描述性统计如观察均值与中位数的差异、标准差大小或基于标准差如3σ原则的方法进行初筛。例如一个企业的“注册资金”为负数或一个异常大的天文数字显然是错误。这个阶段的代码输出应该是一份数据质量报告它不修改原始数据而是为后续的清洗决策提供证据。我会将关键统计量缺失率、唯一值数、异常值数量汇总到一个DataFrame或生成一份简单的文本报告供后续步骤参考。2.2 第二阶段数据清洗与规整——解决“脏”和“乱”有了EDA的报告清洗就有了针对性。这一步是体力活但讲究方法和策略。缺失值处理是首要难题。粗暴地删除含有缺失值的样本行删除在样本量不大时是奢侈的。更常用的方法是填充Imputation。我的策略是分类型处理数值型特征若缺失率低如5%且分布相对均匀使用中位数填充通常比均值更稳健能避免极端值的影响。若特征与目标变量明显相关可考虑按目标类别分组后的中位数填充。分类特征最常用的是用“未知”或“Missing”作为一个新的类别进行填充。这比随意填充一个众数更能保留“缺失”这一信息本身可能蕴含的模式例如不愿填写某项信息的企业可能具有某种共性。时间序列特征对于时间序列数据前后向填充ffill/bfill或插值法可能更合适。高缺失率特征对于缺失率超过30%-50%的特征我会慎重考虑是否直接删除该特征因为不可靠的信息可能引入更多噪声。异常值处理需要结合业务判断。一个在统计学上是异常的值在业务上可能完全合理例如某高科技企业研发费用极高。因此我通常不直接删除而是采用盖帽法Capping或分箱法Binning。盖帽法是将超出特定分位数如1%和99%的值替换为该分位数的值。分箱法则是将连续值离散化到几个区间中异常值会被归入最高或最低的箱从而削弱其极端影响。这两种方法都能保留样本同时缓解异常值的破坏性。格式统一与错误纠正这是最琐碎但必不可少的一步。包括日期字段统一转换为datetime格式。金额、百分比等字符串字段去除“元”、“%”、“”等字符转换为数值型。统一分类特征的取值例如将“有限责任公司”、“有限公司”、“Ltd.”统一为“有限公司”。检查并修正明显的逻辑错误如“成立日期”晚于“首次信贷日期”。2.3 第三阶段特征工程——从原始数据中“创造”价值清洗后的干净数据只是原材料特征工程才是将其加工成“美味佳肴”的关键。在信贷场景下特征工程的核心是从企业静态信息和动态行为中提炼出反映其还款能力和还款意愿的指标。衍生特征构造是主要手段。例如从“利润总额”和“营业收入”可以计算销售利润率利润总额/营业收入这比单纯的利润额更能反映企业的盈利能力。从“流动资产”和“流动负债”可以计算流动比率这是衡量短期偿债能力的核心指标。从“资产负债率”总负债/总资产可以判断企业的长期财务风险。基于时间序列可以计算增长类指标如营业收入同比增长率、利润环比增长率等反映企业经营活力。交互特征也很有价值。例如将“行业类型”与“利润率”结合可以观察不同行业平均利润水平的差异将“企业规模”如注册资本分箱与“负债率”结合可以看不同规模企业的杠杆偏好。这个阶段非常依赖对金融和财务知识的理解。好的特征工程能够显著提升模型性能甚至比更换模型本身更有效。在代码实现上它表现为一系列基于已有列进行算术、逻辑运算生成新列的过程。2.4 第四阶段数据转换与编码——让数据“适配”模型经过特征工程我们得到了一个包含原始特征和衍生特征的数据集。但很多机器学习模型对数据格式有要求因此需要进行最后的转换。数值型特征标准化/归一化当特征量纲差异巨大时如“注册资本”是亿级“员工数”是百级基于距离的模型如KNN、SVM或使用梯度下降的模型如神经网络、线性回归会受到很大影响。常用方法有Z-score标准化将特征转换为均值为0、标准差为1的分布。适用于特征大致符合正态分布的情况。公式为(x - mean) / std。Min-Max归一化将特征缩放到[0, 1]区间。公式为(x - min) / (max - min)。对异常值比较敏感。在信贷建模中我通常优先使用RobustScaler基于分位数缩放因为它对异常值不敏感更适合金融数据可能存在偏态分布的特点。分类特征编码模型无法直接处理“制造业”、“服务业”这样的文本。必须将其转换为数值。标签编码Label Encoding为每个类别分配一个整数。慎用这会无意中引入顺序关系如012而很多分类特征是无序的。独热编码One-Hot Encoding为每个类别创建一个新的二值特征0/1。这是最安全、最常用的方法但缺点是如果类别很多高基数特征会产生大量稀疏特征增加计算负担。对于高基数特征如“企业所在城市”可以考虑使用目标编码Target Encoding即用该类别的目标变量如违约率的统计量如均值来替代类别本身。处理类别不平衡信贷数据中违约企业坏样本通常远少于正常企业好样本。这种严重的类别不平衡会导致模型倾向于预测多数类对少数类我们更关心的违约客户的预测能力很差。在预处理阶段我们可以使用过采样如SMOTE算法或欠采样来调整训练集的样本分布。注意这类操作仅针对训练集验证集和测试集必须保持原始分布以评估真实效果。3. 代码架构与模块化实现构建可复用的预处理流水线理解了流程接下来就是用代码将其固化。我强烈建议采用模块化、函数式的编程思想而不是写一个从头到尾的“面条代码”。这样不仅代码清晰、易于调试更能方便地复用到其他类似项目。下面我以一个假设的、结构化的DataPreprocessor类为例展示核心代码框架。3.1 类结构与初始化定义数据处理的“蓝图”我们首先定义一个预处理器类它将数据和处理逻辑封装在一起。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, OneHotEncoder, RobustScaler from sklearn.impute import SimpleImputer import warnings warnings.filterwarnings(ignore) class CreditDataPreprocessor: 中小微企业信贷数据预处理器。 封装了从EDA到特征编码的完整流程。 def __init__(self, df, target_colNone): 初始化预处理器。 参数: df (pd.DataFrame): 原始数据框。 target_col (str): 目标变量列名如‘是否违约’。用于指导某些处理策略。 self.df df.copy() # 关键处理数据的副本避免污染原数据 self.target_col target_col self.original_shape df.shape self.numerical_cols None self.categorical_cols None self.scaler None self.encoder None self.imputer_num None self.imputer_cat None print(f初始化完成原始数据形状: {self.original_shape})关键点在于使用df.copy()确保所有操作不影响传入的原始数据这是一个重要的编程习惯。3.2 EDA模块实现自动化生成数据体检报告我们将EDA过程封装成方法并输出结构化的报告。def perform_eda(self, save_reportFalse): 执行探索性数据分析生成数据质量报告。 report {} # 1. 基本概况 report[shape] self.df.shape report[dtypes] self.df.dtypes.to_dict() # 2. 缺失值分析 missing_stats self.df.isnull().sum() missing_pct (missing_stats / len(self.df)) * 100 missing_df pd.DataFrame({ 缺失数量: missing_stats, 缺失比例%: missing_pct.round(2) }).sort_values(缺失比例%, ascendingFalse) report[missing_info] missing_df[missing_df[缺失数量] 0] # 3. 数值型特征统计 self.numerical_cols self.df.select_dtypes(include[np.number]).columns.tolist() if self.numerical_cols: num_desc self.df[self.numerical_cols].describe().T num_desc[IQR] num_desc[75%] - num_desc[25%] # 计算四分位距 # 识别潜在异常值基于IQR法则 num_desc[lower_bound] num_desc[25%] - 1.5 * num_desc[IQR] num_desc[upper_bound] num_desc[75%] 1.5 * num_desc[IQR] # 统计超出边界的样本数近似异常值数量 outlier_counts {} for col in self.numerical_cols: lower num_desc.at[col, lower_bound] upper num_desc.at[col, upper_bound] count ((self.df[col] lower) | (self.df[col] upper)).sum() outlier_counts[col] count num_desc[异常值数量(近似)] pd.Series(outlier_counts) report[numerical_stats] num_desc # 4. 分类特征统计 self.categorical_cols self.df.select_dtypes(include[object, category]).columns.tolist() if self.target_col and self.target_col in self.categorical_cols: self.categorical_cols.remove(self.target_col) # 目标变量如果是分类的单独处理 cat_info {} for col in self.categorical_cols: cat_info[col] { 唯一值数量: self.df[col].nunique(), 最高频值: self.df[col].mode().iloc[0] if not self.df[col].mode().empty else None, 最高频占比%: (self.df[col].value_counts(normalizeTrue).iloc[0] * 100).round(2) } report[categorical_info] pd.DataFrame(cat_info).T self.eda_report report print(EDA报告已生成。) print(f数值型特征: {len(self.numerical_cols)} 个) print(f分类特征: {len(self.categorical_cols)} 个) if save_report: # 可以将report保存为JSON或Excel便于查阅 pass return report这个perform_eda方法不仅打印信息还将关键统计量存储在self.eda_report和类属性中为后续的清洗步骤提供数据支持。3.3 清洗与填充模块基于策略的智能处理清洗逻辑需要根据EDA报告来动态调整。这里展示一个包含策略选择的清洗方法。def clean_and_impute(self, missing_num_strategymedian, missing_cat_strategyconstant, drop_high_missingTrue, missing_threshold0.5): 数据清洗与缺失值填充。 参数: missing_num_strategy: 数值型缺失填充策略mean, median, constant。 missing_cat_strategy: 分类型缺失填充策略most_frequent, constant。 drop_high_missing: 是否删除高缺失率特征。 missing_threshold: 判定为高缺失率的阈值比例。 df self.df # 1. 处理高缺失率特征 if drop_high_missing and hasattr(self, eda_report): missing_pct_series self.eda_report[missing_info][缺失比例%] / 100 high_missing_cols missing_pct_series[missing_pct_series missing_threshold].index.tolist() if high_missing_cols: print(f正在删除缺失率 {missing_threshold*100}% 的特征: {high_missing_cols}) df.drop(columnshigh_missing_cols, inplaceTrue) # 更新特征列表 self.numerical_cols [col for col in self.numerical_cols if col not in high_missing_cols] self.categorical_cols [col for col in self.categorical_cols if col not in high_missing_cols] # 2. 分离数值和分类特征再次确认因为可能删除了列 current_num_cols [col for col in self.numerical_cols if col in df.columns] current_cat_cols [col for col in self.categorical_cols if col in df.columns] # 3. 数值型特征缺失值填充 if current_num_cols: self.imputer_num SimpleImputer(strategymissing_num_strategy) df[current_num_cols] self.imputer_num.fit_transform(df[current_num_cols]) print(f数值型特征缺失值已使用{missing_num_strategy}策略填充。) # 4. 分类特征缺失值填充 if current_cat_cols: fill_value Missing if missing_cat_strategy constant else None self.imputer_cat SimpleImputer(strategymissing_cat_strategy, fill_valuefill_value) df[current_cat_cols] self.imputer_cat.fit_transform(df[current_cat_cols]) print(f分类特征缺失值已使用{missing_cat_strategy}策略填充。) # 5. 异常值处理示例对数值型特征进行盖帽处理Winsorization for col in current_num_cols: q_low df[col].quantile(0.01) q_high df[col].quantile(0.99) df[col] df[col].clip(lowerq_low, upperq_high) print(数值型特征已进行1%-99%分位数盖帽处理以缓解极端值影响。) self.df df print(数据清洗与填充完成。)注意盖帽法.clip的参数0.01, 0.99需要根据实际数据分布调整。对于非常偏态的数据可能需要使用对数变换后再进行盖帽。3.4 特征工程模块注入业务逻辑的灵魂这里我们实现几个常见的信贷衍生特征。在实际比赛中你需要根据题目给出的具体字段进行更有针对性的创造。def create_features(self): 基于业务逻辑创建衍生特征。 注意此函数高度依赖原始数据的具体列名需根据实际情况修改。 df self.df # 假设原始数据中存在以下字段根据2020国赛C题数据假设 # 流动资产合计, 流动负债合计, 利润总额, 营业收入, 负债合计, 资产总计 # 1. 偿债能力指标 if all(col in df.columns for col in [流动资产合计, 流动负债合计]): df[流动比率] df[流动资产合计] / (df[流动负债合计] 1e-5) # 防止除零 print(已创建特征流动比率) # 2. 盈利能力指标 if all(col in df.columns for col in [利润总额, 营业收入]): df[销售利润率] df[利润总额] / (df[营业收入].abs() 1e-5) # 营业收入可能为负或零 print(已创建特征销售利润率) # 3. 财务风险指标 if all(col in df.columns for col in [负债合计, 资产总计]): df[资产负债率] df[负债合计] / (df[资产总计] 1e-5) print(已创建特征资产负债率) # 4. 增长类指标假设有历史年份数据这里以简化示例 # 如果数据包含多期报表可以计算同比增长率等 self.df df print(衍生特征创建完成。) # 更新数值型特征列表因为新创建的特征是数值型的 self.numerical_cols df.select_dtypes(include[np.number]).columns.tolist() if self.target_col and self.target_col in self.numerical_cols: self.numerical_cols.remove(self.target_col)3.5 编码与缩放模块为模型准备“标准餐”这是流水线的最后一步将数据转换为模型友好的格式。def encode_and_scale(self, scale_numTrue, encode_catTrue, scaler_typerobust): 对特征进行编码和缩放。 参数: scale_num: 是否缩放数值特征。 encode_cat: 是否编码分类特征。 scaler_type: 缩放器类型standardZ-score或robust基于分位数。 df self.df # 分离特征和目标如果存在 if self.target_col and self.target_col in df.columns: y df[self.target_col] X df.drop(columns[self.target_col]) # 更新特征列表排除目标列 self.numerical_cols [col for col in self.numerical_cols if col ! self.target_col] else: X df y None # 1. 分类特征编码 (One-Hot) if encode_cat and self.categorical_cols: # 确保分类特征列表中的列仍然在X中 cat_cols_to_encode [col for col in self.categorical_cols if col in X.columns] if cat_cols_to_encode: self.encoder OneHotEncoder(sparse_outputFalse, handle_unknownignore) # 注意sparse_output参数名可能随版本变化 X_encoded self.encoder.fit_transform(X[cat_cols_to_encode]) # 获取编码后的特征名 encoded_feature_names self.encoder.get_feature_names_out(cat_cols_to_encode) X_encoded_df pd.DataFrame(X_encoded, columnsencoded_feature_names, indexX.index) # 删除原始分类列拼接编码后的列 X X.drop(columnscat_cols_to_encode) X pd.concat([X, X_encoded_df], axis1) print(f分类特征 {cat_cols_to_encode} 已进行独热编码。) # 2. 数值特征缩放 if scale_num and self.numerical_cols: # 确保数值特征列表中的列仍然在X中 num_cols_to_scale [col for col in self.numerical_cols if col in X.columns] if num_cols_to_scale: if scaler_type robust: self.scaler RobustScaler() else: self.scaler StandardScaler() X[num_cols_to_scale] self.scaler.fit_transform(X[num_cols_to_scale]) print(f数值特征 {num_cols_to_scale} 已使用 {scaler_type} 进行缩放。) # 重新组合数据 if y is not None: self.df_processed pd.concat([X, y], axis1) else: self.df_processed X print(特征编码与缩放完成。) return self.df_processed3.6 完整流水线调用示例将上述模块组合起来就形成了一条完整的预处理流水线。# 假设 raw_df 是你的原始Pandas DataFramelabel是目标变量列名 preprocessor CreditDataPreprocessor(raw_df, target_collabel) # 第一步探索数据 report preprocessor.perform_eda(save_reportTrue) # 第二步清洗与填充根据EDA报告调整参数 preprocessor.clean_and_impute(missing_num_strategymedian, missing_cat_strategyconstant, drop_high_missingTrue, missing_threshold0.3) # 第三步特征工程根据你的数据字段定制 preprocessor.create_features() # 第四步编码与缩放得到最终可用于建模的数据 final_df preprocessor.encode_and_scale(scale_numTrue, encode_catTrue, scaler_typerobust) print(f预处理完成。原始形状: {preprocessor.original_shape}, 处理后形状: {final_df.shape})4. 实战中的关键技巧与避坑指南有了框架代码并不意味着就能高枕无忧。在实际操作中尤其是像国赛这样时间紧、任务重的场景以下几个技巧和坑点至关重要。4.1 技巧一保持训练集与测试集处理的一致性这是建模中最容易出错的地方之一。所有从数据中学习到的参数如填充值、缩放器的均值标准差、编码器的类别映射都必须仅从训练集中学习然后应用到验证集和测试集上。否则就会导致数据泄露即测试集信息“污染”了训练过程使模型评估结果虚高失去泛化能力。在我们的CreditDataPreprocessor类中SimpleImputer、RobustScaler、OneHotEncoder等转换器都是在fit_transform训练集时确定的参数。在预测新数据测试集时必须使用transform方法而不是再次fit_transform。因此在实际项目中你需要将预处理类实例化两次一次用于训练集调用fit_transform系列方法保存下这些转换器另一次用于测试集加载这些转换器并只调用transform方法。4.2 技巧二谨慎处理高基数分类特征与稀有类别对于像“企业详细地址”这类具有成千上万个唯一值的分类特征直接进行独热编码会产生维度灾难。常见的处理方法是目标编码用该类别下目标变量的均值对于回归或正例比例对于分类来替代类别本身。这非常有效但必须严格在训练集内进行并注意平滑处理以防止过拟合。频率编码用该类别的出现频率来编码。出现次数少的类别会被赋予相似的小数值。聚类或嵌入对于更复杂的场景可以将高基数特征先进行聚类或者使用类似NLP中的词嵌入思想学习其分布式表示。此外对于分类特征中的稀有类别出现次数极少在独热编码后它们在训练集中可能只出现一两次导致模型无法学习其规律反而可能引入噪声。一个实用的技巧是将这些稀有类别统一归为“其他”类别。4.3 技巧三利用管道构建自动化流程对于更复杂的流程包括后续的建模步骤强烈推荐使用sklearn.pipeline.Pipeline。它可以将预处理、特征选择、建模等多个步骤封装成一个整体对象确保每一步都在交叉验证中正确进行避免数据泄露并使代码极其简洁。from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestClassifier # 假设我们有一个自定义的转换器类继承BaseEstimator, TransformerMixin # 这里简化表示实际需要将我们的预处理步骤包装成转换器 numeric_transformer Pipeline(steps[ (imputer, SimpleImputer(strategymedian)), (scaler, RobustScaler()) ]) categorical_transformer Pipeline(steps[ (imputer, SimpleImputer(strategyconstant, fill_valuemissing)), (onehot, OneHotEncoder(handle_unknownignore)) ]) # 使用ColumnTransformer分别处理数值和分类列 from sklearn.compose import ColumnTransformer preprocessor ColumnTransformer( transformers[ (num, numeric_transformer, numerical_cols), (cat, categorical_transformer, categorical_cols) ]) # 构建从预处理到建模的完整管道 clf Pipeline(steps[ (preprocessor, preprocessor), (classifier, RandomForestClassifier()) ]) # 现在你可以像使用一个普通分类器一样使用clf # clf.fit(X_train, y_train) # clf.predict(X_test)4.4 避坑指南国赛C题数据预处理中的典型陷阱结合历年国赛C题信贷、征信类的经验有几个陷阱需要特别留意时间戳陷阱题目数据往往包含多个时间点如不同年份的报表。务必检查时间逻辑确保用于预测的特征在时间上都早于目标事件如违约。绝对不能使用未来的信息预测过去。指标计算陷阱在计算财务比率时分母可能为零或负数如营业收入为负。代码中必须加入极小的保护值如 1e-5或进行特殊处理如将比率设为缺失值或一个极大/极小的标志值。样本定义陷阱明确你要预测的是什么。是“当前申请贷款的企业未来是否会违约”还是“历史上所有贷款中哪些会违约”这决定了你如何划分训练集和测试集以及如何构造样本。在时间序列背景下必须使用时间序列交叉验证或按时间划分数据集而不是随机划分。内存与效率陷阱当数据量较大或进行独热编码后特征维度可能爆炸。在比赛环境中要注意代码的运行效率和内存占用。可以考虑使用稀疏矩阵存储独热编码结果或使用category数据类型来节省内存。数据预处理是数学建模和数据分析工作中最耗时、但也最见功力的环节。它没有一成不变的“银弹”需要你根据数据的具体情况和业务目标灵活运用各种工具和方法。2020年国赛C题的信贷策略问题其核心挑战之一就在于如何从那份不完美、不完整的企业数据中提炼出最能表征其信用风险的特征。本文提供的代码框架和思路旨在为你构建一个坚实、可靠的起点。真正的智慧在于你根据对题目的深入理解在这个框架上进行的每一次微调、创造的每一个新特征、以及为避开每一个陷阱所做出的谨慎判断。记住干净、有信息量的数据是任何优秀模型的生命线。
返回列表