1. 业务背景:自营消费金融的风控痛点

在自营消费金融业务中,风控模型直接决定每一笔放款的资产质量。传统评分卡模型(如 FICO 变体)虽然稳定,但特征工程成本高、对非线性关系建模能力弱,在互联网金融场景下往往表现不佳。而深度学习模型虽然精度高,但黑盒特性导致无法向监管和业务团队解释每一笔拒贷的原因,这在强监管的金融行业是不可接受的。

我们本次落地的项目是将 XGBoost 模型作为主模型(兼顾精度与可解释性),结合 SHAP(SHapley Additive exPlanations)提供逐笔决策的归因分析。具体业务场景是面向个人消费贷的自动化审批,模型在 Test 集上的 KS 达到 0.42,AUC 达到 0.87,满足金融级风控标准(KS≥0.3, AUC≥0.75)。

2. XGBoost + SHAP 可解释风控模型设计

金融风控模型的可解释性不是「可选项」,而是「必选项」。监管要求(如银保监会《人工智能在金融领域应用指引》)明确要求金融机构能够解释 AI 决策的主要依据。我们选择 XGBoost 而非深度神经网络,正是因为 XGBoost 天然支持特征重要性统计,配合 SHAP 框架可以提供每个样本级别的归因解释。

特征工程阶段,我们从征信报告、运营商数据和电商消费记录三个维度构建了 187 个特征。经过 IV(Information Value)筛选,保留了 IV > 0.02 的 52 个核心特征。以下是特征重要性与 SHAP 归因的核心代码实现:

# 1. 安装核心依赖
pip install xgboost shap scikit-learn pandas numpy matplotlib

# 2. 特征工程与模型训练(train_model.py)
import pandas as pd
import numpy as np
import xgboost as xgb
import shap
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, roc_curve, ks_2samp
import joblib
import json

# 加载特征工程后的数据集
df = pd.read_parquet("features/credit_features_v3.parquet")

# 特征列与标签列
feature_cols = [c for c in df.columns if c not in ["user_id", "label", "apply_date"]]
X = df[feature_cols]
y = df["label"]

# 时间窗口切分:前 12 个月训练,后 3 个月验证
train_mask = df["apply_date"] < "2025-10-01"
val_mask = (df["apply_date"] >= "2025-10-01") & (df["apply_date"] < "2025-12-01")

X_train, y_train = X[train_mask], y[train_mask]
X_val, y_val = X[val_mask], y[val_mask]

# XGBoost 模型配置(金融风控标准参数)
model = xgb.XGBClassifier(
    n_estimators=400,
    max_depth=5,
    learning_rate=0.05,
    subsample=0.8,
    colsample_bytree=0.7,
    reg_alpha=1.0,
    reg_lambda=5.0,
    scale_pos_weight=np.sum(y_train == 0) / np.sum(y_train == 1),
    random_state=42,
    n_jobs=16,
    tree_method="hist",
    eval_metric="auc"
)

model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)

# 离线评估
val_pred = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, val_pred)
ks, _ = ks_2samp(val_pred[y_val == 1], val_pred[y_val == 0])
print(f"AUC: {auc:.4f}, KS: {ks:.4f}")
# 预期输出:AUC ≈ 0.87, KS ≈ 0.42
💡 行业经验:金融风控模型的时间切分(Time-based Split)比随机切分更合理。随机切分会导致「未来信息泄露」,即训练集包含验证集时间窗口内的信息,使离线评估虚高。我们必须在训练集截止日之前收集数据,验证集严格在时间轴上位于训练集之后。

3. 模型训练与离线验证

模型离线验证通过后,需要对模型产出进行全面的业务逻辑校验。我们在验证阶段重点关注三个指标:切分稳定性(PSI < 0.1)、特征稳定性(IV 波动 < 20%)和基尼系数(Gini > 0.4)。以下命令展示了完整验证流程:

# 3. 保存模型与特征清单
joblib.dump(model, "models/risk_model_v3.pkl")
with open("models/feature_cols.json", "w") as f:
    json.dump(feature_cols, f)

# 4. SHAP 可解释性解释器初始化(使用 TreeExplainer,速度最快)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_val[:1000])

# 5. 全局特征重要性可视化(用于监管报告)
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
shap.summary_plot(shap_values, X_val[:1000], feature_names=feature_cols, show=False)
plt.savefig("reports/shap_summary_v3.png", dpi=150, bbox_inches="tight")

# 6. 单样本归因(逐笔决策解释)
sample = X_val.iloc[0]
sample_shap = explainer.shap_values(sample)
base_value = explainer.expected_value
# 生成可读归因:哪些特征推动了「通过」vs 「拒绝」
shap.force_plot(
    base_value, sample_shap, sample,
    feature_names=feature_cols,
    matplotlib=True, show=False
)
plt.savefig("reports/shap_force_v3_user_001.png", dpi=150)

