1. 构建信贷申请数据清洗流水线
任务说明
构建一条可复用的信贷申请数据清洗流水线,对原始申请数据依次执行:缺失值处理、异常值识别与修正、重复记录剔除,最终输出可直接用于后续特征工程和建模的干净数据集。本案例使用Python + pandas实现,以一份包含典型脏数据的模拟申请数据作为输入,验证清洗逻辑的有效性。
环境依赖
仅需 pandas 与 numpy,建议使用Python 3.9+。若需复现模拟数据,还需 random(标准库)。
完整实现
import pandas as pd
import numpy as np
# ---------- 1. 构造含脏数据的模拟信贷申请样本 ----------
def create_sample_data():
np.random.seed(42)
n = 200
data = {
'applicant_id': range(1001, 1001 + n),
'age': np.random.randint(22, 60, n).astype(float),
'annual_income': np.random.normal(200000, 50000, n).round(2),
'credit_score': np.random.randint(300, 850, n).astype(float),
'loan_amount': np.random.randint(5000, 50000, n).astype(float),
'employment_years': np.random.uniform(0, 30, n).round(1),
}
df = pd.DataFrame(data)
# 注入缺失值
df.loc[5, 'age'] = np.nan
df.loc[12, 'annual_income'] = np.nan
df.loc[33, 'credit_score'] = np.nan
df.loc[47, 'employment_years'] = np.nan
# 注入异常值
df.loc[8, 'age'] = 150 # 年龄不可能
df.loc[21, 'credit_score'] = 950 # 超出最高分
df.loc[30, 'annual_income'] = -5000 # 收入为负
# 注入重复记录(完全重复 + 部分重复)
df = pd.concat([df, df.iloc[[10]], df.iloc[[15]]], ignore_index=True)
return df
# ---------- 2. 数据清洗流水线 ----------
def clean_credit_data(df):
df = df.copy()
# 2.1 删除完全重复的记录
df = df.drop_duplicates(subset=['applicant_id'], keep='first')
# 2.2 缺失值处理:数值列用中位数填充
numeric_cols = ['age', 'annual_income', 'credit_score', 'loan_amount', 'employment_years']
for col in numeric_cols:
median_val = df[col].median()
df[col] = df[col].fillna(median_val)
# 2.3 异常值处理:基于业务规则修正
# 年龄:合理范围 [18, 80],超出则用中位数替换
age_median = df['age'].median()
df.loc[(df['age'] < 18) | (df['age'] > 80), 'age'] = age_median
# 信用分:合理范围 [300, 850]
credit_median = df['credit_score'].median()
df.loc[(df['credit_score'] < 300) | (df['credit_score'] > 850), 'credit_score'] = credit_median
# 年收入:必须为正
income_median = df['annual_income'].median()
df.loc[df['annual_income'] <= 0, 'annual_income'] = income_median
# 贷款金额:必须为正
loan_median = df['loan_amount'].median()
df.loc[df['loan_amount'] <= 0, 'loan_amount'] = loan_median
# 工作年限:合理范围 [0, 40]
emp_median = df['employment_years'].median()
df.loc[(df['employment_years'] < 0) | (df['employment_years'] > 40), 'employment_years'] = emp_median
# 2.4 重置索引
df = df.reset_index(drop=True)
return df
# ---------- 3. 运行与验证 ----------
if __name__ == '__main__':
raw_df = create_sample_data()
print(f'清洗前记录数: {len(raw_df)}')
print(f'清洗前缺失值总数: {raw_df.isnull().sum().sum()}')
print(f'清洗前重复 applicant_id 数: {raw_df.duplicated(subset=["applicant_id"]).sum()}')
cleaned_df = clean_credit_data(raw_df)
print(f'\n清洗后记录数: {len(cleaned_df)}')
print(f'清洗后缺失值总数: {cleaned_df.isnull().sum().sum()}')
print(f'清洗后重复 applicant_id 数: {cleaned_df.duplicated(subset=["applicant_id"]).sum()}')
# 验证异常值已修正
assert cleaned_df['age'].between(18, 80).all(), '年龄仍有异常'
assert cleaned_df['credit_score'].between(300, 850).all(), '信用分仍有异常'
assert (cleaned_df['annual_income'] > 0).all(), '收入仍有非正值'
assert (cleaned_df['loan_amount'] > 0).all(), '贷款金额仍有非正值'
assert cleaned_df['employment_years'].between(0, 40).all(), '工作年限仍有异常'
print('\n所有业务规则校验通过:异常值已全部修正。')
print('\n清洗后数据预览(前5行):')
print(cleaned_df.head())
运行方式
将上述代码保存为 credit_data_cleaning.py,在终端执行:
python credit_data_cleaning.py
验证方法与通过标准
运行脚本后,观察控制台输出并确认以下条件全部满足:
- 记录数变化:清洗前为202条(200条原始 + 2条重复),清洗后为200条,重复的
applicant_id被剔除。 - 缺失值清零:清洗后缺失值总数为0,所有数值列均被中位数填充。
- 异常值修正:程序末尾的断言语句全部通过,无异常抛出,说明年龄、信用分、收入、贷款金额、工作年限均落入业务合理区间。
- 数据可用性:清洗后的DataFrame索引连续,可直接作为下游特征工程的输入。
若断言失败或输出与预期不符,请检查原始数据注入的脏数据位置是否被正确覆盖。
常见问题
- 中位数填充是否适用于所有列? 本案例中所有特征均为数值型且分布相对集中,中位数对离群值不敏感,适合作为填充值。若实际数据包含类别特征,需单独设计填充策略。
- 异常值为何用中位数替换而非删除? 信贷建模中样本量通常有限,直接删除会损失信息。用中位数替换可在保留样本的同时削弱异常值影响。若异常值占比过高,应优先排查数据采集环节的问题。
- 重复记录仅按
applicant_id判断是否足够? 本案例模拟的是同一申请编号重复录入的场景。真实业务中若同一客户多次申请,应结合时间戳等多列综合判断,避免误删有效记录。