机器学习 风控模型

风险模型构建实战:从特征工程到模型部署

2026-07-11 · 约12分钟阅读 · 风控模型 · 特征工程 · XGBoost · 模型监控 · 生产部署

风险模型是金融科技领域的核心技术之一。我在多个风控项目中积累了从数据准备到模型上线的完整经验,这篇文章分享风险模型构建的全流程,包括特征工程、模型选择、评估指标和生产部署。

业务场景与数据来源

我参与过的风控项目主要包括:

  • 信用评分模型:基于用户行为数据预测违约概率,用于授信决策
  • 欺诈检测模型:实时识别异常交易行为,防止资金损失
  • 反洗钱模型:检测可疑交易模式,满足监管合规要求

数据来源通常包括:

  1. 用户基础信息:年龄、性别、职业、收入水平
  2. 历史行为数据:交易记录、还款记录、登录日志
  3. 外部数据:征信报告、运营商数据、电商行为数据
  4. 设备信息:手机型号、IP地址、地理位置

数据准备与预处理

风控数据的特点是不平衡——正常样本远多于异常样本。这一步的核心是处理缺失值、异常值和类别变量。

import pandas as pd
import numpy as np

data = pd.read_csv('risk_data.csv')

missing_ratio = data.isnull().mean()
cols_to_drop = missing_ratio[missing_ratio > 0.3].index.tolist()
data = data.drop(cols_to_drop, axis=1)

num_cols = data.select_dtypes(include=[np.number]).columns
for col in num_cols:
    data[col] = data[col].fillna(data[col].median())

cat_cols = data.select_dtypes(include=['object']).columns
for col in cat_cols:
    data[col] = data[col].fillna('missing')

特征工程:风控模型的灵魂

风控模型的效果80%取决于特征工程。我总结了一套可复用的特征工程流程:

1. 分箱处理(WOE编码)

连续变量分箱是风控特征工程的核心技术,将连续值转化为离散区间,然后计算每个区间的WOE(Weight of Evidence)值。

import pandas as pd
import numpy as np

def woe_binning(df, feature, target, n_bins=5):
    df = df.copy()
    df['bin'] = pd.qcut(df[feature], q=n_bins, duplicates='drop')
    
    grouped = df.groupby('bin').agg(
        bad=('target', 'sum'),
        total=('target', 'count')
    )
    grouped['good'] = grouped['total'] - grouped['bad']
    
    grouped['bad_rate'] = grouped['bad'] / grouped['bad'].sum()
    grouped['good_rate'] = grouped['good'] / grouped['good'].sum()
    
    grouped['woe'] = np.log(grouped['bad_rate'] / grouped['good_rate'])
    grouped['iv'] = (grouped['bad_rate'] - grouped['good_rate']) * grouped['woe']
    
    iv = grouped['iv'].sum()
    
    bin_map = grouped['woe'].to_dict()
    df['woe'] = df['bin'].map(bin_map)
    
    return df, bin_map, iv

for feature in ['age', 'income', 'loan_amount']:
    data, bin_map, iv = woe_binning(data, feature, 'target')
✅ IV值判断标准:IV < 0.02(无预测能力)、0.02-0.1(弱)、0.1-0.3(中等)、0.3-0.5(强)、> 0.5(可疑,可能过拟合)

2. 时间窗口特征

基于时间窗口提取用户行为特征,这是风控模型最重要的特征来源之一。

def create_time_window_features(df):
    df = df.copy()
    df['transaction_date'] = pd.to_datetime(df['transaction_date'])
    
    windows = [7, 14, 30, 90]
    
    for window in windows:
        df[f'tx_count_{window}d'] = df.groupby('user_id')['transaction_date'].transform(
            lambda x: x.rolling(f'{window}D').count()
        )
        
        df[f'tx_amount_sum_{window}d'] = df.groupby('user_id')['amount'].transform(
            lambda x: x.rolling(f'{window}D').sum()
        )
        
        df[f'tx_amount_std_{window}d'] = df.groupby('user_id')['amount'].transform(
            lambda x: x.rolling(f'{window}D').std()
        )
        
        df[f'active_days_{window}d'] = df.groupby('user_id')['transaction_date'].transform(
            lambda x: x.rolling(f'{window}D').apply(lambda y: y.nunique())
        )
    
    return df

3. 用户画像特征

