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 条 格式统一后仅剩极少数异常

清洗后的数据可直接用于后续的触达率计算、趋势分析和异常波动检测。下一阶段将基于这份干净数据构建催收触达的核心指标体系。