SHAP 归因结果的可读性对于风控运营团队至关重要。我们要求每笔拒贷决策都必须附带 SHAP force plot 图,展示最重要的 3-5 个负面因素。这意味着在线服务必须同时返回预测概率和 SHAP 归因结果,这对服务性能提出了额外要求。

4. FastAPI 在线服务部署

在线服务必须满足两个核心 SLI(Service Level Indicator):P99 延迟 < 100ms,可用性 > 99.95%。我们选择 FastAPI + Uvicorn 构建推理服务,并用 Redis 做特征查询缓存,将平均响应时间控制在 30ms 以内。

# 7. 创建在线服务(/opt/risk-service/app.py)
cat > /opt/risk-service/app.py << 'PYEOF'
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field, validator
from typing import List, Optional, Dict
import joblib, json, numpy as np, pandas as pd, time, logging
from prometheus_client import Counter, Histogram, start_http_server

app = FastAPI(title="Risk Model API v3", version="3.2.1")

# Prometheus 监控指标
REQUEST_LATENCY = Histogram("risk_request_latency_seconds", "Request latency", buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.5])
REQUEST_COUNT = Counter("risk_requests_total", "Total requests", ["model_version", "decision"])

# 加载模型与配置
MODEL_PATH = "/opt/risk-service/models/risk_model_v3.pkl"
FEATURE_PATH = "/opt/risk-service/models/feature_cols.json"
model = joblib.load(MODEL_PATH)
with open(FEATURE_PATH) as f:
    FEATURE_COLS = json.load(f)

class RiskRequest(BaseModel):
    user_id: str
    features: Dict[str, float]
    channel: str = "app"
    request_id: Optional[str] = None

class RiskResponse(BaseModel):
    user_id: str
    probability: float = Field(..., ge=0, le=1)
    decision: str  # "approve" / "reject" / "review"
    threshold: float
    top_factors: List[Dict]
    latency_ms: float

@app.post("/v1/risk/score", response_model=RiskResponse)
@REQUEST_LATENCY.time()
def score_risk(req: RiskRequest):
    try:
        start = time.time()
        # 构建特征向量(缺失值填充为 -1)
        x = pd.DataFrame([[req.features.get(c, -1) for c in FEATURE_COLS]], columns=FEATURE_COLS)
        prob = model.predict_proba(x)[0][1]
        
        # 阈值决策(可配置)
        if prob >= 0.75:
            decision = "approve"
        elif prob >= 0.30:
            decision = "review"
        else:
            decision = "reject"
        
        # 特征重要性归因
        top_idx = np.argsort(np.abs(model.feature_importances_))[::-1][:5]
        top_factors = [
            {"feature": FEATURE_COLS[i], "importance": round(float(model.feature_importances_[i]), 4)}
            for i in top_idx
        ]
        
        latency = (time.time() - start) * 1000
        REQUEST_COUNT.labels(model_version="v3.2", decision=decision).inc()
        
        return {
            "user_id": req.user_id,
            "probability": round(float(prob), 4),
            "decision": decision,
            "threshold": 0.30,
            "top_factors": top_factors,
            "latency_ms": round(latency, 2)
        }
    except Exception as e:
        logging.exception("Scoring failed")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
def health():
    return {"status": "ok", "model_version": "v3.2.1", "loaded_features": len(FEATURE_COLS)}

if __name__ == "__main__":
    import uvicorn
    start_http_server(9100)  # Prometheus 指标端点
    uvicorn.run(app, host="0.0.0.0", port=8080, workers=8)
PYEOF

# 8. 创建 systemd 服务
sudo tee /etc/systemd/system/risk-service.service > /dev/null << 'EOF'
[Unit]
Description=Risk Scoring Service v3.2
After=network.target

[Service]
Type=simple
User=risk
Group=risk
WorkingDirectory=/opt/risk-service
ExecStart=/opt/risk-service/venv/bin/python app.py
Restart=always
RestartSec=5
LimitNOFILE=65536
Environment="PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus"

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now risk-service

5. A/B 测试与灰度发布策略

模型上线前必须经过严格的影子模式(Shadow Mode)验证和 A/B 测试。影子模式下,新模型在后台并行产生评分,但不影响真实决策,用于对比新旧模型的 KS 差异和错杀率差异。A/B 测试阶段将 5% 的流量切给新模型,观察期不少于 2 周。

