什么是目标编码?
目标编码(Target Encoding),又称均值编码、似然编码,是一种将分类变量转换为数值特征的技术,通过利用目标变量的统计信息来捕捉类别与目标之间的关系。
核心思想
目标编码用目标变量的统计量(如均值、中位数、出现概率等)替代每个分类变量的类别。例如,在二分类任务中,可将每个类别替换为该类别下正例的比例;在回归任务中,则可能使用目标变量的均值。
具体步骤
- 统计量计算:对每个类别$x_i$,计算目标变量Y 的统计量(如均值 $E(Y|X=x_i)$)。
- 处理未见类别:测试集中未出现的类别,用全局统计量(如全体训练数据的Y 均值)替代。
- 防止数据泄露:在训练时仅使用训练集数据计算统计量,验证/测试集使用训练集统计量,避免信息泄露。
- 平滑处理(可选):引入平滑因子平衡小类别的噪声,公式为:$\text{编码值} = \frac{n \cdot \text{类别均值} + m \cdot \text{全局均值}}{n + m}$,其中n 为类别样本数,m 为平滑参数(超参数)。
优缺点分析
优点:
- 高效降维:避免独热编码的高维问题,适合高基数(类别多)特征。
- 保留语义信息:编码后的数值反映类别与目标的关系,提升模型表现。
缺点:
- 过拟合风险:尤其当类别样本较少时,需通过平滑或交叉验证缓解。
- 信息泄露:需严格分割训练与测试数据,防止目标信息泄露。
应用场景
- 高基数特征:如用户ID、地区等,独热编码不适用时。
- 非树模型:线性模型、神经网络等无法直接处理类别特征,目标编码提供有效转换。
实践建议
- 平滑技术:对低频类别,结合全局统计量提升鲁棒性。
- 交叉验证:在训练过程中分折计算编码,避免泄露。
- 库实现:Python的category_encoders库提供TargetEncoder,支持平滑和未知类别处理。
变体与扩展
- 多分类目标:对每个目标类别生成单独编码特征,或使用多维统计量(如期望值)。
- 时间序列数据:需按时间顺序计算滚动统计量,避免使用未来信息。
注意事项
- 模型选择:树模型(如随机森林)可能对目标编码不敏感,优先考虑其他编码方式。
- 评估验证:通过交叉验证检查编码效果,避免过拟合。
category_encoders库简介
category_encoders 是一个强大的 Python 库,专门用于将分类变量(Categorical Features)转换为数值特征,支持多种编码方法,适用于机器学习和数据分析任务。
概述
- 功能:提供超过 15 种分类变量编码方法,涵盖从经典的独热编码到复杂的目标编码。
- 兼容性:与scikit-learn 无缝集成,支持管道(Pipeline)和交叉验证。
- 优势:
- 支持高基数(High-Cardinality)分类变量(如用户ID、地址等)。
- 处理缺失值和未知类别。
- 提供灵活的编码策略,适应不同模型需求(如线性模型、树模型)。
支持的编码方法
以下是常用的编码方法及适用场景:
独热编码(One-Hot Encoding)
方法:为每个类别生成一个二值(0/1)特征。
适用场景:类别数量少(低基数)的特征。
代码示例:
from category_encoders import OneHotEncoder encoder = OneHotEncoder(cols=['city']) X_encoded = encoder.fit_transform(X)
目标编码(Target Encoding)
方法:用目标变量的均值替代类别(支持平滑处理)。
适用场景:高基数特征,适用于非树模型(如线性回归、神经网络)。
参数:
- smoothing:平滑因子,平衡类别均值和全局均值。
- min_samples_leaf:控制小类别的平滑强度。
from category_encoders import TargetEncoder encoder = TargetEncoder(cols=['city'], smoothing=10) X_encoded = encoder.fit_transform(X, y)
计数编码(Count Encoding)
方法:用类别在训练集中的出现频次替代类别。
适用场景:捕捉类别的常见程度。
from category_encoders import CountEncoder encoder = CountEncoder(cols=['city']) X_encoded = encoder.fit_transform(X)
序数编码(Ordinal Encoding)
方法:为类别分配有序整数(如 “小/中/大” → 1/2/3)。
适用场景:有序分类变量(如评级、等级)。
from category_encoders import OrdinalEncoder encoder = OrdinalEncoder(cols=['size'], mapping=[{'col': 'size', 'mapping': {'小':1, '中':2, '大':3}}]) X_encoded = encoder.fit_transform(X)
CatBoost 编码
方法:基于目标变量的排序编码,避免目标泄露。
适用场景:高基数特征,适用于树模型(如XGBoost、LightGBM)。
from category_encoders import CatBoostEncoder encoder = CatBoostEncoder(cols=['city']) X_encoded = encoder.fit_transform(X, y)
留一法编码(LeaveOneOut Encoding)
方法:在计算类别均值时排除当前样本,防止过拟合。
适用场景:小数据集或高基数特征。
from category_encoders import LeaveOneOutEncoder encoder = LeaveOneOutEncoder(cols=['city']) X_encoded = encoder.fit_transform(X, y)
其他编码方法
- WOE编码(Weight of Evidence):用于二分类任务,衡量类别与目标的相关性。
- James-Stein编码:基于统计收缩的编码,适用于高基数特征。
- GLMM编码:基于广义线性混合模型的编码。
高级功能
处理未知类别
- 库会自动将未见过的测试集类别替换为训练集的统计量(如全局均值)。
- 可通过handle_unknown=’value’ 或 handle_missing=’value’ 参数自定义。
管道集成
与 scikit-learn 的 Pipeline 结合:
from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression pipeline = Pipeline([ ('encoder', TargetEncoder(cols=['city'])), ('model', LogisticRegression()) ]) pipeline.fit(X_train, y_train)
多列同时编码
通过 cols 参数指定需要编码的列:
encoder = TargetEncoder(cols=['city', 'gender'])
编码方法选择指南
编码方法 | 适用场景 | 优点 | 缺点 |
One-Hot | 低基数特征,线性模型 | 无信息损失 | 高维稀疏,不适合高基数 |
Target | 高基数特征,非树模型 | 保留类别与目标关系 | 需防止过拟合和数据泄露 |
CatBoost | 高基数特征,树模型 | 避免目标泄露 | 计算成本较高 |
Ordinal | 有序分类变量 | 保留顺序信息 | 不适用于无序类别 |
Count | 捕捉类别频率信息 | 简单快速 | 可能引入噪声 |
注意事项
- 过拟合风险:目标编码、CatBoost编码等方法需通过交叉验证或平滑处理避免过拟合。
- 数据泄露:确保在训练集上拟合编码器,测试集仅调用transform。
- 类别顺序:序数编码需手动定义映射关系,避免错误排序。
手撕版目标编码
代码内容
import pandas as pd import numpy as np from sklearn.base import BaseEstimator, TransformerMixin class TargetEncoder(BaseEstimator, TransformerMixin): """ 目标编码器(Target Encoding) 该编码器用于将类别变量转换为数值变量,计算每个类别对应的目标变量的加权均值,并进行平滑处理,以减少过拟合。 参数: - smooth: 平滑参数,用于调整类别均值与全局均值之间的平衡,默认值为 0.5。 属性: - encodings_: 存储类别变量的目标编码映射。 - global_mean_: 存储目标变量的全局均值(所有类别整体的均值)。 """ def __init__(self, smooth=0.5): self.smooth = smooth # 平滑参数,防止小样本类别的编码值过拟合 self.encodings_ = None # 存储类别到目标均值的映射 self.global_mean_ = None # 存储目标变量的全局均值 def fit(self, X, y, sample_weight=None): """ 训练目标编码器,计算每个类别的加权均值,并存储编码映射。 参数: - X: 类别变量(1D 数组或 Pandas Series)。 - y: 目标变量(1D 数组)。 - sample_weight: 样本权重(可选),用于加权计算类别均值。 返回: - self(返回自身,以便支持 scikit-learn 兼容接口)。 """ X = X.ravel() # 将输入转换为一维数组 df = pd.DataFrame({'category': X, 'target': y}) # 创建 DataFrame 以便处理类别数据 # 如果未提供样本权重,则默认为 1 if sample_weight is None: sample_weight = np.ones(len(X)) df['weight'] = sample_weight # 将样本权重添加到 DataFrame # 计算全局加权均值(用于平滑处理和未知类别填充) total_weight = df['weight'].sum() if total_weight <= 1e-7: # 避免除零错误 self.global_mean_ = 0.0 else: self.global_mean_ = (df['target'] * df['weight']).sum() / total_weight # 计算每个类别的加权目标均值 grouped = df.groupby('category').agg( sum_product=pd.NamedAgg(column='target', aggfunc=lambda x: (x * df.loc[x.index, 'weight']).sum()), sum_weight=pd.NamedAgg(column='weight', aggfunc='sum'), count=pd.NamedAgg(column='target', aggfunc='size') ) # 计算平滑后的目标编码值 numerator = grouped['sum_product'] + self.smooth * self.global_mean_ # 平滑处理 denominator = grouped['sum_weight'] + self.smooth self.encodings_ = numerator / denominator # 计算最终的目标编码值 return self # 返回自身,符合 scikit-learn 规范 def transform(self, X): """ 使用训练好的目标编码器将类别变量转换为数值变量。 参数: - X: 类别变量(1D 数组或 Pandas Series)。 返回: - 转换后的目标编码值(NumPy 数组,形状为 (n_samples, 1))。 """ categroy = pd.Series(X.ravel()) # 转换为 Pandas Series 以便映射 encoded = categroy.map(self.encodings_).fillna(self.global_mean_) # 映射类别到编码值,缺失类别用全局均值填充 return encoded.values.reshape(-1, 1) # 保持 scikit-learn 兼容性,返回二维数组
代码解读
与category_encoders.TargetEncoder 的对比:
特性 | category_encoders.TargetEncoder | 手写 TargetEncoder |
直接支持 Pandas DataFrame | ✅ | ❌(手写版要求 X 是 NumPy 数组) |
平滑处理 | ✅ (smoothing 参数) | ✅ (smooth 参数) |
兼容 Scikit-learn Pipeline | ✅ | ✅ |
处理未知类别 | ✅(可以指定 handle_unknown) | ✅(用全局均值填充) |
处理数据权重 | ❌(默认不支持) | ✅(手写版支持 sample_weight) |
- 如果你的数据没有权重要求,直接使用TargetEncoder 更方便。
- 如果需要加权目标均值(考虑样本权重),则仍需使用自定义 TargetEncoder。
- 两者都支持平滑处理,防止小样本类别的过拟合。
平滑系数计算逻辑:
numerator = grouped['sum_product'] + self.smooth * self.global_mean_ denominator = grouped['sum_weight'] + self.smooth self.encodings_ = numerator / denominator
- 分子: 类别的加权目标值 + 平滑因子 × 全局均值
- 分母: 类别的总权重 + 平滑因子
- 平滑编码公式: $\text{编码值} = \frac{\sum(y \times w) + \text{smooth} \times \text{全局均值}}{\sum(w) + \text{smooth}}$
- 作用:
- 若类别样本多,则编码值接近该类别的加权均值。
- 若类别样本少,则编码值趋向全局均值。
类别特征的分组
代码内容
def category_feature_binning(df, category_col='category_feature', target_col='target_feature', max_bins=10, min_group_ratio=0.01): """ 对类别变量进行分箱(Binning),使用目标编码(Target Encoding)和决策树(Decision Tree)进行分类。 该函数动态调整 min_samples_frac,确保在不同数据量下合理设置最小样本数,避免过度分裂或合并。 参数: df : pandas.DataFrame 输入数据集,包含类别特征和目标变量。 category_col : str 需要进行分箱的类别特征列名。 target_col : str 目标变量列名。 max_bins : int, optional 最大分箱数量,默认值为10。 返回: list 经过分箱后的类别列表,每个列表中的元素属于同一个分箱。 """ # 取出类别特征和目标变量 category_data = df[category_col] target = df[target_col].astype('category').cat.codes total_samples = len(df) # **动态调整 min_samples_frac** if total_samples < 10000: min_samples_frac = 0.001 # 小数据集 elif total_samples < 100000: min_samples_frac = 0.0005 # 中等数据集 else: min_samples_frac = 0.0001 # 大数据集 # 计算类别出现次数(用于后续排序) category_counts = category_data.value_counts().to_dict() # 计算目标变量的类别分布,防止样本不均衡 class_counts = df[target_col].value_counts() class_weights = {k: total_samples / (len(class_counts) * v) for k, v in class_counts.items()} # 目标变量的权重映射 sample_weights = df[target_col].map(class_weights).fillna(1).values # 存储分组 groups = [] stack = [(category_data, target, sample_weights)] current_bins = 0 # 当前分箱数量 while stack: if current_bins >= max_bins: break # 达到最大分箱数,停止 curr_category, curr_target, curr_weights = stack.pop() unique_category = curr_category.unique() # 如果类别数量小于 2,则直接归入一组 if len(unique_category) < 2: groups.append(list(set(unique_category))) continue # 目标编码 try: encoder = TargetEncoder() encoded = encoder.fit_transform( curr_category.to_numpy().reshape(-1, 1), curr_target.to_numpy(), sample_weight=curr_weights ) except: groups.append(list(set(unique_category))) continue # 目标编码失败,直接加入分组 # 训练决策树 try: tree = DecisionTreeClassifier( max_depth=1, # 仅允许一次分裂 min_impurity_decrease=0.001, min_weight_fraction_leaf=max(min_samples_frac * 0.1, 0.001) # 动态调整最小权重 ) tree.fit(encoded, curr_target, sample_weight=curr_weights) except: groups.append(list(set(unique_category))) continue # 决策树训练失败,直接加入分组 # 确保分裂有效 if tree.tree_.node_count <= 1: groups.append(list(set(unique_category))) continue # 分裂无效,直接加入分组 # 获取分裂阈值 threshold = tree.tree_.threshold[0] left_mask = np.squeeze(encoded) <= threshold # 计算左右分组的样本总权重 min_weight = total_samples * min_samples_frac left_weight = curr_weights[left_mask].sum() right_weight = curr_weights[~left_mask].sum() # 确保左右分支样本数足够,否则不分裂 if left_weight < min_weight or right_weight < min_weight: groups.append(list(set(unique_category))) continue current_bins += 1 # 计数分箱数 # 递归处理左右分支 stack.append((curr_category[~left_mask], curr_target[~left_mask], curr_weights[~left_mask])) stack.append((curr_category[left_mask], curr_target[left_mask], curr_weights[left_mask])) # 处理未分箱类别 while stack: remaining_cities, _, _ = stack.pop() groups.append(list(set(remaining_cities.unique()))) # **合并小类别** merged_groups = [] for group in groups: if not merged_groups: merged_groups.append(group) continue # 计算类别的占比 group_ratio = category_data.isin(group).mean() if group_ratio < min_group_ratio: merged_groups[-1].extend(group) # 直接合并 merged_groups[-1] = list(set(merged_groups[-1])) # 去重 else: merged_groups.append(group) # **控制最终分箱数量** if len(merged_groups) > max_bins: # 按类别出现频率排序 sorted_groups = sorted( merged_groups, key=lambda g: -sum(category_counts.get(c, 0) for c in g), reverse=True ) # 只保留前 max_bins-1 组 merged_groups = sorted_groups[:max_bins - 1] remaining = [c for group in sorted_groups[max_bins - 1:] for c in group] # 其他类别归为一组 if remaining: merged_groups.append(remaining) # **最终排序** final_groups = [] for group in merged_groups: sorted_group = sorted( list(set(group)), key=lambda x: (-category_counts.get(x, 0), x) # 先按频率降序,再按字母序 ) final_groups.append(sorted_group) return final_groups[:max_bins] # 限制最大分箱数
代码解读
这段代码实现了对类别特征进行分箱的功能,结合目标编码(Target Encoding)和决策树(Decision Tree)的方法,动态调整分箱策略以确保合理性。以下是对代码的详细解读和关键点总结:
核心逻辑
- 动态参数调整。根据数据量调整最小样本比例 min_samples_frac,避免小数据集过拟合或大数据集欠拟合):
- 小数据集(<10k样本):min_samples_frac = 0.001
- 中数据集(10k-100k):min_samples_frac = 0.0005
- 大数据集(>100k):min_samples_frac = 0.0001
- 目标编码与决策树分箱
- 目标编码:将类别特征转换为目标变量的统计量(如均值),生成连续值用于分裂。
- 单层决策树:使用max_depth=1 的决策树寻找最佳分裂点,确保每次分裂有意义(min_impurity_decrease=0.001)。
- 递归分裂与终止条件
- 利用栈(Stack)递归处理每个子集,分裂左右分支直至达到max_bins 或无法有效分裂。
- 检查分裂后的样本权重是否足够(min_weight = total_samples * min_samples_frac),否则放弃分裂。
- 后处理与合并
- 合并小分箱:全局占比小于1 %的组合并到前一个分箱。
- 控制分箱数:按类别频率排序,保留高频分箱,剩余类别归为“其他”。