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),且与逻辑回归等线性模型兼容性更好。
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
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模型训练奠定坚实基础。