4. 数据质量初探:缺失值、异常值、重复值对A/B测试特征的影响

在催收触达A/B测试中,特征数据的质量直接决定实验结论的可靠性。缺失值、异常值和重复值是数据清洗阶段最常遇到的“三座大山”。本章将系统分析这三类问题对A/B测试的潜在影响,并给出针对催收场景的初步清洗方法。

核心观点: 数据质量问题若未妥善处理,会导致A/B测试的统计功效下降、效应量估计偏倚,甚至得出完全相反的结论。在催收场景中,这种偏差可能直接转化为坏账损失或客户投诉。
三类数据质量问题对A/B测试的影响路径 缺失值 异常值 重复值 样本量缩减 / 分组失衡 统计功效下降 / 方差膨胀 效应量偏倚 / 假阳性风险 A/B测试结论 不可靠 / 不可复现

4.1 缺失值:无声的样本流失

催收数据中缺失值极为常见。例如:客户联系人信息不全、历史还款记录部分缺失、第三方征信数据接口超时等。缺失值在A/B测试中会引发以下问题:

  • 样本量缩减: 直接删除含缺失值的样本,可能导致实验组和对照组样本量不对称,降低统计功效。
  • 分组偏差: 若缺失机制与实验分组相关(例如A组催收策略导致更多客户挂断电话,从而缺失后续行为数据),则产生非随机缺失(MNAR),严重破坏随机化。
  • 特征重要性误判: 缺失率高的特征在模型中被低估,可能遗漏关键催收触达变量。

4.1.1 缺失机制分类

缺失机制 定义 催收场景举例 对A/B测试影响
完全随机缺失(MCAR) 缺失与任何变量无关 系统随机丢包导致部分通话记录丢失 仅损失样本量,不影响无偏性
随机缺失(MAR) 缺失与其他观测变量相关 高收入客户更倾向不提供第二联系人 可通过其他变量校正,但需谨慎
非随机缺失(MNAR) 缺失与缺失值本身相关 逾期天数越长的客户,还款意愿数据越容易缺失 最危险,直接破坏实验有效性
⚠ 催收场景特别警示: 逾期阶段越靠后的客户,其数据缺失往往越严重。若直接删除这些样本,相当于剔除了最关键的“高风险”群体,导致A/B测试结论仅适用于低风险客户,产生严重的选择性偏差。

4.1.2 初步清洗方法

  1. 缺失率评估: 计算每个特征的缺失率,设定阈值(如30%)。缺失率过高的特征建议直接剔除或标记后单独分析。
  2. 简单填充策略:
    • 数值型特征:使用中位数填充(对异常值鲁棒)
    • 分类型特征:使用众数填充,或新增“未知”类别
    • 时间型特征:使用前向填充(如最近一次联系时间)
  3. 缺失指示变量: 创建二值变量标记是否缺失,保留缺失模式信息。
  4. 分组一致性检查: 确保实验组和对照组的缺失模式无显著差异(卡方检验)。
# Python示例:缺失值初步处理
import pandas as pd
import numpy as np

# 加载催收A/B测试特征数据
df = pd.read_csv('collection_ab_features.csv')

# 1. 计算缺失率
missing_rate = df.isnull().mean().sort_values(ascending=False)
print("特征缺失率:\n", missing_rate[missing_rate > 0])

# 2. 高缺失率特征处理(阈值30%)
high_missing_cols = missing_rate[missing_rate > 0.3].index.tolist()
df_clean = df.drop(columns=high_missing_cols)

# 3. 数值特征中位数填充
num_cols = df_clean.select_dtypes(include=[np.number]).columns
df_clean[num_cols] = df_clean[num_cols].fillna(df_clean[num_cols].median())

# 4. 分类特征众数填充 + 缺失指示变量
cat_cols = df_clean.select_dtypes(include=['object']).columns
for col in cat_cols:
    df_clean[col + '_missing'] = df_clean[col].isnull().astype(int)
    df_clean[col] = df_clean[col].fillna(df_clean[col].mode()[0])

# 5. 分组缺失模式检验(示例:实验组A vs 对照组B)
from scipy.stats import chi2_contingency
contingency = pd.crosstab(df['group'], df['feature_missing'])
chi2, p, _, _ = chi2_contingency(contingency)
print(f"分组缺失模式卡方检验 p值: {p:.4f}")

4.2 异常值:隐藏在分布尾部的“地雷”

催收数据中的异常值往往携带重要信息,但也可能扭曲统计推断。例如:某客户单日催收通话次数高达50次(系统错误或恶意刷量),或某账户逾期金额为正常值的100倍(数据录入错误)。

4.2.1 异常值对A/B测试的破坏机制

  • 均值偏移: 单个极端值即可显著改变组均值,导致效应量估计失真。
  • 方差膨胀: 异常值增大组内方差,降低t检验或ANOVA的统计功效。
  • 分组不均: 若异常值集中在某一实验组,会引入系统性偏差。

4.2.2 催收场景异常值识别方法

方法 适用场景 催收特征示例 阈值设定建议
Z-Score法 近似正态分布 客户年龄、信用评分 |Z| > 3
IQR法 偏态分布 逾期天数、催收通话时长 Q1 - 1.5×IQR 或 Q3 + 1.5×IQR
业务规则法 领域知识明确 单日催收次数、承诺还款金额 根据业务经验设定硬阈值
孤立森林 高维特征空间 多维度客户行为画像 异常得分 > 0.6

