摘要

本文系统分析 Kaggle Home Credit Default Risk 竞赛方案,介绍从数据预处理、特征工程到模型集成的完整机器学习流程。文章讨论该大规模信用风险预测任务中的架构选择、实现策略与性能优化方法,涵盖数据质量评估、关系型数据库的特征提取、梯度提升模型优化和堆叠集成,为类似金融风险结构化数据预测任务提供实践参考。

关键词

信用风险建模、梯度提升、特征工程、模型堆叠、LightGBM、XGBoost、CatBoost、机器学习流水线


1. 引言与业务背景

1.1 问题领域与研究动机

金融服务普及已成为全球经济发展的重要挑战。传统信用评估高度依赖既有信用记录、稳定就业记录和可抵押资产;按照世界银行 2017 年全球普惠金融数据库的统计,全球约有 17 亿成年人无法获得正规银行服务,这些标准使他们难以进入传统信贷体系。

Home Credit Group 是一家在 10 多个国家开展业务的国际非银行金融机构,主要服务缺乏既有信用记录、通常被传统银行拒绝的“信用不可见”人群。核心业务挑战是利用替代数据,在约 5 分钟的有限时间内完成准确的违约预测。

1.2 信用风险预测任务

任务定义:预测违约概率(PD)的二分类问题。

\[ Y_i = \begin{cases} 1, & \text{if client } i \text{ defaults (90+ days past due)} \\\\ 0, & \text{if client } i \text{ repays as scheduled} \end{cases} \]

评价指标:受试者工作特征曲线下面积(ROC-AUC)。

选择 AUC,是因为它对类别不平衡相对稳健,重点衡量排序能力,而非绝对概率的校准程度:

\[ \text{AUC} = \int_0^1 \text{TPR}(\tau) \, d(\text{FPR}(\tau)) \]

其中,\(\text{TPR}\) 表示真正率,\(\text{FPR}\) 表示阈值 \(\tau\) 下的假正率。

指标解读

  • AUC = 0.50:随机预测,没有区分能力。
  • AUC ∈ [0.60, 0.70]:表现较差。
  • AUC ∈ [0.70, 0.80]:表现可接受。
  • AUC ∈ [0.80, 0.90]:表现良好。
  • AUC > 0.90:表现优秀,但需要核查过拟合。

1.3 信用评估中的替代数据

该竞赛数据集体现了向替代信用评分转变的思路,涉及非传统数据形态:

传统数据替代性代理数据数据提供方
信用评分手机充值模式、通话时长电信运营商
收入证明POS 交易记录、租金支付历史支付机构、房产平台
银行流水分期还款历史、信用卡账单消费金融公司
就业证明电商活动、社交媒体互动互联网平台

1.4 竞赛成果与方法影响

2018 年 Home Credit Default Risk 竞赛吸引了全球 7,194 支队伍。优秀方案体现了以下方法创新:

  1. 特征工程:从基础聚合发展到时间窗口特征和趋势分析。
  2. 集成架构:系统应用多层堆叠策略。
  3. 数据预处理:改进缺失值填补与异常值处理。

这些方法也已应用于保险欺诈检测、营销响应预测和客户流失建模等相关领域。


2. 数据集架构与表结构分析

2.1 数据规模与关联结构

数据集由七张相互关联的关系表组成,总记录数超过 5,000 万,体现了企业金融系统常见的复杂关系型数据库结构。

数据库表结构

数据表统计概览

数据表行数存储大小说明主键外键
application_{train,test}307,511 / 48,74445MB / 7MB主申请表SK_ID_CURR
bureau1,716,428172MB征信记录SK_ID_BUREAUSK_ID_CURR
bureau_balance27,299,925574MB每月征信状态SK_ID_BUREAU
previous_application1,670,214150MB历史申请SK_ID_PREVSK_ID_CURR
installments_payments13,605,401730MB分期还款记录SK_ID_PREV
POS_CASH_balance10,001,358970MBPOS 现金贷款账单SK_ID_PREV
credit_card_balance3,840,312400MB信用卡账单SK_ID_PREV

2.2 实体关系模型

数据库采用分层关联结构,包含三个主要标识符域:

1
2
3
SK_ID_CURR: Client-level identifier (primary entity key)
SK_ID_PREV: Previous application identifier (transaction-level)
SK_ID_BUREAU: External credit bureau record identifier

关系拓扑

1
2
3
4
5
6
application [1] ───<N>─── bureau [1] ───<N>─── bureau_balance
     ├─<N>─── previous_application [1] ───<N>─── installments_payments
     │                                    ├─<N>─── POS_CASH_balance
     │                                    └─<N>─── credit_card_balance
     └─<N>─── credit_card_balance

一对多(1:N)关系要求在特征工程中进行聚合,将时间序列和多条记录转换为机器学习模型可用的静态特征向量。

2.3 字段详解

2.3.1 申请表:主实体

申请表是核心实体,训练集部分包含目标标签。

人口统计与申请特征

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
SK_ID_CURR: Integer (primary identifier)
TARGET: Binary (0=non-default, 1=default) - training set only
CODE_GENDER: Categorical (M/F/XNA)
FLAG_OWN_CAR: Binary (Y/N)
FLAG_OWN_REALTY: Binary (Y/N)
CNT_CHILDREN: Integer (count of children)
AMT_INCOME_TOTAL: Float (annual income in local currency)
AMT_CREDIT: Float (loan amount requested)
AMT_ANNUITY: Float (monthly installment amount)
AMT_GOODS_PRICE: Float (price of goods being financed)

时间特征:以相对申请日期的天数编码,负值表示过去。

1
2
3
4
5
DAYS_BIRTH: Integer (age in days, e.g., -10000  27.4 years)
DAYS_EMPLOYED: Integer (employment duration, special value 365243 indicates unemployed)
DAYS_REGISTRATION: Integer (registration change recency)
DAYS_ID_PUBLISH: Integer (identity document issuance recency)
DAYS_LAST_PHONE_CHANGE: Integer (mobile phone change recency)

外部评分特征:具有较强预测能力的归一化评分。

1
2
3
4
EXT_SOURCE_1: Float [0,1] (normalized external score 1)
EXT_SOURCE_2: Float [0,1] (normalized external score 2)
EXT_SOURCE_3: Float [0,1] (normalized external score 3)
# Sourced from third-party credit bureaus

2.3.2 征信表:外部信用历史

记录客户与外部金融机构之间的信贷关系。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
SK_ID_CURR: Integer (foreign key to application)
SK_ID_BUREAU: Integer (unique bureau record identifier)
CREDIT_ACTIVE: Categorical (Active/Closed/Sold/Demand/Bad debt)
CREDIT_CURRENCY: Categorical (currency code)
DAYS_CREDIT: Integer (days since credit application)
CREDIT_DAY_OVERDUE: Integer (current days past due)
DAYS_CREDIT_ENDDATE: Integer (remaining duration to maturity)
DAYS_ENDDATE_FACT: Integer (actual closure date)
AMT_CREDIT_MAX_OVERDUE: Float (maximum historical overdue amount)
CNT_CREDIT_PROLONG: Integer (count of credit prolongations)
AMT_CREDIT_SUM: Float (total credit exposure)
AMT_CREDIT_SUM_DEBT: Float (outstanding debt)
AMT_CREDIT_SUM_LIMIT: Float (credit limit)
AMT_CREDIT_SUM_OVERDUE: Float (current overdue amount)
CREDIT_TYPE: Categorical (loan type: consumer, mortgage, etc.)
DAYS_CREDIT_UPDATE: Integer (recency of bureau update)
AMT_ANNUITY: Float (monthly payment obligation)

