☰
模式识别课设实战:银行信用卡四类风险评估模型Python实现
2026/9/26 14:03:04 网站建设 项目流程

简介:这份资源面向模式识别课程学习者与金融风控入门者,围绕银行信用卡场景完整落地四类评级模型:申请人评级、行为评级、收款(催收)评级与欺诈评级。申请人模型依据个人与财务信息评估违约可能,行为模型从还款与逾期记录预测未来信用表现,收款模型结合违约概率匹配催收措施,欺诈模型则识别欺诈特征并对新客户做风险预判。项目按数据预处理与分析、模型构建、评估优化三阶段推进,涵盖缺失值、重复值、异常值处理、特征选择与可视化,并采用降维与随机森林等算法,通过交叉验证检验准确度与稳定性。压缩包共18个文件、约7.67MB,含8个csv数据文件、4个py源码、4个pkl模型文件及字体与说明文档,便于直接复现与二次开发。已有130人学习,适合作为课程设计或风控建模练手参考。

1. 从一份模式识别课设说起:银行信用卡风险评估模型到底在评什么

很多人第一次接触「模式识别」这门课,都是被那套贝叶斯决策、Fisher 判别、SVM 推导折腾得够呛,公式会推,一到课程设计就不知道从哪下手。而银行信用卡风险评估这个题目,恰好是把模式识别从纸面拉到真实业务里的经典场景——它要你同时处理申请人评级、行为评级、收款(催收)评级、欺诈评级四条业务线,每条线的样本分布、标签定义、评价指标都不一样。你如果只拿一份 Kaggle 的 creditcard.csv 跑个逻辑回归交差,答辩时老师一句「你这四类模型怎么区分」就能把你问住。

这篇笔记就是顺着这个标题,把一套能跑通、能讲清楚、能写进报告的模式识别课设方案拆开讲。核心思路是:四条业务线对应四个二分类(或多分类)任务,共用一套特征工程和评估框架,但样本构造、阈值策略、代价敏感处理各不相同。Python 源码层面,我一般会用 pandas 做特征、sklearn 做基线、imbalanced-learn 处理不平衡、matplotlib/seaborn 出图,最后用一份文档说明把「数据字典—特征含义—模型选择—指标解释」串起来。适合正在做模式识别课设、金融风控入门,或者想拿一套完整 Python 代码练手的人。下面从数据、特征、四类模型、避坑到进阶,一层层落地。

2. 四类评级模型的数据底座:样本怎么造、标签怎么定

2.1 为什么不能只用一份数据跑四个模型

银行信用卡的四类评级,业务含义完全不同。**申请人评级(Application Score)**发生在发卡前,用的是申请时填写的资料,比如年龄、收入、职业、征信查询次数,标签是「未来 12 个月是否逾期 90 天以上」。**行为评级(Behavior Score)**发生在发卡后,用的是还款记录、额度使用率、消费笔数这些动态变量,标签是「未来 6 个月是否违约」。**收款评级(催收评级,Collection Score)**针对已经逾期的账户,预测「催回概率」或「回款金额」,变量里会加入逾期天数、历史催收响应。**欺诈评级(Fraud Score)**则是另一套逻辑,标签是「该笔交易是否为欺诈」,样本极度不平衡,通常低于 0.5%。

这四类任务的样本粒度都不一样:申请人评级一行是一个申请人,行为评级一行是「客户—账期」快照,催收评级一行是「逾期账户—催收动作」,欺诈评级一行是一笔交易。如果你用同一张宽表硬套,特征泄漏和标签错位几乎必然发生。常见做法是按业务线分别构造样本表,再统一到一套特征工程流水线。

2.2 用 Python 构造四类样本的最小骨架

下面这段代码演示如何从一张原始交易/账户流水表出发,切出四类样本。真实数据可能来自银行脱敏数据或公开数据集(如 Give Me Some Credit、IEEE-CIS Fraud),这里用字段名示意,重点是样本切分逻辑。

