简介:面向金融风控建模学习者的拍拍贷风控大赛原始数据集,包含训练主表、日志信息表等CSV数据,配套可视化图表与说明文档,可用于信用风险评估、用户违约预测等实战练习。资源共176个文件,以CSV数据文件为核心,辅以JPG/PNG可视化图片、HTML/JS/CSS前端展示文件、XLSX表格、IPYNB分析笔记及文档,整包约37.37MB,结构清晰便于按需调用。已有1123人学习浏览,比较适合希望接触真实信贷数据、从特征工程到模型评估完整走一遍风控流程的初学者和竞赛参与者。压缩包内不仅提供了多个结构化业务数据表,还保留了网页分析记录与可视化输出,可帮助理解数据探索思路,缩短上手时间。用户可以据此练习逻辑回归、随机森林等常见风控模型,并结合AUC-ROC等指标评估模型区分能力。
1. ppd风控大赛数据集:一个7z压缩包能解决什么问题
打开这个 7z 压缩包,你得到的不是能直接喂给模型的干净表格,而是一堆带噪音的真实信贷业务数据。我在处理这类竞赛数据时最深的体感是:真正卡人的地方从来不是模型调参,而是“把业务表格变成训练样本”这个过程。ppd 数据集之所以值得折腾,是因为它同时包含借款申请信息、用户历史行为文本描述和还款状态标签,能完整串起一条“原始数据 → 特征 → 二分类模型”的风控建模链路。如果你正在学信贷风控、准备转岗风控算法,或者只是想知道脱敏后的真实借贷数据长什么样,花一个周末把它跑通,比看十篇教程都管用。本文不涉及任何隐私信息,只讲数据集本身的处理方法和工程习惯。
2. 解压与拆包:7z里的文件结构和字段清单先对齐
2.1 先校验压缩包完整性,再解压到独立目录
我拿到这个 7z 包的第一件事不是急着双击解压,而是先做完整性测试。很多竞赛数据包下载过程中容易断流,解压报错时你甚至分不清是文件坏了还是密码错了。用命令行工具先测一遍:
# 测试压缩包是否完整,7z 会逐个文件校验 CRC 7z t ppd拍拍贷风控大赛数据集.7z # 测试通过后,解压到独立目录,-o 后直接跟路径,不要加空格 7z x ppd拍拍贷风控大赛数据集.7z -oppd_data第一条命令输出里如果出现Everything is Ok,说明压缩包完整,可以放心解压;如果报某个文件 CRC Failed,那就是下载损坏,重新下载比强行修复省时间。第二条命令里的x表示保留完整目录结构解压,-o指定输出目录。我一般会在项目根目录建一个raw/子目录专门放这种原始数据,后面所有清洗脚本都从raw/读入,绝不直接改动原始文件,这样跑坏了随时能回到起点。
不想装 7-Zip 的话,Python 里有 py7zr 库也能解压,但命令行更省事,且解压速度通常更快。注意压缩包内文件名可能是中文,Windows 下用 7-Zip 图形界面解压偶尔会出现文件名乱码,命令行工具配合 UTF-8 编码一般能规避。
2.2 把文件结构摸清楚:表与表之间靠什么关联
解压之后不要急着head看数据,先列一下目录结构和文件大小,建立整体认识:
ls -lh ppd_data/常见版本里,这个包一般会拆成几张 CSV:一张记录借款标的信息的表、一张记录借款用户信息的表,以及附带的还款状态或标的进度字段。不同竞赛场次,表名和字段名不完全一样,但核心思路是一样的——先搞清楚“哪张表是主表,哪张表是维表,靠什么 key 关联”。
我习惯把字段清单整理成一张映射表,方便后续写特征时随时查阅。ppd 数据里值得关注的几类字段大致如下:
| 字段(常见叫法) | 类型 | 在特征工程里的用途 |
|---|---|---|
| 借款日期 / 申请时间 | 时间 | 构造时间特征、切分训练集 |
| 借款金额 | 数值 | 金额类特征、组合特征 |
| 借款利率 | 数值 | 风险定价因子,区间划分 |
| 借款期限 | 数值 | 与金额组合成月还款额 |
| 历史成功借款次数 | 数值 | 用户活跃度 |
| 历史逾期次数 | 数值 | 用户信用历史核心变量 |
| 历史正常还款次数 | 数值 | 与逾期次数组合成履约率 |
| 借款描述 | 文本 | 文本长度、关键词特征 |
| 还款状态 | 离散 | 目标 label 的唯一来源 |
注意一个容易翻车的细节:表内粒度。ppd 数据的借款表通常是一个用户有多笔借款记录,每笔一行;用户维表则可能是每个用户一行。如果你直接把两张表merge之后再 groupby 算用户特征,会出现同一用户的维表字段被复制到多行里,导致统计翻倍。正确做法是先确定你要建模的粒度——是“每笔借款”还是“每个用户”——再决定共享特征的计算方式。绝大多数竞赛场景默认按借款粒度做二分类,因为同一用户的不同借款会有不同结果,样本量也更大。
3. 从数据集到训练样本:定标签、设粒度、对齐时间
3.1 读 CSV 前,先想清楚编码、类型和缺失值
ppd 这份数据里中文文本多,CSV 读进来最常见的翻车点是编码。我见过不少同学用默认编码读,结果借款描述列全是乱码,还以为是数据脱敏把文本打码了。我一般这样设定读取参数:
import pandas as pd # 先用 utf-8 尝试读,报 UnicodeDecodeError 就换 gbk 或 utf-8-sig df = pd.read_csv( "ppd_data/标的表.csv", encoding="utf-8-sig", dtype={"借款描述": "string"}, # 强制字符串类型,避免被解析成其他类型 parse_dates=["借款日期"], # 日期列直接转成 datetime,方便后续切片 )utf-8-sig能自动处理带 BOM 的文件头,比单纯utf-8更稳。dtype参数里指定文本列为string是关键一步:如果不指定,Pandas 遇到一列中绝大多数是空值时,会把整列类型推断成float,后面做字符串操作全部报错。parse_dates把日期列提前转好,比你事后pd.to_datetime()逐列处理要快得多。
读进来以后先打印三个东西:df.shape、df.dtypes、df.isnull().sum()。这里能看到最真实的样本量和缺失率。我见过有人忽略 shape 直接开始写特征脚本,结果把训练集和测试集拼接起来一起做了标准化,导致信息泄漏,这就是没先建立数据全貌的代价。
3.2 还款状态列:怎么翻译成 0/1 标签
ppd 数据集里最重要的列是还款状态。它通常不是干净的 0/1,而是多取值状态,比如“还款中”“已还清”“逾期”“严重逾期”等等。这里需要做一道判断题:哪些状态算正样本,哪些算负样本。
# 先看分布,再决定切分口径 print(df["还款状态"].value_counts(dropna=False)) # 常规做法:把非正常结清的状态统一标记为坏样本(1),正常结清为 0 # 注意:不同比赛口径可能不同,以你下载版本的说明为准 df["target"] = (df["还款状态"] != "已还清").astype(int)value_counts(dropna=False)里dropna=False的细节不要省,因为数据里可能存在空状态,你不看它就永远不知道缺失状态到底算好还是算坏。切分口径上,我倾向于只把明确“已还清”当作 0,其余一律归为 1,这样坏样本的定义更保守,模型学到的规律也更接近风控的真实含义。如果你发现某类状态占比奇高,比如一半样本都是“还款中”,说明这批数据的观测窗口还没走完,此时最好直接丢掉“还款中”的样本,而不是硬归到哪一边,否则标签噪声会严重拉低模型上限。
3.3 备份一份纯数值干净的宽表,再做特征
特征工程之前,我习惯先做一步数据清洗并另存宽表,后续所有特征都在这张表上追加。这样做的好处是:万一某个特征写坏了,从宽表重新跑就行,不用重新读原始 CSV。
# 只保留必要列,减少内存占用 base = df[["借款id", "用户id", "借款日期", "借款金额", "借款利率", "借款期限", "target"]].copy() # 排序并重置索引,方便时间窗口切片 base = base.sort_values("借款日期").reset_index(drop=True) # 保存中间结果,后续特征脚本从这里读 base.to_pickle("ppd_data/base.pkl")用to_pickle保存中间结果是个被低估的好习惯。CSV 每次读取都重新解析一遍类型和日期,而 pickle 直接保存 Pandas 内部结构,读回来速度快一个量级,且类型不会漂移。数据量不大时随便用,如果后续数据量涨到几百万行,同样套路换成parquet格式更稳。
4. 基于ppd原始字段做特征工程:金额、历史行为与借款描述
4.1 金额、利率和期限的组合特征,先于单字段特征
做特征的第一条原则我通常这样执行:先把业务上明显相关的字段组合起来,再考虑单字段。借款金额和借款期限单独看信息量一般,但组合成“月还款额”后,就能和用户收入水平形成对照,模型更容易抓到“月供过高导致违约”的模式。这个动作有几个常见写法:
# 在 base 基础上追加组合特征 base["月还款额"] = base["借款金额"] / base["借款期限"] base["期限利率积"] = base["借款期限"] * base["借款利率"] base["金额分档"] = pd.qcut(base["借款金额"], q=5, labels=False, duplicates="drop")pd.qcut按分位数把金额切成五档,比直接扔连续值更稳,因为金额长尾严重,连续值进树模型容易被极值拉扯。duplicates="drop"是防止分位数边界重复时报警。注意组合特征不要无限做,两个字段做成加减乘除四则运算就够了,再往上拼不仅意义不大,还会让树模型的特征重要性分散,增加排查成本。
做完这一步后跑一个简单的corr看一下,月还款额和借款金额的相关系数通常高达 0.9 以上,这是预期内的——特征之间线性相关对树模型无害,但对后续做逻辑回归或线性模型的人是个坑。如果你计划对比线性模型,就得考虑先做去相关或正则化。
4.2 用户历史行为:先想清楚是求和还是取最近值
ppd 数据里关于用户历史的字段不少,常见的有历史成功借款次数、历史逾期次数、历史正常还款次数。头次接触的人容易犯一个错:直接用这些字段的原始值进模型。但同一用户有多笔借款记录时,这些字段在每行里是重复的,本质上没有行间区分度。我一般这样处理:
# 先按用户聚合,算用户的整体信用画像 user_feat = df.groupby("用户id").agg( 历史借款笔数=("借款id", "count"), 历史逾期次数最大=("历史逾期次数", "max"), 历史正常还款次数最大=("历史正常还款次数", "max"), 平均借款金额=("借款金额", "mean"), ).reset_index() # 再合并回借款表,注意是左连接,保证每笔借款都拿到用户画像 base = base.merge(user_feat, on="用户id", how="left")这里的关键区别在于max还是sum:“历史逾期次数”在大多数脱敏数据里已经是截至当前借款前的历史累计值,直接sum会把多笔借款的累计值重复相加,造成严重偏差。拿不准字段口径时,可以对同一用户做一次df.groupby("用户id")["历史逾期次数"].nunique()检查——正常累计值在同一用户的不同借款记录里应该保持一致,如果每行都不一样,说明字段本身就是按借款周期统计的,那时才可以考虑sum。这个检查只要一分钟,但能帮你避开一个很隐蔽的错误特征。
4.3 借款描述文本:长度、关键词和空值标记
ppd 数据里有一个文本列:借款描述或借款用途。文本类特征在树模型里不好直接利用,但可以提取三个层面的信息。第一是长度,借款人写得多不多,某种程度上反映申请的认真程度。第二是关键词,比如“周转”“医疗”“装修”等用途词。第三是是否有内容——如果大量用户此列为空,空本身也是一个信号。
# 文本特征三件套 base["描述长度"] = df["借款描述"].str.len() base["描述是否为空"] = df["借款描述"].isna().astype(int) base["描述含周转"] = df["借款描述"].fillna("").str.contains("周转|资金周转").astype(int) # 长度列常存在极端值,做一次截断更稳 base["描述长度_截断"] = base["描述长度"].clip(upper=200).clip(upper=200)把长度限制在 200 以内,防止个别几千字的长文把特征尺度拉爆。关键词那部分我一般只选两到三个业务上最明确的词,不要搞一个几十词的词典——词表越大,过拟合风险越高,而且换一批数据基本不可复用。文本特征在 ppd 这种数据上的收益通常不大,但聊胜于无,它能帮你看见“用户主动描述”这个行为本身是否与违约相关。
4.4 时间对齐:把所有特征锁在“借款申请日之前”
风控建模里最严肃的一条底线是特征时效性——任何特征都不能包含样本发生后才能知道的信息。ppd 数据里最容易踩这个线的地方是用户历史类字段,因为脱敏时经常把整张表导出,某些“历史逾期次数”可能包含了借款之后新产生的逾期记录。验证方法很简单:
# 构造一个带截止日期的特征版本:只用申请日之前的数据 cutoff = pd.Timestamp("2019-06-30") valid_df = base[base["借款日期"] < cutoff].copy()这个cutoff是模拟你在某个时间点做决策,把这之前的数据当训练集,之后的数据当验证集。如果某列特征在cutoff前后分布出现突变,多半就是那个列里混进了未来信息,需要回到原始 CSV 重新确认字段口径。时间对齐这事没有捷径,只能靠这种“按时间切分后对比特征分布”的办法反复排查。宁可少几个特征,也不能留一个穿越特征,否则模型上线必翻车。
5. 做ppd风控建模必踩的坑:缺失值、统计泄漏与时间穿越
5.1 认证类字段缺失率 90%,填充还是丢弃
现象:某列认证信息缺失率超过 90%,直接fillna("未认证")之后,发现模型把这个特征当成最强变量,验证 AUC 高得异常。
原因:这类字段只在用户主动上传或授权时才存在,缺失本身不代表“未认证”,而可能是脱敏时只保留了部分记录。填充成“未认证”等于自己制造了一个与标签高度相关的假变量,纯属统计泄漏。
解决:先看缺失率,缺失率超过 95% 的列直接丢弃;缺失率在 50% 到 95% 之间的列,把缺失本身变成一个二值标志位,值域再放进模型。不要用均值填充,也不要用中位数填充,信贷数据的缺失模式本身就是有业务含义的,强行填充只会掩盖信息。
5.2 训练 AUC 0.99,验证 AUC 0.70
现象:随机切分训练集时模型表现极好,一换到按时间划分的验证集就崩。
原因:这是典型的字段泄漏。ppd 数据里有些列跟目标列同源,例如“是否逾期”“当前状态”这类字段本身就是还款状态的变体,被当成特征喂进了模型。另外,如果同一用户的某笔借款结果已经确定,而他的另一笔借款记录里带上了这次结果的历史汇总,也会造成泄漏。
解决:建特征前把所有列名列出来,凡是名字里带“状态”“结果”“是否”的列,先全部移出特征池,等模型基线跑通之后再决定要不要加回。统一规则是:特征列必须是在借款申请时点已经确定的信息,任何涉及还款结果的列都是标签,不是特征。
5.3 随机 K 折切分导致 AUC 虚高
现象:同一套特征,用train_test_split(random_state=42)得到的 AUC 是 0.85,换成时间序列切分后变成 0.73。
原因:ppd 数据里同一个用户有多笔借款,随机切分时同一用户的借款会同时出现在训练集和验证集,模型直接记住了用户 ID 或用户维度的统计特征,验证分数自然虚高。
解决:用TimeSeriesSplit或GroupKFold做验证。GroupKFold按用户 ID 分组,保证同一个用户的全部借款只落在训练集或验证集的一侧,这样测出来的分数才是模型真正面对新用户时的表现。如果两组分数差异大于 0.08,说明特征里用户级信息过重,需要重新审视。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) # 每一折都用更早的时间段训练、更晚的时间段验证 # 比随机 K 折更贴近真实风控场景 for train_idx, val_idx in tscv.split(base): train = base.iloc[train_idx] val = base.iloc[val_idx] # 这里再单独做 GroupKFold 按用户去重校验5.4 借款描述列读进来全变成 NaN
现象:head()看前几行文本正常,一跑str.len()就报错说对象不是字符串。
原因:Pandas 在读取时如果发现某文本列包含空值,会把整列推断为float类型,后续字符串操作全部失效;还有一部分原因是文件里包含中文引号或特殊转义字符,导致解析错位。
解决:读取时强制指定dtype={"借款描述": "string"},并且给str操作统一加.fillna(""),保证类型正确再操作。要是读进来的文本自带换行符,先.str.replace("\n", " ")清洗一遍,再做长度和关键词提取。这个坑不深但很烦人,提前指定 dtype 能省下半小时。
6. 用SHAP值回看模型:验证特征依赖是否符合业务逻辑
6.1 树模型训练完之后,用 SHAP 替代 feature_importance
特征工程跑完,模型训练是后半段的事。我用 LightGBM 做二分类时习惯直接看 SHAP 值,而不是默认的feature_importance,因为默认重要性只看“用过多少次”,而 SHAP 能告诉你“特征多大程度上把预测推向 0 或 1”。这一步是验证模型是否学歪的关键。
import lightgbm as lgb import shap X = base[feature_cols] y = base["target"] train_data = lgb.Dataset(X_train, y_train) val_data = lgb.Dataset(X_val, y_val, reference=train_data) params = { "objective": "binary", "learning_rate": 0.05, "max_depth": -1, "num_leaves": 63, "min_child_samples": 100, "feature_fraction": 0.8, "bagging_fraction": 0.8, "verbose": -1, } model = lgb.train( params, train_data, num_boost_round=500, valid_sets=val_data, callbacks=[lgb.early_stopping(50)], ) explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val)early_stopping(50)表示连续 50 轮验证集分数不提升就停止,能省下大量无效迭代。min_child_samples=100是风控数据里常用的防过拟合参数,保证叶子节点至少有 100 个样本——信贷数据正负样本不平衡,叶子节点太小学到的全是噪声。summary_plot画出来的图上,纵轴按特征重要性排序,横轴看 SHAP 值方向,红色代表特征取值高,蓝色代表取值低。拿这张图去和业务常识对一遍:借款利率高、历史逾期次数多的用户,预测坏概率是不是相应更高。如果是反的,优先怀疑标签切错或特征中混入了未来信息。
6.2 把 SHAP 结论还原成一句话,再决定是否信任模型
SHAP 图确认特征方向正确之后,我习惯挑两三个高重要性特征,做一次分组统计来交叉验证。比如按历史逾期次数分组,看每组的坏样本率是否单调递增;按借款利率分档,看高利率档位违约率是否更高。这种分组验证比任何指标都直观,也是给业务方展示模型逻辑时的最好素材。
实际动手做这个数据集时我发现,借款金额和利率方向很稳定,但借款描述里的关键词特征在不同版本数据上方向会变——某些词在训练集里是坏的信号,换一批样本变成好信号。所以文本关键词这列我在最终模型里要么舍掉,要么只保留长度和空值标志,不让它参与关键决策。
做完数据集的完整流程,我对竞赛数据的处理习惯有一些变化:每张表先确认粒度,每个潜在标签先看分布,每条时间特征先判断时点。这三个习惯救过我很多次,也让模型的验证分数经得起推敲。希望这次关于 ppd 风控数据集从解压、定标签、特征工程到验证的完整拆解,帮你在面对真实信贷数据时少走几步弯路。
本文还有配套的精品资源,点击获取