当前位置:首页>python>信贷风控的“发动机”:从数据到评分卡,Python全流程实战

信贷风控的“发动机”:从数据到评分卡,Python全流程实战

  • 2026-09-30 03:07:58
信贷风控的“发动机”:从数据到评分卡,Python全流程实战

别人调包你调参,别人调参你建模,这差距就是这么拉开的

项目准备:先把家伙什儿备齐

先梳理一下整个流程:

数据清洗(缺失值、异常值)→ 特征分箱(连续变量离散化)→ WOE/IV计算 → 逻辑回归建模 → 评分刻度转化 → 模型评估

import pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport seaborn as snsfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegressionfrom sklearn.metrics import roc_auc_score, roc_curve, confusion_matriximport warningswarnings.filterwarnings('ignore')# 设置中文显示plt.rcParams['font.sans-serif'] = ['SimHei']plt.rcParams['axes.unicode_minus'] = False# 读取数据(这里以信贷常用数据为例,实际替换为自己的数据源)# df = pd.read_csv('your_credit_data.csv')

第一步:数据预处理,别让脏数据毁了模型

数据预处理占整个建模时间的70%,不是夸张,是血泪教训。

defdata_preprocess(df):"""    数据清洗标准流程    """    df_clean = df.copy()# 1. 缺失值处理    missing_rate = df_clean.isnull().sum() / len(df_clean)# 缺失率超过50%的列直接剔除    drop_cols = missing_rate[missing_rate > 0.5].index.tolist()    df_clean = df_clean.drop(columns=drop_cols)# 数值型变量:用中位数填充    num_cols = df_clean.select_dtypes(include=[np.number]).columnsfor col in num_cols:        df_clean[col].fillna(df_clean[col].median(), inplace=True)# 分类型变量:用众数填充    cat_cols = df_clean.select_dtypes(include=['object']).columnsfor col in cat_cols:        df_clean[col].fillna(df_clean[col].mode()[0], inplace=True)# 2. 异常值处理(3σ原则或百分位截尾)for col in num_cols:        lower = df_clean[col].quantile(0.01)        upper = df_clean[col].quantile(0.99)        df_clean[col] = df_clean[col].clip(lower, upper)return df_clean# df_clean = data_preprocess(df)

第二步:分箱——把连续变量“切碎”的学问

分箱听起来简单,实则暗藏玄机。好的分箱能让模型既稳定又区分度高。

这里实现一个卡方分箱的简化版本(实际生产中用得更复杂,但原理相通):

defmono_bin(df, col, target, n=10):"""    等频分箱 + 单调性检验    """    total = df[col].count()    df_temp = df[[col, target]].copy()    df_temp['bin'] = pd.qcut(df_temp[col], q=n, duplicates='drop')# 计算每个箱体的坏账率    bin_stats = df_temp.groupby('bin')[target].agg(['count', 'sum'])    bin_stats['bad_rate'] = bin_stats['sum'] / bin_stats['count']    bin_stats['good_rate'] = 1 - bin_stats['bad_rate']# 计算每个箱体的占比    bin_stats['dist'] = bin_stats['count'] / totalreturn bin_statsdefiv_calc(df, col, target):"""    计算单个变量的IV值    """    df_temp = df[[col, target]].copy()    grouped = df_temp.groupby(col)[target].agg(['count', 'sum'])    grouped.columns = ['total', 'bad']    grouped['good'] = grouped['total'] - grouped['bad']# 计算好/坏占比    total_bad = grouped['bad'].sum()    total_good = grouped['good'].sum()    grouped['bad_pct'] = grouped['bad'] / total_bad    grouped['good_pct'] = grouped['good'] / total_good# 计算WOE和IV    grouped['woe'] = np.log(grouped['good_pct'] / grouped['bad_pct'])    grouped['iv'] = (grouped['good_pct'] - grouped['bad_pct']) * grouped['woe']return grouped['iv'].sum(), grouped['woe'].to_dict()

关于分箱,有个经验法则跟大家分享:IV值小于0.02的变量基本没啥预测力,可以直接扔掉;IV值大于0.5的变量要警惕,可能是过度拟合的信号。

第三步:WOE编码——让逻辑回归脱胎换骨

WOE(Weight of Evidence)是评分卡的灵魂。它把原始变量转化成与目标变量的单调关系,这正是逻辑回归最爱的“食材”。

