4. 数据清洗基础:缺失值与重复值处理

在构建分群评分沙盘样本时,原始数据几乎总是包含不完整或冗余的记录。缺失值会破坏统计计算的连续性,重复值则会导致样本偏差和过拟合风险。本章系统讲解 Pandas 中缺失值与重复值的检测与处理方法,为后续样本构建打下干净的数据基础。

4.1 缺失值检测:isnull 与 notnull

缺失值在 Pandas 中通常表示为 NaN(Not a Number)。isnull() 返回布尔型 DataFrame,标记每个元素是否为缺失值;notnull() 是其反操作。

方法 返回值 典型用途
df.isnull() 布尔 DataFrame 定位缺失值位置
df.notnull() 布尔 DataFrame 定位非缺失值位置
df.isnull().sum() 每列缺失计数 快速评估数据完整性
df.isnull().any(axis=1) 每行布尔值 筛选包含缺失值的行
关键指标: 在分群评分场景中,若某特征的缺失率超过 40%,通常建议直接删除该特征,而非填充。
import pandas as pd
import numpy as np

# 模拟含缺失值的评分数据
data = {
    '用户ID': ['U001', 'U002', 'U003', 'U004'],
    '年龄': [25, np.nan, 30, 22],
    '消费分': [85, 92, np.nan, 78],
    '活跃分': [90, 88, 95, np.nan]
}
df = pd.DataFrame(data)

# 检测缺失值
print(df.isnull())
print("\n每列缺失数量:")
print(df.isnull().sum())

4.2 缺失值删除:dropna

dropna() 用于删除包含缺失值的行或列。在沙盘样本构建中,删除操作需谨慎,避免损失关键样本。

参数 说明 示例
axis=0 删除行(默认) df.dropna()
axis=1 删除列 df.dropna(axis=1)
how='any' 行/列中任意缺失即删除 df.dropna(how='any')
how='all' 行/列全部缺失才删除 df.dropna(how='all')
thresh=n 保留至少有 n 个非空值的行/列 df.dropna(thresh=3)
subset 只在指定列中检查缺失 df.dropna(subset=['年龄'])
# 删除包含任何缺失值的行
df_clean = df.dropna()
print("删除后行数:", len(df_clean))

# 只保留至少有3个非空值的行
df_thresh = df.dropna(thresh=3)
print("thresh=3 保留行数:", len(df_thresh))

# 仅在'年龄'和'消费分'列检查缺失
df_subset = df.dropna(subset=['年龄', '消费分'])
print("subset 保留行数:", len(df_subset))

4.3 缺失值填充:fillna

fillna() 用指定值或统计量替换缺失值。在分群评分场景中,填充策略直接影响聚类结果,需根据业务含义选择。

填充策略选择建议:
• 连续型特征(如消费分):优先使用中位数填充,对异常值不敏感。
• 离散型特征(如等级):使用众数填充。
• 时间序列特征:使用前向填充(ffill)或后向填充(bfill)。
• 评分类特征:若缺失比例低(<5%),可用均值填充。
# 用固定值填充
df_filled = df.fillna(0)

# 用列均值填充
df_mean = df.copy()
df_mean['年龄'] = df_mean['年龄'].fillna(df_mean['年龄'].mean())
df_mean['消费分'] = df_mean['消费分'].fillna(df_mean['消费分'].mean())

# 用列中位数填充
df_median = df.copy()
df_median['年龄'] = df_median['年龄'].fillna(df_median['年龄'].median())

# 前向填充(用上一行值填充)
df_ffill = df.fillna(method='ffill')

print("均值填充结果:")
print(df_mean)
缺失值处理决策流程 原始数据含缺失值 缺失比例 < 5%? 是 均值/中位数填充 否 删除该特征或行 干净数据集

4.4 重复值检测:duplicated

duplicated() 返回布尔 Series,标记重复行(除第一次出现外)。在沙盘样本中,重复记录可能来自数据采集异常或多次录入。

参数 说明 示例
subset 指定列判断重复 df.duplicated(subset=['用户ID'])
keep='first' 标记除第一次外的重复(默认) df.duplicated(keep='first')
keep='last' 标记除最后一次外的重复 df.duplicated(keep='last')
keep=False 标记所有重复行 df.duplicated(keep=False)
# 模拟含重复值的评分数据
data_dup = {
    '用户ID': ['U001', 'U002', 'U002', 'U003', 'U003', 'U004'],
    '消费分': [85, 92, 92, 78, 78, 88],
    '活跃分': [90, 88, 88, 95, 95, 82]
}
df_dup = pd.DataFrame(data_dup)

