高基数类别特征预处理:平均数编码 | 京东云技术团队

一 前言

对于一个类别特征,如果这个特征的取值非常多,则称它为高基数(high-cardinality)类别特征。在深度学习场景中,对于类别特征我们一般采用Embedding的方式,通过预训练或直接训练的方式将类别特征值编码成向量。在经典机器学习场景中,对于有序类别特征,我们可以使用LabelEncoder进行编码处理,对于低基数无序类别特征(在lightgbm中,默认取值个数小于等于4的类别特征),可以采用OneHotEncoder的方式进行编码,但是对于高基数无序类别特征,若直接采用OneHotEncoder的方式编码,在目前效果比较好的GBDT、Xgboost、lightgbm等树模型中,会出现特征稀疏性的问题,造成维度灾难, 若先对类别取值进行聚类分组,然后再进行OneHot编码,虽然可以降低特征的维度,但是聚类分组过程需要借助较强的业务经验知识。本文介绍一种针对高基数无序类别特征非常有效的预处理方法:平均数编码(Mean Encoding)。在很多数据挖掘类竞赛中,有许多人使用这种方法取得了非常优异的成绩。

二 原理

平均数编码,有些地方也称之为目标编码(Target Encoding),是一种基于目标变量统计(Target Statistics)的有监督编码方式。该方法基于贝叶斯思想,用先验概率和后验概率的加权平均值作为类别特征值的编码值,适用于分类和回归场景。平均数编码的公式如下所示:

其中:

1. prior为先验概率,在分类场景中表示样本属于某一个_y__i_的概率

​其中_n__y__i_​​表示y =_y__i_​时的样本数量,_n__y_​表示y的总数量;在回归场景下,先验概率为目标变量均值:

2. posterior为后验概率,在分类场景中表示类别特征为k时样本属于某一个_y__i_​的概率

在回归场景下表示 类别特征为k时对应目标变量的均值。

3. _λ_为权重函数,本文中的权重函数公式相较于原论文做了变换,是一个单调递减函数,函数公式:

其中 输入是特征类别在训练集中出现的次数n,权重函数有两个参数:

① k:最小阈值,当n = k时,λ= 0.5,先验概率和后验概率的权重相同;当n < k时,λ> 0.5, 先验概率所占的权重更大。

② f:平滑因子,控制权重函数在拐点处的斜率,f越大,曲线坡度越缓。下面是k=1时,不同f对于权重函数的影响:

由图可知,f越大,权重函数S型曲线越缓,正则效应越强。

对于分类问题,在计算后验概率时,目标变量有C个类别,就有C个后验概率,且满足

一个 _y__i_​ 的概率值必然和其他 _y__i_​ 的概率值线性相关,因此为了避免多重共线性问题,采用平均数编码后数据集将增加C-1列特征。对于回归问题,采用平均数编码后数据集将增加1列特征。

三 实践

平均数编码不仅可以对单个类别特征编码,也可以对具有层次结构的类别特征进行编码。比如地区特征,国家包含了省,省包含了市,市包含了街区,对于街区特征,每个街区特征对应的样本数量很少,以至于每个街区特征的编码值接近于先验概率。平均数编码通过加入不同层次的先验概率信息解决该问题。下面将以分类问题对这两个场景进行展开:

1. 单个类别特征编码:

在具体实践时可以借助category_encoders包,代码如下:

1import pandas as pd 2from category_encoders import TargetEncoder 3 4df = pd.DataFrame({'cat': ['a', 'b', 'a', 'b', 'a', 'a', 'b', 'c', 'c', 'd'], 5 'target': [1, 0, 0, 1, 0, 0, 1, 1, 0, 1]}) 6te = TargetEncoder(cols=["cat"], min_samples_leaf=2, smoothing=1) 7df["cat_encode"] = te.transform(df)["cat"] 8print(df) 9# 结果如下: 10 11 cat target cat_encode 120 a 1 0.279801 131 b 0 0.621843 142 a 0 0.279801 153 b 1 0.621843 164 a 0 0.279801 175 a 0 0.279801 186 b 1 0.621843 197 c 1 0.500000 208 c 0 0.500000 219 d 1 0.634471 22

2. 层次结构类别特征编码:

对以下数据集,方位类别特征具有{'N': ('N', 'NE'), 'S': ('S', 'SE'), 'W': 'W'}层级关系,以compass中类别NE为例计算_y__i_​=1,k = 2 f = 2时编码值,计算公式如下:

其中_p_1为HIER_compass_1中类别N的编码值,计算可以参考单个类别特征编码: 0.74527,posterior=3/3=1,λ= 0.37754 ,则类别NE的编码值:0.37754 * 0.74527 + (1 - 0.37754)* 1 = 0.90383。

代码如下:

