在金融风控的世界里,数据从来不是天生干净的。就像食材需要清洗、切配、调味,数据也需要一套严谨的“烹饪工艺”。
说实话,模型只是冰山浮在水面上的那一角。冰山下面,是占比超过70%的数据清洗与预处理工作。
如果用做饭来比喻:模型是最后的“烹饪”,而数据清洗,则是从买菜、择菜、洗菜、切菜到配料的全部过程。没有这个环节,再好的厨师也做不出一桌好菜。
一、 数据结构:风控工程师的“工具箱”
在开始清洗之前,我们得先搞清楚:我们要处理的“食材”长什么样?
金融风控的数据,通常以表格形式存在——行是客户,列是特征。在Python的世界里,Pandas库的DataFrame就是我们的“工作台”。
1. Series:一维的“调料罐”每一列数据,比如“年龄”、“申请金额”、“逾期次数”,都是一个Series。它像一个个调料罐,里面装着同一种“味道”的数据。
2. DataFrame:二维的“案板”整个数据集是DataFrame,就像一个宽敞的案板,上面摆满了各种食材。我们要在这张案板上完成所有预处理工作。
3. 索引:每块食材的“身份证”每一行数据都有一个索引,通常是客户ID或者申请编号。这个索引,是整个清洗过程中最重要的锚点——丢失了索引,数据就变成了“无主之物”。
二、 数据加载:把“原材料”搬进厨房
风控数据的来源五花八门:数据库、日志文件、第三方接口、Excel报表……
Python的pandas.read_*系列函数,就是我们搬运食材的工具。
实战经验:
# 加载CSV文件时,这几件事要顺手做df = pd.read_csv('apply_data.csv', dtype={'mobile': str, 'id_no': str}, # 指定数据类型,避免手机号变成科学计数法 parse_dates=['apply_time'], # 时间字段直接解析 encoding='utf-8', # 编码问题要提前确认 low_memory=False) # 大文件避免内存警告
有个细节很重要:先看后读。对于超大文件,我会先用nrows=1000加载一小部分,预览数据结构,确认列名、分隔符、编码无误后,再完整加载。这样可以避免“辛辛苦苦加载半小时,发现编码错了”的悲剧。
三、 数据初探:摸清“食材”的家底
食材搬进厨房,第一件事是“盘点”。在Python里,我们有一整套“盘库”工具:
1. 快速概览
df.info() # 每一列的类型、非空数量、内存占用df.describe() # 数值列的统计信息:均值、标准差、分位数df.head(10) # 看前10行,感受数据长什么样
2. 缺失值扫描
missing_ratio = df.isnull().sum() / len(df)missing_ratio[missing_ratio > 0].sort_values(ascending=False)
这一步能告诉你:哪些列缺数据严重?缺得有多严重?缺失本身也是信号——比如“工作单位”字段为空,可能意味着自由职业者,也可能是用户在刻意隐瞒。
3. 唯一值探查
df['channel'].value_counts() # 渠道分布df['product_code'].nunique() # 产品种类数
这些信息,是后续特征工程的“原料清单”。
四、 数据清洗:最考验功力的“刀工”
这是整个流程中最耗时、最考验耐心的环节。就像处理一条鱼,去鳞、去内脏、剔骨、片肉,每一步都要精准。
1. 缺失值处理:三种策略
- • 删除:当某列缺失率超过80%,且业务价值不大时,果断删除。不要舍不得,烂食材该扔就扔。
df.drop(columns=['worthless_col'], inplace=True)
- • 填充:对于重要的特征,用均值、中位数、众数填充。
df['income'].fillna(df['income'].median(), inplace=True)
- • 标记:有时候,缺失本身是一个强信号。我们可以新建一个标志列。
df['income_missing_flag'] = df['income'].isnull().astype(int)
2. 异常值处理:识别“离谱”的数据
一个申请人的年龄是200岁,月收入是99999999,这些显然不是真实数据。
# 使用业务逻辑过滤df = df[(df['age'] >= 18) & (df['age'] <= 80)]# 使用分位数过滤极端值q1 = df['loan_amount'].quantile(0.01)q99 = df['loan_amount'].quantile(0.99)df = df[(df['loan_amount'] >= q1) & (df['loan_amount'] <= q99)]
风控经验:异常值不能简单删除。比如“申请金额”的极端值,可能正是欺诈团伙的特征之一。我通常会把异常值单独编码为一个类别,而不是粗暴剔除。
3. 格式标准化:让“方言”变成“普通话”
风控数据最怕“同一意思,不同表达”:
# 手机号统一格式df['mobile'] = df['mobile'].astype(str).str.replace(r'\D', '', regex=True)# 日期统一格式df['apply_date'] = pd.to_datetime(df['apply_date'], errors='coerce')# 类别统一df['gender'].replace({'男': 'M', '女': 'F', 'male': 'M', 'female': 'F'}, inplace=True)
这一步,就像让操着不同方言的人统一说普通话,模型才能听懂。
4. 重复值处理:剔除“克隆人”
# 基于所有列去重df.drop_duplicates(inplace=True)# 基于关键列去重(保留第一条)df.drop_duplicates(subset=['id_no', 'apply_date'], keep='first', inplace=True)
五、 特征工程的前奏:为“烹饪”做准备
清洗完成后,我们开始为模型准备“食材”。这不是正式的建模,而是建模前的“备菜”。
1. 时间特征的提取
df['apply_hour'] = df['apply_time'].dt.hourdf['apply_weekday'] = df['apply_time'].dt.weekdaydf['is_weekend'] = df['apply_weekday'].isin([5, 6]).astype(int)
风控经验:凌晨3点的申请,欺诈比例往往高于上午10点。这个“小时”特征,比原始的“时间戳”更有价值。
2. 类别特征的编码
# 频率编码:用“该类别出现次数”替换原始值channel_freq = df['channel'].value_counts().to_dict()df['channel_freq'] = df['channel'].map(channel_freq)# 目标编码:用“该类别下坏账率”替换原始值(需要防泄漏)
3. 业务逻辑的组合
# 收入负债比,比单独的“收入”和“负债”更有意义df['debt_to_income'] = df['total_debt'] / (df['monthly_income'] + 1e-6)# 年龄与申请金额的交互df['age_loan_interaction'] = df['age'] * np.log(df['loan_amount'] + 1)
六、 数据质量检查:上菜前的“试吃”
清洗完成后,千万别急着建模。就像大厨上菜前要试吃一样,我们要做最后的“质量检查”。
1. 数据量检查清洗后还剩多少样本?如果损失超过30%,要回溯确认是否误删。
2. 分布一致性检查
# 对比清洗前后的数值分布df['income'].hist(bins=50)
3. 逻辑合理性检查
4. 时间线检查这是风控最容易踩坑的地方。确保训练集的“标签”(是否逾期)是基于“未来”的数据,而不是用“未来”的信息去预测“过去”。
# 错误的做法:用2024年的特征预测2023年的逾期(未来信息泄漏)# 正确的做法:按时间切分train = df[df['apply_date'] < '2023-01-01']test = df[(df['apply_date'] >= '2023-01-01') & (df['apply_date'] < '2024-01-01')]
结语:数据清洗,是风控人的“工匠精神”
有人问我:“数据清洗这么枯燥,为什么不做点‘高级’的事情?”
我的回答是:在风控领域,没有“高级”和“低级”之分,只有“准确”和“错误”之分。
一个模型的好坏,70%取决于数据质量,20%取决于特征工程,只有10%取决于算法选择。那些愿意在数据清洗上下“笨功夫”的团队,往往能在关键时刻守住底线。
Python给了我们强大的工具,但工具背后的“工匠精神”,才是真正的护城河。每一行代码、每一次检查、每一个细节的坚持,最终都会体现在资产质量的稳健上。
数据干净,模型才干净;模型干净,风控才干净。