4. 数据清洗:处理缺失值、重复值、异常时间戳、统一电话号码格式
原始催收数据如同未经打磨的矿石,直接分析会导致结论偏差甚至错误。本章聚焦数据清洗四大核心任务:缺失值处理、重复值剔除、异常时间戳修正以及电话号码格式统一。这是构建可靠触达分析模型的基石。
4.1 数据清洗全景流程
在动手编码前,先通过一张流程图理解清洗工作的完整链路。每个环节的输出将作为下一环节的输入,形成标准化的数据预处理管线。
4.2 缺失值处理策略
催收数据中常见的缺失场景包括:通话时长为空(未接通)、催收员ID缺失(系统日志异常)、客户意向标签为空(未完成评估)。不同字段的缺失含义不同,需采用差异化策略。
| 字段 | 缺失含义 | 处理策略 | 代码实现 |
|---|---|---|---|
| 通话时长 | 未接通或挂断 | 填充 0 | df['duration'].fillna(0) |
| 催收员ID | 系统未记录 | 填充 'UNKNOWN' | df['agent_id'].fillna('UNKNOWN') |
| 客户意向标签 | 未评估 | 填充 'PENDING' | df['intent'].fillna('PENDING') |
| 联系结果 | 未触达 | 填充 'NO_CONTACT' | df['result'].fillna('NO_CONTACT') |
关键原则:不要随意删除含缺失值的行。催收数据中“未触达”本身就是重要分析维度,删除会丢失负样本。优先使用业务含义明确的填充值。
4.3 重复值检测与剔除
重复数据通常由系统重试、接口超时重传或人工重复录入导致。重复记录会虚增触达次数、拉低接通率。检测重复需基于业务主键,而非全字段匹配。
import pandas as pd
# 加载原始数据
df = pd.read_csv('collection_raw.csv')
# 查看缺失值概况
print("缺失值统计:")
print(df.isnull().sum())
# 处理缺失值
df['duration'] = df['duration'].fillna(0)
df['agent_id'] = df['agent_id'].fillna('UNKNOWN')
df['intent'] = df['intent'].fillna('PENDING')
df['result'] = df['result'].fillna('NO_CONTACT')
# 检测重复:基于客户ID + 催收员ID + 联系时间 判断
duplicate_mask = df.duplicated(subset=['customer_id', 'agent_id', 'contact_time'], keep='first')
print(f"发现重复记录数:{duplicate_mask.sum()}")
# 剔除重复,保留第一条
df_clean = df[~duplicate_mask].copy()
print(f"去重后数据量:{len(df_clean)}")
避坑提示:使用 keep='first' 保留首次记录,因为首次触达的时效性最高。如果业务要求保留最后一次联系记录,则使用 keep='last'。
4.4 异常时间戳修正
时间戳异常是催收数据中的高频问题,常见表现包括:年份错误(如 2099 年)、时间顺序颠倒(结束时间早于开始时间)、格式不统一(混用 YYYY-MM-DD 和 MM/DD/YYYY)。
以下代码演示如何检测并修正异常时间戳:
# 统一时间格式
df_clean['contact_time'] = pd.to_datetime(df_clean['contact_time'], errors='coerce')
# 检测异常年份(超出当前年份 ±1 年视为异常)
current_year = pd.Timestamp.now().year
year_mask = df_clean['contact_time'].dt.year.between(current_year - 1, current_year + 1)
print(f"异常年份记录数:{(~year_mask).sum()}")
# 将异常年份修正为当前年份(保留月日时分)
df_clean.loc[~year_mask, 'contact_time'] = df_clean.loc[~year_mask, 'contact_time'].apply(
lambda x: x.replace(year=current_year) if pd.notna(x) else x
)
# 检测结束时间早于开始时间
invalid_order = df_clean['end_time'] < df_clean['start_time']
print(f"时间顺序颠倒记录数:{invalid_order.sum()}")
# 修正:交换开始和结束时间
swap_idx = invalid_order[invalid_order].index
df_clean.loc[swap_idx, ['start_time', 'end_time']] = df_clean.loc[swap_idx, ['end_time', 'start_time']].values
4.5 电话号码格式统一
电话号码格式混乱会直接导致触达失败或统计失真。常见问题包括:含国家代码(+86 或 0086)、含空格或横线、长度不一致(手机号 11 位 vs 座机号含区号)。统一规则如下:
- 去除所有非数字字符:空格、横线、括号、加号
- 去除国家代码前缀:+86、0086、86(仅保留后 11 位手机号或后 12 位座机号)
- 长度校验:手机号必须为 11 位,座机号必须为 12 位(含区号)
- 异常标记:不符合长度规则的号码标记为 'INVALID'
import re
def clean_phone(phone):
if pd.isna(phone):
return 'MISSING'
# 去除非数字字符
digits = re.sub(r'\D', '', str(phone))
# 去除国家代码
if digits.startswith('86') and len(digits) > 11:
digits = digits[2:]
elif digits.startswith('0086') and len(digits) > 11:
digits = digits[4:]
# 长度校验
if len(digits) == 11 or len(digits) == 12:
return digits
else:
return 'INVALID'
df_clean['phone_clean'] = df_clean['phone_raw'].apply(clean_phone)
# 统计清洗结果
phone_status = df_clean['phone_clean'].value_counts()
print("电话号码清洗结果统计:")
print(phone_status)
注意:座机号含区号通常为 12 位(如 01088886666),但部分数据可能缺失区号。对于长度不足 12 位的座机号,建议标记为 'INCOMPLETE' 而非直接丢弃,以便后续人工核实。
4.6 清洗效果验证
完成清洗后,必须进行验证以确保数据质量。以下表格展示了清洗前后的关键指标对比:
| 指标 | 清洗前 | 清洗后 | 变化说明 |
|---|---|---|---|
| 总记录数 | 50,000 | 48,200 | 剔除 1,800 条重复 |
| 缺失值占比 | 12.3% | 0% | 全部填充 |
| 异常时间戳 | 342 条 | 0 条 | 修正年份与顺序 |
| 无效电话号码 | 2,156 条 | 87 条 | 格式统一后仅剩极少数异常 |
清洗后的数据可直接用于后续的触达率计算、趋势分析和异常波动检测。下一阶段将基于这份干净数据构建催收触达的核心指标体系。