2.3.3 征信月度余额表:征信状态变化

每条征信记录的月度状态快照,可用于趋势分析。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SK_ID_BUREAU: Integer (foreign key to bureau)
MONTHS_BALANCE: Integer (relative month index, -1=last month, -2=two months ago)
STATUS: Categorical encoding:
    '0': Current (no delinquency)
    '1': 1-29 days past due
    '2': 30-59 days past due
    '3': 60-89 days past due
    '4': 90-119 days past due
    '5': 120-149 days past due
    'C': Closed (paid off)
    'X': Status unknown

2.3.4 历史申请表

Home Credit 系统内部的历史申请。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SK_ID_CURR: Integer (foreign key)
SK_ID_PREV: Integer (unique previous application identifier)
NAME_CONTRACT_TYPE: Categorical (Cash/Consumer/Revolving loans)
AMT_ANNUITY: Float (proposed monthly payment)
AMT_APPLICATION: Float (requested amount)
AMT_CREDIT: Float (approved amount)
AMT_DOWN_PAYMENT: Float (down payment amount)
RATE_DOWN_PAYMENT: Float (down payment ratio)
RATE_INTEREST_PRIMARY: Float (primary interest rate)
RATE_INTEREST_PRIVILEGED: Float (preferential interest rate)
NAME_CONTRACT_STATUS: Categorical (Approved/Refused/Canceled/Unused)
DAYS_DECISION: Integer (days since decision)
CODE_REJECT_REASON: Categorical (rejection reason if applicable)
NAME_CLIENT_TYPE: Categorical (New/Repeat customer)
CNT_PAYMENT: Integer (proposed term in months)

2.3.5 分期还款表

细粒度还款交易记录。

1
2
3
4
5
6
7
8
SK_ID_CURR: Integer (foreign key)
SK_ID_PREV: Integer (foreign key to previous_application)
NUM_INSTALMENT_VERSION: Integer (version of installment schedule)
NUM_INSTALMENT_NUMBER: Integer (installment sequence number)
DAYS_INSTALMENT: Integer (scheduled payment date)
DAYS_ENTRY_PAYMENT: Integer (actual payment date)
AMT_INSTALMENT: Float (scheduled amount)
AMT_PAYMENT: Float (actual amount paid)

衍生指标

  • 逾期天数(DPD):\(DPD = DAYS_ENTRY_PAYMENT - DAYS_INSTALMENT\)
  • 还款金额偏差:\(\Delta AMT = AMT_PAYMENT - AMT_INSTALMENT\)

2.4 数据质量概况

类别分布

1
2
3
Class 0 (Non-default): 282,686 observations (91.93%)
Class 1 (Default):      24,825 observations (8.07%)
Imbalance Ratio: 11.4:1

缺失值概览

  • EXT_SOURCE_1:缺失率 56.38%。
  • EXT_SOURCE_3:缺失率 19.83%。
  • AMT_ANNUITY:缺失率 0.003%。
  • OCCUPATION_TYPE:缺失率 31.35%。

异常编码

  • DAYS_EMPLOYED = 365,243(约 1,000 年):表示失业的哨兵值。
  • CODE_GENDER = ‘XNA’:未指定的性别类别。
  • AMT_INCOME_TOTAL:出现 117,000,000 的极端值,可能是数据错误。

3. 系统架构与流水线设计

3.1 框架选择:Steppy 流水线架构

方案采用 Steppy,这是面向模块化、可复现数据科学流程的轻量级机器学习流水线库。Steppy 借鉴 Apache Airflow、Spotify Luigi 等工作流编排系统的设计原则,并针对机器学习任务进行适配。

Steppy 框架架构

使用流水线框架的原因

传统命令式机器学习代码存在以下架构局限:

1
2
3
4
5
6
7
# Anti-pattern: Tightly coupled workflow
data = load_data()
data = clean_data(data)
data = extract_features(data)
X_train, X_test, y_train, y_test = split_data(data)
model = train_model(X_train, y_train)
predictions = model.predict(X_test)

主要不足

  1. 耦合:修改某个阶段,需要理解下游依赖。
  2. 可复现性:中间结果难以缓存或进行版本管理。
  3. 并行化:顺序执行限制了计算资源优化。
  4. 实验追踪:难以系统比较不同参数配置。

Steppy 的声明式方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from steppy.base import BaseTransformer

class DataLoader(BaseTransformer):
    """Loads raw data from persistent storage."""
    
    def transform(self, filepath):
        data = pd.read_csv(filepath)
        return {'data': data}

class DataCleaningTransformer(BaseTransformer):
    """Applies data quality transformations."""
    
    def transform(self, data):
        cleaned = self._handle_outliers(data)
        cleaned = self._impute_missing(cleaned)
        return {'cleaned_data': cleaned}
    
    def _handle_outliers(self, df):
        # Implementation
        pass

class FeatureExtractionTransformer(BaseTransformer):
    """Engineers features from cleaned data."""
    
    def transform(self, cleaned_data):
        features = self._aggregate_features(cleaned_data)
        return {'features': features}

设计原则

  1. 标准接口:所有组件继承 BaseTransformer,提供 fit()transform() 方法。
  2. 显式数据流:使用带命名键的字典传递输入输出,便于追踪。
  3. 可组合性:通过 StepAdapter 抽象连接各步骤。
  4. 持久化:中间产物支持缓存与检查点。

3.2 端到端流水线

完整数据处理流水线

阶段 1:数据读取与清洗

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def build_data_ingestion_pipeline(config):
    """Constructs the data loading and cleaning pipeline stage."""
    
    # Load all seven tables
    raw_data = DataLoader(config.data_paths).transform()
    
    # Apply table-specific cleaning transformers
    cleaning_transformers = {
        'application': ApplicationCleaning(),
        'bureau': BureauCleaning(),
        'bureau_balance': BureauBalanceCleaning(),
        'previous_application': PreviousApplicationCleaning(),
        'installments_payments': InstallmentPaymentsCleaning(),
        'pos_cash_balance': PosCashBalanceCleaning(),
        'credit_card_balance': CreditCardBalanceCleaning()
    }
    
    cleaned_data = {
        table: transformer.transform(raw_data[table])
        for table, transformer in cleaning_transformers.items()
    }
    
    return cleaned_data

阶段 2:特征工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def build_feature_engineering_pipeline(cleaned_data):
    """Constructs the feature extraction pipeline stage."""
    
    # Table-specific feature extraction
    bureau_features = BureauFeatureExtractor().transform(
        cleaned_data['bureau'], 
        cleaned_data['bureau_balance']
    )
    
    prev_app_features = PreviousApplicationFeatureExtractor().transform(
        cleaned_data['previous_application']
    )
    
    installment_features = InstallmentFeatureExtractor().transform(
        cleaned_data['installments_payments']
    )
    
    # Feature consolidation
    all_features = FeatureConcatenator().transform([
        cleaned_data['application'],
        bureau_features,
        prev_app_features,
        installment_features
    ])
    
    # Categorical encoding
    encoded_features = CategoricalEncoder().transform(
        all_features,
        method='target_encoding'
    )
    
    return {
        'features': encoded_features,
        'target': cleaned_data['application']['TARGET'],
        'feature_names': encoded_features.columns.tolist()
    }

