4. 数据质量初探:缺失值、异常值、重复值对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 初步清洗方法
- 缺失率评估: 计算每个特征的缺失率,设定阈值(如30%)。缺失率过高的特征建议直接剔除或标记后单独分析。
- 简单填充策略:
- 数值型特征:使用中位数填充(对异常值鲁棒)
- 分类型特征:使用众数填充,或新增“未知”类别
- 时间型特征:使用前向填充(如最近一次联系时间)
- 缺失指示变量: 创建二值变量标记是否缺失,保留缺失模式信息。
- 分组一致性检查: 确保实验组和对照组的缺失模式无显著差异(卡方检验)。
# 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 初步清洗策略
- 区分“真异常”与“假异常”: 催收场景中,高逾期天数本身就是风险信号,不应直接剔除。建议先标记,单独分析异常样本的业务含义。
- 截尾处理(Winsorization): 将极端值替换为指定百分位数的值(如1%和99%分位数),保留样本量同时抑制极端影响。
- 分组独立处理: 对实验组和对照组分别进行异常值检测,避免因分组差异导致误判。
- 敏感性分析: 分别在有/无异常值的数据集上运行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 初步清洗方法
- 完全重复删除: 使用
df.drop_duplicates()直接移除。 - 关键字段去重: 基于客户ID + 时间戳 + 催收员ID 等组合键去重,保留最新或最完整的一条记录。
- 业务去重规则: 例如同一客户同一天内多次催收通话,仅保留首次或最后一次(取决于分析目标)。
- 去重前后对比: 记录去重前后的样本量变化,评估重复值对实验分组的影响。
# 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测试设计紧密耦合的迭代过程。以下流程建议在特征筛选前执行:
4.4.1 分组一致性校验要点
- 缺失模式对比: 对每个特征,检验实验组和对照组的缺失率是否有显著差异(使用双比例Z检验)。
- 异常值分布对比: 检查异常值在两组中的占比是否均衡。
- 重复值分布对比: 确认去重操作对两组样本量的削减比例一致。
- 清洗前后效应量稳定性: 计算清洗前后关键指标(如平均回收率)的组间差异,若变化超过10%,需重新审视清洗策略。
💡 最佳实践: 在催收A/B测试中,建议保留清洗前后的两套数据集。主分析使用清洗后数据,敏感性分析使用清洗前数据。若结论一致,则结果稳健;若不一致,需深入排查数据质量问题。
4.5 本章小结
| 数据问题 | 对A/B测试的核心影响 | 初步清洗方法 | 催收场景特别注意事项 |
|---|---|---|---|
| 缺失值 | 样本量缩减、分组偏差、特征重要性误判 | 中位数/众数填充、缺失指示变量、剔除高缺失率特征 | 逾期高风险客户缺失更严重,避免直接删除 |
| 异常值 | 均值偏移、方差膨胀、效应量失真 | 截尾处理、业务规则标记、敏感性分析 | 区分“真异常”(风险信号)与“假异常”(录入错误) |
| 重复值 | 样本量虚增、标准误低估、假阳性上升 | 完全去重、关键字段去重、业务规则去重 | 同一客户多次催收记录需按业务逻辑保留 |