import pandas as pd import numpy as np # 假设 raw 是账户-账期-交易级别的宽表 # 字段示例:cust_id, apply_date, obs_date, age, income, job_type, # credit_query_3m, utilization, repay_status, overdue_days, # trans_amt, trans_type, is_fraud, dpd90_flag raw = pd.read_csv("credit_raw.csv", parse_dates=["apply_date", "obs_date"]) # 1) 申请人评级样本:一行一个申请人,取申请时点特征 app = raw.sort_values("apply_date").groupby("cust_id").first().reset_index() app["label_app"] = (app["dpd90_flag"] == 1).astype(int) app_feat = ["age", "income", "job_type", "credit_query_3m"] # 2) 行为评级样本:一行一个客户-账期快照 beh = raw.copy() beh["label_beh"] = (beh["repay_status"].isin([1, 2, 3])).astype(int) beh_feat = ["utilization", "repay_status", "overdue_days", "trans_amt"] # 3) 催收评级样本:只保留已逾期账户 col = raw[raw["overdue_days"] > 0].copy() col["label_col"] = (col["overdue_days"] <= 30).astype(int) # 30天内回款为正样本 col_feat = ["overdue_days", "utilization", "trans_amt", "repay_status"] # 4) 欺诈评级样本:交易级别 fraud = raw.copy() fraud["label_fraud"] = fraud["is_fraud"].astype(int) fraud_feat = ["trans_amt", "trans_type", "utilization", "overdue_days"] print(app["label_app"].mean(), beh["label_beh"].mean(), col["label_col"].mean(), fraud["label_fraud"].mean())

逻辑说明:groupby("cust_id").first()取每个申请人最早一条记录,避免用未来信息;行为样本保留所有账期,标签用repay_status映射;催收样本先过滤overdue_days > 0,再用「30 天内是否回款」定义正样本;欺诈样本直接沿用交易级标签。参数上,dpd90_flag是 90 天以上逾期标志,repay_status的取值需要按数据字典映射(通常 0 正常、1 逾期 1-30 天、2 逾期 31-60 天、3 逾期 60 天以上)。跑完打印四个标签均值,你会看到欺诈样本可能只有 0.2% 左右,这就是后面必须处理不平衡的原因。

2.3 特征工程:把业务字段变成模型能吃的数值

四类模型共用一套特征处理框架,但侧重点不同。申请人评级重「静态属性 + 征信查询」,行为评级重「趋势 + 比率」,催收评级重「逾期时长 + 历史响应」,欺诈评级重「交易频次 + 金额异常」。我一般会做三类特征:

特征类型示例适用模型处理方式
静态属性年龄、收入、职业申请人分箱 + One-Hot
行为比率额度使用率、还款比率行为、催收标准化 + 分箱
时序聚合近 3 月查询次数、近 7 天交易笔数全部滑窗统计
异常标记夜间交易、大额突增欺诈规则 + 分位数

代码上,用pd.cut做分箱、StandardScaler做标准化、SimpleImputer补缺失。注意:分箱边界必须只用训练集计算,再应用到验证集和测试集,否则就是数据泄漏。这一步在课设报告里要写清楚,答辩时是加分项。

3. 申请人评级与行为评级:从逻辑回归到集成模型的选型路径

3.1 申请人评级:为什么逻辑回归仍是基线首选

申请人评级的数据特点是样本量中等、特征维度不高、业务可解释性要求高。银行监管往往要求模型能给出「为什么拒绝」的理由,所以逻辑回归(LR)配合 WOE(Weight of Evidence)编码是行业标准做法。WOE 把每个分箱映射为ln(正样本占比 / 负样本占比),IV(Information Value)用来筛特征,一般 IV < 0.02 的变量直接丢掉。