阶段 3:模型训练

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def build_model_training_pipeline(feature_data, config):
    """Constructs the model training pipeline stage."""
    
    # Train/validation split
    X_train, X_valid, y_train, y_valid = train_test_split(
        feature_data['features'],
        feature_data['target'],
        test_size=0.2,
        stratify=feature_data['target'],
        random_state=config.random_seed
    )
    
    # Model initialization and training
    model = GradientBoostingModel(config.model_params)
    model.fit(X_train, y_train, validation_data=(X_valid, y_valid))
    
    # Performance evaluation
    validation_auc = roc_auc_score(y_valid, model.predict(X_valid))
    
    return {
        'model': model,
        'validation_auc': validation_auc,
        'feature_importance': model.feature_importances_
    }

阶段 4:构建堆叠集成(Stacking)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def build_stacking_ensemble(base_models, meta_learner, X, y, X_test):
    """
    Implements two-level stacking ensemble architecture.
    
    Level 1: Base learners generate out-of-fold predictions
    Level 2: Meta-learner trains on base model outputs
    """
    
    # Generate OOF predictions
    oof_predictions = {}
    test_predictions = {}
    
    for name, model in base_models.items():
        oof_pred, test_pred = generate_oof_predictions(
            model, X, y, X_test, n_folds=5
        )
        oof_predictions[name] = oof_pred
        test_predictions[name] = test_pred
    
    # Train meta-learner
    meta_features = np.column_stack([
        oof_predictions[name] for name in base_models.keys()
    ])
    
    meta_learner.fit(meta_features, y)
    
    # Generate final predictions
    meta_test_features = np.column_stack([
        test_predictions[name] for name in base_models.keys()
    ])
    final_predictions = meta_learner.predict_proba(meta_test_features)[:, 1]
    
    return final_predictions

3.3 项目目录组织

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
open-solution-home-credit/
├── src/                          # Source code modules
   ├── __init__.py
   ├── pipeline_manager.py       # Orchestration layer
   ├── pipelines.py              # Pipeline definitions (9 model configurations)
   ├── pipeline_blocks.py        # Step factory methods
   ├── feature_extraction.py     # Feature transformers (20+ implementations)
   ├── data_cleaning.py          # Data quality transformers (7 table-specific)
   ├── models.py                 # Model wrappers (LGB/XGB/CTB/NN/RF/LR)
   ├── pipeline_config.py        # Configuration constants and hyperparameters
   ├── hyperparameter_tuning.py  # Optimization strategies
   ├── callbacks.py              # Training monitoring callbacks
   ├── utils.py                  # Utility functions
   └── neptune_hacks.py          # Offline experiment tracking support
├── configs/                      # Configuration files
   └── neptune.yaml              # Main configuration (paths/hyperparameters)
├── data/                         # Data directory
   ├── raw/                      # Original competition data
   └── workdir/                  # Intermediate processing artifacts
├── notebooks/                    # Exploratory data analysis
├── blog/                         # Documentation
   └── images/                   # Visualization assets
├── main.py                       # CLI entry point
├── requirements.txt              # Dependency specification
└── README.md                     # Project documentation

3.4 配置管理

项目采用混合配置策略:YAML 管理实验参数,Python 模块管理代码级常量。

实验配置(configs/neptune.yaml)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
parameters:
  # Data paths
  train_filepath: /data/application_train.csv
  test_filepath: /data/application_test.csv
  
  # Model selection
  pipeline_name: lightGBM
  
  # Feature toggles
  use_application: true
  use_bureau: true
  use_bureau_balance: true
  use_previous_application: true
  use_installments_payments: true
  use_pos_cash_balance: true
  use_credit_card_balance: true
  
  # LightGBM hyperparameters
  lgbm__objective: binary
  lgbm__metric: auc
  lgbm__num_leaves: 35
  lgbm__learning_rate: 0.02
  lgbm__n_estimators: 5000
  lgbm__min_child_samples: 70
  lgbm__subsample: 1.0
  lgbm__colsample_bytree: 0.03
  lgbm__reg_lambda: 100.0
  lgbm__reg_alpha: 0.0
  
  # Cross-validation configuration
  n_cv_splits: 5
  validation_size: 0.2
  stratified_cv: true
  shuffle: true
  random_seed: 90210