# 检测完全重复行
print("完全重复行标记:")
print(df_dup.duplicated())

# 基于用户ID检测重复
print("\n基于用户ID重复:")
print(df_dup.duplicated(subset=['用户ID']))

# 查看所有重复行
print("\n所有重复行:")
print(df_dup[df_dup.duplicated(keep=False)])

4.5 重复值删除:drop_duplicates

drop_duplicates() 删除重复行,返回去重后的 DataFrame。参数与 duplicated() 对应。

注意: 在分群评分沙盘中,若重复记录的业务含义是“同一用户多次评分”,应保留最新评分(按时间戳排序后使用 keep='last'),而非简单删除。
# 删除完全重复行
df_unique = df_dup.drop_duplicates()
print("完全去重后行数:", len(df_unique))

# 基于用户ID去重,保留第一次出现
df_unique_first = df_dup.drop_duplicates(subset=['用户ID'], keep='first')
print("基于用户ID去重(保留首次):", len(df_unique_first))

# 基于用户ID去重,保留最后一次出现
df_unique_last = df_dup.drop_duplicates(subset=['用户ID'], keep='last')
print("基于用户ID去重(保留末次):", len(df_unique_last))

# 原地修改
df_dup.drop_duplicates(inplace=True)

4.6 综合实战:沙盘样本清洗流水线

在真实的分群评分项目中,缺失值和重复值处理通常组合成一条清洗流水线。以下是一个完整的示例流程:

def clean_sandbox_data(df):
    """
    沙盘样本清洗函数
    步骤:
    1. 删除全部为空的列
    2. 删除用户ID重复的记录(保留最新)
    3. 对数值型特征用中位数填充缺失值
    4. 对类别型特征用众数填充缺失值
    """
    df_clean = df.copy()

    # 步骤1:删除全空列
    df_clean = df_clean.dropna(axis=1, how='all')

    # 步骤2:基于用户ID去重,保留最后一条
    if '用户ID' in df_clean.columns:
        df_clean = df_clean.drop_duplicates(subset=['用户ID'], keep='last')

    # 步骤3:数值型特征中位数填充
    num_cols = df_clean.select_dtypes(include=[np.number]).columns
    for col in num_cols:
        df_clean[col] = df_clean[col].fillna(df_clean[col].median())

    # 步骤4:类别型特征众数填充
    cat_cols = df_clean.select_dtypes(include=['object']).columns
    for col in cat_cols:
        if col != '用户ID':
            mode_val = df_clean[col].mode()
            if not mode_val.empty:
                df_clean[col] = df_clean[col].fillna(mode_val[0])

    return df_clean

# 应用清洗函数
df_raw = pd.DataFrame({
    '用户ID': ['U001', 'U002', 'U002', 'U003', None],
    '年龄': [25, np.nan, 30, 22, 28],
    '消费分': [85, 92, 92, np.nan, 80],
    '等级': ['A', 'B', 'B', None, 'A']
})
df_cleaned = clean_sandbox_data(df_raw)
print("清洗后数据:")
print(df_cleaned)
沙盘样本清洗流水线架构 原始数据 删除全空列 dropna(axis=1) 用户ID去重 drop_duplicates 数值填充 中位数填充 类别填充 众数填充 干净样本 流水线设计原则: 先删除后填充 → 先全局后局部 → 数值与类别分开处理 → 保留业务主键完整性

4.7 本章小结

  • 缺失值检测: isnull() / notnull() 定位缺失,sum() 统计缺失量。
  • 缺失值删除: dropna() 支持行/列删除,thresh 参数控制非空阈值。
  • 缺失值填充: fillna() 支持固定值、统计量、前向/后向填充。
  • 重复值检测: duplicated() 标记重复,subset 指定判断列。
  • 重复值删除: drop_duplicates() 去重,keep 参数控制保留策略。
  • 实战要点: 构建清洗流水线时,先处理结构性问题(全空列),再处理业务逻辑(去重),最后处理数值填充。