import numpy as np import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, roc_curve def calc_woe_iv(df, feature, target, bins=5): df = df[[feature, target]].copy() df["bin"] = pd.qcut(df[feature], q=bins, duplicates="drop") grouped = df.groupby("bin")[target].agg(["count", "sum"]) grouped.columns = ["total", "bad"] 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["good_rate"] / grouped["bad_rate"]) grouped["iv"] = (grouped["good_rate"] - grouped["bad_rate"]) * grouped["woe"] return grouped, grouped["iv"].sum() # 以 age 为例 app = app.dropna(subset=["age", "label_app"]) woe_table, iv = calc_woe_iv(app, "age", "label_app", bins=5) print(woe_table) print("IV =", iv) # 用 WOE 映射后训练 LR app["age_woe"] = pd.qcut(app["age"], q=5, duplicates="drop").map( woe_table["woe"].to_dict()) X = app[["age_woe", "income", "credit_query_3m"]].fillna(0) y = app["label_app"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) lr = LogisticRegression(class_weight="balanced", max_iter=1000) lr.fit(X_tr, y_tr) print("AUC =", roc_auc_score(y_te, lr.predict_proba(X_te)[:, 1]))

逻辑说明:calc_woe_iv先按分位数分箱,再算每个箱的 WOE 和 IV;pd.qcut(...).map(woe_table["woe"].to_dict())把原始值替换成 WOE 值。参数上,bins=5是经验值,样本少可以降到 3;class_weight="balanced"处理正负样本不均衡;max_iter=1000防止收敛警告。AUC 一般能到 0.75 以上算合格,低于 0.7 要回头查特征和标签定义。

3.2 行为评级:梯度提升树为什么更合适

行为评级的变量多、非线性强、交互复杂,逻辑回归容易欠拟合。这时候 XGBoost、LightGBM 这类梯度提升树(GBDT)是主流选择。它们能自动处理缺失值、捕捉特征交互,而且有scale_pos_weight直接应对不平衡。课设里我一般用 LightGBM,训练快、调参少。

import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, average_precision_score beh = beh.dropna(subset=["label_beh"]) X = beh[["utilization", "repay_status", "overdue_days", "trans_amt"]].fillna(-1) y = beh["label_beh"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 31, "scale_pos_weight": (y_tr == 0).sum() / (y_tr == 1).sum(), "verbose": -1, "seed": 42 } dtrain = lgb.Dataset(X_tr, label=y_tr) dvalid = lgb.Dataset(X_te, label=y_te, reference=dtrain) model = lgb.train(params, dtrain, num_boost_round=300, valid_sets=[dvalid], callbacks=[lgb.early_stopping(30)]) pred = model.predict(X_te) print("AUC =", roc_auc_score(y_te, pred)) print("AP =", average_precision_score(y_te, pred))

逻辑说明:scale_pos_weight设为负正样本比,让模型更关注少数类;early_stopping(30)在验证集 AUC 30 轮不提升时停止,防止过拟合。参数上,learning_rate=0.05配合num_boost_round=300是稳妥组合,num_leaves=31控制树复杂度。行为评级除了 AUC,还要看 KS 值(max(TPR - FPR)),业务上一般要求 KS > 0.3。注意:行为评级的验证集必须按时间切分,不能随机切,否则会用未来信息预测过去,AUC 虚高。

3.3 两类模型的阈值怎么定

模型输出概率后,需要选一个阈值把概率转成「通过/拒绝」或「正常/违约」。申请人评级常用评分卡刻度,把概率映射到 300-850 分,再按通过率定 cutoff;行为评级常用KS 最大点或业务成本最小点。代码上:

from sklearn.metrics import roc_curve fpr, tpr, thresholds = roc_curve(y_te, pred) ks = tpr - fpr best_thr = thresholds[np.argmax(ks)] print("Best KS =", ks.max(), "threshold =", best_thr)

参数说明:best_thr是 KS 最大时的阈值,但实际业务还要结合通过率调整。如果通过率要求 70%,就取概率排序后 70% 分位点作为阈值。这一步在报告里要写清楚「阈值选择依据」,不能只写「取 0.5」。

4. 催收评级与欺诈评级:不平衡样本和代价敏感怎么落地

4.1 催收评级:把「回款」定义清楚再建模

催收评级的标签定义比前两类更微妙。同样是逾期账户,「回款」可以定义为「30 天内还清最低还款」「60 天内回款超过 X 元」或「催收后 7 天内有响应」。定义不同,模型完全不一样。课设里建议用二分类 + 时间窗口:label_col = 1表示逾期后 30 天内回款,0表示未回款。样本只保留overdue_days > 0的账户。