代码级配置(src/pipeline_config.py)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""Constants and aggregation recipes for feature engineering."""

import numpy as np

# Reproducibility constants
RANDOM_SEED = 90210
DEV_SAMPLE_SIZE = 1000

# Column type definitions
CATEGORICAL_COLUMNS = [
    'CODE_GENDER', 'FLAG_OWN_CAR', 'FLAG_OWN_REALTY',
    'NAME_TYPE_SUITE', 'NAME_INCOME_TYPE', 'NAME_EDUCATION_TYPE',
    'NAME_FAMILY_STATUS', 'NAME_HOUSING_TYPE', 'OCCUPATION_TYPE',
    'WEEKDAY_APPR_PROCESS_START', 'ORGANIZATION_TYPE', 'FONDKAPREMONT_MODE',
    'HOUSETYPE_MODE', 'WALLSMATERIAL_MODE', 'EMERGENCYSTATE_MODE'
]

NUMERICAL_COLUMNS = [
    'AMT_INCOME_TOTAL', 'AMT_CREDIT', 'AMT_ANNUITY', 'AMT_GOODS_PRICE',
    'DAYS_BIRTH', 'DAYS_EMPLOYED', 'DAYS_REGISTRATION', 'DAYS_ID_PUBLISH',
    'EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3'
]

# Aggregation recipes for feature extraction
BUREAU_AGGREGATION_RECIPES = [
    (['SK_ID_CURR'], [
        ('SK_ID_BUREAU', 'count'),
        ('AMT_CREDIT_SUM', ['sum', 'mean', 'max', 'std']),
        ('AMT_CREDIT_SUM_DEBT', ['sum', 'mean']),
        ('AMT_CREDIT_SUM_OVERDUE', ['sum', 'mean', 'max']),
        ('DAYS_CREDIT', ['min', 'max', 'mean']),
        ('CREDIT_DAY_OVERDUE', ['sum', 'max', 'mean']),
        ('CNT_CREDIT_PROLONG', 'sum')
    ])
]

PREVIOUS_APPLICATION_AGGREGATION_RECIPES = [
    (['SK_ID_CURR'], [
        ('SK_ID_PREV', 'count'),
        ('AMT_APPLICATION', ['sum', 'mean', 'max']),
        ('AMT_CREDIT', ['sum', 'mean', 'max']),
        ('AMT_DOWN_PAYMENT', ['sum', 'mean']),
        ('RATE_INTEREST_PRIMARY', ['mean', 'max']),
        ('DAYS_DECISION', ['min', 'max', 'mean'])
    ])
]

这种配置分工带来以下特点:

  • 易用性:通过 YAML 快速调整实验,无需修改代码。
  • 类型安全:Python 模块提供编译阶段校验。
  • 覆盖能力:支持命令行和环境变量覆盖配置。

4. 探索性数据分析与质量评估

4.1 EDA 方法框架

这里的探索性数据分析(EDA)围绕五个基本问题展开:

  1. 数据质量:有哪些异常、缺失值或编码不一致?
  2. 分布特征:各特征的集中趋势、离散程度与分布形态如何?
  3. 业务洞察:不同人群是否表现出不同的行为?
  4. 预测信号:哪些特征与目标变量存在统计关联?
  5. 特征工程方向:哪些变换或聚合可能提高预测能力?

4.2 关键发现

4.2.1 类别不平衡分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Load training data
train_df = pd.read_csv('data/application_train.csv')

# Class distribution analysis
target_distribution = train_df['TARGET'].value_counts()
print("Class Distribution:")
print(target_distribution)
print(f"\nClass Proportions:")
print(train_df['TARGET'].value_counts(normalize=True))

# Output:
# Class Distribution:
# 0    282686
# 1     24825
# Name: TARGET, dtype: int64
#
# Class Proportions:
# 0    0.919271
# 1    0.080729
# Name: TARGET, dtype: float64

解读:11.4:1 的类别比例要求谨慎选择指标。仅预测多数类就能达到 91.9% 的准确率,因此准确率可能产生误导,应结合 AUC 和精确率—召回率指标。

4.2.2 外部评分的预测能力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Analyze EXT_SOURCE features
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for idx, col in enumerate(['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']):
    # Distribution comparison
    sns.kdeplot(
        data=train_df[train_df['TARGET'] == 0][col].dropna(),
        label='Non-default',
        ax=axes[idx],
        fill=True,
        alpha=0.5
    )
    sns.kdeplot(
        data=train_df[train_df['TARGET'] == 1][col].dropna(),
        label='Default',
        ax=axes[idx],
        fill=True,
        alpha=0.5
    )
    axes[idx].set_title(f'{col} Distribution by Target')
    axes[idx].legend()

plt.tight_layout()
plt.savefig('images/ext_source_kde.png', dpi=150)

主要观察

  • 违约客户的外部评分普遍更低。
  • EXT_SOURCE_1 的区分能力最强,信息值(IV)最高。
  • 缺失率存在差异:EXT_SOURCE_1 为 56.4%,EXT_SOURCE_2 为 0.2%,EXT_SOURCE_3 为 19.8%。

4.2.3 收入分布分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Income distribution with log transformation
train_df['AMT_INCOME_TOTAL_LOG'] = np.log1p(train_df['AMT_INCOME_TOTAL'])

# Descriptive statistics
print(train_df['AMT_INCOME_TOTAL'].describe())

# Detect extreme outliers
q99 = train_df['AMT_INCOME_TOTAL'].quantile(0.99)
extreme_outliers = train_df[train_df['AMT_INCOME_TOTAL'] > q99 * 10]
print(f"\nExtreme outliers (>10x 99th percentile): {len(extreme_outliers)}")

统计概览

1
2
3
4
5
6
7
8
count    3.075110e+05
mean     1.687979e+05
std      2.371894e+05
min      2.565000e+04
25%      1.125000e+05
50%      1.471500e+05
75%      2.025000e+05
max      1.170000e+08  # Data quality concern

启示:分布明显右偏,偏度约为 3.2,适合考虑对数变换。最大值为 117M,而均值为 168K,这种极端差异提示可能存在需要处理的录入错误。

4.2.4 时间特征分析:年龄与违约风险

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Age calculation and risk profiling
train_df['AGE_YEARS'] = -train_df['DAYS_BIRTH'] / 365.25

# Binned analysis
train_df['AGE_BIN'] = pd.cut(
    train_df['AGE_YEARS'],
    bins=[0, 25, 30, 35, 40, 45, 50, 60, 100],
    labels=['<25', '25-30', '30-35', '35-40', '40-45', '45-50', '50-60', '60+']
)

default_by_age = train_df.groupby('AGE_BIN')['TARGET'].agg(['mean', 'count'])
print(default_by_age)

# Visualization
plt.figure(figsize=(10, 6))
default_by_age['mean'].plot(kind='bar', color='steelblue')
plt.title('Default Rate by Age Cohort')
plt.xlabel('Age Group')
plt.ylabel('Default Rate')
plt.axhline(y=train_df['TARGET'].mean(), color='r', linestyle='--', label='Overall Average')
plt.legend()
plt.tight_layout()
plt.savefig('images/default_rate_by_age.png', dpi=150)

发现:违约率与年龄呈负向关系,25 岁以下客户的违约率约为 40—50 岁客户的 2.5 倍。这与信用风险理论中关于收入稳定性及金融经验的认识一致。

4.2.5 就业状态的异常编码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Investigate DAYS_EMPLOYED anomaly
anomaly_count = (train_df['DAYS_EMPLOYED'] == 365243).sum()
anomaly_rate = anomaly_count / len(train_df)

print(f"Anomalous DAYS_EMPLOYED (365243): {anomaly_count} ({anomaly_rate:.2%})")

# Compare default rates
train_df['EMPLOYMENT_STATUS'] = np.where(
    train_df['DAYS_EMPLOYED'] == 365243,
    'Unemployed/Unknown',
    'Employed'
)

employment_risk = train_df.groupby('EMPLOYMENT_STATUS')['TARGET'].mean()
print("\nDefault Rate by Employment Status:")
print(employment_risk)

结果

1
2
3
4
5
Anomalous DAYS_EMPLOYED (365243): 55,374 (18.01%)

Default Rate by Employment Status:
Employed              0.0753
Unemployed/Unknown    0.1047

解读:365,243 是哨兵编码,约相当于 1,000 年,表示失业或数据不可用。该人群违约率较高(10.5% 对比 7.5%),说明这一编码具有业务意义。

4.3 数据预处理策略

基于 EDA 发现,构建系统化的预处理流水线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ApplicationDataCleaner(BaseTransformer):
    """
    Implements data quality transformations for the primary application table.
    """
    
    def transform(self, df: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        df_cleaned = df.copy()
        
        # 1. Sentinel value treatment
        df_cleaned['DAYS_EMPLOYED'].replace(365243, np.nan, inplace=True)
        df_cleaned['CODE_GENDER'].replace('XNA', np.nan, inplace=True)
        
        # 2. Infinity value handling
        df_cleaned = df_cleaned.replace([np.inf, -np.inf], np.nan)
        
        # 3. Categorical missing value imputation
        categorical_columns = df_cleaned.select_dtypes(
            include=['object']
        ).columns
        df_cleaned[categorical_columns] = df_cleaned[
            categorical_columns
        ].fillna('Unknown')
        
        # 4. Numerical features: preserve missing values
        # Gradient boosting models handle missing values natively
        
        return {'application_cleaned': df_cleaned}

5. 特征工程方法

5.1 聚合问题

这个数据集的核心特征工程难点来自关系结构:同一客户在征信、历史申请和还款等附属表中拥有多条记录,而预测模型需要每位客户对应一个固定维度的特征向量。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Client A - Bureau Records:
├─ Record 1: SK_ID_BUREAU=101, AMT_CREDIT_SUM=5000, DAYS_CREDIT=-365
├─ Record 2: SK_ID_BUREAU=102, AMT_CREDIT_SUM=3000, DAYS_CREDIT=-180
├─ Record 3: SK_ID_BUREAU=103, AMT_CREDIT_SUM=8000, DAYS_CREDIT=-90
└─ Record 4: SK_ID_BUREAU=104, AMT_CREDIT_SUM=2000, DAYS_CREDIT=-30

Required Transformation (Single Row):
- bureau_count: 4
- bureau_amt_sum: 18000
- bureau_amt_mean: 4500
- bureau_amt_max: 8000
- bureau_days_min: -365
- bureau_days_max: -30

5.2 聚合方法

特征聚合方法

聚合算子

算子数学定义使用场景业务含义
COUNT\(n = |{r_1, r_2, …, r_n}|\)记录频次贷款或申请次数
SUM\(\Sigma = \sum_{i=1}^{n} x_i\)总敞口累计债务、总还款额
MEAN\(\mu = \frac{1}{n}\sum_{i=1}^{n} x_i\)集中趋势平均贷款金额
MEDIAN\(\tilde{x} = Q_2(x)\)稳健的集中趋势收入中位数,对异常值较稳健
MAX/MIN\(\max(x), \min(x)\)极值最大贷款、最早记录
STD\(\sigma = \sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_i - \mu)^2}\)波动程度收入稳定性、还款一致性
NUNIQUE\(|{x_1, x_2, …}|\)不同值的数量不同放贷机构的数量

5.3 各数据表的特征提取

5.3.1 征信特征工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class BureauFeatureExtractor(BaseTransformer):
    """
    Extracts aggregated features from credit bureau records.
    """
    
    def transform(self, bureau: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        # Primary aggregations
        bureau_agg = bureau.groupby('SK_ID_CURR').agg({
            # Exposure metrics
            'SK_ID_BUREAU': 'count',
            'AMT_CREDIT_SUM': ['sum', 'mean', 'max', 'min', 'std'],
            'AMT_CREDIT_SUM_DEBT': ['sum', 'mean', 'max'],
            'AMT_CREDIT_SUM_OVERDUE': ['sum', 'mean', 'max'],
            
            # Delinquency metrics
            'CNT_CREDIT_PROLONG': ['sum', 'mean'],
            'CREDIT_DAY_OVERDUE': ['sum', 'max', 'mean'],
            
            # Temporal metrics
            'DAYS_CREDIT': ['min', 'max', 'mean'],
            'DAYS_CREDIT_ENDDATE': ['min', 'max'],
            'DAYS_CREDIT_UPDATE': ['min', 'max'],
        })
        
        # Flatten multi-level columns
        bureau_agg.columns = [
            '_'.join(col).strip() 
            for col in bureau_agg.columns.values
        ]
        
        # Active credit subset analysis
        active_mask = bureau['CREDIT_ACTIVE'] == 'Active'
        active_loans = bureau[active_mask].groupby('SK_ID_CURR').agg({
            'AMT_CREDIT_SUM': ['sum', 'count'],
            'AMT_CREDIT_SUM_DEBT': 'sum',
        })
        active_loans.columns = [
            'bureau_active_' + '_'.join(col) 
            for col in active_loans.columns
        ]
        
        # Combine feature sets
        features = bureau_agg.join(active_loans, how='left')
        
        return {'bureau_features': features}

生成特征示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    'SK_ID_BUREAU_count': 5,                    # Total credit relationships
    'AMT_CREDIT_SUM_sum': 45000,                # Total credit exposure
    'AMT_CREDIT_SUM_mean': 9000,                # Average loan size
    'AMT_CREDIT_SUM_max': 20000,                # Maximum single exposure
    'DAYS_CREDIT_min': -730,                    # Oldest relationship
    'DAYS_CREDIT_max': -30,                     # Most recent relationship
    'bureau_active_AMT_CREDIT_SUM_sum': 15000,  # Active exposure
    'bureau_active_SK_ID_BUREAU_count': 2,      # Number of active accounts
}

5.3.2 历史申请特征工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class PreviousApplicationFeatureExtractor(BaseTransformer):
    """
    Extracts features from historical Home Credit applications.
    """
    
    def transform(self, prev_app: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        # Core aggregations
        prev_agg = prev_app.groupby('SK_ID_CURR').agg({
            # Application frequency
            'SK_ID_PREV': 'count',
            
            # Approval metrics
            'NAME_CONTRACT_STATUS': [
                lambda x: (x == 'Approved').sum(),
                lambda x: (x == 'Refused').sum(),
                lambda x: (x == 'Canceled').sum()
            ],
            
            # Financial metrics
            'AMT_APPLICATION': ['sum', 'mean', 'max', 'min'],
            'AMT_CREDIT': ['sum', 'mean', 'max'],
            'AMT_DOWN_PAYMENT': ['sum', 'mean'],
            'AMT_ANNUITY': ['mean', 'max'],
            
            # Pricing metrics
            'RATE_INTEREST_PRIMARY': ['mean', 'max'],
            'RATE_DOWN_PAYMENT': ['mean', 'max'],
            
            # Temporal metrics
            'DAYS_DECISION': ['min', 'max', 'mean'],
        })
        
        # Derived metrics
        total_apps = prev_agg[('SK_ID_PREV', 'count')]
        approved_apps = prev_agg[('NAME_CONTRACT_STATUS', '<lambda_0>')]
        prev_agg['approval_rate'] = approved_apps / total_apps
        
        prev_agg['credit_to_application_ratio'] = (
            prev_agg[('AMT_CREDIT', 'sum')] / 
            prev_agg[('AMT_APPLICATION', 'sum')]
        )
        
        # Flatten column structure
        prev_agg.columns = [
            '_'.join(col).strip() if isinstance(col, tuple) else col 
            for col in prev_agg.columns
        ]
        
        return {'previous_application_features': prev_agg}

关键衍生特征

  • approval_rate:历史获批概率。
  • credit_to_application_ratio:获批金额与申请金额的比率。
  • avg_down_payment_rate:典型首付行为。

5.3.3 分期还款特征工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class InstallmentFeatureExtractor(BaseTransformer):
    """
    Extracts repayment behavior features from installment records.
    """
    
    def transform(self, installments: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        # Calculate derived metrics
        installments['DPD'] = (
            installments['DAYS_ENTRY_PAYMENT'] - 
            installments['DAYS_INSTALMENT']
        )
        installments['AMT_DIFF'] = (
            installments['AMT_PAYMENT'] - 
            installments['AMT_INSTALMENT']
        )
        
        # Aggregations
        install_agg = installments.groupby('SK_ID_CURR').agg({
            # Volume metrics
            'NUM_INSTALMENT_VERSION': 'count',
            
            # Delinquency metrics
            'DPD': ['mean', 'max', 'sum', lambda x: (x > 0).sum()],
            
            # Payment amount metrics
            'AMT_INSTALMENT': ['sum', 'mean', 'max'],
            'AMT_PAYMENT': ['sum', 'mean', 'max'],
            'AMT_DIFF': [
                'mean', 'sum', 'max', 'min', 
                lambda x: (x > 0).sum()
            ],
        })
        
        # Flatten columns
        install_agg.columns = [
            '_'.join(col).strip() 
            for col in install_agg.columns.values
        ]
        
        return {'installment_features': install_agg}

关键衍生指标

  • DPD_mean:平均逾期天数。
  • DPD_max:最严重的一次逾期。
  • AMT_DIFF_mean:平均还款金额偏差,反映多还或少还。

5.4 时间窗口特征

征信特征提取流水线

假设:近期行为比历史平均水平包含更强的预测信号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TemporalWindowFeatureExtractor(BaseTransformer):
    """
    Extracts time-windowed aggregations for trend analysis.
    """
    
    def transform(
        self, 
        data: pd.DataFrame, 
        time_col: str = 'MONTHS_BALANCE'
    ) -> Dict[str, pd.DataFrame]:
        
        window_sizes = [3, 6, 12, 24]  # months
        all_features = {}
        
        for window in window_sizes:
            # Subset to recent history
            recent_mask = data[time_col] >= -window
            recent_data = data[recent_mask]
            
            # Window-specific aggregations
            window_agg = recent_data.groupby('SK_ID_CURR').agg({
                'AMT_BALANCE': ['mean', 'max', 'sum'],
                'SK_ID_PREV': 'count',
            })
            
            # Rename with window suffix
            window_agg.columns = [
                f'{col}_last_{window}m' 
                for col in window_agg.columns
            ]
            
            all_features[f'window_{window}m'] = window_agg
        
        return all_features

5.5 类别变量编码

编码策略选择

方法适用情况优点缺点
标签编码有序类别,如教育水平简单、维度低对无序类别引入虚假顺序
独热编码低基数无序类别,如性别不假设类别有序可能导致维度膨胀
目标编码高基数类别,如职业、地区捕捉与目标的关系存在过拟合风险
频率编码高基数标识符简单,可反映出现频率存在信息损失

实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from category_encoders import TargetEncoder, OneHotEncoder

class CategoricalEncodingPipeline(BaseTransformer):
    """
    Applies appropriate encoding strategies by variable type.
    """
    
    def __init__(self):
        self.encoders = {}
        
    def fit(self, X: pd.DataFrame, y: pd.Series):
        # Target encoding for high-cardinality features
        high_cardinality = [
            'OCCUPATION_TYPE', 'ORGANIZATION_TYPE', 
            'NAME_FAMILY_STATUS'
        ]
        
        for col in high_cardinality:
            encoder = TargetEncoder(cols=[col], smoothing=10.0)
            encoder.fit(X[[col]], y)
            self.encoders[col] = encoder
            
        return self
    
    def transform(self, X: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        X_encoded = X.copy()
        
        for col, encoder in self.encoders.items():
            X_encoded[col] = encoder.transform(X[[col]])
            
        return {'features_encoded': X_encoded}

5.6 特征选择

目标:降低维度、消除噪声、提高训练效率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from sklearn.feature_selection import mutual_info_classif, SelectKBest

class FeatureSelectionTransformer(BaseTransformer):
    """
    Selects top-K features based on mutual information.
    """
    
    def __init__(self, k: int = 500):
        self.k = k
        self.selector = None
        
    def fit(self, X: pd.DataFrame, y: pd.Series):
        self.selector = SelectKBest(
            score_func=mutual_info_classif,
            k=self.k
        )
        self.selector.fit(X, y)
        self.selected_features = X.columns[
            self.selector.get_support()
        ].tolist()
        return self
    
    def transform(self, X: pd.DataFrame) -> Dict[str, pd.DataFrame]:
        X_selected = X[self.selected_features]
        return {
            'features': X_selected,
            'feature_names': self.selected_features
        }

6. 模型选择、训练与评估

6.1 梯度提升决策树

理论基础

梯度提升通过函数空间中的梯度下降,构建弱学习器(通常为决策树)的加性集成:

\[F_m(x) = F_{m-1}(x) + \nu \cdot h_m(x)\]

其中,$h_m(x)$ 是针对伪残差拟合的弱学习器:

\[r_{im} = -\left[\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right]_{F=F_{m-1}}\]

处理表格数据的优势

  1. 自动建模特征交互:树分裂天然能够表示特征组合。
  2. 缺失值处理:原生支持缺失值,无需预先填补。
  3. 非线性表达能力:捕捉复杂的决策边界。
  4. 可解释性:支持特征重要性和部分依赖分析。

6.2 LightGBM、XGBoost 与 CatBoost 对比

模型架构对比

算法特性

特性LightGBMXGBoostCatBoost
树生长方式按叶生长按层生长按层生长
分裂点搜索基于直方图直方图 + 精确搜索对称树
关键优化GOSS、EFB缓存感知访问有序提升
类别特征支持有限支持手动编码原生支持
训练速度最快中等中等
内存效率最优中等良好

基于梯度的单边采样(GOSS):LightGBM 保留梯度较大、误差较高的样本,同时随机抽取小梯度样本,在维持数据分布的同时加快训练。

互斥特征捆绑(EFB):LightGBM 将很少同时为非零值的互斥特征捆绑,在不丢失信息的前提下降低维度。

有序提升(Ordered Boosting):CatBoost 通过训练数据的有序排列消除预测偏移,提供无偏梯度估计。

选择建议

  • 快速实验:LightGBM,具有 10 倍训练速度优势。
  • 追求最高准确性:XGBoost,提升幅度较小但较稳定。
  • 类别数据丰富:CatBoost,原生处理类别特征。

6.3 LightGBM 实现

模型训练流水线

超参数配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import lightgbm as lgb

# Model configuration
LGBM_PARAMS = {
    'objective': 'binary',
    'metric': 'auc',
    'boosting_type': 'gbdt',
    
    # Tree structure
    'num_leaves': 35,
    'max_depth': -1,
    'min_child_samples': 70,
    
    # Learning dynamics
    'learning_rate': 0.02,
    'n_estimators': 5000,
    
    # Regularization
    'reg_lambda': 100.0,
    'reg_alpha': 0.0,
    
    # Sampling
    'subsample': 1.0,
    'colsample_bytree': 0.03,
    
    # Categorical handling
    'categorical_feature': 'auto',
    
    'verbose': -1,
    'random_state': 42
}

训练过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

# Data preparation
X_train, X_valid, y_train, y_valid = train_test_split(
    X, y,
    test_size=0.2,
    stratify=y,
    random_state=42
)

# Dataset construction
train_dataset = lgb.Dataset(X_train, label=y_train)
valid_dataset = lgb.Dataset(X_valid, label=y_valid, reference=train_dataset)

# Model training
model = lgb.train(
    LGBM_PARAMS,
    train_dataset,
    num_boost_round=5000,
    valid_sets=[train_dataset, valid_dataset],
    valid_names=['train', 'valid'],
    callbacks=[
        lgb.early_stopping(stopping_rounds=100),
        lgb.log_evaluation(period=100)
    ]
)

# Performance evaluation
y_pred = model.predict(X_valid, num_iteration=model.best_iteration)
validation_auc = roc_auc_score(y_valid, y_pred)
print(f'Validation AUC: {validation_auc:.4f}')

# Feature importance analysis
importance_df = pd.DataFrame({
    'feature': model.feature_name(),
    'importance_gain': model.feature_importance(importance_type='gain'),
    'importance_split': model.feature_importance(importance_type='split')
}).sort_values('importance_gain', ascending=False)

print("\nTop 20 Features by Gain:")
print(importance_df.head(20))

超参数调整建议

  • num_leaves:以 \(2^{\text{max\_depth}}\) 为参考基准,适当降低以控制过拟合。
  • learning_rate:通常在 0.01—0.1 范围内,较低的值需要更多迭代。
  • reg_lambda:对于噪声较多的数据,可从 1 增至 100。
  • colsample_bytree:对于高维特征,可从 1.0 降至 0.3。

6.4 XGBoost 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import xgboost as xgb

XGB_PARAMS = {
    'objective': 'binary:logistic',
    'eval_metric': 'auc',
    'max_depth': 6,
    'learning_rate': 0.05,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'reg_alpha': 0.1,
    'reg_lambda': 1.0,
    'tree_method': 'hist',
    'seed': 42
}

# DMatrix construction
dtrain = xgb.DMatrix(X_train, label=y_train, enable_categorical=True)
dvalid = xgb.DMatrix(X_valid, label=y_valid, enable_categorical=True)

# Training
eval_results = {}
model = xgb.train(
    XGB_PARAMS,
    dtrain,
    num_boost_round=1000,
    evals=[(dtrain, 'train'), (dvalid, 'valid')],
    evals_result=eval_results,
    early_stopping_rounds=100,
    verbose_eval=100
)

# Evaluation
y_pred = model.predict(dvalid)
auc = roc_auc_score(y_valid, y_pred)

6.5 CatBoost 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from catboost import CatBoostClassifier, Pool

# Identify categorical features
categorical_features = [
    i for i, col in enumerate(X_train.columns)
    if X_train[col].dtype == 'object'
]

# Data pools
train_pool = Pool(X_train, y_train, cat_features=categorical_features)
valid_pool = Pool(X_valid, y_valid, cat_features=categorical_features)

# Model configuration
model = CatBoostClassifier(
    iterations=1000,
    learning_rate=0.05,
    depth=6,
    l2_leaf_reg=3.0,
    early_stopping_rounds=100,
    verbose=100,
    random_seed=42
)

# Training
model.fit(train_pool, eval_set=valid_pool)

# Evaluation
y_pred = model.predict_proba(valid_pool)[:, 1]
auc = roc_auc_score(y_valid, y_pred)

6.6 交叉验证与折外预测

使用交叉验证的原因

  1. 稳定性评估:降低单次训练集与测试集划分带来的方差。
  2. 防止过拟合:验证模型的泛化能力。
  3. 生成 OOF:为模型集成提供无偏的折外预测。

分层 K 折实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from sklearn.model_selection import StratifiedKFold

N_FOLDS = 5
kf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42)

oof_predictions = np.zeros(len(X_train))
test_predictions = np.zeros(len(X_test))
fold_scores = []

for fold, (train_idx, valid_idx) in enumerate(kf.split(X_train, y_train)):
    print(f'\nFold {fold + 1}/{N_FOLDS}')
    
    # Data partitioning
    X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[valid_idx]
    y_tr, y_val = y_train.iloc[train_idx], y_train.iloc[valid_idx]
    
    # Model training
    model = lgb.LGBMClassifier(**LGBM_PARAMS)
    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        callbacks=[lgb.early_stopping(100), lgb.log_evaluation(0)]
    )
    
    # Out-of-fold predictions
    oof_predictions[valid_idx] = model.predict_proba(X_val)[:, 1]
    
    # Test set predictions (ensemble across folds)
    test_predictions += model.predict_proba(X_test)[:, 1] / N_FOLDS
    
    # Fold-level evaluation
    fold_auc = roc_auc_score(y_val, oof_predictions[valid_idx])
    fold_scores.append(fold_auc)
    print(f'Fold AUC: {fold_auc:.4f}')

# Aggregate performance
overall_auc = roc_auc_score(y_train, oof_predictions)
print(f'\nOverall OOF AUC: {overall_auc:.4f} (+/- {np.std(fold_scores):.4f})')

7. 集成学习与模型融合

7.1 集成学习理论

单模型的局限

不同模型存在各自的不足:

  • LightGBM:容易在稀疏特征上过拟合。
  • XGBoost:训练计算成本较高。
  • CatBoost:为获得稳健性,可能牺牲少量准确性。

集成的优势

  • 降低方差:平均预测有助于减少波动。
  • 降低偏差:不同模型捕捉互补模式。
  • 稳定性:减轻单个模型失效的影响。

堆叠集成架构

7.2 两层堆叠架构

架构说明

第 1 层:基学习器,采用不同的梯度提升实现。

  • LightGBM:按叶优化。
  • XGBoost:按层生长,使用精确贪心算法。
  • CatBoost:有序提升。

第 2 层:元学习器,采用简单的线性模型。

  • 逻辑回归或岭回归。
  • 原因:基学习器已提取充分信号,复杂的元学习器可能过拟合。

7.3 折外预测的生成

关键约束:训练元学习器所用的预测,必须来自未使用该目标样本训练的基模型,以防止数据泄漏。

OOF 预测生成

数据泄漏提醒

1
2
3
# INCORRECT: Training set predictions (data leakage)
model.fit(X_train, y_train)
train_pred = model.predict(X_train)  # Model has seen these instances!

正确生成 OOF

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from sklearn.model_selection import StratifiedKFold

kf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof_preds = np.zeros(len(X_train))

for train_idx, valid_idx in kf.split(X_train, y_train):
    X_tr, X_val = X_train[train_idx], X_train[valid_idx]
    y_tr = y_train[train_idx]
    
    model.fit(X_tr, y_tr)
    # Predict on held-out validation set only
    oof_preds[valid_idx] = model.predict_proba(X_val)[:, 1]

7.4 Stacking 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from sklearn.linear_model import LogisticRegression
from typing import Dict, Tuple

class StackingEnsemble:
    """
    Two-level stacking ensemble with OOF prediction generation.
    """
    
    def __init__(
        self, 
        base_models: Dict[str, object],
        meta_learner: object
    ):
        self.base_models = base_models
        self.meta_learner = meta_learner
        self.base_predictions = {}
        
    def fit(
        self, 
        X: pd.DataFrame, 
        y: pd.Series,
        cv: int = 5
    ) -> 'StackingEnsemble':
        """
        Generate OOF predictions and train meta-learner.
        """
        kf = StratifiedKFold(
            n_splits=cv, 
            shuffle=True, 
            random_state=42
        )
        
        # Matrix to store OOF predictions
        n_models = len(self.base_models)
        oof_features = np.zeros((len(X), n_models))
        
        # Generate OOF predictions for each base model
        for idx, (name, model) in enumerate(self.base_models.items()):
            print(f'Generating OOF predictions: {name}...')
            
            for train_idx, valid_idx in kf.split(X, y):
                X_tr = X.iloc[train_idx]
                X_val = X.iloc[valid_idx]
                y_tr = y.iloc[train_idx]
                
                # Fit on training fold
                model.fit(X_tr, y_tr)
                
                # Predict on validation fold
                oof_features[valid_idx, idx] = (
                    model.predict_proba(X_val)[:, 1]
                )
            
            self.base_predictions[name] = oof_features[:, idx].copy()
        
        # Train meta-learner on OOF features
        print('Training meta-learner...')
        self.meta_learner.fit(oof_features, y)
        
        # Retrain base models on full dataset
        print('Retraining base models on full data...')
        for name, model in self.base_models.items():
            model.fit(X, y)
            
        return self
    
    def predict(self, X: pd.DataFrame) -> np.ndarray:
        """
        Generate ensemble predictions.
        """
        # Generate base model predictions
        n_models = len(self.base_models)
        base_features = np.zeros((len(X), n_models))
        
        for idx, (name, model) in enumerate(self.base_models.items()):
            base_features[:, idx] = model.predict_proba(X)[:, 1]
        
        # Meta-learner prediction
        return self.meta_learner.predict_proba(base_features)[:, 1]

# Usage
base_models = {
    'lightgbm': lgb.LGBMClassifier(**lgb_params),
    'xgboost': xgb.XGBClassifier(**xgb_params),
    'catboost': CatBoostClassifier(**ctb_params, verbose=0)
}

meta_model = LogisticRegression(
    C=1.0,
    solver='lbfgs',
    max_iter=1000
)

ensemble = StackingEnsemble(base_models, meta_model)
ensemble.fit(X_train, y_train)
final_predictions = ensemble.predict(X_test)

7.5 超参数优化

超参数调优方法

方法对比

方法策略优势局限计算成本
网格搜索穷举所有组合覆盖全面规模呈指数增长
随机搜索随机采样探索效率较高可能遗漏较优组合中等
贝叶斯优化概率代理模型样本利用效率高实现较复杂低至中等

贝叶斯优化实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from skopt import BayesSearchCV
from skopt.space import Real, Integer

# Define search space
search_spaces = {
    'num_leaves': Integer(20, 50),
    'learning_rate': Real(0.01, 0.1, prior='log-uniform'),
    'min_child_samples': Integer(10, 100),
    'reg_lambda': Real(1e-8, 10.0, prior='log-uniform'),
    'subsample': Real(0.5, 1.0),
    'colsample_bytree': Real(0.3, 1.0)
}

# Bayesian optimization
opt = BayesSearchCV(
    lgb.LGBMClassifier(
        objective='binary',
        metric='auc',
        boosting_type='gbdt',
        n_estimators=1000,
        verbose=-1
    ),
    search_spaces,
    n_iter=50,
    scoring='roc_auc',
    cv=3,
    n_jobs=-1,
    random_state=42,
    verbose=1
)

opt.fit(X_train, y_train)

print(f'Best CV Score: {opt.best_score_:.4f}')
print(f'Optimal Parameters: {opt.best_params_}')

7.6 性能汇总

模型配置交叉验证 AUC公开排行榜私有排行榜相对提升
LightGBM(单模型)0.79020.7910.792基线
XGBoost(单模型)0.78540.7870.788-0.004
CatBoost(单模型)0.78810.7890.790-0.002
简单平均0.79200.7930.794+0.002
Stacking(LGB+XGB+CTB+LR)0.80530.8070.808+0.016

主要认识

  • 堆叠集成相对最佳单模型提升了 1.6 个百分点。
  • 在 Kaggle 排行榜中,AUC 提升 0.01 往往对应数百名的排名变化。
  • 简单的逻辑回归元学习器通过减少过拟合,优于更复杂的替代方案。

8. 结论与最佳实践

8.1 完整技术流程回顾

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Raw Data (7 tables, 50M+ records)
    
Data Preprocessing
    ├─ Anomaly detection and treatment
    ├─ Missing value imputation
    └─ Categorical encoding
    
Feature Engineering (1,000+ features)
    ├─ Aggregation operations (count/sum/mean/max/std)
    ├─ Temporal window features (3m/6m/12m/24m)
    ├─ Ratio and interaction features
    └─ Target encoding for high-cardinality variables
    
Model Development
    ├─ LightGBM (primary model)
    ├─ XGBoost (accuracy complement)
    └─ CatBoost (robustness validation)
    
Ensemble Construction
    ├─ Out-of-fold prediction generation
    ├─ Meta-learner training
    └─ Final prediction aggregation
    
Submission (AUC 0.808, Top 5%)

8.2 核心技术认识

数据架构

  • 关系型数据库结构需要系统化的聚合策略。
  • 一对多关系需要谨慎提取特征,避免信息损失。
  • 时间序列比静态快照提供更丰富的信号。

特征工程

  • 领域知识从根本上决定了特征构造的可能空间。
  • 均值、中位数和最大值等不同聚合函数,隐含不同的业务假设。
  • 时间窗口特征比历史平均值更能捕捉行为趋势。

建模策略

  • 梯度提升仍是结构化数据预测的领先方法。
  • 交叉验证兼顾稳定性评估和集成准备。
  • 堆叠集成能够带来持续、显著的性能提升。

8.3 可复现的工程实践

  1. 流水线架构:模块化设计便于组件测试与替换。
  2. 配置管理:集中管理参数,便于实验追踪。
  3. 开发模式:通过子采样策略(--dev_mode)缩短迭代周期。
  4. 实验追踪:系统记录实验日志,避免重复计算。

8.4 后续研究方向

短期优化:1—2 周

  • 探索人工设定之外的特征交互。
  • 根据验证集表现优化加权集成。
  • 细化超参数搜索空间。

中期扩展:1—2 个月

  • 使用深度学习提取特征,例如自编码器表示。
  • 使用图神经网络对关系数据建模。
  • 通过 SHAP 值分解分析模型可解释性。

长期探索:3 个月以上

  • 构建在线学习系统,适应分布漂移。
  • 建设用于生产模型验证的 A/B 测试框架。
  • 使用联邦学习架构开展跨机构协作。

8.5 推荐资源

官方文档

代表性论文

  • Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. KDD.
  • Ke, G., et al. (2017). LightGBM: A Highly Efficient Gradient Boosting Decision Tree. NIPS.
  • Prokhorenkova, L., et al. (2018). CatBoost: Unbiased Boosting with Categorical Features. NIPS.

竞赛资源


总结

本文对 Home Credit Default Risk 竞赛方案的分析,展示了机器学习方法在实际信用风险评估中的系统应用。项目的价值不仅在于取得的 AUC(0.808),还体现在:

  1. 工程规范:流水线架构保证可复现性和可维护性。
  2. 以数据为中心:探索性分析直接指导特征工程决策。
  3. 系统优化:从单模型逐步改进为复杂集成。

基本原则

特征工程决定理论性能上限,机器学习算法则逐步逼近这一上限。持续投入数据理解与特征构建,通常比仅调整超参数更有效。

本文的方法也可迁移到以下相关领域:

  • 保险欺诈检测。
  • 营销响应建模。
  • 客户流失预测。
  • 信用评分系统开发。

本文对 Kaggle Home Credit Default Risk 竞赛方案进行了完整技术分析。开源实现见 GitHub