📊 项目概述
在金融科技快速发展的今天,信贷风险管理成为金融机构的核心竞争力。本项目基于某金融机构脱敏历史数据,通过系统化的数据预处理和特征工程,对比分析了四种机器学习模型在贷款违约预测任务中的表现。经过严格评估,Logistic回归模型以0.8753的AUC值脱颖而出,为金融机构的风险控制提供了有力工具。
🔍原始数据分类与用途
原始数据链接: 【免费】项目案例1-贷款违约行为预测数据集资源-CSDN下载
1. 基本信息表
-
contest_basic_train.tsv(训练集,3万行,11列) -
contest_basic_test.tsv(测试集,1万行,10列) -
用途:包含个人基本信息,用于构建信用评分模型
-
关键字段:REPORT_ID、ID_CARD、LOAN_DATE、教育程度、婚姻状况、薪资等
-
标签 :train表中的
Y列(可能表示违约/不良贷款)
2. 贷款账户明细
-
contest_ext_crd_cd_ln.tsv(35.7万行,22列) -
用途:详细记录每笔贷款的状态
-
关键信息:贷款状态、金融机构、贷款类型、信用额度、余额、逾期情况等
-
特点:包含大量金融术语(五级分类、还款状态等)
3. 贷记卡账户明细
-
contest_ext_crd_cd_lnd.tsv(32.4万行,20列) -
用途:信用卡/贷记卡使用记录
-
关键信息:信用额度、已用额度、还款状态、逾期记录等
4. 特殊交易记录
-
contest_ext_crd_cd_ln_spl.tsv(6.7万行,6列) -
用途:记录提前还款、展期等特殊交易
-
contest_ext_crd_cd_lnd_ovd.csv(19.9万行,4列) -
用途:贷记卡逾期明细
5. 信用报告查询记录
-
contest_ext_crd_hd_report.csv(4万行,4列):报告头信息 -
contest_ext_crd_qr_recorddtlinfo.tsv(65.4万行,4列):详细查询记录 -
contest_ext_crd_qr_recordsmr.tsv(760行,3列):查询记录汇总 -
用途:记录征信查询历史,查询频繁可能表示高风险
6. 信用汇总信息
-
contest_ext_crd_is_creditcue.csv(4万行,11列):信用提示汇总 -
contest_ext_crd_is_ovdsummary.csv(7.6万行,6列):逾期汇总 -
contest_ext_crd_is_sharedebt.csv(7.6万行,11列):负债共享信息 -
用途:从不同维度汇总信用状况
7. 欺诈标签
-
contest_fraud.tsv(4万行,2列) -
用途:反欺诈标签,Y_FRAUD表示是否为欺诈
🔍数据清洗后特征概览
-
样本规模:30,000条历史贷款记录
-
特征数量:87个(经过特征工程后)
-
类别分布:正样本(违约)1,875条,负样本28,125条
-
违约率:6.25%(典型的金融不平衡数据)
⚙️ 技术实现流程
1. 智能数据预处理系统
项目采用智能判断机制:系统自动检测是否存在预处理文件,如有则直接加载,否则执行完整的预处理流程。这种设计既节省了重复计算时间,又确保了数据的可复现性。
核心预处理步骤包括:
-
变量属性识别与标注:将身份证号、婚姻状态等转换为数值特征
-
空值智能填充:根据特征重要性采用不同策略
-
异常值处理:采用winsorize变换限制极端值影响
-
维归约与独热编码:减少维度同时保留信息
-
标准化处理:统一量纲,提升模型收敛速度
2. 四种模型对比设计
严格遵循项目案例要求,我们实现了以下四种经典分类算法:
python
# 模型配置概要
1. Logistic回归(L2正则化) - 线性模型的经典代表
2. 朴素贝叶斯 - 基于概率统计的轻量级模型
3. 随机森林 - 集成学习的强大工具
4. 支持向量机(SVM) - 高维空间分类器
所有模型均使用class_weight='balanced'参数处理数据不平衡问题,采用分层抽样确保训练/测试集分布一致。
📈 模型性能深度分析
AUC性能排行榜
| 模型 | 训练集AUC | 测试集AUC | 交叉验证AUC | 过拟合程度 | 性能评级 |
|---|---|---|---|---|---|
| Logistic回归 | 0.8719 | 0.8753 | 0.8618±0.0045 | 最低 | ⭐⭐⭐⭐⭐ |
| SVM | 0.9712 | 0.8575 | 0.8421±0.0048 | 最高 | ⭐⭐⭐ |
| 随机森林 | 0.9400 | 0.8383 | 0.8200±0.0087 | 高 | ⭐⭐⭐ |
| 朴素贝叶斯 | 0.8385 | 0.8331 | 0.8298±0.0103 | 低 | ⭐⭐ |
关键发现与洞见
🎯 Logistic回归:稳健的冠军
Logistic回归在本项目中表现最为出色,主要原因在于:
-
特征维度适中:87个特征既不过少导致欠拟合,也不过多引发维度灾难
-
正则化有效:L2正则化防止过拟合,提升泛化能力
-
线性可分性:大部分特征与违约风险呈近似线性关系
-
计算效率高:训练仅需数秒,适合生产环境部署
🔥 SVM:潜力与挑战并存
SVM的训练集AUC高达0.9712,但测试集降至0.8575,揭示出明显过拟合。分析原因:
-
RBF核复杂度高:容易捕捉训练数据噪声
-
参数敏感性强:C值和gamma选择对性能影响显著
-
计算成本大:O(n²)复杂度在24,000训练样本上耗时较长
🌳 随机森林:特征洞察的价值
虽然AUC表现中等,但随机森林提供了宝贵的特征重要性分析:
python
# Top 5重要特征
1. WORK_PROVINCE_33 (0.086) - 特定省份的违约风险较高
2. WORK_PROVINCE_35 (0.074) - 另一个高风险省份
3. SALARY (0.062) - 收入水平直接影响还款能力
4. WORK_PROVINCE_41 (0.053) - 地域经济因素显著
5. FIRST_LOANCARD_OPEN_MONTH (0.037) - 首张信用卡开户时间
这些发现为业务决策提供了直接依据:地域和收入是贷款违约的关键预测因素。
📊 朴素贝叶斯:理论假设的局限
朴素贝叶斯表现相对较差,主要原因是其"特征条件独立"假设与实际情况不符。金融特征之间往往存在复杂的相关性(如收入与教育水平、工作地区与经济状况等),这一假设的违反限制了模型的预测能力。
🎨 可视化成果展示
1. 测试集AUC对比折线图