col = col.dropna(subset=["label_col"]) X = col[["overdue_days", "utilization", "trans_amt", "repay_status"]].fillna(-1) y = col["label_col"] # 用 SMOTE 过采样少数类 from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) sm = SMOTE(random_state=42, k_neighbors=5) X_res, y_res = sm.fit_resample(X_tr, y_tr) rf = RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42) rf.fit(X_res, y_res) print(classification_report(y_te, rf.predict(X_te)))

逻辑说明:SMOTE在训练集上生成少数类合成样本,k_neighbors=5控制合成范围;随机森林max_depth=8防止过拟合。参数上,n_estimators=200是精度和速度的平衡点。注意:SMOTE 只能在训练集上做,验证集和测试集必须保持原始分布,否则评估指标会失真。催收评级除了分类指标,还要看回款金额回归,如果课设要求,可以再加一个回归模型预测回款比例。

4.2 欺诈评级:0.2% 正样本下的评估陷阱

欺诈评级的正样本可能只有 0.1%-0.5%,这时候准确率完全没意义——全预测为正常也有 99.5% 准确率。必须用AUC、AP(Average Precision)、召回率@固定精确率。我一般用imbalanced-learn的RandomUnderSampler或EasyEnsemble,配合代价敏感学习。

from imblearn.ensemble import EasyEnsembleClassifier from sklearn.metrics import average_precision_score, precision_recall_curve fraud = fraud.dropna(subset=["label_fraud"]) X = fraud[["trans_amt", "trans_type", "utilization", "overdue_days"]].fillna(-1) y = fraud["label_fraud"] X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) ee = EasyEnsembleClassifier(n_estimators=10, random_state=42) ee.fit(X_tr, y_tr) pred = ee.predict_proba(X_te)[:, 1] print("AP =", average_precision_score(y_te, pred)) prec, rec, thr = precision_recall_curve(y_te, pred) # 找召回率 >= 0.8 时的精确率 idx = np.where(rec >= 0.8)[0][0] print("Precision@Recall0.8 =", prec[idx])

逻辑说明:EasyEnsembleClassifier通过多轮欠采样 + AdaBoost 集成,缓解极度不平衡;precision_recall_curve用来找业务可接受的召回率下的精确率。参数上,n_estimators=10是集成轮数,轮数越多越稳但越慢。欺诈评级还要做规则兜底,比如「同一设备 1 小时内 5 笔以上大额交易」直接拦截,模型和规则互补。

4.3 四类模型的统一评估表

课设报告里最好有一张统一评估表,把四类模型的 AUC、KS、AP、召回率列在一起,方便对比。

模型样本量正样本率AUCKSAP召回率@阈值
申请人评级50008%0.780.420.350.65
行为评级200005%0.820.480.400.70
催收评级300030%0.750.380.620.72
欺诈评级500000.3%0.900.550.450.80

这张表的数据是示意,实际跑出来会有差异。重点是每类模型的评估指标要匹配业务:申请人看 KS 和通过率,行为看 AUC 和稳定性,催收看回款率,欺诈看 AP 和召回。

5. 避坑与排查:四类模型落地时最容易翻车的 5 个点

5.1 标签泄漏:用未来信息预测过去

现象:行为评级 AUC 跑到 0.95 以上,答辩时被问「你怎么保证没用未来数据」答不上来。原因:随机切分验证集,或者特征里混入了标签时点之后的信息,比如用「当前逾期天数」预测「未来是否逾期」。解决:所有时序相关模型必须按时间切分,特征只取标签时点之前的数据。代码上可以用train_test_split(..., shuffle=False)或按obs_date排序后切分。

5.2 样本不平衡处理顺序错误

现象:欺诈评级用了 SMOTE,AUC 反而下降。原因:SMOTE 在切分之前做了,合成样本泄漏到验证集;或者对极度不平衡数据用 SMOTE 生成噪声样本。解决:先切分,再在训练集上做采样;欺诈评级优先用欠采样 + 集成,而不是盲目过采样。imblearn的Pipeline可以保证采样只作用于训练折。

5.3 WOE 分箱边界用了全量数据

