ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python金融风控建模实战:从数据到评分卡部署

Python金融风控建模实战:从数据到评分卡部署 简介这份资源面向金融风控方向的学生与开发者提供一套基于机器学习的Python大数据风控建模实战项目可直接用于毕业设计、期末大作业或课程设计。项目围绕信贷违约预测等典型场景展开涵盖数据清洗、特征工程、模型训练与评估的完整流程代码注释详尽新手也能读懂。压缩包共106个文件约20.13MB其中40个py脚本承载核心建模逻辑16个csv与19个pkl分别存放原始数据与训练好的模型文件另有3个ipynb便于分步复现20张png展示可视化结果并附文档说明与依赖whl包部署后即可运行。目前已有1159人学习下载作者自述为手打98分项目。读者可从中获得一套结构完整的风控建模方案、可复用的特征处理与模型调优思路以及清晰的目录组织便于快速理解业务与算法结合方式也适合作为二次开发与排错参考。1. 从一份风控源码说起Python 金融大数据建模到底在解决什么金融风控建模这件事很多人第一次接触是从一份「源代码文档说明」的压缩包开始的。解压之后看到一堆.py文件、几个.csv样本、一份 README心里第一反应往往是这玩意儿能跑起来吗跑起来之后又能说明什么我当年也是这么入坑的后来在消费金融和信贷场景里做了几年模型才慢慢理解这套东西真正的价值不在代码本身而在于它把「数据 → 特征 → 模型 → 决策」这条链路完整地摆在你面前。Python 在这个领域几乎是默认选项原因很实际pandas 处理宽表够快scikit-learn 把逻辑回归、XGBoost、LightGBM 这些风控主力模型封装得足够顺手评分卡相关的分箱、WOE、IV 计算也有成熟实现。金融大数据的特点是样本量大、特征维度高、正负样本极度不平衡同时还要满足可解释性和监管要求这就决定了风控建模和一般机器学习任务在流程上有明显差异。这份实战方案适合三类人想从零搭一套信贷评分卡的新手、手里有数据但不知道怎么规范流程的从业者、以及需要快速验证某个特征或模型是否值得上线的工程师。接下来我会按真实落地顺序把数据准备、特征工程、模型训练、评估上线这几段拆开讲清楚中间穿插能直接抄的代码和参数说明。2. 数据准备与特征工程把原始宽表变成模型能吃的矩阵2.1 风控数据的典型结构和读取方式金融风控拿到的原始数据通常是「一行为一个客户、一列为一个字段」的宽表字段大致分几类身份类年龄、学历、婚姻、申请类申请金额、期限、行为类近 6 个月查询次数、逾期次数、外部类征信评分、多头借贷标识。标签一般是二分类的label1 代表坏客户0 代表好客户。读取的时候有几个细节要注意。第一编码问题很多业务系统导出的 CSV 是 GBK直接pd.read_csv会报 UnicodeDecodeError。第二缺失值表示方式不统一有的用空字符串有的用 -999有的用 NULL读进来之后要统一成np.nan。第三日期字段要显式解析否则后面算账龄、账期会出错。import pandas as pd import numpy as np # 读取时统一处理编码和缺失值标识 df pd.read_csv( loan_data.csv, encodinggbk, # 业务系统常见编码UTF-8 报错时换这个 na_values[, NULL, -999, null], # 统一缺失值 parse_dates[apply_date, first_loan_date] # 显式解析日期 ) # 标签分布检查风控里坏样本通常只占 1%~10% print(df[label].value_counts(normalizeTrue)) # 字段类型概览object 类型要重点关注 print(df.dtypes.value_counts())这段代码的关键在na_values和parse_dates两个参数。na_values把业务系统里各种「伪缺失」统一成 NaN避免后面做统计时把 -999 当成真实数值算进均值。parse_dates让 pandas 直接转成 datetime省去后面手动pd.to_datetime的步骤。标签分布那行一定要看如果坏样本比例低于 1%后面训练时就得考虑采样策略否则模型会倾向于全预测为好客户。2.2 缺失值处理和异常值截断的实操参数风控数据里缺失值不是随机出现的往往本身就有信息量。比如「近 3 个月查询次数」缺失可能意味着这个客户根本没有征信记录属于「白户」风险特征和普通缺失完全不同。所以我的习惯是先算缺失率再决定处理方式。缺失率低于 5% 的字段直接用中位数或众数填充5% 到 30% 之间的填充的同时加一个「是否缺失」的指示列超过 30% 的除非业务上明确重要否则直接丢掉。异常值方面连续变量用分位数截断一般卡在 1% 和 99% 分位避免极端值把 WOE 分箱带偏。def handle_missing_and_outlier(df, cols, missing_threshold0.3): 缺失值填充 异常值截断 drop_cols [] for col in cols: miss_rate df[col].isnull().mean() if miss_rate missing_threshold: drop_cols.append(col) continue # 缺失指示列保留缺失本身的信息 if miss_rate 0.05: df[f{col}_is_missing] df[col].isnull().astype(int) # 数值型用中位数类别型用众数 if df[col].dtype in [float64, int64]: fill_value df[col].median() # 1% 和 99% 分位截断 lower, upper df[col].quantile([0.01, 0.99]) df[col] df[col].clip(lower, upper) else: fill_value df[col].mode()[0] df[col] df[col].fillna(fill_value) return df.drop(columnsdrop_cols) df handle_missing_and_outlier(df, numeric_cols category_cols)missing_threshold设 0.3 是个经验值业务上如果某个字段特别重要可以放宽到 0.5。clip那两行是防止极端值影响后续分箱注意截断要在填充之前做否则中位数会被极端值拉偏。缺失指示列看起来简单但在评分卡里经常能贡献不错的 IV 值别省这一步。2.3 WOE 分箱和 IV 筛选评分卡的核心工序WOEWeight of Evidence和 IVInformation Value是风控特征工程的标志性工序。WOE 衡量的是「某个分箱里坏样本占比和好样本占比的差异」IV 则是 WOE 的加权和用来判断一个特征整体有没有区分能力。IV 的经验阈值小于 0.02 基本没用0.02 到 0.1 弱0.1 到 0.3 中等0.3 到 0.5 强超过 0.5 要警惕数据泄漏。分箱方式有等频、等距、决策树分箱几种。实操里我一般先用等频分箱粗分再根据单调性手动合并。单调性很重要比如「逾期次数」的 WOE 应该随次数增加而单调上升如果出现波动说明分箱不合理或者数据有问题。def calc_woe_iv(df, feature, target, bins10): 等频分箱计算 WOE 和 IV # 等频分箱重复值多时用 rank 避免报错 df[bin] pd.qcut(df[feature].rank(methodfirst), bins, duplicatesdrop) grouped df.groupby(bin)[target].agg([count, sum]) grouped.columns [total, bad] grouped[good] grouped[total] - grouped[bad] # 防止某箱好或坏样本为 0 导致除零 grouped[bad] grouped[bad].replace(0, 0.5) grouped[good] grouped[good].replace(0, 0.5) 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] return grouped, grouped[iv].sum() woe_table, iv_value calc_woe_iv(df, query_count_3m, label) print(fIV {iv_value:.4f})replace(0, 0.5)这步是血泪经验某箱里坏样本为 0 时np.log会算出无穷大整个 IV 就废了。用 0.5 做平滑是行业常见做法。pd.qcut加rank是为了处理大量重复值的情况直接 qcut 会报「Bin edges must be unique」。算完 IV 之后一般保留 IV 大于 0.02 的特征进入模型同时人工检查 WOE 的单调性。3. 模型训练与调参逻辑回归和树模型的取舍3.1 为什么评分卡首选逻辑回归在强监管的信贷场景里逻辑回归至今仍是评分卡的主力原因不是它效果最好而是它可解释。每个特征的系数直接对应「这个特征每变化一个单位坏账几率变化多少倍」业务和监管都能看懂。树模型虽然 AUC 通常高几个点但解释性差很多场景下不被接受。逻辑回归在风控里通常配合 WOE 转换使用把每个特征的分箱 WOE 值作为输入这样系数更稳定也避免了原始值量纲差异带来的问题。正则化一般选 L2C参数控制正则强度C 越小正则越强。样本不平衡用class_weightbalanced让模型自动加权。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # 假设 X 已经完成 WOE 转换 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy # stratify 保证训练测试集坏样本比例一致 ) lr LogisticRegression( penaltyl2, C1.0, # 正则强度越小越强常用 0.1~10 之间调 class_weightbalanced, # 自动处理样本不平衡 solverlbfgs, max_iter1000 ) lr.fit(X_train, y_train) pred lr.predict_proba(X_test)[:, 1] print(fAUC {roc_auc_score(y_test, pred):.4f})stratifyy这个参数在小样本或坏样本极少时特别重要不加的话可能测试集里坏样本只有个位数AUC 波动会很大。C的调参建议用交叉验证范围从 0.01 到 10 扫一遍。max_iter设 1000 是为了避免 lbfgs 不收敛的警告如果还报 ConvergenceWarning说明特征共线性严重得先做 VIF 筛选。3.2 XGBoost 和 LightGBM 在风控里的参数起点树模型在风控里主要用在两个地方一是作为逻辑回归的补充模型做模型融合二是用在反欺诈这种对解释性要求没那么高的场景。XGBoost 和 LightGBM 我都用过数据量在百万级以下时两者差距不大超过千万行 LightGBM 的速度优势明显。参数方面风控场景有几个和通用机器学习不同的地方。第一max_depth不要设太深一般 3 到 6太深容易过拟合而且解释性更差。第二scale_pos_weight设成负样本数除以正样本数处理不平衡。第三subsample和colsample_bytree设 0.7 到 0.9 之间增加泛化能力。import lightgbm as lgb params { objective: binary, metric: auc, max_depth: 5, # 风控里不宜过深 num_leaves: 31, # 控制复杂度一般 2^max_depth 以内 learning_rate: 0.05, # 小学习率配多轮迭代 scale_pos_weight: (y_train 0).sum() / (y_train 1).sum(), subsample: 0.8, colsample_bytree: 0.8, min_child_samples: 50, # 叶子最小样本数防止过拟合 reg_alpha: 0.1, # L1 正则 reg_lambda: 1.0 # L2 正则 } dtrain lgb.Dataset(X_train, labely_train) dvalid lgb.Dataset(X_test, labely_test, referencedtrain) model lgb.train( params, dtrain, num_boost_round500, valid_sets[dvalid], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] )early_stopping(50)表示验证集 AUC 连续 50 轮不提升就停这是防止过拟合最实用的手段。min_child_samples设 50 是经验值样本量小可以降到 20样本量大可以提到 100。scale_pos_weight那行直接算比例不用手动填。训练完记得看特征重要性如果某个特征重要性异常高要怀疑是不是泄漏了标签信息。3.3 模型融合和分数校准单一模型往往不够稳实操里常用逻辑回归加树模型做融合。最简单的方式是加权平均权重按验证集 AUC 分配。更严谨的做法是用 stacking把两个模型的预测值作为新特征再训一个逻辑回归。分数校准是另一个容易被忽略的环节。树模型输出的概率往往偏高或偏低不能直接当违约概率用。常用方法是 Platt Scaling 或 Isotonic Regression把预测分数映射到真实概率。评分卡场景下还要把概率转成 300 到 850 的分数公式是score offset factor * ln(odds)其中factor pdo / ln(2)pdo 是「分数翻倍所需的 odds 变化」一般设 20 或 50。from sklearn.isotonic import IsotonicRegression # 用验证集做概率校准 iso IsotonicRegression(out_of_boundsclip) iso.fit(pred_valid, y_valid) calibrated iso.predict(pred_test) # 概率转评分卡分数 import numpy as np pdo 50 factor pdo / np.log(2) offset 500 # 基准分 odds (1 - calibrated) / calibrated # 好坏人比 score offset factor * np.log(odds) print(f分数范围: {score.min():.0f} ~ {score.max():.0f})out_of_boundsclip防止测试集出现训练集没见过的概率值时校准失效。分数转换那几行里offset和factor要根据业务定基准分 500、pdo 50 是常见配置。算出来的分数要检查分布正常应该集中在 500 到 700 之间如果大量堆在两端说明模型区分度有问题。4. 避坑与排查风控建模里最容易翻车的五个地方4.1 时间穿越导致 AUC 虚高现象离线 AUC 跑到 0.85 以上上线后 KS 只有 0.2模型几乎失效。原因特征里混入了未来信息。比如用「当前逾期天数」预测「是否违约」但当前逾期天数本身就是违约之后才知道的。或者做时间切分时训练集用了未来数据做统计。解决所有特征必须确认在预测时点可得。时间切分要严格按申请日期训练集用早期数据测试集用后期数据中间留一段观察期。做特征统计时均值、分位数这些只能用训练集算不能全量算完再切。4.2 样本定义不一致导致标签噪声现象模型训练时 AUC 正常但业务反馈「预测坏客户里很多其实是好客户」。原因坏样本定义不统一。有的按「逾期 30 天以上」有的按「逾期 90 天以上」混在一起训练标签就乱了。观察期和表现期没对齐也是常见问题。解决建模前先和业务确认坏样本定义写进文档。观察期申请到放款和表现期放款到判定好坏要明确一般表现期取 6 到 12 个月。不同定义的数据不要混在一起训。4.3 WOE 分箱在新样本上映射失败现象训练时 WOE 转换正常上线后新样本分箱报错或映射到错误区间。原因分箱边界是用训练集算的新样本的取值可能落在边界外或者训练集某个箱在新样本里没有样本。解决把分箱边界持久化保存上线时用同一套边界做pd.cut。边界外的值统一归到最近的一箱。WOE 映射表也要保存新样本按边界找对应 WOE 值找不到就填 0 或训练集的均值。4.4 特征共线性让逻辑回归系数失真现象逻辑回归系数符号和业务常识相反比如「收入越高违约率越高」。原因特征之间高度相关比如「月收入」和「年收入」同时入模系数会互相抵消甚至翻转。解决入模前算 VIF超过 10 的考虑剔除。或者先做相关性矩阵相关系数超过 0.7 的只保留一个。WOE 转换本身能缓解一部分共线性但不能完全消除。4.5 模型监控缺失导致效果衰减无人知现象模型上线半年后 KS 从 0.4 掉到 0.15但没人发现直到坏账率飙升才排查。原因没有做 PSI 监控。PSI 衡量的是当前样本分布和训练样本分布的差异超过 0.25 说明分布漂移严重模型需要重新训练。解决上线后每月算一次 PSI特征层面和分数层面都要算。PSI 超过 0.1 关注超过 0.25 预警。同时监控 KS 和坏账率三者结合判断模型是否失效。def calc_psi(expected, actual, bins10): 计算 PSIexpected 是训练集分布actual 是当前样本 breakpoints np.percentile(expected, np.linspace(0, 100, bins 1)) breakpoints[0], breakpoints[-1] -np.inf, np.inf expected_perc np.histogram(expected, breakpoints)[0] / len(expected) actual_perc np.histogram(actual, breakpoints)[0] / len(actual) # 防止除零 expected_perc np.where(expected_perc 0, 0.0001, expected_perc) actual_perc np.where(actual_perc 0, 0.0001, actual_perc) return np.sum((actual_perc - expected_perc) * np.log(actual_perc / expected_perc))np.percentile那行用训练集算分位点保证两批样本用同一套边界。除零保护是必须的某个区间没样本时 PSI 会算出 inf。这个函数建议封装成工具每月跑一次结果存表画趋势图。5. 从离线到上线评分卡部署和效果验证的几个关键动作模型训完只是开始真正决定价值的是上线之后的表现。我一般会把部署拆成三块分数计算服务、监控看板、重训触发机制。分数计算服务最简单的做法是把 WOE 映射表和模型系数导出成 JSON线上用 Python 或 Java 加载后按公式算分。注意线上环境不一定有 pandas所以 WOE 映射要用纯字典实现别依赖pd.cut。模型系数和 WOE 表要版本化管理每次重训生成新版本方便回滚。监控看板至少要有四个指标每日申请量、分数分布、PSI、KS。分数分布看是否偏移PSI 看特征漂移KS 看区分度。KS 的计算用上线后积累的带表现标签样本一般要等 3 到 6 个月才有足够样本。重训触发条件我设三条PSI 连续两个月超过 0.25、KS 下降超过 30%、业务侧反馈明显异常。满足任意一条就启动重训流程。重训不是全量重来而是先做特征稳定性分析看是哪些特征漂移了再决定是替换特征还是调整分箱。import json # 导出 WOE 映射表和模型系数供线上使用 woe_map {} for feature in features: woe_map[feature] { bins: bin_edges[feature], # 分箱边界 woe: woe_values[feature] # 每个箱对应的 WOE } model_artifact { version: 20240101, features: features, woe_map: woe_map, coef: lr.coef_[0].tolist(), intercept: lr.intercept_[0], score_offset: 500, score_factor: 50 / np.log(2) } with open(scorecard_v20240101.json, w) as f: json.dump(model_artifact, f, ensure_asciiFalse, indent2)导出成 JSON 的好处是跨语言、易版本管理。version字段用日期方便追溯。线上加载后对每个特征先按bins找区间再取对应woe乘系数求和加截距最后按score_offset和score_factor转成分数。这套流程跑通之后重训就是替换 JSON 文件的事风险可控。最后说个我自己的习惯每次模型上线前我会手动挑 20 个样本从原始数据开始一步步算到最终分数和线上服务的结果对一遍。这个动作看起来笨但帮我抓到过好几次 WOE 映射错位和系数符号反了的问题。模型这东西离线指标再好看上线对不上就是白搭。希望帮到你。本文还有配套的精品资源点击获取
返回列表