Encoding Categorical Data for Outlier Detection
TL;DR · AI 摘要
本文指出独热编码并非处理分类数据进行异常检测的最佳方法,并探讨了替代编码方案。
核心要点
- 独热编码可能不适合所有异常检测场景,尤其在高维数据中可能导致维度爆炸。
- 使用目标编码或嵌入方法可以更有效地处理分类数据,提升模型性能。
- 大多数异常检测算法假设数据为数值型,因此需要对分类特征进行数值编码。
结构提纲
按章节快速跳转。
- §引言
本文讨论了在异常检测中处理分类数据的挑战和常见方法。
介绍了基于分类数据和数值数据的异常检测算法及其适用场景。
大多数异常检测算法需要数据为数值型,因此分类数据需要进行编码。
独热编码在高维数据中可能导致维度爆炸,影响模型性能。
目标编码和嵌入方法可以更有效地处理分类数据,提升模型表现。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 分类数据编码与异常检测
- 异常检测算法分类
- 基于分类数据的算法(如FPOF)
- 基于数值数据的算法(如LOF、Isolation Forest)
- 数据编码方法
- 独热编码(One-Hot Encoding)
- 目标编码(Target Encoding)
- 嵌入方法(Embedding Methods)
金句 / Highlights
值得收藏与分享的关键句。
大多数异常检测算法假设数据为数值型,因此需要对分类特征进行数值编码。
使用目标编码或嵌入方法可以更有效地处理分类数据,提升模型性能。
独热编码可能不适合所有异常检测场景,尤其在高维数据中可能导致维度爆炸。
用于异常值检测的分类数据编码 | Towards Data Science
数据科学
用于异常值检测的分类数据编码
为什么独热编码不总是最佳选择,以及替代编码方法
W Brett Kennedy
2026年6月22日
21分钟阅读
分享
本文继续我的异常值检测系列文章。在本文中,我们将探讨如何处理分类数据。
通常在使用表格数据进行异常值检测时,我们首先会将数据转换为完全分类或完全数值的形式。虽然有一些例外情况,但大多数情况下这是必要的:大多数异常值检测算法会假设数据严格属于其中一种格式,我们需要将数据转换为检测器所期望的格式。
如果检测器期望分类数据,那么数值特征需要转换为分类格式,这通常意味着对它们进行分箱处理。如果检测器期望数值数据,任何分类特征都需要进行数值编码。这是更常见的情况(大多数异常值检测算法假设数据是数值的),也是本文要讨论的内容。
该系列的其他文章包括:表格和图像数据上的深度学习用于异常值检测,用于异常值检测的距离度量学习,使用PCA进行异常值检测的简介,可解释的异常值检测:频繁模式异常因子(FPOF),以及使用特征子集更有效地进行异常值检测。
本文还涵盖了一些来自《Python中的异常值检测》一书的内容。
异常值检测器
一些假设分类数据的异常值检测算法包括:频繁模式异常因子(FPOF)、关联规则和基于熵的方法。一些适用于数值数据的算法包括:隔离森林、局部异常因子(LOF)、第k近邻(kNN)和椭圆包络。
如果你熟悉任何异常值检测算法,那么很可能是数值算法,尤其是隔离森林和LOF;这些可能是最常用的算法。此外,scikit-learn和PYOD(Python异常值检测)中包含的所有异常值检测算法都假设数据是完全数值的。
同时,现实世界中的大多数表格数据实际上是混合的(包含数值和分类列),这意味着在进行异常值检测时,需要对分类列进行编码的情况非常常见。
之所以如此,是因为混合数据更难进行异常值检测。仅处理一种类型的数据(全部分类或全部数值)可以简化查找数据中最不寻常项的工作。
此外,如果我们处理数值数据,我们还可以进一步受益于将数据几何化:作为空间中的点。例如,如果一个表格中有20个数值列,那么数据的每一行都可以视为20维空间中的一个点。至少,我们可以在概念上想象它们在20维空间中——人类的思维实际上无法想象这一点。但我们能够想象二维和三维空间,并可以推断出一般概念:我们正在寻找与大多数其他点物理距离非常远的点。例如,在二维空间中,我们可能会有这样的数据:
在此,我们假设数据只有两个特征,分别称为 A 和 B,且均为数值型。数据中的每一行要么以蓝色圆点表示,要么以红色星号表示,其位置由 A 和 B 列中的值决定。蓝色圆点表示典型的正常数据点,而红色星号表示可能被合理视为异常值的点子集:一些位于聚类边缘的点,以及位于聚类外部的点(数据包含三个主要聚类,还有一些位于这些聚类之外的点)。
在低维空间中,这种现象是很自然的。但在高维空间中,情况则有所不同,这是由于所谓的“维度灾难”(curse of dimensionality)所导致的,我们对此必须保持警惕。不过,从概念上讲,将异常值视为高维空间中相对孤立的点,这个想法还是相对直观的。
大多数数值型异常值检测方法通过计算每对点之间的距离,并利用这些距离来识别最不寻常的点——即那些周围几乎没有其他点、并且与其他大多数点距离较远的点。尽管在实际操作中(为了提高效率),算法通常不会计算所有点对之间的距离(某些距离的计算可以跳过,而不会对异常值评分产生显著影响),但原则上,这正是大多数数值型异常值检测方法所做的事情。
因此,我们需要一些方法,将分类数据转换为一种数值格式,以支持这种计算;也就是说,这种转换方式需要使得在将分类值编码为数值之后,计算行与行之间的距离是有意义的。
编码分类数据的方法
在预测问题中,最常见的编码方法可能包括:
- 一位热编码(One-hot encoding)
- 序数编码(Ordinal encoding)
- 目标编码(Target encoding)
但在异常值检测中,可选的方法略有不同,每种方法的优缺点也有所不同。在上述三种方法中,只有“一位热编码”在异常值检测中表现良好。在异常值检测中,最有效的方法可能包括:
- 计数编码(Count encoding)
我将逐一描述这些方法的工作原理,并解释为什么某些方法在异常值检测中表现得更好。同时,我还会解释为什么“计数编码”(在预测问题中很少使用)在异常值检测中可能非常有用。
此外,除了这些编码方法之外,还有一些其他方法在预测问题中可能非常有用。一个用于编码方法的优秀库是 Category Encoders。它很可能涵盖了你所需的所有方法。不过,许多提供的方法(如目标编码和 CatBoost 编码)都需要一个目标列,而通常在异常值检测中,目标列是不可用的。
例如,如果我们有一个表格,表示某企业客户的过往信息,其中可能有一个分类列“最近购买的产品”,以及一个目标列“未来6个月内是否会流失”。该“最近购买的产品”列可能包含不同的值:“产品A”、“产品B”和“产品C”。为了对这些值进行编码,我们可以计算目标列在训练数据中为“True”的频率,可能将其编码为 0.12、0.43、0.02(这意味着,当“最近购买的产品”是“产品A”时,目标列为“True”的概率是12%,客户将在未来6个月内流失;类似地,“产品B”为43%,“产品C”为2%)。
但使用异常值检测时,我们处于一个严格无监督的环境中:没有关于每行异常程度的真实值,因此无法设置目标列。我们只能使用无监督编码方法,包括独热编码(One-hot)和计数编码(Count encoding)。
独热编码(One-hot encoding)
为了了解独热编码,我将首先描述它是如何进行的,然后看看它在距离计算中的表现。假设我们从如下表格开始:
表 1:员工费用表
该表格描述了员工费用,每一行代表一个费用报销。假设我们计划使用一个或多个数值异常检测器,我们需要将分类列(员工 ID、部门和账户)转换为数字。
日期和时间列也需要转换为数值。Python 中的异常检测涵盖了如何处理日期和时间数据。我在这篇文章中没有太多空间详细说明,但可以快速指出它们可以通过多种方式转换。一种简单的方法是通过计算从某个起点(称为纪元,epoch)开始的时间。可以使用该列中的最小日期或时间,或者某个代表逻辑起点的日期。
假设我们使用 1990 年 1 月 1 日。那么所有日期都可以表示为从该时间点开始的天数。不过,我们也希望获取有关日期的更多信息,例如星期几(例如,如果周末的员工费用不寻常,这可能相关),是否是节假日等,因此我们可能还希望查看其他编码方法。不过,本文中,我们将仅关注分类列。
暂时只考虑部门列,通过独热编码该列,我们将该列替换为一系列新列,每个唯一值对应一列。假设该列有五个不同的值:销售(Sales)、市场(Marketing)、工程(Engineering)、人力资源(HR)和沟通(Communications)。那么我们将有五个新列来表示这些值,如下表(表 2)所示。该表仅显示了员工 ID 列和与部门相关的新增列。(其他列也存在,但为了简化在此省略。例如,账户列也会生成类似的列集。)
表 2:部门列经过独热编码后的员工费用表(部分列未显示)
独热编码的部门列中的每个单元格的值将是 0 或 1,表示该行是否具有该值。例如,在第一行中,员工 9000483 的费用报销属于销售部门。因此,“部门销售”列的值为 1,其他与部门相关的列的值为 0。同样,对于其他每一行:部门列中恰好有一个列的值为 1,其余所有列的值为 0。
独热编码在异常检测中被广泛使用,当特征具有低基数(low cardinality)时,它是一个不错的选择。然而,当列具有非常高的基数时,它可能会出现一定程度的失效。例如,如果原始员工费用表中的部门列有 100 个不同的值,那么它将生成 100 个新列,这可能会导致难以处理的表格。不过,我将在下面说明,实际上它在距离计算中与低基数情况一样有效,因此仍然可能是可行的。
同时,高基数列在异常值检测方面的实用性通常不如低基数列。因此,我们可能不希望将“员工ID”列包含在异常值检测过程中。不过,我们也可以选择包含它——例如,如果我们想找出某员工的高额开支、开支次数异常的员工,或在短时间内有许多相似开支的员工等,该列仍然可能提供有用的信息。
使用孤立森林进行独热编码
生成大量额外列的问题程度取决于所使用的异常值检测算法。最常用的一种异常值检测算法是孤立森林(Isolation Forest),它不使用距离计算。相反,它在特征空间中识别低密度子空间,并标记出现在这些子空间中的行。这意味着,它仍然在寻找远离其他点的点,但不需要计算点之间的距离。
在这里我无法深入讲解孤立森林的细节(希望能在未来的一篇文章中详述),但可以快速说明的是,如果在编码后,某一列被扩展为许多列(如独热编码和其他一些编码方案),这些列会在孤立森林算法的分析中被过度代表,而这通常是我们不希望看到的。
由于孤立森林算法内部运作的一些有趣细节,实际上,使用序数编码(Ordinal encoding)进行孤立森林分析通常是效果最好的。需要说明的是,孤立森林是少数几种这种说法成立的异常值检测算法之一——对于大多数其他检测器,序数编码的效果通常很差。我将在下文描述这一点,并解释原因。
使用独热编码进行距离计算
然而,大多数数值型异常值检测器是基于计算并评估点之间的距离(或每个点与数据中心、聚类中心之间的距离)。这包括:局部异常因子(Local Outlier Factor)、k近邻(k-Nearest Neighbors)、半径(Radius)、高斯混合模型(Gaussian Mixture Models)、核密度估计(KDE, Kernel Density Estimation)、椭圆包络(Elliptic Envelope)、单类支持向量机(OCSVM, One-Class Support Vector Machine)等众多其他方法。
编码方法会影响计算的距离,从而影响对每一行给出的异常值评分。独热编码通常在大多数基于距离计算的数值型异常检测器中表现相对较好,但它确实有一个缺点:与孤立森林类似,独热编码会导致分类特征在距离计算中被过度代表,尽管其影响程度比孤立森林要轻。
举个例子,考虑下表(表3),它展示了包含四行数据和两个特征的数据集。颜色(Colour)列有五个值(当前数据中包含两个值)——红色(red)、蓝色(blue)、绿色(green)、白色(white)和黄色(yellow)。尺寸(Size)列有两个值:大(big)和小(small)。
表3:包含颜色和尺寸特征的数据集
下表(表4)显示了这四行数据之间的成对距离。我们可以使用许多不同的距离计算方法;此方法将数据保持为分类变量(我们尚未进行任何数值编码),并测量两行之间的距离为不同值的数量。
由于有两个特征,一对行之间的距离可以是零、一或二(它们可以有零、一个或两个特征不同)。该表仅显示每对唯一行之间的距离,并且每个距离只显示一次(例如,行 1 和行 2 之间的距离,但不显示行 2 和行 1 之间的距离,因为它们是相同的;也不显示行 1 和行 1 之间的距离),因此只在主对角线以上显示值。
表 4:使用一种距离度量方法计算每对行之间的距离,该方法考虑特征值是否相同。
如果我们对原始数据进行 One-hot 编码(来自表 3),我们将得到:
表 5:经过 One-hot 编码后的数据集
如果我们使用 One-hot 编码和曼哈顿或欧几里得距离计算行之间的成对距离,我们将得到下表中所示的距离。在这种情况下,由于所有值都是 0 或 1,曼哈顿距离和欧几里得距离实际上是相同的。
表 6:成对曼哈顿/欧几里得距离
使用曼哈顿(或欧几里得)距离度量时,距离与使用匹配值数量的计数成比例(如我们在表 4 中所做的那样),但值是两倍的:当原始数据中的两个值不匹配时,One-hot 编码中将有两个单元格不匹配。在处理纯分类数据时,这通常不是问题,但在处理混合数据时,它确实会带来一种不理想的情况。
考虑表 7,其中有两个特征:颜色和重量,其中重量是数值型的。
表 7:包含一个分类特征和一个数值特征的数据集
一旦进行 One-hot 编码,我们将得到表 8:
表 8:包含一个分类特征和一个数值特征的 One-hot 编码
在这里,当我们计算行之间的欧几里得距离时。(我们也可以使用曼哈顿、堪培拉或其他任何距离度量,但在此示例中,使用欧几里得距离)。我们将在下表(表 9)中显示欧几里得距离:
表 9:基于欧几里得距离的距离
行 1 和行 2 在颜色上不同(重量相同),它们的欧几里得距离为 1.4。行 3 和行 4 在重量上不同(颜色相同),它们的欧几里得距离仅为 0.6。我们可以看到颜色的差异在确定距离时比重量的差异更重要,尽管这可能并不合理。
这里,分类特征比数值特征更重要,有两个因素。第一个是匹配与不匹配影响两个 One-hot 列,而数值差异只影响一个列。第二个是二进制列中的距离比数值特征中的距离更大。在这里,行 1 和行 4 的重量值分别为 0.1 和 0.9,它们之间的显著差异是 0.8 — 但这小于两个不匹配的分类值之间的差异,这将是 2.0(因为两个二进制列将不匹配)。
下面的代码示例展示了如何使用曼哈顿和欧几里得距离进行计算。在第一种情况下,我们创建两个向量,表示之前数据中的前两行,其中颜色有五个 One-hot 列,还有一个重量列。然后我们创建另一对向量,以模拟如果颜色的基数是 2 的情况,仅使用两个二进制列。
这里我们展示一些测试曼哈顿和欧几里得距离的代码:
from sklearn.metrics.pairwise import euclidean_distances, \
manhattan_distances创建两行数据,其中五个二进制列用于一个分类变量
row_1 = [1, 0, 0, 0, 0, 0.1] row_2 = [0, 1, 0, 0, 0, 0.1] print(manhattan_distances([row_1], [row_2])) print(euclidean_distances([row_1], [row_2]))
创建类似的数据,但使用两个二进制列表示一个分类列
row_1 = [1, 0, 0.1] row_2 = [0, 1, 0.2] print(manhattan_distances([row_1], [row_2])) print(euclidean_distances([row_1], [row_2]))
有趣的是,在这两种情况下,这两行的曼哈顿距离都是 2.1,欧几里得距离是 1.4:当我们仅使用两个二进制特征来表示颜色而不是五个时,距离是相同的。同样,增加基数(使用超过五个二进制列来表示颜色)不会影响距离度量。无论与颜色相关的独热编码列有多少,只要两行的颜色相同,它们之间的差异将是 0;如果颜色不同,它们之间的差异将是 2(其他所有列都将为零,因此匹配)。
因此,正如所提到的,分类变量和数值特征之间存在不平衡,但这种不平衡不会因分类变量的基数而加剧。
我的建议是,为了减少距离计算中的过度强调,可以将独热编码列中的 1.0 值替换为 0.25。这样,不同值的行在该原始列上的总差异将为 0.5 而不是 2.0,这将使它们与数值特征处于更相似的尺度上。
## 序数编码
序数编码通过简单地为分类列中的每个唯一值分配一个唯一数字来工作。在上面的例子中,我们可以为颜色列中的值分配如下数字:
red: 1 blue: 2 green: 3 white: 4 yellow: 5
因此,所有“red”值将被替换为 1,依此类推。同样地,对于“Size”列,我们可以将“small”替换为 1,将“big”替换为 2,或者使用任何其他数值。
如前所述,这确实对孤立森林(Isolation Forest)有效。但对于大多数其他基于距离的数值异常检测器来说,这种方法并不适用。序数编码确实避免了创建额外的列:每个分类列都被转换为一个单独的数值列。然而,距离计算将变得毫无意义。
使用上述数值,值为“yellow”的行将被认为与值为“red”的行相距 4.0,而值为“white”的行与值为“yellow”的行之间的距离仅为 1.0,这显然没有意义。距离最终变得完全任意。
## 计数编码
计数编码作为异常检测中的编码技术,其重要性远高于预测。与序数编码一样,它将每个分类列转换为一个单独的数值列,但计数编码以一种数值不是随机的方式进行转换;这些数值具有意义,并且这种意义对于异常检测是相关的。
计数编码还生成的数值对于距离计算来说是直观的。
在计数编码中,生成的数值表示该值的频率(稀有值将被赋予较小的数值,常见值将被赋予较大的数值),这在进行异常检测时具有实际的信息价值。
以员工开支表为例,如果我们有部门值的分布如下:
Sales: 1,000 Marketing: 500 Engineering: 100 HR: 10 Communications: 3
然后,这些计数将成为编码。也就是说,1000条记录(属于Sales)将被赋予值1000;500条记录将被赋予值500,依此类推。这种方法的优势在于,它可以对值进行编码,使得稀有值往往与其他值相距较远。在这种情况下,值10和3彼此接近,这意味着这13条记录也会彼此接近,但它们仍然只有13条,与另外1600条记录相距较远。值1000与其他值相距较远,但有1000条记录使用了这种编码,因此这些1000条记录彼此之间都接近于其他999条记录,因此不会被标记为异常值。
在下面的代码中,我们使用这些值生成一个简单的单特征数据集,表示部门,并创建一个局部异常因子(LOF)检测器来评估这种情况。在处理多个列时,需要对任何使用计数编码的特征进行标准化,以确保所有特征在同一尺度上,但因为这个例子只包含一个特征,所以可以跳过这一步。LOF能够正确识别稀有值作为异常值:这13个稀有值被预测为-1(在scikit-learn实现中表示异常值),而其他所有值都被预测为1(表示正常值)。
import numpy as np import pandas as pd from sklearn.neighbors import LocalOutlierFactor
创建一个包含单个分类列的数据集
vals = np.array(['Sales']*1000 + ['Marketing']*500 + ['Engineering']*100 + ['HR']*10 + ['Communications']*3)
对列进行计数编码
df = pd.DataFrame({"C1": vals}) vc = df['C1'].value_counts() map = {x:y for x,y in zip(vc.index, vc.values)} df['Ordinal C1'] = df['C1'].map(map)
使用LOF确定列中的异常值
clf = LocalOutlierFactor(contamination=0.01) df['LOF Score'] = clf.fit_predict(df[['Ordinal C1']])
需要注意的是,计数编码的一个问题是,如果某些原始值恰好具有相同的计数,它们可能会被赋予相同的数值编码。例如,如果Sales和Marketing都有1000行,它们都会被赋予1000的编码。或者,如果Sales有1000行,而Marketing有1001行,它们将被赋予几乎相同的编码。对于大多数检测器来说,这不是一个问题,但再次强调,孤立森林(Isolation Forest)有所不同,能够区分真正不同的值是更好的选择,这在序数编码中是可能的。
## 确定最佳的编码方法
哪种编码方法效果最好,将取决于数据集、异常检测算法以及您希望找到的异常类型。不幸的是,像数据科学中的许多其他问题一样,没有明确的答案;每种方法在某些情况下可能更优。在某些情况下,使用不同的编码方法对不同的特征可能实际上效果最好。
与异常检测常见的主题一样,采用集成方法可能很有用,即以多种方式对行进行编码。真正异常的行在每种编码方法下都会作为异常值突出显示,而那些异常程度较轻的行可能仅通过其中一种或另一种编码方法才能被识别出来。
在预测问题中,选择编码方法会更加容易。在预测问题中,我们通常会有一个验证集,可以尝试不同的编码方法,并通过实验确定哪种方法效果最好。然而,在异常值检测中,问题通常是完全无监督的(再次强调,这里没有目标列,因为没有关于每一行异常程度的地面真实值)。这意味着评估所使用的编码方法变得更加困难。不过,我们可以使用一种用于评估异常值检测系统的技术,称为“掺杂”(Doping)。
此外,当异常值检测系统随时间运行时,可能可以收集到标记数据,并使用这些数据来评估不同的预处理方法,包括对分类列的编码。
## 缩放
如果我们一开始处理的是完全数值型的数据,那么我们不需要对任何分类列进行编码,但仍需要对数据进行缩放,至少对于大多数数值型异常值检测器来说是如此。再次强调,孤立森林(Isolation Forest)是一个例外,但任何基于距离计算的方法都需要每个维度(每个特征)处于相同的尺度上。否则,点之间的距离(或点与聚类中心之间的距离等)将主要由那些处于更大尺度上的特征所主导。
编码后的分类列也是如此。无论使用哪种编码方法,新生成的数值特征可能现在与原本就是数值的特征(以及转换后的日期或时间特征)处于不同的尺度上。此外,如果对不同的分类列使用了不同的编码方法,那么这些分类列之间也可能处于不同的尺度上。
对这些列进行缩放所使用的方法与数值列相同——我们只需要确保包含这些新列。关于如何具体操作的细节,希望会在未来的一篇文章中进行介绍,但简要来说,我们通常使用最小-最大缩放、稳健的z-缩放或样条缩放。
所有图片均由作者创作
作者:W Brett Kennedy
查看W Brett Kennedy的所有文章
异常检测
分类数据
深入探讨
异常值检测
分享本文
- 在Facebook上分享
- 在LinkedIn上分享
- 在X上分享
Towards Data Science是一份社区出版物。提交您的见解,以触达全球受众,并通过TDS作者支付计划获得收益。
将href更新为您的实际提交URL
为TDS撰写文章
✦ 结束CTA ✦