def create_user_profile_features(df):
    df = df.copy()
    
    user_stats = df.groupby('user_id').agg(
        total_transactions=('transaction_id', 'count'),
        total_amount=('amount', 'sum'),
        avg_transaction_amount=('amount', 'mean'),
        max_transaction_amount=('amount', 'max'),
        first_transaction_date=('transaction_date', 'min'),
        last_transaction_date=('transaction_date', 'max'),
        device_count=('device_id', 'nunique'),
        ip_count=('ip_address', 'nunique'),
        city_count=('city', 'nunique')
    ).reset_index()
    
    user_stats['tenure_days'] = (user_stats['last_transaction_date'] - 
                                  user_stats['first_transaction_date']).dt.days
    
    user_stats['transaction_frequency'] = user_stats['total_transactions'] / \
                                          user_stats['tenure_days'].replace(0, 1)
    
    return user_stats

模型选择与训练

风控模型的选择需要平衡模型效果可解释性。以下是我常用的模型对比:

1. Logistic Regression(首选)

逻辑回归是风控领域的"瑞士军刀"——简单、稳定、可解释性强,适合作为基准模型。

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score

X_train, X_test, y_train, y_test = train_test_split(
    features, target, test_size=0.2, stratify=target, random_state=42
)

model = LogisticRegression(
    penalty='l2',
    C=1.0,
    class_weight='balanced',
    random_state=42
)

model.fit(X_train, y_train)
y_pred_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_pred_proba)

2. XGBoost(进阶选择)

当逻辑回归的效果达不到业务要求时,XGBoost是首选的非线性模型。

import xgboost as xgb

dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

params = {
    'objective': 'binary:logistic',
    'eval_metric': 'auc',
    'max_depth': 6,
    'learning_rate': 0.05,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'scale_pos_weight': len(y_train[y_train == 0]) / len(y_train[y_train == 1]),
    'seed': 42
}

model = xgb.train(
    params,
    dtrain,
    num_boost_round=200,
    evals=[(dtest, 'test')],
    early_stopping_rounds=20,
    verbose_eval=10
)

3. LightGBM(性能优化)

LightGBM在处理大规模数据时比XGBoost更快,内存占用更低。

import lightgbm as lgb

lgb_train = lgb.Dataset(X_train, y_train)
lgb_test = lgb.Dataset(X_test, y_test, reference=lgb_train)

params = {
    'objective': 'binary',
    'metric': 'auc',
    'boosting_type': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.9,
    'bagging_fraction': 0.8,
    'bagging_freq': 5,
    'scale_pos_weight': len(y_train[y_train == 0]) / len(y_train[y_train == 1])
}

model = lgb.train(
    params,
    lgb_train,
    num_boost_round=200,
    valid_sets=[lgb_test],
    early_stopping_rounds=20
)

模型评估:不止于AUC

风控模型的评估需要关注多个维度,不能只看AUC:

from sklearn.metrics import (
    roc_auc_score,
    precision_score,
    recall_score,
    f1_score,
    confusion_matrix
)

def evaluate_model(y_true, y_pred, y_pred_proba):
    metrics = {}
    metrics['auc'] = roc_auc_score(y_true, y_pred_proba)
    metrics['precision'] = precision_score(y_true, y_pred)
    metrics['recall'] = recall_score(y_true, y_pred)
    metrics['f1'] = f1_score(y_true, y_pred)
    
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    metrics['accuracy'] = (tp + tn) / (tp + tn + fp + fn)
    metrics['false_positive_rate'] = fp / (fp + tn)
    metrics['false_negative_rate'] = fn / (fn + tp)
    
    return metrics

业务视角的评估指标:

  • KS值:评估模型区分能力,KS > 0.3表示模型有较好的区分度
  • Lift值:top 10%样本的命中率是随机选择的倍数
  • PSI值:群体稳定性指标,PSI < 0.1表示模型稳定
  • 业务收益:模型上线后减少的损失金额
def calculate_ks(y_true, y_pred_proba):
    df = pd.DataFrame({'y': y_true, 'score': y_pred_proba})
    df = df.sort_values('score', ascending=False).reset_index(drop=True)
    
    df['cum_bad'] = df['y'].cumsum()
    df['cum_good'] = (1 - df['y']).cumsum()
    
    df['bad_rate'] = df['cum_bad'] / df['y'].sum()
    df['good_rate'] = df['cum_good'] / (1 - df['y']).sum()
    
    df['ks'] = abs(df['bad_rate'] - df['good_rate'])
    
    return df['ks'].max()