# 9. 影子模式验证脚本(shadow_validator.py)
cat > /opt/risk-service/shadow_validator.py << 'PYEOF'
import requests, json, time, csv
from datetime import datetime

OLD_MODEL_URL = "http://localhost:8080/v1/risk/score"  # 线上模型
NEW_MODEL_URL = "http://localhost:8081/v1/risk/score"  # 候选模型

results = []
with open("data/shadow_sample_10k.csv") as f:
    for line in f:
        user_id, features = line.strip().split("\t", 1)
        payload = {"user_id": user_id, "features": json.loads(features)}
        
        r_old = requests.post(OLD_MODEL_URL, json=payload).json()
        r_new = requests.post(NEW_MODEL_URL, json=payload).json()
        
        results.append({
            "user_id": user_id,
            "old_prob": r_old["probability"],
            "new_prob": r_new["probability"],
            "old_dec": r_old["decision"],
            "new_dec": r_new["decision"],
            "delta": abs(r_old["probability"] - r_new["probability"])
        })

# 统计一致性
same_decisions = sum(1 for r in results if r["old_dec"] == r["new_dec"])
print(f"决策一致性: {same_decisions/len(results):.2%}")
print(f"平均概率差: {sum(abs(r['delta']) for r in results)/len(results):.4f}")
PYEOF

# 10. A/B 流量切分(使用 Nginx 权重加权)
# 在 Nginx upstream 配置中添加权重:
# upstream risk_backend {
#   server 10.0.1.10:8080 weight=95;  # 旧模型(v3.1)
#   server 10.0.1.11:8080 weight=5;   # 新模型(v3.2)
# }
# A/B 测试期观察:
# - 新模型 KS 提升 ≥ 2pp 且 P-value < 0.05
# - 新模型错杀率(False Negative Rate)不高于旧模型 10%
# - 新模型 P99 延迟 < 100ms

6. 生产监控与模型漂移治理

模型上线并不意味着工作结束。金融风控模型随宏观经济变化会发生漂移(Model Drift),表现为模型区分能力下降、拒贷率异常波动或特征分布偏移。我们建立了一套三层监控体系:

# 11. 监控告警规则(Prometheus + Alertmanager)
# 规则文件:/etc/prometheus/risk_alerts.yml
cat > /opt/risk-service/prometheus_rules.yml << 'EOF'
groups:
  - name: risk_model_alerts
    rules:
      - alert: RiskModelHighLatency
        expr: histogram_quantile(0.99, sum(rate(risk_request_latency_seconds_bucket[5m])) by (le)) > 0.12
        for: 5m
        labels: {severity: "critical"}
        annotations:
          summary: "Risk model P99 latency breach"
          description: "P99 latency {{ $value }}s exceeds 120ms threshold"

      - alert: RiskModelDriftPSI
        expr: psi_score > 0.15
        for: 1h
        labels: {severity: "warning"}
        annotations:
          summary: "Feature drift detected"
          description: "Feature {{ $labels.feature }} PSI {{ $value }} exceeds 0.15"

      - alert: RiskModelApprovalRateAnomaly
        expr: abs(rate(approval_count - approval_baseline) / approval_baseline) > 0.2
        for: 30m
        labels: {severity: "critical"}
        annotations:
          summary: "Approval rate anomaly"
          description: "Approval rate changed by {{ $value }}% from baseline"
EOF

# 12. PSI 漂移检测脚本(drift_monitor.py)
# 每日凌晨跑一次,对比最近 7 天与训练集特征分布
python /opt/risk-service/drift_monitor.py \
  --train-ref /data/train_features.parquet \
  --recent /data/recent_7d.parquet \
  --features age,income,credit_utilization,query_count_30d \
  --psi-threshold 0.15 \
  --alert-webhook https://hooks.slack.com/services/T00/B00/xxx

漂移监控的自动化流程:当 PSI > 0.15 持续 2 小时,自动触发告警并唤醒数据科学团队。如果 PSI > 0.25,自动切换至兜底评分卡(规则模型),确保业务不受影响,同时启动模型重训流程。

🚨 监管合规红线:任何模型变更必须经过「三审」流程:数据科学团队技术评审 → 风险管理部业务评审 → 合规部合规审查。变更必须记录在模型注册表(Model Registry)中,保留完整的训练记录、验证报告和上线审批单。未经三审的模型绝不允许上线。

通过这套完整的方案,我们将 XGBoost 风控模型从实验环境成功落地到生产环境,实现了 ≤ 30ms 的在线推理延迟、99.95% 的服务可用性和自动化的漂移监控。这套体系已经在我们的消费金融业务中稳定运行 6 个月,累计完成超过 200 万笔贷款审批,模型 KS 稳定在 0.40 以上。