ApproachingCategoricalVar处理分类变量
1. 认识分类变量
分类变量/特征是指任何特征类型,可分为两⼤类:
- 无序
- 有序
无序变量是指两个或两个以上类别的变量,类别没有任何相关顺序。例如,如果将性别分为两组,即男性和⼥性,则可将其视为名义变量。 有序变量是指两个或两个以 上类别的变量,其中每个类别都有顺序。例如,如果将教育程度分为“小学”、“初中”、“高中”、“大学”等,则可将其视为序数变量。
就定义⽽⾔,我们也可以将分类变量分为⼆元变量,即只有两个类别的分类变量。有些⼈甚⾄把分类变量称为 "循环 "变量。周期变量以 "周期 "的形式存在,例如⼀周中的天数: 周⽇、周⼀、周⼆、周三、周四、周五和周六。周六过后,⼜是周⽇。这就是⼀个循环。另⼀个例⼦是⼀天中的⼩时数,如果我们将它们视为类别的话。
分类变量有很多不同的定义,很多⼈也谈到要根据分类变量的类型来处理不同的分类变量。不过,我认为没有必要这样做。所有涉及分类变量的问题都可以⽤同样的⽅法处理。
开始之前,我们需要⼀个数据集(⼀如既往)。要了解分类变量,最好的免费数据集之⼀是 Kaggle分类特征编码挑战赛中的 cat-in-the-dat 。共有两个挑战,我们将使⽤第⼆个挑战的数据,因为它⽐前⼀个版本有更多变量,难度也更⼤。
数据集由各种分类变量组成:
- ⽆序
- 有序
- 循环
- ⼆元
目标变量对于学习分类变量来说并不十分重要,但最终我们将建立一个端到端模型,因此我们需要看图2的目标变量分布。目标是偏斜的,因此,对于这个二元分类问题来说,最好的指标是ROC曲线下面积(AUC)。我们也可以使用精确度和召回率,但AUC结合了这两个指标。因此,AUC是评估方法的最好选择。
总体⽽⾔,有:
- 5个⼆元变量
- 10个⽆序变量
- 6个有序变量
- 2个循环变量
- 1个⽬标变量
让我们看看数据集中的ord_2特征。它包括6个不同的类别:
- 冰冻
- 温暖
- 寒冷
- 较热
- 热
- 非常热
因此,我们需要转换为文本数据,我们很容易能联想到字典能够帮助我们完成这一份任务。
# 映射字典
mapping = {
"Freezing":0,
"Warm":1,
"Cold":2,
"Boiling Hot":3,
"Hot":4,
"Lava Hot":5
}
2. 标签编码
import pandas as pd
# 读取数据
df = pd.read_csv("../input/train.csv")
# 取*ord_2*列,并使用映射将类别转换为数字
df.loc[:, "*ord_2*"] = df.*ord_2*.map(mapping)
# 映射前的数值计数
df.*ord_2*.value_counts()
Freezing 142726
Warm 124239
Cold 97822
Boiling Hot 84790
Hot 67508
Lava Hot 64840
Name: *ord_2*, dtype: int64
这种分类变量的编码⽅式被称为标签编码(Label Encoding)我们将每个类别编码为⼀个数字标签。
我们也可以使用scikit-learn中的LabelEncoder进行编码。
import pandas as pd
from sklearn import preprocessing
# 读取数据
df = pd.read_csv("../input/train.csv")
# 将缺失值填充为"NONE"
df.loc[:, "*ord_2*"] = df.*ord_2*.fillna("NONE")
# LabelEncoder编码
lbl_enc = preprocessing.LabelEncoder()
# 转换数据
df.loc[:, "*ord_2*"] = lbl_enc.fit_transform(df.*ord_2*.values)
你会看到我使⽤了 pandas 的 fillna。原因是 scikit-learn 的 LabelEncoder ⽆法处理 NaN 值,⽽ord_2 列中有 NaN 值。
我们可以在许多基于树的模型中直接使⽤它:
- 决策树
- 随机森林
- 提升树 或任何⼀种提升树模型
- XGBoost
- GBM
- LightGBM 这种编码⽅式不能⽤于线性模型、⽀持向量机或神经⽹络,因为它们希望数据是标准化的。
对于这些类型的模型,我们可以对数据进⾏⼆值化(binarize)处理。
Freezing → 0 → 0 0 0
Warm → 1 → 0 0 1
Cold → 2 → 0 1 0
Boiling Hot→ 3 → 0 1 1
Hot → 4 → 1 0 0
Lava Hot → 5 → 1 0 1
这只是将类别转换为数字,然后再转换为⼆值化表⽰。这样,我们就把⼀个特征分成了三个(在本例中)特征(或列)。如果我们有更多的类别,最终可能会分成更多的列。
如果我们⽤稀疏格式存储⼤量⼆值化变 量,就可以轻松地存储这些变量。稀疏格式不过是⼀种在内存中存储数据的表⽰或⽅式,在这种格式中,你并不存储所有的值,⽽只存储重要的值。在上述⼆进制变量的情况中,最重要的就是有 1 的地⽅。
很难想象这样的格式,但举个例⼦就会明⽩。
假设上⾯的数据帧中只有⼀个特征: ord_2 。
Index Feature
0 Warm
1 Hot
2 Lava hot
⽬前,我们只看到数据集中的三个样本。让我们将其转换为⼆值表⽰法,即每个样本有三个项⽬。 这三个项⽬就是三个特征。
Index Feature_0 Feature_1 Feature_2
0 0 0 1
1 1 0 0
2 1 0 1
因此,我们的特征存储在⼀个有 3 ⾏ 3 列(3x3)的矩阵中。矩阵的每个元素占⽤ 8 个字节。因此,
这个数组的总内存需求为 8x3x3 = 72 字节。
我们还可以使⽤⼀个简单的 python 代码段来检查这⼀点。
import numpy as np
example = np.array(
[
[0, 0, 1],
[1, 0, 0],
[1, 0, 1]
]
)
print(example.nbytes)
注:应当打印出72,但打印出来36.此时矩阵的每个元素占用4个字节。
这段代码将打印出 72,就像我们之前计算的那样。但我们需要存储这个矩阵的所有元素吗?如前所述,我们只对 1 感兴趣。0 并不重要,因为任何与 0 相乘的元素都是 0,⽽ 0 与任何元素相加或相减也没有任何区别。只⽤ 1 表⽰矩阵的⼀种⽅法是某种字典⽅法,其中键是⾏和列的索引,值是 1。
(0, 2) 1
(1, 0) 1
(2, 0) 1
(2, 2) 1
这样的符号占⽤的内存要少得多,因为它只需存储四个值(在本例中)。使⽤的总内存为 8x4 = 32 字节。任何 numpy 数组都可以通过简单的 python 代码转换为稀疏矩阵。
import numpy as np
from scipy import sparse
example = np.array(
[
[0, 0, 1],
[1, 0, 0],
[1, 0, 1]
]
)
sparse_example = sparse.csr_matrix(example)
print(sparse_example.data.nbytes)
print(
sparse_example.data.nbytes +
sparse_example.indptr.nbytes +
sparse_example.indices.nbytes
)
注:打印出48.
这将打印出 64 个元素,仍然少于我们的密集数组。遗憾的是,我不会详细介绍这些元素。你可以在scipy ⽂档中了解更多。当我们拥有更⼤的数组时,⽐如说拥有数千个样本和数万个特征的数组,⼤⼩差异就会变得⾮常⼤。例如,我们使⽤基于计数特征的⽂本数据集。(实际上运行是48个元素)
import numpy as np
from scipy import sparse
n_rows = 10000
n_cols = 100000
# 生成符合伯努利分布的随机数组,维度为[10000, 100000]
example = np.random.binomial(1, p=0.05, size=(n_rows, n_cols))
print(f"Size of dense array: {example.nbytes}")
# 将随机矩阵转换为稀疏矩阵
sparse_example = sparse.csr_matrix(example)
print(f"Size of sparse array: {sparse_example.data.nbytes}")
full_size = (
sparse_example.data.nbytes +
sparse_example.indptr.nbytes +
sparse_example.indices.nbytes
)
print(f"Full size of sparse array: {full_size}")
因此,密集阵列需要 ~8000MB 或⼤约 8GB 内存。⽽稀疏阵列只占⽤ 399MB 内存。
这就是为什么当我们的特征中有⼤量零时,我们更喜欢稀疏阵列⽽不是密集阵列的原因。
请注意,稀疏矩阵有多种不同的表⽰⽅法。这⾥我只展⽰了其中⼀种(可能也是最常⽤的)⽅法。深⼊探讨这些⽅法超出了本书的范围,因此留给读者⼀个练习。
尽管⼆值化特征的稀疏表⽰⽐其密集表⽰所占⽤的内存要少得多,但对于分类变量来说,还有⼀种转换所占⽤的内存更少。这就是所谓的 "独热编码"。
3. 独热编码
独热编码也是⼀种⼆值编码,因为只有 0 和 1 两个值。但必须注意的是,它并不是⼆值表⽰法。我们 可以通过下⾯的例⼦来理解它的表⽰法。
假设我们⽤⼀个向量来表⽰ ord_2 变量的每个类别。这个向量的⼤⼩与 ord_2 变量的类别数相同。在 这种特定情况下,每个向量的⼤⼩都是 6,并且除了⼀个位置外,其他位置都是 0。让我们来看看这 个特 殊的向量表。
Freezing 0 0 0 0 0 1
Warm 0 0 0 0 1 0
Cold 0 0 0 1 0 0
Boiling Hot 0 0 1 0 0 0
Hot 0 1 0 0 0 0
Lava Hot 1 0 0 0 0 0
我们看到向量的⼤⼩是 1x6,即向量中有6个元素。这个数字是怎么来的呢?如果你仔细观察,就会发现如前所述,有6个类别。在进⾏独热编码时,向量的⼤⼩必须与我们要查看的类别数相同。每个向量都有⼀个 1,其余所有值都是 0。现在,让我们⽤这些特征来代替之前的⼆值化特征,看看能节省多少内存。
如果你还记得以前的数据,它看起来如下:
Index Feature
0 Warm
1 Hot
2 Lava hot
每个样本有3个特征。但在这种情况下,独热向量的⼤⼩为 6。因此,我们有6个特征,⽽不是3个。
Index F_0 F_1 F_2 F_3 F_4 F_5
0 0 0 0 0 1 0
1 0 1 0 0 0 0
2 1 0 1 0 0 0
因此,我们有 6 个特征,⽽在这个 3x6 数组中,只有 3 个1。使⽤ numpy 计算⼤⼩与⼆值化⼤⼩计算脚本⾮常相似。你需要改变的只是数组。让我们看看这段代码:
import numpy as np
from scipy import sparse
example = np.array(
[
[0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0]
]
)
print(f"Size of dense array: {example.nbytes}")
sparse_example = sparse.csr_matrix(example)
print(f"Size of sparse array: {sparse_example.data.nbytes}")
full_size = (
sparse_example.data.nbytes +
sparse_example.indptr.nbytes +
sparse_example.indices.nbytes
)
print(f"Full size of sparse array: {full_size}")
我们可以看到,密集矩阵的⼤⼩远远⼤于⼆值化矩阵的⼤⼩。不过,稀疏数组的⼤⼩要更⼩。让我们⽤更⼤的数组来试试。在本例中,我们将使⽤ scikit-learn 中的 OneHotEncoder 将包含 1001 个类别的特征数组转换为密集矩阵和稀疏矩阵。
import numpy as np
from sklearn import preprocessing
# 生成符合均匀分布的随机整数,维度为[1000000, 10000000]
example = np.random.randint(1000, size=1000000)
# 独热编码,非稀疏矩阵
# 独热编码函数sparse参数已经改变:Added in version 1.2: sparse was renamed to sparse_output
ohe = preprocessing.OneHotEncoder(sparse_output=False)
# 将随机数组展平
ohe_example = ohe.fit_transform(example.reshape(-1, 1))
print(f"Size of dense array: {ohe_example.nbytes}")
# 独热编码,稀疏矩阵
ohe = preprocessing.OneHotEncoder(sparse_output=True)
# 将随机数组展平
ohe_example = ohe.fit_transform(example.reshape(-1, 1))
print(f"Size of sparse array: {ohe_example.data.nbytes}")
full_size = (
ohe_example.data.nbytes +
ohe_example.indptr.nbytes +
ohe_example.indices.nbytes
)
print(f"Full size of sparse array: {full_size}")
结果:
Size of dense array: 8000000000
Size of sparse array: 8000000
Full size of sparse array: 16000004
这⾥的密集阵列⼤⼩约为 8GB,稀疏阵列为 8MB。如果可以选择,你会选择哪个?在我看来,选择很简单,不是吗?
这三种⽅法(标签编码、稀疏矩阵、独热编码)是处理分类变量的最重要⽅法。不过,你还可以⽤很多其他不同的⽅法来处理分类变量。将分类变量转换为数值变量就是其中的⼀个例⼦。
假设我们回到之前的分类特征数据(原始数据中的 cat-in-the-dat-ii)。在数据中, ord_2 的值为“热”的 id 有多少?
我们可以通过计算数据的形状(shape)轻松计算出这个值,其中 ord_2 列的值为 Boiling Hot 。
In [X]: df[df.ord_2 == "Boiling Hot"].shape
Out[X]: (84790, 25)
我们可以看到,有 84790 条记录具有此值。我们还可以使⽤ pandas 中的 groupby 计算所有类别的该 值。
In [X]: df.groupby(["ord_2"])["id"].count()
Out[X]:
ord_2
Boiling Hot 84790
Cold 97822
Freezing 142726
Hot 67508
Lava Hot 64840
Warm 124239
Name: id, dtype: int64
如果我们只是将 ord_2 列替换为其计数值,那么我们就将其转换为⼀种数值特征了。我们可以使⽤
pandas 的 transform 函数和 groupby 来创建新列或替换这⼀列。
In [X]: df.groupby(["ord_2"])["id"].transform("count")
Out[X]:
0 67508.0
1 124239.0
2 142726.0
3 64840.0
4 97822.0
...
599995 142726.0
599996 84790.0
599997 142726.0
599998 124239.0
599999 84790.0
Name: id, Length: 600000, dtype: float64
你可以添加所有特征的计数,也可以替换它们,或者根据多个列及其计数进⾏分组。例如,以下代码通过对 ord_1 和 ord_2 列分组进⾏计数。
In [X]: df.groupby(
...: [
...: "ord_1",
...: "ord_2"
...: ]
...: )["id"].count().reset_index(name="count")
Out[X]:
ord_1 ord_2 count
0 Contributor Boiling Hot 15634
1 Contributor Cold 17734
2 Contributor Freezing 26082
3 Contributor Hot 12428
4 Contributor Lava Hot 11919
5 Contributor Warm 22774
6 Expert Boiling Hot 19477
7 Expert Cold 22956
8 Expert Freezing 33249
9 Expert Hot 15792
10 Expert Lava Hot 15078
11 Expert Warm 28900
12 Grandmaster Boiling Hot 13623
13 Grandmaster Cold 15464
14 Grandmaster Freezing 22818
15 Grandmaster Hot 10805
16 Grandmaster Lava Hot 10363
17 Grandmaster Warm 19899
18 Master Boiling Hot 10800
请注意,我已经从输出中删除了⼀些⾏,以便在⼀⻚中容纳这些⾏。这是另⼀种可以作为功能添加的计数。您现在⼀定已经注意到,我使⽤ id 列进⾏计数。不过,你也可以通过对列的组合进⾏分组,对其他列进⾏计数。
还有⼀个⼩窍⻔,就是从这些分类变量中创建新特征。你可以从现有的特征中创建新的分类特征,⽽且可以毫不费⼒地做到这⼀点。
In [X]: df["new_feature"] = (
...: df.ord_1.astype(str)
...: + "_"
...: + df.ord_2.astype(str)
...: )
In [X]: df.new_feature
Out[X]:
0 Contributor_Hot
1 Grandmaster_Warm
2 nan_Freezing
3 Novice_Lava Hot
4 Grandmaster_Cold
...
599999 Contributor_Boiling Hot
Name: new_feature, Length: 600000, dtype: object
在这⾥,我们⽤下划线将 ord_1 和 ord_2 合并,然后将这些列转换为字符串类型。请注意,NaN 也会转换为字符串。不过没关系。我们也可以将NaN 视为⼀个新的类别 。这样,我们就有了⼀个由这两个特征组合⽽成的新特征。您还可以将三列以上或四列甚⾄更多列组合在⼀起。
In [X]: df["new_feature"] = (
...: df.ord_1.astype(str)
...: + "_"
...: + df.ord_2.astype(str)
...: + "_"
...: + df.ord_3.astype(str)
...: )
In [X]: df.new_feature
Out[X]:
0 Contributor_Hot_c
1 Grandmaster_Warm_e
2 nan_Freezing_n
3 Novice_Lava Hot_a
4 Grandmaster_Cold_h
...
599999 Contributor_Boiling Hot_b
Name: new_feature, Length: 600000, dtype: object
4. 尝试结合类别
那么,我们应该把哪些类别结合起来呢?这并没有⼀个简单的答案。这取决于您的数据和特征类型。
⼀些领域知识对于创建这样的特征可能很有⽤。但是,如果你不担⼼内存和 CPU 的使⽤,你可以采⽤⼀种贪婪的⽅法,即创建许多这样的组合,然后使⽤⼀个模型来决定哪些特征是有⽤的,并保留它们。我们将在本书稍后部分介绍这种⽅法。
⽆论何时获得分类变量,都要遵循以下简单步骤:
-
填充 NaN 值(这⼀点⾮常重要!)。