defwoe_transform(df, col, target, bins):"""    将变量转换为WOE值    """    df_woe = df.copy()    woe_dict = {}# 这里假设bins已经通过分箱确定for bin_interval, woe_val in bins.items():        df_woe.loc[df_woe[col].isin(bin_interval), col+'_woe'] = woe_valreturn df_woe, woe_dictdeffeature_selection_by_iv(df, target, threshold=0.02):"""    基于IV值筛选特征    """    selected_features = []    iv_dict = {}for col in df.columns:if col == target:continue# 只对数值型变量计算IVif df[col].dtype in ['int64', 'float64']:# 先分箱try:                df_temp = df[[col, target]].copy()                df_temp['bin'] = pd.qcut(df_temp[col], q=5, duplicates='drop')                iv, _ = iv_calc(df_temp, 'bin', target)                iv_dict[col] = ivif iv >= threshold:                    selected_features.append(col)except:continuereturn selected_features, iv_dict

第四步:逻辑回归——评分卡的心脏

逻辑回归之所以是评分卡的首选,不是因为精度最高,而是可解释性最强。在金融监管日益严格的今天,能说清楚“为什么拒绝这个客户”比模型的精度更重要。

defbuild_scorecard(X_train, y_train, X_test, y_test, features):"""    构建逻辑回归评分卡    """# 提取特征    X_train_selected = X_train[features]    X_test_selected = X_test[features]# 训练逻辑回归    lr = LogisticRegression(        penalty='l2',         C=0.1,  # 正则化强度,需要调参        solver='liblinear',        max_iter=1000,        class_weight='balanced'# 处理样本不平衡    )    lr.fit(X_train_selected, y_train)# 预测    y_pred_proba = lr.predict_proba(X_test_selected)[:, 1]    y_pred = lr.predict(X_test_selected)# 评估    auc = roc_auc_score(y_test, y_pred_proba)print(f"AUC: {auc:.4f}")# 混淆矩阵    cm = confusion_matrix(y_test, y_pred)print("混淆矩阵:")print(cm)# 特征系数    coef_df = pd.DataFrame({'feature': features,'coefficient': lr.coef_[0],'p_value': lr.coef_[0] / np.std(lr.coef_[0])  # 简化版    })    coef_df['odds_ratio'] = np.exp(coef_df['coefficient'])print("\n特征系数:")print(coef_df)return lr, y_pred_proba, auc

这里有个细节值得注意:逻辑回归要求变量间不存在严重的多重共线性,所以在建模前通常会做VIF(方差膨胀因子)检验,去掉VIF大于10的变量。

第五步:评分刻度转化——把概率变成分数

模型出的概率值,老板看不懂、业务用不了。评分刻度转化的本质,就是把概率映射成整数分数,让一线人员一眼就能判断风险高低。

标准评分卡公式:Score = A - B × ln(odds)

其中 odds = p/(1-p),A和B是两个待定参数,通常根据两个锚点来确定:

defscore_transform(proba, base_score=600, pdo=50, odds_base=20):"""    将概率转化为标准评分    base_score: 基准分(通常设为600)    pdo: 分数每增加PDO分,odds翻倍(通常设为50)    odds_base: 基准odds(通常设为20)    """import math# 计算A和B    B = pdo / math.log(2)    A = base_score + B * math.log(odds_base)# 转换    odds = proba / (1 - proba + 1e-10)    score = A - B * np.log(odds)return score.astype(int)# 应用评分转化# test_proba = lr.predict_proba(X_test_selected)[:, 1]# test_scores = score_transform(test_proba)# 评分分布查看# print(f"评分区间: {test_scores.min()} - {test_scores.max()}")# print(f"平均分: {test_scores.mean():.0f}")

模型监控:上线不是终点,而是起点

评分卡上线后,持续的模型监控才是真正考验功力的地方。一般要盯这几个维度:

PSI(群体稳定性指数):衡量评分分布是否发生显著偏移,PSI大于0.25就需要预警了。

区分度衰减:AUC是否持续下降,说明变量的预测能力在减弱。

评分迁移矩阵:不同评分段的客群占比变化,判断客群结构是否发生偏移。

一旦发现异常,就需要考虑模型迭代或策略调整。这就是为什么大厂的风控团队永远在招人——模型维护的工作量不比开发小。

最新文章

随机文章