4.2.3 初步清洗策略

  1. 区分“真异常”与“假异常”: 催收场景中,高逾期天数本身就是风险信号,不应直接剔除。建议先标记,单独分析异常样本的业务含义。
  2. 截尾处理(Winsorization): 将极端值替换为指定百分位数的值(如1%和99%分位数),保留样本量同时抑制极端影响。
  3. 分组独立处理: 对实验组和对照组分别进行异常值检测,避免因分组差异导致误判。
  4. 敏感性分析: 分别在有/无异常值的数据集上运行A/B测试,观察结论是否稳健。
# Python示例:异常值检测与截尾处理
def detect_and_cap_outliers(df, col, method='iqr', lower_quantile=0.01, upper_quantile=0.99):
    """检测并截尾处理异常值"""
    if method == 'iqr':
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
    elif method == 'winsorize':
        lower_bound = df[col].quantile(lower_quantile)
        upper_bound = df[col].quantile(upper_quantile)
    
    # 标记异常值
    df[col + '_outlier'] = ((df[col] < lower_bound) | (df[col] > upper_bound)).astype(int)
    
    # 截尾处理
    df[col + '_capped'] = df[col].clip(lower=lower_bound, upper=upper_bound)
    
    return df

# 对催收通话时长进行截尾处理
df = detect_and_cap_outliers(df, 'call_duration_min', method='winsorize')
print(f"异常值占比: {df['call_duration_min_outlier'].mean():.2%}")

4.3 重复值:被“复制”的虚假样本

重复值在催收数据中可能源于:同一客户多次录入、数据合并时的重复记录、系统重试机制导致的重复日志。重复值会人为放大样本量,导致标准误被低估,假阳性率上升。

4.3.1 重复值的类型与影响

重复类型 定义 催收场景举例 对A/B测试影响
完全重复 所有字段完全相同 系统批量任务重复写入 样本量虚增,标准误低估
关键字段重复 客户ID、时间等关键字段重复 同一催收员重复提交同一客户记录 破坏独立性假设,产生伪重复
业务含义重复 不同系统但代表同一事件 催收系统和IVR系统同时记录同一通通话 特征冗余,多重共线性

4.3.2 初步清洗方法

  1. 完全重复删除: 使用 df.drop_duplicates() 直接移除。
  2. 关键字段去重: 基于客户ID + 时间戳 + 催收员ID 等组合键去重,保留最新或最完整的一条记录。
  3. 业务去重规则: 例如同一客户同一天内多次催收通话,仅保留首次或最后一次(取决于分析目标)。
  4. 去重前后对比: 记录去重前后的样本量变化,评估重复值对实验分组的影响。
# Python示例:重复值处理
# 1. 完全重复删除
df_dedup = df.drop_duplicates()
print(f"完全重复删除: {len(df) - len(df_dedup)} 条")

# 2. 基于关键字段去重(保留最新记录)
df_dedup_key = df.sort_values('timestamp').drop_duplicates(
    subset=['customer_id', 'call_date', 'collector_id'], 
    keep='last'
)
print(f"关键字段去重: {len(df) - len(df_dedup_key)} 条")

# 3. 检查去重对分组的影响
from collections import Counter
before = Counter(df['group'])
after = Counter(df_dedup_key['group'])
print("去重前分组分布:", before)
print("去重后分组分布:", after)

4.4 综合清洗流程与A/B测试适配

数据清洗不是一次性操作,而是与A/B测试设计紧密耦合的迭代过程。以下流程建议在特征筛选前执行:

数据清洗与A/B测试适配流程 原始特征数据 缺失值处理 异常值处理 重复值处理 分组一致性校验 清洗后特征 进入A/B测试

4.4.1 分组一致性校验要点

  • 缺失模式对比: 对每个特征,检验实验组和对照组的缺失率是否有显著差异(使用双比例Z检验)。
  • 异常值分布对比: 检查异常值在两组中的占比是否均衡。
  • 重复值分布对比: 确认去重操作对两组样本量的削减比例一致。
  • 清洗前后效应量稳定性: 计算清洗前后关键指标(如平均回收率)的组间差异,若变化超过10%,需重新审视清洗策略。
💡 最佳实践: 在催收A/B测试中,建议保留清洗前后的两套数据集。主分析使用清洗后数据,敏感性分析使用清洗前数据。若结论一致,则结果稳健;若不一致,需深入排查数据质量问题。

4.5 本章小结

数据问题 对A/B测试的核心影响 初步清洗方法 催收场景特别注意事项
缺失值 样本量缩减、分组偏差、特征重要性误判 中位数/众数填充、缺失指示变量、剔除高缺失率特征 逾期高风险客户缺失更严重,避免直接删除
异常值 均值偏移、方差膨胀、效应量失真 截尾处理、业务规则标记、敏感性分析 区分“真异常”(风险信号)与“假异常”(录入错误)
重复值 样本量虚增、标准误低估、假阳性上升 完全去重、关键字段去重、业务规则去重 同一客户多次催收记录需按业务逻辑保留