别人调包你调参,别人调参你建模,这差距就是这么拉开的
项目准备:先把家伙什儿备齐
先梳理一下整个流程:
数据清洗(缺失值、异常值)→ 特征分箱(连续变量离散化)→ 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是否持续下降,说明变量的预测能力在减弱。
评分迁移矩阵:不同评分段的客群占比变化,判断客群结构是否发生偏移。
一旦发现异常,就需要考虑模型迭代或策略调整。这就是为什么大厂的风控团队永远在招人——模型维护的工作量不比开发小。