def calculate_psi(expected, actual):
    bins = np.histogram_bin_edges(expected, bins=10)
    
    expected_counts = np.histogram(expected, bins=bins)[0]
    actual_counts = np.histogram(actual, bins=bins)[0]
    
    expected_probs = expected_counts / len(expected)
    actual_probs = actual_counts / len(actual)
    
    psi = 0
    for e, a in zip(expected_probs, actual_probs):
        if e > 0 and a > 0:
            psi += (a - e) * np.log(a / e)
    
    return psi

模型部署与监控

模型上线不是终点,持续监控才是关键。

1. 模型打包与部署

import joblib

model = xgb.Booster()
model.load_model('risk_model.model')

joblib.dump({
    'model': model,
    'bin_maps': bin_maps,
    'feature_names': feature_names,
    'version': '1.0.0',
    'deploy_date': pd.Timestamp.now()
}, 'risk_model_v1.joblib')

from flask import Flask, request, jsonify
import joblib

app = Flask(__name__)
model_dict = joblib.load('risk_model_v1.joblib')
model = model_dict['model']

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    features = preprocess(data)
    dmatrix = xgb.DMatrix(features)
    probability = model.predict(dmatrix)[0]
    
    return jsonify({
        'risk_score': float(probability),
        'risk_level': 'high' if probability > 0.7 else 'medium' if probability > 0.4 else 'low',
        'model_version': model_dict['version']
    })

2. 模型监控

import time

class ModelMonitor:
    def __init__(self):
        self.predictions = []
        self.actuals = []
        self.feature_distributions = {}
    
    def log_prediction(self, features, prediction, timestamp=None):
        if timestamp is None:
            timestamp = time.time()
        
        self.predictions.append({
            'features': features,
            'prediction': prediction,
            'timestamp': timestamp
        })
    
    def log_actual(self, prediction_id, actual):
        for pred in self.predictions:
            if pred['prediction_id'] == prediction_id:
                pred['actual'] = actual
                self.actuals.append(pred)
                break
    
    def calculate_drift(self, feature_name):
        if feature_name not in self.feature_distributions:
            return None
        
        recent_features = [p['features'][feature_name] for p in self.predictions[-1000:]]
        baseline = self.feature_distributions[feature_name]
        
        recent_mean = np.mean(recent_features)
        baseline_mean = np.mean(baseline)
        
        drift_score = abs(recent_mean - baseline_mean) / baseline_mean
        
        return drift_score

monitor = ModelMonitor()

@app.route('/predict', methods=['POST'])
def predict():
    data = request.json
    features = preprocess(data)
    
    monitor.log_prediction(features, prediction)
    
    dmatrix = xgb.DMatrix(features)
    probability = model.predict(dmatrix)[0]
    
    return jsonify({'risk_score': float(probability)})

实际项目踩的坑

坑一:样本选择偏差。训练数据只包含已授信用户,而实际预测对象是未授信用户,两者分布不同。解决方案:使用时间切片验证,确保训练数据时间早于验证数据。

坑二:特征泄露。使用了预测时不可用的特征。比如用"是否逾期"预测"是否逾期",或者用未来的交易数据。解决方案:严格按照时间顺序划分训练集和测试集。

坑三:模型漂移。模型上线后效果逐渐下降,因为用户行为和市场环境在变化。解决方案:设置PSI监控阈值,超过阈值自动触发模型重新训练。

坑四:可解释性不足。业务方需要知道"为什么这个用户被拒绝"。解决方案:使用SHAP或LIME解释模型预测结果,给出特征贡献度。

import shap

explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

shap.summary_plot(shap_values, X_test)
shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])

总结

风险模型构建是一个"数据驱动 + 业务理解"的迭代过程。核心要点:

  1. 从简单模型开始:先用逻辑回归建立基准,验证特征有效性
  2. 特征工程是核心:好的特征比好的模型更重要
  3. 评估指标要全面:不止看AUC,还要关注KS、PSI、业务收益
  4. 监控不可少:模型上线后需要持续监控,及时发现漂移

记住:风控模型的目标不是追求最高的AUC,而是在控制误拒率的前提下最大化风险识别能力。一个完美的模型如果误拒率太高,业务方也不会接受。

🤖
AI助手
ONLINE