现象:申请人评级训练集 AUC 0.85,测试集掉到 0.65。原因:pd.qcut用了全量数据计算分位点,验证集信息泄漏到训练集。解决:分箱边界只在训练集上计算,用pd.cut把边界应用到验证集。或者用sklearn的KBinsDiscretizer配合Pipeline。

5.4 催收评级标签定义模糊

现象:催收模型准确率 90%,但业务方说「没用」。原因:标签定义成「是否回款」但没限定时间窗口,或者正样本包含了催收前就回款的账户。解决:明确「逾期后 N 天内回款」为标签,排除催收动作前已回款的样本。报告里要写清楚标签定义和样本过滤条件。

5.5 欺诈评级只看准确率

现象:欺诈模型准确率 99.7%,但一笔欺诈都没抓到。原因:正样本只有 0.3%,模型全预测为正常。解决:改用 AP、召回率@固定精确率、F1。业务上还要看误杀率,因为把正常交易拦成欺诈的代价也很高。代码上可以用average_precision_score和precision_recall_curve联合评估。

6. 进阶技巧:用评分卡刻度把概率变成业务语言

四类模型跑通后,最后一步是把概率输出变成业务能直接用的分数。申请人评级常用评分卡刻度:score = A - B * ln(odds),其中odds = p / (1-p),A、B 由基准分和 PDO(Points to Double the Odds)决定。比如基准分 600、PDO 50,则B = 50 / ln(2) ≈ 72.13,A = 600 + B * ln(1/50)。这样每个申请人得到一个 300-850 的分数,业务按分数段定策略。

import numpy as np def prob_to_score(p, base_score=600, base_odds=50, pdo=50): """把违约概率转成评分卡分数""" B = pdo / np.log(2) A = base_score + B * np.log(1 / base_odds) odds = p / (1 - p + 1e-6) score = A - B * np.log(odds) return np.clip(score, 300, 850) # 对申请人评级输出打分 app_pred = lr.predict_proba(X_te)[:, 1] app_score = prob_to_score(app_pred) print("分数分布:", np.percentile(app_score, [5, 25, 50, 75, 95]))

逻辑说明:prob_to_score按标准评分卡公式把概率映射到分数,np.clip限制在 300-850。参数上,base_score=600、base_odds=50、pdo=50是行业常见配置,课设里可以按数据调整。行为评级、催收评级也可以用同样方法,只是基准分和 PDO 不同。欺诈评级一般不用评分卡,直接用概率 + 规则阈值。

验证方法上,我习惯做分数稳定性检验:把测试集按时间分成 3 段,看每段的分数分布是否漂移。如果 PSI(Population Stability Index)大于 0.25,说明模型不稳定,要回头查特征或重新训练。PSI 计算很简单:sum((实际占比 - 预期占比) * ln(实际占比 / 预期占比))。

def psi(expected, actual, bins=10): breakpoints = np.percentile(expected, np.linspace(0, 100, bins + 1)) breakpoints[0], breakpoints[-1] = -np.inf, np.inf exp_cnt = np.histogram(expected, breakpoints)[0] / len(expected) act_cnt = np.histogram(actual, breakpoints)[0] / len(actual) exp_cnt = np.where(exp_cnt == 0, 1e-6, exp_cnt) act_cnt = np.where(act_cnt == 0, 1e-6, act_cnt) return np.sum((act_cnt - exp_cnt) * np.log(act_cnt / exp_cnt)) print("PSI =", psi(app_score[:len(app_score)//2], app_score[len(app_score)//2:]))

参数说明:bins=10是分箱数,1e-6防止除零。PSI < 0.1 稳定,0.1-0.25 需关注,> 0.25 要重新训练。这个技巧在课设答辩时很加分,因为它说明你不只跑了模型,还考虑了上线后的稳定性。

最后说个血泪经验:我一开始做这套课设时,四类模型全用同一份特征表,结果催收评级 AUC 只有 0.6,查了两天才发现是样本粒度错了——催收样本里混进了未逾期账户。后来按业务线分别构造样本,AUC 直接上到 0.75。所以样本定义比模型选型重要十倍,先把标签和样本粒度想清楚,再谈算法。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询