4. 字段类型转换与标准化:将日期字段转为 datetime 类型,将状态码转为分类变量,统一手机号格式

原始催收数据中,字段类型混乱是常态:日期存为字符串、状态码是数字编码、手机号夹杂空格和特殊符号。本节将系统解决这三类问题,为后续时间序列分析、状态聚合和客户触达奠定干净的数据基础。

清洗目标: 日期字段 → datetime 类型;状态码 → category 分类变量;手机号 → 统一纯数字格式(去除空格、-、+86 前缀等)。

4.1 日期字段:从字符串到 datetime

催收数据中常见的日期格式包括 "2024-01-15 14:30:00"、"2024/01/15"、"20240115" 等。使用 pandas.to_datetime() 可一次性完成转换,并利用 errors='coerce' 将无法解析的值转为 NaT,便于后续排查。

import pandas as pd

# 示例数据
df = pd.DataFrame({
    'contact_date': ['2024-01-15 14:30:00', '2024/01/16', '20240117', '无效日期', None]
})

# 统一转为 datetime,无法解析的置为 NaT
df['contact_date'] = pd.to_datetime(df['contact_date'], errors='coerce')

print(df.dtypes)
print(df)

转换后,contact_date 字段类型变为 datetime64[ns],可直接提取年、月、日、星期等特征。

原始值 转换结果 说明
'2024-01-15 14:30:00' 2024-01-15 14:30:00 标准格式,直接解析
'2024/01/16' 2024-01-16 00:00:00 自动识别分隔符
'20240117' 2024-01-17 00:00:00 无分隔符格式
'无效日期' NaT 无法解析,标记为缺失
None NaT 空值统一处理
注意: 使用 errors='coerce' 后,务必检查 NaT 的数量。若超过阈值(如 5%),需回溯数据源,确认是否存在未知日期格式,避免大量数据被静默丢弃。

4.2 状态码:从数值到分类变量

催收状态码通常为整数编码(如 0=未联系、1=已联系、2=承诺还款、3=拒绝沟通),直接作为数值参与计算会产生误导(例如计算均值无意义)。应转为 category 类型,并建立可读映射。

# 状态码映射字典
status_map = {
    0: '未联系',
    1: '已联系',
    2: '承诺还款',
    3: '拒绝沟通',
    9: '其他'
}

# 转换前检查唯一值
print("原始唯一值:", df['status_code'].unique())

# 转为分类变量,并添加映射标签
df['status_code'] = df['status_code'].astype('category')
df['status_label'] = df['status_code'].map(status_map)

print(df['status_code'].dtype)
print(df[['status_code', 'status_label']].head())

转换后,status_code 的 dtype 变为 category,排序和分组操作效率更高;同时保留原始编码和可读标签两列,便于分析和报表输出。

最佳实践: 分类变量建议使用 pd.CategoricalDtype 指定顺序(如 ordered=True),使 未联系 < 已联系 < 承诺还款 < 拒绝沟通 的语义在排序和可视化中自然体现。

4.3 手机号:统一格式清洗

手机号字段常混入空格、短横线、括号、+86 前缀等。清洗目标:保留纯数字,长度统一为 11 位(中国大陆手机号),异常值标记为缺失。

import re

def clean_phone(phone):
    if pd.isna(phone):
        return None
    # 转为字符串,去除所有非数字字符
    cleaned = re.sub(r'\D', '', str(phone))
    # 去除 +86 前缀(若存在)
    if cleaned.startswith('86') and len(cleaned) == 13:
        cleaned = cleaned[2:]
    # 长度校验:仅保留 11 位数字
    if len(cleaned) == 11:
        return cleaned
    else:
        return None

# 示例数据
df['phone'] = ['138-1234-5678', '+86 139 1234 5678', ' 15912345678 ', '12345', None]
df['phone_clean'] = df['phone'].apply(clean_phone)

print(df[['phone', 'phone_clean']])
原始手机号 清洗后 处理说明
'138-1234-5678' '13812345678' 去除短横线
'+86 139 1234 5678' '13912345678' 去除 +86 前缀和空格
' 15912345678 ' '15912345678' 去除首尾空格
'12345' None 非 11 位,标记为缺失
None None 空值保留

4.4 字段类型转换与标准化流程总览

以下 SVG 图展示了从原始脏数据到标准化数据集的完整转换链路,包含三个并行清洗分支和最终的数据质量检查点。

字段类型转换与标准化流程 原始脏数据 日期字段清洗 pd.to_datetime() errors='coerce' 状态码清洗 astype('category') map(status_map) 手机号清洗 re.sub(r'\D', '') 长度校验 11 位 标准化数据集 数据质量检查 & 异常标记

4.5 清洗效果验证

完成上述三步转换后,建议执行一次快速验证,确保字段类型和值范围符合预期。

# 验证字段类型
print("=== 字段类型检查 ===")
print(df.dtypes)

# 验证日期范围
print("\n=== 日期范围 ===")
print(f"最早日期: {df['contact_date'].min()}")
print(f"最晚日期: {df['contact_date'].max()}")

# 验证状态码分布
print("\n=== 状态码分布 ===")
print(df['status_label'].value_counts())

# 验证手机号格式
print("\n=== 手机号格式检查 ===")
invalid_phones = df[df['phone_clean'].isna() & df['phone'].notna()]
print(f"无效手机号数量: {len(invalid_phones)}")
清洗后数据特征:
  • contact_date:datetime64[ns],可直接用于时间序列分析
  • status_code:category,分组聚合效率提升 30% 以上
  • phone_clean:纯数字 11 位,可用于客户去重和触达验证

字段类型转换与标准化是数据清洗的基石。完成本节操作后,催收数据将具备正确的语义类型和统一的格式,为后续的异常检测、趋势分析和报表生成提供可靠的数据输入。