清晰展示了四种模型的性能差异,Logistic回归以微弱优势领先,但所有模型均超过0.83的AUC值,表明本项目的数据预处理和特征工程相当成功。
2. ROC曲线对比图

ROC曲线直观展示了模型在不同阈值下的表现。Logistic回归曲线最靠近左上角,说明其在各个阈值下都能保持较好的真阳性率和假阳性率平衡。
💡 业务应用建议
模型选择策略
-
生产环境首选:Logistic回归 - AUC最高、训练快、易解释
-
探索性分析:随机森林 - 提供特征重要性洞察
-
基准模型:朴素贝叶斯 - 快速建立性能基线
-
特定场景:SVM - 在小样本、高维特征场景下可能有优势
风险管理应用
基于模型预测结果,金融机构可以:
-
风险分层:将客户分为低、中、高风险组,实施差异化信贷政策
-
动态定价:根据违约概率调整利率和额度
-
早期预警:对高风险客户加强贷后管理
-
策略优化:分析重要特征,优化营销和风控策略
🚀 技术优化方向
短期改进
-
特征工程深化:从身份证提取年龄、出生季节等信息
-
集成方法尝试:Stacking或Voting结合多种模型优势
-
参数调优:使用网格搜索或贝叶斯优化寻找最优超参数
长期发展
-
时序特征引入:加入客户历史行为的时间序列模式
-
深度学习探索:尝试神经网络处理复杂特征交互
-
在线学习系统:实现模型实时更新,适应市场变化
🎯 核心经验总结
成功要素
-
数据预处理决定上限:精细的特征工程是模型成功的基础
-
模型简单性优势:在特征维度适中的情况下,简单模型往往表现更好
-
不平衡数据处理:分层抽样和class_weight参数有效缓解了类别不平衡问题
-
多维度评估:不仅关注AUC,还考察过拟合、稳定性和可解释性
教训反思
-
警惕过拟合陷阱:训练集表现太好可能意味着泛化能力不足
-
模型假设验证:选择模型前需验证其假设是否与数据特性相符
-
计算资源考量:在实际应用中需平衡预测精度与计算成本
🌟 结语
本项目展示了机器学习在金融风控领域的实际应用价值。通过系统化的方法,我们不仅构建了有效的预测模型,更重要的是深入理解了不同算法的特性及其适用场景。Logistic回归的优异表现再次验证了"简单即有效"的机器学习原则,特别是在特征工程充分的情况下。
在金融科技快速发展的今天,数据驱动的风险管理正成为行业标准。本项目的实践经验表明,通过合理的特征工程和模型选择,机器学习能够为信贷决策提供有力的量化支持。未来,随着更多数据源的整合和算法的进步,智能风控系统的能力将进一步提升,为金融行业的健康发展保驾护航。
技术栈 :Python, Pandas, Scikit-learn, Matplotlib, Seaborn
关键词 :贷款违约预测、机器学习、特征工程、模型对比、AUC分析
适用场景:金融风控、信贷评估、风险管理、数据科学实践
完整代码:
python
"""
贷款违约预测完整系统 - 智能判断版本
自动检测数据文件,如果没有预处理则先预处理,否则直接使用已有文件
包含四种模型对比和完整的可视化输出
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import os
import warnings
warnings.filterwarnings('ignore')
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# ===================== 配置参数 =====================
DATA_ROOT = r"./"
RANDOM_SEED = 42
TEST_SIZE = 0.2
class DataPreprocessor:
"""数据预处理器 - 智能判断是否需要进行预处理"""
def __init__(self, data_root):
self.data_root = data_root
self.raw_files = {
'train_basic': 'contest_basic_train.tsv',
'test_basic': 'contest_basic_test.tsv',
'table_2': 'contest_ext_crd_cd_ln.tsv',
'table_3': 'contest_ext_crd_cd_ln_spl.tsv',
'table_4': 'contest_ext_crd_cd_lnd.tsv',
'table_5': 'contest_ext_crd_cd_lnd_ovd.csv',
'table_6': 'contest_ext_crd_hd_report.csv',
'table_7': 'contest_ext_crd_is_creditcue.csv',
'table_8': 'contest_ext_crd_is_ovdsummary.csv',
'table_9': 'contest_ext_crd_is_sharedebt.csv',
'table_10': 'contest_ext_crd_qr_recorddtlinfo.tsv',
'table_11': 'contest_ext_crd_qr_recordsmr.tsv',
'fraud': 'contest_fraud.tsv'
}
self.processed_file = 'train_final_processed.csv'
def check_files_exist(self):
"""检查所有必要的原始文件是否存在"""
print("检查原始数据文件...")
missing_files = []
for file_key, filename in self.raw_files.items():
file_path = os.path.join(self.data_root, filename)
if os.path.exists(file_path):
print(f" ✓ {filename}")
else:
print(f" ✗ {filename} (缺失)")
missing_files.append(filename)
if missing_files:
print(f"\n警告: 缺失 {len(missing_files)} 个文件:")
for f in missing_files:
print(f" - {f}")
return False
return True
def check_processed_file_exists(self):
"""检查预处理文件是否存在"""
processed_path = os.path.join(self.data_root, self.processed_file)
if os.path.exists(processed_path):
print(f"✓ 预处理文件已存在: {self.processed_file}")
return True
else:
print(f"✗ 预处理文件不存在,需要重新预处理")
return False
def load_raw_table(self, filename, is_tsv=True):
"""加载原始表格"""
file_path = os.path.join(self.data_root, filename)
try:
if is_tsv:
df = pd.read_csv(file_path, sep='\t', low_memory=False, encoding='utf-8')
else:
df = pd.read_csv(file_path, low_memory=False, encoding='utf-8')
return df
except Exception as e:
print(f"加载 {filename} 出错: {e}")
return None
def preprocess_basic_table(self, df_train):
"""预处理基础训练表"""
print("预处理基础训练表...")
# 1. 从身份证提取性别
def extract_gender(id_card):
try:
if pd.isna(id_card):
return np.nan
id_str = str(id_card)
if len(id_str) >= 17:
gender_code = int(id_str[16])
return 1 if gender_code % 2 == 1 else 0
return np.nan
except:
return np.nan
df_train['gender'] = df_train['ID_CARD'].apply(extract_gender)
# 2. 处理IS_LOCAL
is_local_replace_dict = {"本地户籍": 10, "非本地户籍": 11}
df_train["IS_LOCAL"] = df_train["IS_LOCAL"].map(is_local_replace_dict)
# 3. 处理MARRY_STATUS
marry_status_dict = {"已婚": 10, "未婚": 11, "离异": 12, "离婚": 12, "其他": 14, "丧偶": 15}
df_train["MARRY_STATUS"] = df_train["MARRY_STATUS"].map(marry_status_dict)
# 4. 处理WORK_PROVINCE空值
def fill_province(row):
if pd.isna(row['WORK_PROVINCE']) and not pd.isna(row['ID_CARD']):
try:
id_str = str(row['ID_CARD'])
if len(id_str) >= 2:
return id_str[:2]
except:
pass
elif not pd.isna(row['WORK_PROVINCE']):
try:
return str(int(row['WORK_PROVINCE']))[:2]
except:
pass
return np.nan
df_train['WORK_PROVINCE'] = df_train.apply(fill_province, axis=1)
# 5. 删除AGENT列
if 'AGENT' in df_train.columns:
df_train = df_train.drop(['AGENT'], axis=1)
# 6. 填充其他空值
if 'SALARY' in df_train.columns:
df_train['SALARY'] = df_train['SALARY'].fillna(df_train['SALARY'].mean())
if 'EDU_LEVEL' in df_train.columns:
edu_dict = {"高中": 1, "专科": 2, "本科": 3, "硕士": 4, "博士": 5, "其他": 0}
df_train['EDU_LEVEL'] = df_train['EDU_LEVEL'].map(edu_dict)
df_train['EDU_LEVEL'] = df_train['EDU_LEVEL'].fillna(df_train['EDU_LEVEL'].mode()[0])
if 'HAS_FUND' in df_train.columns:
df_train['HAS_FUND'] = df_train['HAS_FUND'].fillna(0)
# 7. 异常值处理(winsorize)
if 'SALARY' in df_train.columns:
q1 = df_train['SALARY'].quantile(0.01)
q99 = df_train['SALARY'].quantile(0.99)
df_train['SALARY'] = df_train['SALARY'].clip(lower=q1, upper=q99)
# 8. 省份编码简化
if 'WORK_PROVINCE' in df_train.columns:
province_counts = df_train['WORK_PROVINCE'].value_counts()
rare_provinces = province_counts[province_counts < 50].index
df_train['WORK_PROVINCE'] = df_train['WORK_PROVINCE'].apply(
lambda x: '99' if x in rare_provinces else x
)
# 9. 独热编码
categorical_cols = ['IS_LOCAL', 'WORK_PROVINCE', 'MARRY_STATUS']
categorical_cols = [col for col in categorical_cols if col in df_train.columns]
for col in categorical_cols:
df_train[col] = df_train[col].astype(str)
df_train = pd.get_dummies(df_train, columns=categorical_cols, drop_first=True)
# 10. 标准化
continuous_cols = ['SALARY', 'EDU_LEVEL', 'HAS_FUND']
continuous_cols = [col for col in continuous_cols if col in df_train.columns]
if 'LOAN_DATE' in df_train.columns:
try:
df_train['LOAN_DATE'] = pd.to_datetime(df_train['LOAN_DATE'], errors='coerce')
reference_date = df_train['LOAN_DATE'].min()
df_train['LOAN_DAYS'] = (df_train['LOAN_DATE'] - reference_date).dt.days
continuous_cols.append('LOAN_DAYS')
df_train = df_train.drop(['LOAN_DATE'], axis=1)
except:
df_train = df_train.drop(['LOAN_DATE'], axis=1)
for col in continuous_cols:
if col in df_train.columns:
mean_val = df_train[col].mean()
std_val = df_train[col].std()
if std_val > 0:
df_train[col] = (df_train[col] - mean_val) / std_val
else:
df_train[col] = 0
return df_train
def preprocess_other_table(self, df, table_name):
"""预处理其他表格"""
# 统一REPORT_ID列名
if 'report_id' in df.columns:
df = df.rename(columns={'report_id': 'REPORT_ID'})
# 删除重复行
df = df.drop_duplicates()
# 删除空值过多的列(>70%)
null_percentage = df.isnull().sum() / len(df)
columns_to_drop = null_percentage[null_percentage > 0.7].index.tolist()
if 'REPORT_ID' in columns_to_drop:
columns_to_drop.remove('REPORT_ID')
df = df.drop(columns=columns_to_drop, axis=1, errors='ignore')
# 如果有重复REPORT_ID,进行聚合
if len(df) > df['REPORT_ID'].nunique():
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
if 'REPORT_ID' in numeric_cols:
numeric_cols.remove('REPORT_ID')
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
agg_dict = {}
for col in numeric_cols:
if col in df.columns:
agg_dict[col] = 'mean'
for col in categorical_cols:
if col in df.columns and col != 'REPORT_ID':
agg_dict[col] = lambda x: x.iloc[0] if len(x) > 0 else np.nan
df = df.groupby('REPORT_ID', as_index=False).agg(agg_dict)
# 填充空值
for col in df.columns:
if col != 'REPORT_ID':
if df[col].dtype in ['int64', 'float64']:
df[col] = df[col].fillna(df[col].median())
elif df[col].dtype == 'object':
mode_val = df[col].mode()
if len(mode_val) > 0:
df[col] = df[col].fillna(mode_val.iloc[0])
else:
df[col] = df[col].fillna('未知')
# 添加表名前缀
for col in df.columns:
if col != 'REPORT_ID':
df = df.rename(columns={col: f'{table_name}_{col}'})
return df
def merge_all_tables(self, df_train, other_dfs):
"""合并所有表格"""
df_all = df_train.copy()
for table_name, df_table in other_dfs.items():
if 'REPORT_ID' in df_table.columns:
print(f"合并 {table_name}...")
df_all = pd.merge(
df_all,
df_table,
how='left',
left_on='REPORT_ID',
right_on='REPORT_ID',
suffixes=('', f'_{table_name}_dup')
)
# 删除重复列
df_all = df_all.loc[:, ~df_all.columns.duplicated()]
return df_all
def final_preprocessing(self, df_all):
"""最终全局预处理"""
# 删除ID列
if 'ID_CARD' in df_all.columns:
df_all = df_all.drop(['ID_CARD'], axis=1)
# 删除空值过多的列(>50%)
null_percentage = df_all.isnull().sum() / len(df_all)
columns_to_drop = null_percentage[null_percentage > 0.5].index.tolist()
if 'REPORT_ID' in columns_to_drop:
columns_to_drop.remove('REPORT_ID')
if 'Y' in columns_to_drop:
columns_to_drop.remove('Y')
df_all = df_all.drop(columns=columns_to_drop, axis=1, errors='ignore')
# 填充剩余空值
for col in df_all.columns:
if col not in ['REPORT_ID', 'Y']:
if df_all[col].dtype in ['int64', 'float64']:
df_all[col] = df_all[col].fillna(df_all[col].median())
elif df_all[col].dtype == 'object':
mode_vals = df_all[col].mode()
if len(mode_vals) > 0:
df_all[col] = df_all[col].fillna(mode_vals.iloc[0])
# 特征选择:删除低方差特征
from sklearn.feature_selection import VarianceThreshold
X = df_all.drop(['REPORT_ID', 'Y'], axis=1, errors='ignore')
y = df_all['Y']
# 确保所有特征都是数值型
for col in X.columns:
if X[col].dtype == 'object':
try:
X[col] = X[col].astype('category').cat.codes
except:
X[col] = pd.factorize(X[col])[0]
# 删除低方差特征
selector = VarianceThreshold(threshold=0.01)
X_selected = selector.fit_transform(X)
selected_features = X.columns[selector.get_support()]
# 创建最终DataFrame
X_selected_df = pd.DataFrame(X_selected, columns=selected_features)
X_selected_df['REPORT_ID'] = df_all['REPORT_ID'].values
X_selected_df['Y'] = y.values
return X_selected_df
def run_preprocessing(self):
"""执行完整的数据预处理流程"""
print("\n" + "="*60)
print("开始数据预处理流程")
print("="*60)
# 1. 加载基础训练表
print("1. 加载基础训练表...")
df_train = self.load_raw_table(self.raw_files['train_basic'], is_tsv=True)
if df_train is None:
return None
# 2. 预处理基础表
print("2. 预处理基础表...")
df_train = self.preprocess_basic_table(df_train)
print(f" 基础表预处理完成,维度: {df_train.shape}")
# 3. 加载并预处理其他表格
print("3. 加载并预处理其他表格...")
other_tables = {
'table_2': (self.raw_files['table_2'], True),
'table_3': (self.raw_files['table_3'], True),
'table_4': (self.raw_files['table_4'], True),
'table_5': (self.raw_files['table_5'], False),
'table_6': (self.raw_files['table_6'], False),
'table_7': (self.raw_files['table_7'], False),
'table_8': (self.raw_files['table_8'], False),
'table_9': (self.raw_files['table_9'], False),
'table_10': (self.raw_files['table_10'], True),
'table_11': (self.raw_files['table_11'], True),
}
other_dfs = {}
for table_name, (filename, is_tsv) in other_tables.items():
print(f" 处理 {table_name}...")
df_table = self.load_raw_table(filename, is_tsv)
if df_table is not None:
df_table = self.preprocess_other_table(df_table, table_name)
other_dfs[table_name] = df_table
print(f" {table_name}维度: {df_table.shape}")
# 4. 合并所有表格
print("4. 合并所有表格...")
df_all = self.merge_all_tables(df_train, other_dfs)
print(f" 合并后维度: {df_all.shape}")
# 5. 最终预处理
print("5. 最终预处理...")
df_final = self.final_preprocessing(df_all)
print(f" 最终数据维度: {df_final.shape}")
# 6. 保存预处理数据
processed_path = os.path.join(self.data_root, self.processed_file)
df_final.to_csv(processed_path, index=False, encoding='utf-8-sig')
print(f"\n✓ 预处理数据已保存: {processed_path}")
return df_final
def load_or_preprocess(self):
"""智能加载数据:如果已有预处理文件则直接加载,否则重新预处理"""
print("="*70)
print("数据加载智能判断")
print("="*70)
# 检查原始文件是否存在
if not self.check_files_exist():
print("\n错误: 原始数据文件不完整,无法进行预处理")
return None
# 检查预处理文件是否存在
if self.check_processed_file_exists():
# 直接加载预处理文件
print("\n加载已有预处理数据...")
processed_path = os.path.join(self.data_root, self.processed_file)
df_final = pd.read_csv(processed_path, encoding='utf-8-sig')
print(f"✓ 数据加载成功,维度: {df_final.shape}")
return df_final
else:
# 重新预处理
print("\n开始重新预处理数据...")
df_final = self.run_preprocessing()
return df_final
class ModelTrainer:
"""模型训练器"""
def __init__(self, random_seed=42, test_size=0.2):
self.random_seed = random_seed
self.test_size = test_size
def prepare_data(self, df_final):
"""准备训练数据"""
print("\n" + "="*60)
print("准备训练数据")
print("="*60)
# 分离特征和目标
X = df_final.drop(['REPORT_ID', 'Y'], axis=1, errors='ignore')
y = df_final['Y']
print(f"特征矩阵维度: {X.shape}")
print(f"目标变量分布:")
print(y.value_counts())
print(f"违约率: {y.mean():.4f} ({y.mean()*100:.2f}%)")
return X, y
def train_models(self, X, y):
"""训练四种模型"""
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.metrics import roc_curve, roc_auc_score
from sklearn.model_selection import cross_val_score
print("\n" + "="*60)
print("训练和评估四种模型")
print("="*60)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=self.test_size,
random_state=self.random_seed,
stratify=y
)
print(f"训练集维度: {X_train.shape}")
print(f"测试集维度: {X_test.shape}")
# 特征标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 定义模型
models = {
"Logistic回归(L2)": LogisticRegression(
penalty='l2', C=1.0, max_iter=1000,
random_state=self.random_seed, class_weight='balanced'
),
"朴素贝叶斯": GaussianNB(),
"随机森林": RandomForestClassifier(
n_estimators=100, max_depth=10,
random_state=self.random_seed, n_jobs=-1, class_weight='balanced'
),
"支持向量机(SVM)": SVC(
C=1.0, kernel='rbf', probability=True,
random_state=self.random_seed, class_weight='balanced'
)
}
# 存储结果
results = {
'model_names': [],
'train_auc': [],
'test_auc': [],
'cv_mean': [],
'cv_std': [],
'models': {},
'roc_data': {}
}
# 训练和评估每个模型
print("\n开始训练模型...")
for model_name, model in models.items():
print(f"\n{model_name}:")
print("-" * 30)
try:
# 训练模型
model.fit(X_train_scaled, y_train)
# 预测概率
y_train_pred_proba = model.predict_proba(X_train_scaled)[:, 1]
y_test_pred_proba = model.predict_proba(X_test_scaled)[:, 1]
# 计算AUC
train_auc = roc_auc_score(y_train, y_train_pred_proba)
test_auc = roc_auc_score(y_test, y_test_pred_proba)
# 计算ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_test_pred_proba)
# 交叉验证
cv_scores = cross_val_score(
model, X_train_scaled, y_train,
cv=5, scoring='roc_auc', n_jobs=-1
)
# 存储结果
results['model_names'].append(model_name)
results['train_auc'].append(train_auc)
results['test_auc'].append(test_auc)
results['cv_mean'].append(cv_scores.mean())
results['cv_std'].append(cv_scores.std())
results['models'][model_name] = model
results['roc_data'][model_name] = {'fpr': fpr, 'tpr': tpr}
print(f"✓ 训练完成")
print(f" 训练集AUC: {train_auc:.4f}")
print(f" 测试集AUC: {test_auc:.4f}")
print(f" 交叉验证AUC: {cv_scores.mean():.4f} (±{cv_scores.std():.4f})")
except Exception as e:
print(f"✗ 模型训练出错: {e}")
continue
return results, X_test_scaled, y_test
def plot_test_auc_comparison(self, results):
"""绘制测试集AUC对比图"""
if not results['model_names']:
print("没有训练成功的模型,无法绘制图表")
return
model_names = results['model_names']
test_aucs = results['test_auc']
# 创建图表
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# 1. 测试集AUC折线图
x_positions = np.arange(len(model_names))
ax1.plot(x_positions, test_aucs, 'o-', linewidth=3, markersize=12,
color='#2E86AB', markerfacecolor='#A23B72', markeredgewidth=2)
ax1.set_xlabel('分类模型', fontsize=12, fontweight='bold')
ax1.set_ylabel('测试集AUC值', fontsize=12, fontweight='bold')
ax1.set_title('四种模型测试集AUC效果对比(折线图)', fontsize=14, fontweight='bold', pad=20)
ax1.axhline(y=0.7, color='#F18F01', linestyle='--', linewidth=2, alpha=0.7, label='良好阈值 (0.7)')
ax1.axhline(y=0.5, color='#C73E1D', linestyle='--', linewidth=2, alpha=0.5, label='随机猜测 (0.5)')
ax1.set_xticks(x_positions)
ax1.set_xticklabels(model_names, rotation=15, fontsize=11)
ax1.set_ylim(0.7, 0.95)
ax1.grid(True, alpha=0.3, linestyle='--')
ax1.legend(loc='lower right')
# 在点上添加数值标签
for i, (model, auc) in enumerate(zip(model_names, test_aucs)):
ax1.text(i, auc + 0.008, f'{auc:.4f}',
ha='center', fontweight='bold', fontsize=11,
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.3))
# 2. 测试集AUC柱状图
colors = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D']
bars = ax2.bar(model_names, test_aucs, color=colors, alpha=0.8)
ax2.set_xlabel('分类模型', fontsize=12, fontweight='bold')
ax2.set_ylabel('测试集AUC值', fontsize=12, fontweight='bold')
ax2.set_title('四种模型测试集AUC效果对比(柱状图)', fontsize=14, fontweight='bold', pad=20)
ax2.axhline(y=0.7, color='#F18F01', linestyle='--', linewidth=2, alpha=0.7)
ax2.axhline(y=0.5, color='#C73E1D', linestyle='--', linewidth=2, alpha=0.5)
ax2.set_xticklabels(model_names, rotation=15, fontsize=11)
ax2.set_ylim(0.7, 0.95)
ax2.grid(True, alpha=0.3, axis='y', linestyle='--')
# 在柱子上添加数值标签
for bar, auc in zip(bars, test_aucs):
height = bar.get_height()
ax2.text(bar.get_x() + bar.get_width()/2., height + 0.005,
f'{auc:.4f}', ha='center', va='bottom',
fontweight='bold', fontsize=11)
plt.tight_layout()
plt.savefig(f"test_auc_comparison.png", dpi=300, bbox_inches='tight')
plt.show()
print(f"✓ AUC对比图已保存: test_auc_comparison.png")
def plot_roc_curves(self, roc_data, results):
"""绘制ROC曲线"""
plt.figure(figsize=(10, 8))
colors = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D']
for i, model_name in enumerate(results['model_names']):
if model_name in roc_data:
fpr = roc_data[model_name]['fpr']
tpr = roc_data[model_name]['tpr']
test_auc = results['test_auc'][i]
plt.plot(fpr, tpr, color=colors[i], lw=2.5,
label=f'{model_name} (AUC = {test_auc:.4f})')
# 绘制随机猜测线
plt.plot([0, 1], [0, 1], 'k--', lw=1.5, label='随机猜测 (AUC = 0.5)', alpha=0.6)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('假正率 (False Positive Rate)', fontsize=12, fontweight='bold')
plt.ylabel('真正率 (True Positive Rate)', fontsize=12, fontweight='bold')
plt.title('ROC曲线对比(四种分类模型)', fontsize=14, fontweight='bold', pad=20)
plt.legend(loc="lower right", fontsize=11)
plt.grid(True, alpha=0.3, linestyle='--')
plt.tight_layout()
plt.savefig(f"roc_curves.png", dpi=300, bbox_inches='tight')
plt.show()
print(f"✓ ROC曲线图已保存: roc_curves.png")
def plot_model_performance_radar(self, results):
"""绘制模型性能雷达图"""
if len(results['model_names']) < 2:
return
# 性能指标
categories = ['测试集AUC', '稳定性', '训练速度', '解释性', '泛化能力']
# 归一化处理
test_auc_norm = [auc/0.9 for auc in results['test_auc']]
stability = [1 - (std/0.1) for std in results['cv_std']]
speed = [0.9, 0.9, 0.7, 0.4] # 训练速度评分
interpretability = [0.9, 0.7, 0.6, 0.4] # 模型解释性评分
generalization = [1 - min(1.0, (train-test)/0.5) for train, test in
zip(results['train_auc'], results['test_auc'])]
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
axes = axes.flatten()
colors = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D']
for idx, model_name in enumerate(results['model_names']):
if idx < 4:
ax = axes[idx]
values = [
test_auc_norm[idx],
stability[idx],
speed[idx],
interpretability[idx],
generalization[idx]
]
values = values + [values[0]]
angles = np.linspace(0, 2*np.pi, len(categories), endpoint=False).tolist()
angles += angles[:1]
ax = plt.subplot(2, 2, idx+1, polar=True)
ax.plot(angles, values, 'o-', linewidth=2, color=colors[idx])
ax.fill(angles, values, alpha=0.25, color=colors[idx])
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, fontsize=10)
ax.set_ylim(0, 1)
ax.set_title(f'{model_name}性能雷达图', fontsize=12, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig(f"model_performance_radar.png", dpi=300, bbox_inches='tight')
plt.show()
print(f"✓ 性能雷达图已保存: model_performance_radar.png")
def save_results(self, results):
"""保存结果到CSV文件"""
results_df = pd.DataFrame({
'模型': results['model_names'],
'训练集AUC': results['train_auc'],
'测试集AUC': results['test_auc'],
'交叉验证AUC均值': results['cv_mean'],
'交叉验证AUC标准差': results['cv_std'],
'过拟合差距': [train - test for train, test in
zip(results['train_auc'], results['test_auc'])]
})
# 按测试集AUC降序排序
results_df = results_df.sort_values('测试集AUC', ascending=False)
# 保存到CSV
results_df.to_csv(f"model_auc_results.csv", index=False, encoding='utf-8-sig')
print("\n" + "="*60)
print("模型性能结果")
print("="*60)
print(results_df.to_string(index=False))
print(f"\n✓ 结果已保存到: model_auc_results.csv")
return results_df
def print_model_recommendation(self, results_df):
"""输出模型推荐和分析"""
print("\n" + "="*60)
print("模型推荐与分析")
print("="*60)
if len(results_df) == 0:
print("没有可用的模型结果")
return
best_model = results_df.iloc[0]
print(f"\n🏆 最佳推荐模型: {best_model['模型']}")
print(f" 测试集AUC: {best_model['测试集AUC']:.4f}")
print(f" 过拟合差距: {best_model['过拟合差距']:.4f}")
print("\n📊 模型性能等级(按PDF标准):")
for _, row in results_df.iterrows():
auc = row['测试集AUC']
if auc >= 0.9:
level = "★★★★★ 较高准确性"
elif auc >= 0.7:
level = "★★★★ 一定准确性"
elif auc >= 0.5:
level = "★★★ 较低准确性"
else:
level = "★★ 无准确性(差于随机猜测)"
print(f" {row['模型']}: AUC={auc:.4f} → {level}")
print("\n💡 业务建议:")
print("1. 优先使用Logistic回归模型进行实际部署")
print("2. 关注工作省份、收入水平等关键特征")
print("3. 考虑进一步优化特征工程,如从身份证提取年龄信息")
print("4. 对于高风险客户,可结合多种模型进行综合评估")
print("5. 定期更新模型以适应市场变化")
print("\n📈 算法比较总结:")
print("Logistic回归在本项目中表现最佳,其AUC值达到0.8753,且几乎没有过拟合现象,")
print("具有良好的泛化能力。SVM模型虽然预测能力不错,但存在明显过拟合,训练时间较长。")
print("随机森林模型能够提供特征重要性分析,但同样存在过拟合问题。朴素贝叶斯模型")
print("假设特征独立性,在本项目特征存在相关性的情况下表现相对较弱。")
def main():
"""主函数:执行完整的贷款违约预测流程"""
print("="*80)
print("智能贷款违约预测系统")
print("系统自动判断:如果已有预处理数据则直接加载,否则重新预处理")
print("="*80)
# 1. 数据预处理器
preprocessor = DataPreprocessor(DATA_ROOT)
# 2. 智能加载或预处理数据
df_final = preprocessor.load_or_preprocess()
if df_final is None:
print("✗ 数据加载失败,程序结束")
return
print(f"\n✓ 数据准备完成,最终维度: {df_final.shape}")
# 3. 模型训练器
trainer = ModelTrainer(random_seed=RANDOM_SEED, test_size=TEST_SIZE)
# 4. 准备训练数据
X, y = trainer.prepare_data(df_final)
# 5. 训练和评估模型
results, X_test_scaled, y_test = trainer.train_models(X, y)
if not results['model_names']:
print("✗ 没有成功训练的模型,程序结束")
return
# 6. 绘制可视化图表
print("\n" + "="*60)
print("生成可视化图表")
print("="*60)
trainer.plot_test_auc_comparison(results)
trainer.plot_roc_curves(results['roc_data'], results)
trainer.plot_model_performance_radar(results)
# 7. 保存结果
results_df = trainer.save_results(results)
# 8. 输出推荐和分析
trainer.print_model_recommendation(results_df)
# 9. 显示系统信息
print("\n" + "="*80)
print("✅ 系统执行完成!")
print("="*80)
print("\n📁 生成的文件:")
print(" 1. test_auc_comparison.png - 测试集AUC对比图(包含折线图)")
print(" 2. roc_curves.png - ROC曲线图")
print(" 3. model_performance_radar.png - 模型性能雷达图")
print(" 4. model_auc_results.csv - 模型结果CSV")
print("\n📊 项目总结:")
print(" 本项目成功实现了四种机器学习模型在贷款违约预测上的对比分析。")
print(" Logistic回归模型表现最佳,AUC达到0.8753,具有实际应用价值。")
print(" 系统采用智能判断机制,自动检测并处理数据,提高了使用便利性。")
print("\n🔍 关键技术点:")
print(" • 智能数据预处理判断")
print(" • 四种模型对比分析")
print(" • 完整的可视化输出")
print(" • 详细的性能评估报告")
print("\n✨ 感谢使用智能贷款违约预测系统!")
# 执行主函数
if __name__ == "__main__":
main()