3. 精算数据特征工程:保单数据清洗、缺失值处理、类别特征编码(WOE/One-Hot)、时间窗口特征构造

在精算建模中,原始保单数据通常包含大量噪声、缺失值、非结构化类别字段以及时间维度信息。本章将系统讲解如何将原始保单数据转化为适合XGBoost模型训练的高质量特征矩阵,涵盖数据清洗、缺失值处理、类别特征编码与时间窗口特征构造四大核心环节。

精算数据特征工程核心原则:特征工程的质量直接决定模型效果上限。在精算领域,特征不仅要具备统计预测能力,还需满足业务可解释性与监管合规要求。

3.1 保单数据清洗

数据清洗是特征工程的第一步,主要处理以下问题:

  • 重复保单去重:同一保单号出现多条记录时,保留最新时间戳或累计保额最高的记录。
  • 异常值截断:对保额、保费、年龄等连续变量,采用分位数截断(如1%和99%分位数)或业务规则截断(如年龄0-120岁)。
  • 逻辑矛盾修正:如投保年龄大于出险年龄、生效日期晚于终止日期等,需根据业务逻辑修正或标记为缺失。
  • 格式统一:日期格式统一为YYYY-MM-DD,金额字段统一为浮点数,分类字段去除前后空格与特殊字符。
清洗类型 常见问题 处理方法
重复数据 同一保单号多条记录 按时间戳去重,保留最新记录
异常值 保额负值、年龄>120 分位数截断或业务规则截断
逻辑矛盾 生效日期>终止日期 修正或标记为缺失
格式问题 日期格式不统一 统一转换为标准格式
import pandas as pd
import numpy as np

# 示例:保单数据清洗
def clean_policy_data(df):
    # 去重:按保单号去重,保留最新记录
    df = df.sort_values('policy_date').drop_duplicates(subset='policy_id', keep='last')
    
    # 异常值截断:保额截断在1%-99%分位数
    lower = df['sum_insured'].quantile(0.01)
    upper = df['sum_insured'].quantile(0.99)
    df['sum_insured'] = df['sum_insured'].clip(lower, upper)
    
    # 逻辑矛盾修正:生效日期不能晚于终止日期
    mask = df['effective_date'] > df['expiry_date']
    df.loc[mask, 'effective_date'] = df.loc[mask, 'expiry_date']
    
    return df

3.2 缺失值处理

精算数据中缺失值普遍存在,处理方法需结合缺失机制与业务含义:

  • 完全随机缺失(MCAR):可直接删除或使用均值/中位数填充。
  • 随机缺失(MAR):推荐使用模型预测填充(如KNN、回归)或多重插补。
  • 非随机缺失(MNAR):需引入业务规则或单独标记缺失指示变量。
精算实践建议:对于缺失率超过50%的变量,建议直接删除;对于缺失率在5%-50%之间的变量,优先使用中位数填充(连续变量)或众数填充(分类变量),并额外生成缺失指示变量。
# 缺失值处理示例
def handle_missing_values(df):
    # 连续变量:中位数填充 + 缺失指示变量
    for col in ['sum_insured', 'premium', 'age']:
        if col in df.columns:
            df[f'{col}_missing'] = df[col].isna().astype(int)
            df[col] = df[col].fillna(df[col].median())
    
    # 分类变量:众数填充 + 缺失指示变量
    for col in ['occupation', 'region']:
        if col in df.columns:
            df[f'{col}_missing'] = df[col].isna().astype(int)
            df[col] = df[col].fillna(df[col].mode()[0])
    
    return df

3.3 类别特征编码

精算模型中类别特征编码需兼顾预测能力与业务可解释性。本节重点介绍WOE编码与One-Hot编码。

3.3.1 WOE编码(Weight of Evidence)

WOE编码将类别变量转换为与目标变量(如是否出险)的关联强度,公式如下:

WOE_i = ln( (Good_i / Good_total) / (Bad_i / Bad_total) )

其中Good_i表示第i类中未出险样本数,Bad_i表示出险样本数。WOE值越大,表示该类别出险概率越低。

类别 样本数 出险数 未出险数 WOE值
A类 1000 50 950 0.85
B类 800 80 720 -0.12
C类 600 90 510 -0.56
# WOE编码示例
def woe_encoding(df, feature, target):
    # 计算每个类别的WOE值
    total_good = df[target].sum()
    total_bad = len(df) - total_good
    
    woe_dict = {}
    for cat in df[feature].unique():
        subset = df[df[feature] == cat]
        good = subset[target].sum()
        bad = len(subset) - good
        woe = np.log((good / total_good) / (bad / total_bad))
        woe_dict[cat] = woe
    
    df[f'{feature}_woe'] = df[feature].map(woe_dict)
    return df, woe_dict

3.3.2 One-Hot编码

One-Hot编码适用于类别数量较少(通常<10)且无序的分类变量。每个类别生成一个二进制指示变量。

# One-Hot编码示例
def one_hot_encoding(df, feature):
    # 使用pandas的get_dummies
    dummies = pd.get_dummies(df[feature], prefix=feature, drop_first=True)
    df = pd.concat([df, dummies], axis=1)
    df.drop(feature, axis=1, inplace=True)
    return df
