贷款违约预测实战:四种机器学习模型的全面对比分析

📊 项目概述

在金融科技快速发展的今天,信贷风险管理成为金融机构的核心竞争力。本项目基于某金融机构脱敏历史数据,通过系统化的数据预处理和特征工程,对比分析了四种机器学习模型在贷款违约预测任务中的表现。经过严格评估,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回归在本项目中表现最为出色,主要原因在于:

  1. 特征维度适中:87个特征既不过少导致欠拟合,也不过多引发维度灾难

  2. 正则化有效:L2正则化防止过拟合,提升泛化能力

  3. 线性可分性:大部分特征与违约风险呈近似线性关系

  4. 计算效率高:训练仅需数秒,适合生产环境部署

🔥 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回归曲线最靠近左上角,说明其在各个阈值下都能保持较好的真阳性率和假阳性率平衡。

💡 业务应用建议

模型选择策略

  1. 生产环境首选:Logistic回归 - AUC最高、训练快、易解释

  2. 探索性分析:随机森林 - 提供特征重要性洞察

  3. 基准模型:朴素贝叶斯 - 快速建立性能基线

  4. 特定场景:SVM - 在小样本、高维特征场景下可能有优势

风险管理应用

基于模型预测结果,金融机构可以:

  1. 风险分层:将客户分为低、中、高风险组,实施差异化信贷政策

  2. 动态定价:根据违约概率调整利率和额度

  3. 早期预警:对高风险客户加强贷后管理

  4. 策略优化:分析重要特征,优化营销和风控策略

🚀 技术优化方向

短期改进

  1. 特征工程深化:从身份证提取年龄、出生季节等信息

  2. 集成方法尝试:Stacking或Voting结合多种模型优势

  3. 参数调优:使用网格搜索或贝叶斯优化寻找最优超参数

长期发展

  1. 时序特征引入:加入客户历史行为的时间序列模式

  2. 深度学习探索:尝试神经网络处理复杂特征交互

  3. 在线学习系统:实现模型实时更新,适应市场变化

🎯 核心经验总结

成功要素

  1. 数据预处理决定上限:精细的特征工程是模型成功的基础

  2. 模型简单性优势:在特征维度适中的情况下,简单模型往往表现更好

  3. 不平衡数据处理:分层抽样和class_weight参数有效缓解了类别不平衡问题

  4. 多维度评估:不仅关注AUC,还考察过拟合、稳定性和可解释性

教训反思

  1. 警惕过拟合陷阱:训练集表现太好可能意味着泛化能力不足

  2. 模型假设验证:选择模型前需验证其假设是否与数据特性相符

  3. 计算资源考量:在实际应用中需平衡预测精度与计算成本

🌟 结语

本项目展示了机器学习在金融风控领域的实际应用价值。通过系统化的方法,我们不仅构建了有效的预测模型,更重要的是深入理解了不同算法的特性及其适用场景。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()
相关推荐
海森大数据2 小时前
超越简单问答:SUPERChem基准揭示大语言模型化学深度推理的机遇与挑战
人工智能·语言模型·自然语言处理
Mintopia2 小时前
⚙️ 模型接口与微调兼容性:AIGC系统整合的底层心脏跳动
人工智能·架构·rust
XiaoMu_0012 小时前
基于深度学习的网络流量异常检测系统
人工智能·深度学习
Cherry的跨界思维2 小时前
27、Python压缩备份安全指南:从zipfile到AES-256加密,生产级自动化备份全方案
人工智能·python·安全·自动化·办公自动化·python自动化·python办公自动化
说私域2 小时前
开源AI智能名片链动2+1模式商城小程序在淘宝首页流量生态中的应用与影响研究
人工智能·小程序·开源
Blossom.1182 小时前
基于MLOps+LLM的模型全生命周期自动化治理系统:从数据漂移到智能回滚的落地实践
运维·人工智能·学习·决策树·stable diffusion·自动化·音视频
墨染星辰云水间2 小时前
Extracting Latent Steering Vectors from Pretrained Language Models
人工智能·语言模型·自然语言处理
牙牙要健康2 小时前
【YOLOv8-Ultralytics】 【目标检测】【v8.3.235版本】 模型专用训练器代码train.py解析
人工智能·yolo·目标检测
~央千澈~2 小时前
如何用AI处理音乐音频消除作品信息里的 AI 痕迹-程序员音乐人卓伊凡
人工智能