https://zhuanlan.zhihu.com/p/26308272

(在另一篇文章中,我正在汇总所有已知的数据挖掘特征工程技巧:【持续更新】机器学习特征工程实用技巧大全 - 知乎专栏。)

前言

读完sklearn.preprocessing所有函数的API文档之后,基础的特征工程就可以算是入门了。然而,进阶的特征工程往往依赖于数据分析师的直觉与经验,而且与具体的数据有密切的联系,比较难找到系统性的“最好”的特征工程方法。

在这里,我希望能向大家分享一种极其有效的、针对高基数定性特征(类别特征)的数据预处理方法。在各类竞赛中,有许多人使用这种方法取得了非常优秀的成绩,但是中文网络上似乎还没有人对此做过介绍。

平均数编码:针对高基数定性特征(类别特征)的数据预处理

Mean Encoding: A Preprocessing Scheme for High-Cardinality Categorical Features

(论文原文下载:http://helios.mm.di.uoa.gr/~rouvas/ssi/sigkdd/sigkdd.vol3.1/barreca.pdf,感谢评论区@jin zhang提供更清晰的pdf版本)

如果某一个特征是定性的(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 pdle = 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文档参见:sklearn.preprocessing.LabelEncoder - scikit-learn 0.18.1 documentation以及sklearn.preprocessing.OneHotEncoder - scikit-learn 0.18.1 documentation

这类简单的预处理能够满足大多数数据挖掘算法的需求。

值得一提的是,LabelEncoder将n种类别编码为从0到n-1的整数,虽然能够节省内存降低算法的运行时间,但是隐含了一个假设:不同的类别之间,存在一种顺序关系。在具体的代码实现里,LabelEncoder会对定性特征列中的所有独特数据进行一次排序,从而得出从原始输入到整数的映射。

定性特征的基数(cardinality)指的是这个定性特征所有可能的不同值的数量。在高基数(high cardinality)的定性特征面前,这些数据预处理的方法往往得不到令人满意的结果。

高基数定性特征的例子:IP地址、电子邮件域名、城市名、家庭住址、街道、产品号码。

主要原因:

  1. LabelEncoder编码高基数定性特征,虽然只需要一列,但是每个自然数都具有不同的重要意义,对于y而言线性不可分。使用简单模型,容易欠拟合(underfit),无法完全捕获不同类别之间的区别;使用复杂模型,容易在其他地方过拟合(overfit)。
  2. OneHotEncoder编码高基数定性特征,必然产生上万列的稀疏矩阵,易消耗大量内存和训练时间,除非算法本身有相关优化(例:SVM)。

因此,我们可以尝试使用平均数编码(mean encoding)的编码方法,在贝叶斯的架构下,利用所要预测的应变量(target variable),有监督地确定最适合这个定性特征的编码方式。在Kaggle的数据竞赛中,这也是一种常见的提高分数的手段。

基本思路与原理

平均数编码是一种有监督(supervised)的编码方式,适用于分类和回归问题。为了简化讨论,以下的所有代码都以分类问题作为例子。

假设在分类问题中,目标y一共有C个不同类别,具体的一个类别用target表示;某一个定性特征variable一共有K个不同类别,具体的一个类别用k表示。

先验概率(prior):数据点属于某一个target(y)的概率,

后验概率(posterior):该定性特征属于某一类时,数据点属于某一个target(y)的概率,

本算法的基本思想:将variable中的每一个k,都表示为(估算的)它所对应的目标y值概率

。(估算的结果都用“^”表示,以示区分)

(备注)因此,整个数据集将增加(C-1)列。是C-1而不是C的原因:
,所以最后一个的概率值必然和其他的概率值线性相关。在线性模型、神经网络以及SVM里,不能加入线性相关的特征列。如果你使用的是基于决策树的模型(gbdt、rf等),个人仍然不推荐这种over-parameterization。

先验概率与后验概率的计算

因为我们没有上帝视角,所以我们在计算中,需要利用已有数据估算先验概率和后验概率。我们在此使用的具体方法被称为Empirical Bayes(Empirical Bayes method)。和一般的贝叶斯方法(如常见的Laplace Smoothing)不同,我们在估算先验概率时,会使用已知数据的平均值,而不是

接下来的计算就是简单的统计:

 = (y = target)的数量 / y 的总数

 = (y = target 并且 variable = k)的数量 / (variable = k)的数量

(其实我本来可以把公式用sigma求和写出来的,但是那样不太方便直观理解)

使用不同的统计方法,以上两个公式的计算方法也会不同。

权重

我们已经得到了先验概率估计和后验概率估计。最终编码所使用的概率估算,应当是先验概率与后验概率的一个凸组合(convex combination)。由此,我们引入先验概率的权重来计算编码所用概率

或:

直觉上,应该具有以下特性:

  1. 如果测试集中出现了新的特征类别(未在训练集中出现),那么
  2. 一个特征类别在训练集内出现的次数越多,后验概率的可信度越高,其权重也越大。

在贝叶斯统计学中,也被称为shrinkage parameter。

权重函数

我们需要定义一个权重函数输入特征类别在训练集中出现的次数n,输出是对于这个特征类别的先验概率的权重。假设一个特征类别的出现次数为n,以下是一个常见的权重函数:

这个函数有两个参数:

k:当时,,先验概率与后验概率的权重相同;当时,

f:控制函数在拐点附近的斜率,f越大,“坡”越缓。

图示:k=1时,不同的f对于函数图象的影响。

当(freq_col - k) / f太大的时候,np.exp可能会产生overflow的警告。我们不需要管这个警告,因为某一类别的频数极高,分母无限时,最终先验概率的权重将成为0,这也表示我们对于后验概率有充足的信任。

延伸

以上的算法设计能解决多个(>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
from sklearn.model_selection import StratifiedKFold
from itertools import productclass 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 weightwhen 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 priorf: larger f --> smaller slope"""self.categorical_features = categorical_featuresself.n_splits = n_splitsself.learned_stats = {}if target_type == 'classification':self.target_type = target_typeself.target_values = []else:self.target_type = 'regression'self.target_values = Noneif 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_funcelse:self.prior_weight_func = lambda x: 1 / (1 + np.exp((x - 2) / 1))@staticmethoddef 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:nf_name = '{}_pred_{}'.format(variable, target)X_train['pred_temp'] = (y_train == target).astype(int)  # classificationelse:nf_name = '{}_pred'.format(variable)X_train['pred_temp'] = y_train  # regressionprior = X_train['pred_temp'].mean()col_avg_y = X_train.groupby(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].valuesnf_test = X_test.join(col_avg_y, on=variable).fillna(prior, inplace=False)[nf_name].valuesreturn nf_train, nf_test, prior, col_avg_ydef 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 inproduct(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.nanfor large_ind, small_ind in skf.split(y, y):nf_large, nf_small, prior, col_avg_y = MeanEncoder.mean_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_smallself.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.nanfor large_ind, small_ind in skf.split(y, y):nf_large, nf_small, prior, col_avg_y = MeanEncoder.mean_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_smallself.learned_stats[nf_name].append((prior, col_avg_y))return X_newdef 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] = 0for 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_splitselse:for variable in self.categorical_features:nf_name = '{}_pred'.format(variable)X_new[nf_name] = 0for 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_splitsreturn X_new

参考资料:

[ 1 ] Micci-Barreca, Daniele. "A preprocessing scheme for high-cardinality categorical attributes in classification and prediction problems.ACM SIGKDD Explorations Newsletter 3.1 (2001): 27-32.

平均数编码:针对高基数定性特征(类别特征)的数据预处理/特征工程相关推荐

  1. 数据预处理--特征归一化

    为什么需要对数值类型的特征归一化? 对数据进行特征归一化(Normalization)处理,可以使得数据的各个特征处于同一数值量级,而不会导致模型学习出来的结果倾向于数值差别比较大的那些特征. 常用的 ...

  2. EOF-DataScience:数据预处理/特征工程之线性变换—四种特征缩放Scaling算法简介、标准化standardization、归一化Normalization的概述与区别

    DataScience:数据预处理/特征工程之线性变换-四种特征缩放Scaling算法简介.标准化standardization.归一化Normalization的概述与区别 目录 数据处理中常见的四 ...

  3. AI算法连载13:统计之数据预处理特征工程

    导语:在人工智能AI如火如荼的大潮下,越来越多的工程师们意识到算法是AI的核心.而面对落地的应用,不懂算法的AI产品经理将是空谈,不仅无法与工程师沟通,更无法深刻理解应用的性能与方式.所以业界逐渐形成 ...

  4. 数据预处理与特征工程—12.常见的数据预处理与特征工程手段总结

    文章目录 引言 1.数据预处理 1.1 数据清洗 1.1.1 异常值处理 1.1.2 缺失值处理 1.2 特征预处理 1.2.1 数值型特征无量纲化 1.2.2 连续数值型特征分箱 1.2.2.1 无 ...

  5. 专栏 | 基于 Jupyter 的特征工程手册:数据预处理(三)

    作者:陈颖祥.杨子晗 编译:AI有道 基于 Jupyter 的特征工程手册:数据预处理的上一篇: 专栏 | 基于 Jupyter 的特征工程手册:数据预处理(一) 专栏 | 基于 Jupyter 的特 ...

  6. 使用jupyter计算正态分布_专栏 | 基于 Jupyter 的特征工程手册:数据预处理(三)...

    红色石头的个人网站: 红色石头的个人博客-机器学习.深度学习之路​www.redstonewill.com 基于 Jupyter 的特征工程手册:数据预处理的上一篇: 专栏 | 基于 Jupyter ...

  7. 专栏 | 基于 Jupyter 的特征工程手册:数据预处理(二)

    作者:陈颖祥.杨子晗 编译:AI有道 基于 Jupyter 的特征工程手册:数据预处理的上一篇: 专栏 | 基于 Jupyter 的特征工程手册:数据预处理(一) 项目地址: https://gith ...

  8. 大数据分析与应用(中级) 数据预处理与特征工程

    目录 一.数据预处理可以包括那些操作 二.数据抽样可以包含那些类型的抽样方式,每一种抽样方式的原理是什么? 1.随机抽样(Random Sampling) 2.系统抽样(Systemactic Sam ...

  9. ML:通过数据预处理(分布图/箱型图/模型寻找异常值/热图/散点图/回归关系/修正分布正态化/QQ分位图/构造交叉特征/平均数编码)利用十种算法模型调优实现工业蒸汽量回归预测(交叉训练/模型融合)之详

    ML之LightGBM:通过数据预处理(分布图/箱型图/模型寻找异常值/热图/散点图/回归关系/修正分布正态化/QQ分位图/构造交叉特征/平均数编码)利用十种算法模型调优实现工业蒸汽量回归预测(交叉训 ...

最新文章

  1. 分布式一致性协议paxos
  2. c++找不到标识符_沪C转沪牌流程攻略大全
  3. 【数据挖掘】关联规则挖掘 Apriori 算法 ( 关联规则简介 | 数据集 与 事物 Transaction 概念 | 项 Item 概念 | 项集 Item Set | 频繁项集 | 示例解析 )
  4. 都2021年了,不会还有人连深度学习都不了解吧(一)- 激活函数篇
  5. web前端分享:性能优化之文档碎片处理
  6. java中ftp删除文件,Java 实现ftp 文件上传、下载和删除
  7. 485光隔离中继器产品特点及应用领域介绍
  8. MySQL数据库的回滚失败(JAVA)
  9. 去越南旅游一个人玩一个月需要多少人民币?
  10. 简单了解 Tendermint
  11. 计算机组成原理——数据通路
  12. 写给独自站在人生面前的自己1-java加密算法
  13. iframe 嵌入html页面,iframe 完美嵌入网页
  14. PhotoSweeper X (重复照片清理工具)
  15. Openstack Trove概要
  16. stm32f405rgt6与as5048a的SPI通信问题
  17. 高级软件工程学习总结
  18. 【MATLAB】MATLAB 可视化之极坐标图
  19. Echart在Openlayers的应用-航班的炫光特效
  20. 腾讯云4核8G12M轻量服务器配置性能评测

热门文章

  1. 修复黑苹果无法播放Apple Music无损音乐的问题
  2. 信息学竞赛有什么好的比赛网站?
  3. Snell定律(折射定律)之导数的应用
  4. 如何写一份优秀的商业计划书
  5. ElasticSearch快速入门(一)
  6. Java 将表格数据导入word文档中
  7. Docker和k8s的区别与介绍
  8. 重装win7系统并激活
  9. Tomcat启动成功访问404:源服务器未能找到目标资源的表示或者是不愿公开一个已经存在的资源表示。
  10. Python文件处理之seek(), tell()用法