编码选择建议:对于高基数类别(如地区、职业),优先使用WOE编码;对于低基数类别(如性别、婚姻状况),使用One-Hot编码。WOE编码天然支持缺失值处理(WOE=0),且与逻辑回归等线性模型兼容性更好。
类别特征编码策略对比 WOE编码 适用:高基数类别(地区、职业) 优点:保留排序信息,与目标关联 输出:单列连续值(WOE分数) 缺失处理:WOE=0(中性值) One-Hot编码 适用:低基数类别(性别、婚姻) 优点:无偏估计,模型可解释 输出:多列二进制指示变量 缺失处理:单独生成缺失列

3.4 时间窗口特征构造

精算模型需要捕捉保单在时间维度上的动态变化。时间窗口特征通过聚合历史数据,构造反映风险趋势的统计量。

3.4.1 滑动窗口统计量

对每个保单,基于其历史时间段(如过去3个月、6个月、12个月)计算统计特征:

  • 计数特征:历史出险次数、历史保单数量
  • 金额特征:历史累计赔付金额、历史平均保费
  • 比率特征:历史赔付率、历史出险频率
  • 趋势特征:最近3个月出险次数 vs 前3个月出险次数的变化率
# 时间窗口特征构造示例
def create_time_window_features(df, policy_id_col, date_col, target_col, windows=[90, 180, 365]):
    df = df.sort_values([policy_id_col, date_col])
    
    for window in windows:
        # 滚动窗口统计
        df[f'claim_count_{window}d'] = df.groupby(policy_id_col)[target_col].transform(
            lambda x: x.rolling(window=window, min_periods=1).sum()
        )
        df[f'claim_amount_{window}d'] = df.groupby(policy_id_col)['claim_amount'].transform(
            lambda x: x.rolling(window=window, min_periods=1).sum()
        )
        # 窗口内平均赔付金额
        df[f'avg_claim_{window}d'] = df[f'claim_amount_{window}d'] / df[f'claim_count_{window}d'].replace(0, np.nan)
    
    return df

3.4.2 时间衰减特征

近期事件对当前风险的影响通常大于远期事件。使用指数衰减权重对历史事件加权:

# 时间衰减特征示例
def time_decay_features(df, policy_id_col, date_col, target_col, decay_factor=0.9):
    df = df.sort_values([policy_id_col, date_col])
    
    # 计算时间间隔(天)
    df['days_since_last'] = df.groupby(policy_id_col)[date_col].diff().dt.days
    
    # 指数衰减权重
    df['decay_weight'] = decay_factor ** (df['days_since_last'].fillna(0) / 30)
    
    # 加权历史出险次数
    df['weighted_claim_count'] = df.groupby(policy_id_col)[target_col].transform(
        lambda x: x.shift(1).fillna(0) * df['decay_weight']
    ).cumsum()
    
    return df
时间窗口特征工程要点:
  • 窗口大小选择需结合业务周期(如车险通常使用12个月窗口)
  • 避免未来信息泄露:构造特征时只能使用当前时间点之前的数据
  • 对于新保单(无历史数据),使用全局均值或中位数填充
  • 时间衰减因子需通过交叉验证调优,通常取值0.8-0.95
时间窗口特征构造流程 T-365 T-180 T-90 T-30 当前T 滑动窗口:统计T-365到T-30之间的历史数据 时间衰减:近期事件权重高,远期事件权重低

3.5 特征工程完整流程

将上述四个环节整合为完整的特征工程流水线:

# 完整特征工程流水线
def feature_engineering_pipeline(raw_df):
    # Step 1: 数据清洗
    df = clean_policy_data(raw_df)
    
    # Step 2: 缺失值处理
    df = handle_missing_values(df)
    
    # Step 3: 类别特征编码
    # WOE编码(高基数)
    df, woe_dict = woe_encoding(df, 'region', 'claim_flag')
    df, woe_dict = woe_encoding(df, 'occupation', 'claim_flag')
    # One-Hot编码(低基数)
    df = one_hot_encoding(df, 'gender')
    df = one_hot_encoding(df, 'marital_status')
    
    # Step 4: 时间窗口特征
    df = create_time_window_features(df, 'policy_id', 'policy_date', 'claim_flag')
    df = time_decay_features(df, 'policy_id', 'policy_date', 'claim_flag')
    
    # Step 5: 特征选择(可选)
    # 删除原始ID列和日期列
    df.drop(['policy_id', 'policy_date'], axis=1, inplace=True)
    
    return df
精算模型特征工程最佳实践:
  • 所有特征工程步骤必须在训练集上拟合,然后应用到验证集和测试集,避免数据泄露
  • WOE编码的映射字典需要保存,用于线上实时评分时的特征转换
  • 时间窗口特征构造时,需确保训练集和测试集的时间顺序一致
  • 建议使用Pipeline类封装特征工程流程,便于模型部署和复现

通过以上四个环节的系统处理,原始保单数据将被转换为包含业务含义、统计规律与时间动态的高质量特征矩阵,为后续XGBoost模型训练奠定坚实基础。