1from category_encoders import TargetEncoder 2from category_encoders.datasets import load_compass 3 4X, y = load_compass() 5# 层次参数hierarchy可以为字典或者dataframe 6# 字典形式 7hierarchical_map = {'compass': {'N': ('N', 'NE'), 'S': ('S', 'SE'), 'W': 'W'}} 8te = TargetEncoder(verbose=2, hierarchy=hierarchical_map, cols=['compass'], smoothing=2, min_samples_leaf=2) 9# dataframe形式,HIER_cols的层级顺序由顶向下 10HIER_cols = ['HIER_compass_1'] 11te = TargetEncoder(verbose=2, hierarchy=X[HIER_cols], cols=['compass'], smoothing=2, min_samples_leaf=2) 12te.fit(X.loc[:,['compass']], y) 13X["compass_encode"] = te.transform(X.loc[:,['compass']]) 14X["label"] = y 15print(X) 16 17# 结果如下,compass_encode列为结果列: 18 index compass HIER_compass_1 compass_encode label 190 1 N N 0.622636 1 201 2 N N 0.622636 0 212 3 NE N 0.903830 1 223 4 NE N 0.903830 1 234 5 NE N 0.903830 1 245 6 SE S 0.176600 0 256 7 SE S 0.176600 0 267 8 S S 0.460520 1 278 9 S S 0.460520 0 289 10 S S 0.460520 1 2910 11 S S 0.460520 0 3011 12 W W 0.403328 1 3112 13 W W 0.403328 0 3213 14 W W 0.403328 0 3314 15 W W 0.403328 0 3415 16 W W 0.403328 1 35

注意事项:

采用平均数编码,容易引起过拟合,可以采用以下方法防止过拟合:

  • 增大正则项f
  • k折交叉验证

以下为自行实现的基于k折交叉验证版本的平均数编码,可以应用于二分类、多分类、回归场景中对单一类别特征或具有层次结构类别特征进行编码,该版本中用prior对unknown类别和缺失值编码。

