平均数编码:针对⾼基数定性特征(类别特征)的数据预处理
特征⼯程
平均数编码:针对⾼基数定性特征(类别特征)的数据预处理
Mean Encoding: A Preprocessing Scheme for High-Cardinality Categorical Features
论⽂原⽂下载:
如果某⼀个特征是定性的(categorical),⽽这个特征的可能值⾮常多(⾼基数),那么平均数编码(mean encoding)是⼀种⾼效的编码⽅式。在实际应⽤中,这类特征⼯程能极⼤提升模型的性能。
在机器学习与数据挖掘中,不论是分类问题(classification)还是回归问题(regression),采集的数据常常会包括定性特征(categorical feature)。因为定性特征表⽰某个数据属于⼀个特定的类别,所以在数值上,定性特征值通常是从0到n的离散整数。例⼦:花瓣的颜⾊(红、黄、蓝)、性别(男、⼥)、地址、某⼀列特征是否存在缺失值(这种NA 指⽰列常常会提供有效的额外信息)。
⼀般情况下,针对定性特征,我们只需要使⽤sklearn的OneHotEncoder或LabelEncoder进⾏编码:(data_df是⼀个pandas dataframe,每⼀⾏是⼀个training example,每⼀列是⼀个特征。在这⾥我们假设"street_address"是⼀个字符类的定性特征。)
from sklearn.preprocessing import OneHotEncoder, LabelEncoder
import numpy as np
import pandas as pd
le = LabelEncoder()
data_df['street_address'] = le.fit_transform(data_df['street_address'])
ohe = OneHotEncoder(n_values='auto', categorical_features='all', dtype=np.float64, sparse=True, handle_unknown='error')
one_hot_matrix = ohe.fit_transform(data_df['street_address'])
LabelEncoder能够接收不规则的特征列,并将其转化为从到的整数值(假设⼀共有种不同的类别);OneHotEncoder 则能通过哑编码,制作出⼀个m*n的稀疏矩阵(假设数据⼀共有m⾏,具体的输出矩阵格式是否稀疏可以由sparse参数控制)。
更详细的API⽂档参见:以及
这类简单的预处理能够满⾜⼤多数数据挖掘算法的需求。
值得⼀提的是,LabelEncoder将n种类别编码为从0到n-1的整数,虽然能够节省内存和降低算法的运⾏时间,但是隐含了⼀个假设:不同的类别之间,存在⼀种顺序关系。在具体的代码实现⾥,LabelEncoder会对定性特征列中的所有独特数据进⾏⼀次排序,从⽽得出从原始输⼊到整数的映射。
定性特征的基数(cardinality)指的是这个定性特征所有可能的不同值的数量。在⾼基数(high cardinality)的定性特征⾯前,这些数据预处理的⽅法往往得不到令⼈满意的结果。
⾼基数定性特征的例⼦:IP地址、电⼦邮件域名、城市名、家庭住址、街道、产品号码。
主要原因:
1. LabelEncoder编码⾼基数定性特征,虽然只需要⼀列,但是每个⾃然数都具有不同的重要意义,对于y⽽⾔线性不可分。使⽤简单模
型,容易⽋拟合(underfit),⽆法完全捕获不同类别之间的区别;使⽤复杂模型,容易在其他地⽅过拟合(overfit)。
2. OneHotEncoder编码⾼基数定性特征,必然产⽣上万列的稀疏矩阵,易消耗⼤量内存和训练时间,除⾮算法本⾝有相关优化(例:
SVM)。
因此,我们可以尝试使⽤平均数编码(mean encoding)的编码⽅法,在贝叶斯的架构下,利⽤所要预测的应变量(target variable),有监督地确定最适合这个定性特征的编码⽅式。在Kaggle的数据竞赛中,这也是⼀种常见的提⾼分数的⼿段。算法设计能解决多个(>2)类别的分类问题,⾃然也能解决更简单的2类分类问题以及回归问题。还有⼀种情况:定性特征本⾝包括了不同级别。例如,国家包含了省,省包含了市,市包含了街区。有些街区可能就包含了⼤量的数据点,⽽有些省却可能只有稀少的⼏个数据点。这时,我们的解决⽅法是,在empirical bayes⾥加⼊不同层次的先验概率估计。
代码实现
原论⽂并没有提到,如果fit时使⽤了全部的数据,transform时也使⽤了全部数据,那么之后的机器学习模型会产⽣过拟合。因此,我们需要将数据分层分为n_splits个fold,每⼀个fold的数据都是利⽤剩下的(n_splits - 1)个fold得出的统计数据进⾏转换。n_splits越⼤,编码的精度越⾼,但也更消耗内存和运算时间。编码完毕后,是否删除原始特征列,应当具体问题具体分析。
附:完整版MeanEncoder代码(python)。
⼀个MeanEncoder对象可以提供fit_transform和transform⽅法,不⽀持fit⽅法,暂不⽀持训练时的sample_weight参数。
import numpy as np
import pandas as pd
del_selection import StratifiedKFold
from itertools import product
class MeanEncoder:
def __init__(self, categorical_features, n_splits=5, target_type='classification', prior_weight_func=None):
"""
:param categorical_features: list of str, the name of the categorical columns to encode
:param n_splits: the number of splits used in mean encoding
:param target_type: str, 'regression' or 'classification'
:param prior_weight_func:
a function that takes in the number of observations, and outputs prior weight
when a dict is passed, the default exponential decay function will be used:
k: the number of observations needed for the posterior to be weighted equally as the prior
f: larger f --> smaller slope
"""
self.categorical_features = categorical_features
self.n_splits = n_splits
self.learned_stats = {}
if target_type == 'classification':
self.target_type = target_type
self.target_values = []
else:
self.target_type = 'regression'
self.target_values = None
if isinstance(prior_weight_func, dict):
self.prior_weight_func = eval('lambda x: 1 / (1 + np.exp((x - k) / f))', dict(prior_weight_func, np=np))
elif callable(prior_weight_func):
self.prior_weight_func = prior_weight_func
else:
self.prior_weight_func = lambda x: 1 / (1 + np.exp((x - 2) / 1))
@staticmethod
variable used in lambdadef mean_encode_subroutine(X_train, y_train, X_test, variable, target, prior_weight_func):
X_train = X_train[[variable]].copy()
X_test = X_test[[variable]].copy()
if target is not None:
if target is not None:
nf_name = '{}_pred_{}'.format(variable, target)
X_train['pred_temp'] = (y_train == target).astype(int) # classification
else:
nf_name = '{}_pred'.format(variable)
X_train['pred_temp'] = y_train # regression
prior = X_train['pred_temp'].mean()
col_avg_y = upby(by=variable, axis=0)['pred_temp'].agg({'mean': 'mean', 'beta': 'size'})
col_avg_y['beta'] = prior_weight_func(col_avg_y['beta'])
col_avg_y[nf_name] = col_avg_y['beta'] * prior + (1 - col_avg_y['beta']) * col_avg_y['mean']
col_avg_y.drop(['beta', 'mean'], axis=1, inplace=True)
nf_train = X_train.join(col_avg_y, on=variable)[nf_name].values
nf_test = X_test.join(col_avg_y, on=variable).fillna(prior, inplace=False)[nf_name].values
return nf_train, nf_test, prior, col_avg_y
def fit_transform(self, X, y):
"""
:param X: pandas DataFrame, n_samples * n_features
:param y: pandas Series or numpy array, n_samples
:return X_new: the transformed pandas DataFrame containing mean-encoded categorical features
"""
X_new = X.copy()
if self.target_type == 'classification':
skf = StratifiedKFold(self.n_splits)
else:
skf = KFold(self.n_splits)
if self.target_type == 'classification':
self.target_values = sorted(set(y))
self.learned_stats = {'{}_pred_{}'.format(variable, target): [] for variable, target in
product(self.categorical_features, self.target_values)}
for variable, target in product(self.categorical_features, self.target_values):
nf_name = '{}_pred_{}'.format(variable, target)
X_new.loc[:, nf_name] = np.nan
for large_ind, small_ind in skf.split(y, y):
nf_large, nf_small, prior, col_avg_y = an_encode_subroutine(
X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, target, self.prior_weight_func) X_new.iloc[small_ind, -1] = nf_small
self.learned_stats[nf_name].append((prior, col_avg_y))
else:
self.learned_stats = {'{}_pred'.format(variable): [] for variable in self.categorical_features}
for variable in self.categorical_features:
nf_name = '{}_pred'.format(variable)
X_new.loc[:, nf_name] = np.nan
for large_ind, small_ind in skf.split(y, y):
nf_large, nf_small, prior, col_avg_y = an_encode_subroutine(
X_new.iloc[large_ind], y.iloc[large_ind], X_new.iloc[small_ind], variable, None, self.prior_weight_func) X_new.iloc[small_ind, -1] = nf_small
self.learned_stats[nf_name].append((prior, col_avg_y))
return X_new
def transform(self, X):
"""
:param X: pandas DataFrame, n_samples * n_features
:return X_new: the transformed pandas DataFrame containing mean-encoded categorical features
"""
X_new = X.copy()
if self.target_type == 'classification':
for variable, target in product(self.categorical_features, self.target_values):
nf_name = '{}_pred_{}'.format(variable, target)
X_new[nf_name] = 0
for prior, col_avg_y in self.learned_stats[nf_name]:
for prior, col_avg_y in self.learned_stats[nf_name]:
X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[ nf_name]
X_new[nf_name] /= self.n_splits
else:
for variable in self.categorical_features:
nf_name = '{}_pred'.format(variable)
X_new[nf_name] = 0
for prior, col_avg_y in self.learned_stats[nf_name]:
X_new[nf_name] += X_new[[variable]].join(col_avg_y, on=variable).fillna(prior, inplace=False)[ nf_name]
X_new[nf_name] /= self.n_splits
return X_new
基本思路与原理可以查看知乎原⽂
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论