1from itertools import product 2from category_encoders import TargetEncoder 3from sklearn.model_selection import StratifiedKFold, KFold 4 5class MeanEncoder: 6 def __init__(self, categorical_features, n_splits=5, target_type='classification', 7 min_samples_leaf=2, smoothing=1, hierarchy=None, verbose=0, shuffle=False, 8 random_state=None): 9 """ 10 Parameters 11 ---------- 12 categorical_features: list of str 13 the name of the categorical columns to encode. 14 n_splits: int 15 the number of splits used in mean encoding. 16 target_type: str, 17 'regression' or 'classification'. 18 min_samples_leaf: int 19 For regularization the weighted average between category mean and global mean is taken. The weight is 20 an S-shaped curve between 0 and 1 with the number of samples for a category on the x-axis. 21 The curve reaches 0.5 at min_samples_leaf. (parameter k in the original paper) 22 smoothing: float 23 smoothing effect to balance categorical average vs prior. Higher value means stronger regularization. 24 The value must be strictly bigger than 0. Higher values mean a flatter S-curve (see min_samples_leaf). 25 hierarchy: dict or dataframe 26 A dictionary or a dataframe to define the hierarchy for mapping. 27 If a dictionary, this contains a dict of columns to map into hierarchies. Dictionary key(s) should be the column name from X 28 which requires mapping. For multiple hierarchical maps, this should be a dictionary of dictionaries. 29 30 If dataframe: a dataframe defining columns to be used for the hierarchies. Column names must take the form: 31 HIER_colA_1, ... HIER_colA_N, HIER_colB_1, ... HIER_colB_M, ... 32 where [colA, colB, ...] are given columns in cols list. 33 1:N and 1:M define the hierarchy for each column where 1 is the highest hierarchy (top of the tree). A single column or multiple 34 can be used, as relevant. 35 verbose: int 36 integer indicating verbosity of the output. 0 for none. 37 shuffle : bool, default=False 38 random_state : int or RandomState instance, default=None 39 When `shuffle` is True, `random_state` affects the ordering of the 40 indices, which controls the randomness of each fold for each class. 41 Otherwise, leave `random_state` as `None`. 42 Pass an int for reproducible output across multiple function calls. 43 """ 44 45 self.categorical_features = categorical_features 46 self.n_splits = n_splits 47 self.learned_stats = {} 48 self.min_samples_leaf = min_samples_leaf 49 self.smoothing = smoothing 50 self.hierarchy = hierarchy 51 self.verbose = verbose 52 self.shuffle = shuffle 53 self.random_state = random_state 54 55 if target_type == 'classification': 56 self.target_type = target_type 57 self.target_values = [] 58 else: 59 self.target_type = 'regression' 60 self.target_values = None 61 62 63 def mean_encode_subroutine(self, X_train, y_train, X_test, variable, target): 64 X_train = X_train[[variable]].copy() 65 X_test = X_test[[variable]].copy() 66 67 if target is not None: 68 nf_name = '{}_pred_{}'.format(variable, target) 69 X_train['pred_temp'] = (y_train == target).astype(int) # classification 70 else: 71 nf_name = '{}_pred'.format(variable) 72 X_train['pred_temp'] = y_train # regression 73 prior = X_train['pred_temp'].mean() 74 te = TargetEncoder(verbose=self.verbose, hierarchy=self.hierarchy, 75 cols=[variable], smoothing=self.smoothing, 76 min_samples_leaf=self.min_samples_leaf) 77 te.fit(X_train[[variable]], X_train['pred_temp']) 78 tmp_l = te.ordinal_encoder.mapping[0]["mapping"].reset_index() 79 tmp_l.rename(columns={"index":variable, 0:"encode"}, inplace=True) 80 tmp_l.dropna(inplace=True) 81 tmp_r = te.mapping[variable].reset_index() 82 if self.hierarchy is None: 83 tmp_r.rename(columns={variable: "encode", 0:nf_name}, inplace=True) 84 else: 85 tmp_r.rename(columns={"index": "encode", 0:nf_name}, inplace=True) 86 col_avg_y = pd.merge(tmp_l, tmp_r, how="left",on=["encode"]) 87 col_avg_y.drop(columns=["encode"], inplace=True) 88 col_avg_y.set_index(variable, inplace=True) 89 nf_train = X_train.join(col_avg_y, on=variable)[nf_name].values 90 nf_test = X_test.join(col_avg_y, on=variable).fillna(prior, inplace=False)[nf_name].values 91 92 return nf_train, nf_test, prior, col_avg_y 93 94 def fit(self, X, y): 95 """ 96 :param X: pandas DataFrame, n_samples * n_features 97 :param y: pandas Series or numpy array, n_samples 98 :return X_new: the transformed pandas DataFrame containing mean-encoded categorical features 99 """ 100 X_new = X.copy() 101 if self.target_type == 'classification': 102 skf = StratifiedKFold(self.n_splits, shuffle=self.shuffle, random_state=self.random_state) 103 else: 104 skf = KFold(self.n_splits, shuffle=self.shuffle, random_state=self.random_state) 105 106 if self.target_type == 'classification': 107 self.target_values = sorted(set(y)) 108 self.learned_stats = {'{}_pred_{}'.format(variable, target): [] for variable, target in 109 product(self.categorical_features, self.target_values)} 110 for variable, target in product(self.categorical_features, self.target_values): 111 nf_name = '{}_pred_{}'.format(variable, target) 112 X_new.loc[:, nf_name] = np.nan 113 for large_ind, small_ind in skf.split(y, y): 114 nf_large, nf_small, prior, col_avg_y = self.mean_encode_subroutine( 115 X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, target) 116 X_new.iloc[small_ind, -1] = nf_small 117 self.learned_stats[nf_name].append((prior, col_avg_y)) 118 else: 119 self.learned_stats = {'{}_pred'.format(variable): [] for variable in self.categorical_features} 120 for variable in self.categorical_features: 121 nf_name = '{}_pred'.format(variable) 122 X_new.loc[:, nf_name] = np.nan 123 for large_ind, small_ind in skf.split(y, y): 124 nf_large, nf_small, prior, col_avg_y = self.mean_encode_subroutine( 125 X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, None) 126 X_new.iloc[small_ind, -1] = nf_small 127 self.learned_stats[nf_name].append((prior, col_avg_y)) 128 return X_new 129 130 def transform(self, X): 131 """ 132 :param X: pandas DataFrame, n_samples * n_features 133 :return X_new: the transformed pandas DataFrame containing mean-encoded categorical features 134 """ 135 X_new = X.copy() 136 137 if self.target_type == 'classification': 138 for variable, target in product(self.categorical_features, self.target_values): 139 nf_name = '{}_pred_{}'.format(variable, target) 140 X_new[nf_name] = 0 141 for prior, col_avg_y in self.learned_stats[nf_name]: 142 X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[ 143 nf_name] 144 X_new[nf_name] /= self.n_splits 145 else: 146 for variable in self.categorical_features: 147 nf_name = '{}_pred'.format(variable) 148 X_new[nf_name] = 0 149 for prior, col_avg_y in self.learned_stats[nf_name]: 150 X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[ 151 nf_name] 152 X_new[nf_name] /= self.n_splits 153 154 return X_new 155

四 总结

本文介绍了一种对高基数类别特征非常有效的编码方式:平均数编码。详细的讲述了该种编码方式的原理,在实际工程应用中有效避免过拟合的方法,并且提供了一个直接上手的代码版本。

作者:京东保险 赵风龙

来源:京东云开发者社区 转载请注明来源

点赞
收藏

评论区

加载中...

相关推荐

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

Capped CRF:节省编码成本和数据流

!(https://oscimg.oschina.net/oscnet/0c11e7df51334156b03fcf70f9206c9c.jpg)CappedCRF编码是一种单通道编码方法,与双通道VBR相比,可以节省编码成本。CappedCRF也是一种简单的pertitle编码方法,可以降低带宽成本并且提高观众的体验质量。

FLV文件格式

1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  

DevOps工具链

Devops工具链DevOps实际是一种文化上的变迁,代表了开发、运维、测试等环节之间的协作,因此DevOps工具是非常多种多样的,甚至可以由多种工具组成一个完整的DevOps工具链。此类工具可以应用于一种或多种类别,并可体现出软件开发和交付过程的不同阶段:编码:代码开发和审阅,版本控制工具、代码合并工具构建:持续集成工具

高基数类别特征预处理:平均数编码 | 京东云技术团队 - HelloWorld