机器学习和数据科学中处理分类变量的指南
在数据科学或机器学习项目中处理分类变量并非易事。这种工作需要对应用领域有深入的了解和对多种方法学有广泛的理解。
因此,本文将重点解释以下概念
- 什么是分类变量以及如何将它们分为不同类型
- 如何根据类型将它们转换为数值
- 主要使用Sklearn进行管理的工具和技术
正确处理分类变量可以极大地提高我们预测模型或分析的结果。实际上,与学习和理解数据相关的大多数信息可能包含在可用的分类变量中。
想想表格数据,按照变量性别
或某种颜色
拆分。基于类别数量的这些拆分可以展现组之间的显著差异,从而可以通知分析师或学习算法。
让我们先定义它们是什么以及它们如何呈现。
分类变量的定义
分类变量是在统计学和数据科学中用于表示定性或名义数据的一种变量类型。这些变量可以被定义为无法连续量化,而只能离散量化的数据类或类别。
例如,分类变量的一个例子可能是人的眼睛颜色,可以是蓝色、绿色或棕色。
大多数学习模型不使用分类格式的数据。我们必须先将它们转换为数值格式,以便信息得以保留。
分类变量可以分为两种类型:
- 名义
- 有序
名义变量是不受精确顺序约束的变量。性别、颜色或品牌是名义变量的例子,因为它们无法排序。
有序变量是分类变量,分为逻辑可排序的级别。数据集中由级别如“第一、第二和第三”组成的列可以被视为有序分类变量。
您可以通过考虑二元和循环变量来更深入地分解分类变量。
二元变量很容易理解:它是一种只能取两个值的分类变量。
另一方面,循环变量的特点是其值的重复。例如,一周的日子和季节都是循环的。
如何转换分类变量
现在,我们已经定义了分类变量是什么以及它们的外观,让我们通过一个实际示例来解决将它们转换的问题——一个名为cat-in-the-dat的Kaggle数据集。
数据集
这是一个开源数据集,是分类特征编码挑战赛II的入门竞赛的基础。您可以直接从以下链接下载数据。
分类特征编码挑战赛II
二元分类,每个特征都是分类的(和交互!)
www.kaggle.com
这个数据集的特点是它仅包含分类数据。因此,它成为本指南的完美用例。它包括名义、有序、循环和二元变量。
我们将看到将每个变量转换为学习模型可用格式的技术。
数据集如下所示:
由于目标变量只能取两个值,这是一个二元分类任务。我们将使用AUC指标来评估我们的模型。
现在我们将应用使用所提供数据集的分类变量管理技术。
1. 标签编码(映射为任意数字)
将类别转换为可用格式的最简单技术是将每个类别分配到任意数字。
例如,ord_2
列包含以下类别:
array(['Hot', 'Warm', 'Freezing', 'Lava Hot', 'Cold', 'Boiling Hot', nan], dtype=object)
可以使用Python和Pandas进行映射:
df_train = train.copy()mapping = { "Cold": 0, "Hot": 1, "Lava Hot": 2, "Boiling Hot": 3, "Freezing": 4, "Warm": 5}df_train["ord_2"].map(mapping)>> 0 1.01 5.02 4.03 2.04 0.0 ... 599995 4.0599996 3.0599997 4.0599998 5.0599999 3.0Name: ord_2, Length: 600000, dtype: float64
然而,这种方法存在一个问题:您必须手动声明映射。对于少量类别,这不是问题,但对于大量类别,可能会有问题。
为此,我们将使用Scikit-Learn和LabelEncoder
对象以更灵活的方式实现相同的结果。
from sklearn import preprocessing# 处理缺失值df_train["ord_2"].fillna("NONE", inplace=True)# 初始化Sklearn编码器le = preprocessing.LabelEncoder()# 拟合 + 转换df_train["ord_2"] = le.fit_transform(df_train["ord_2"])df_train["ord_2"]>>0 31 62 23 44 1 ..599995 2599996 0599997 2599998 6599999 0Name: ord_2, Length: 600000, dtype: int64
映射由Scikit-Learn控制。我们可以将其可视化如下:
mapping = {label: index for index, label in enumerate(le.classes_)}mapping>>{'Boiling Hot': 0, 'Cold': 1, 'Freezing': 2, 'Hot': 3, 'Lava Hot': 4, 'NONE': 5, 'Warm': 6}
请注意上面代码片段中的.fillna(“NONE”)
。实际上,Sklearn的标签编码器不处理空值,并且如果发现任何空值,则会在应用时出错。
正确处理分类变量的最重要的事情之一是始终处理空值。实际上,如果不处理这些空值,大多数相关技术都无法正常工作。
标签编码器将任意数字映射到列中的每个类别,而不需要显式声明映射。这很方便,但对于某些预测模型来说会引入问题:如果该列不是目标列,则需要对数据进行缩放。
实际上,机器学习初学者经常问标签编码器和独热编码器之间的区别,接下来我们将看到。 标签编码器按设计应用于标签,即我们想要预测的目标变量,而不是其他列。
话虽如此,某些在该领域中非常重要的模型即使使用这种类型的编码也能够正常工作。我在谈论树模型,其中XGBoost和LightGBM脱颖而出。
因此,如果您决定使用树模型,则可以放心使用标签编码器,但否则,我们必须使用独热编码。
2. 独热编码
正如我在有关机器学习中向量表示的文章中已经提到的那样,独热编码是一种非常常见和著名的向量化技术(即将文本转换为数字)。
它的工作原理是:对于每个存在的类别,都会创建一个只有0和1两个可能值的正方形矩阵。该矩阵告诉模型,在所有可能的类别中,该观察到的行具有由1表示的值。
例如:
| | | | | | -------------|---|---|---|---|---|--- 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
该数组的大小为n_categories。这是非常有用的信息,因为one-hot编码通常需要将转换后的数据表示为稀疏表示。
这是什么意思呢?这意味着对于大量的类别,矩阵可能会变得同样大。由于只由0和1的值填充,而且只有一个位置可以由1填充,这使得one-hot表示非常冗长和繁琐。
稀疏矩阵解决了这个问题-只保存1的位置,而值等于0的位置不保存。这简化了上述问题,并允许我们以极小的内存使用量换取大量信息。
让我们看看Python中这样一个数组的样子,再次应用之前的代码:
from sklearn import preprocessing# 处理缺失值df_train["ord_2"].fillna("NONE", inplace=True)# 初始化sklearn的编码器ohe = preprocessing.OneHotEncoder()# 拟合 + 转换ohe.fit_transform(df_train["ord_2"].values.reshape(-1, 1))>><600000x7 sparse matrix of type '<class 'numpy.float64'>' with 600000 stored elements in Compressed Sparse Row format>
默认情况下,Python返回一个对象而不是值列表。要获得这样的列表,需要使用.toarray()
ohe.fit_transform(df_train["ord_2"].values.reshape(-1, 1)).toarray()>>array([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 1.], [0., 0., 1., ..., 0., 0., 0.], ..., [0., 0., 1., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 1.], [1., 0., 0., ..., 0., 0., 0.]])
如果您不完全理解该概念,不要担心:我们很快将看到如何将标签和one-hot编码器应用于数据集以训练预测模型。
标签编码和one-hot编码是处理类别变量的最重要的技术。了解这两种技术将使您能够处理涉及类别变量的大多数情况。
3. 转换和聚合
将类别变量转换为数字格式的另一种方法是对变量执行转换或聚合。
通过使用.groupby()
进行分组,可以使用列中存在的值的计数作为转换的输出。
df_train.groupby(["ord_2"])["id"].count()>>ord_2Boiling Hot 84790Cold 97822Freezing 142726Hot 67508Lava Hot 64840Warm 124239Name: id, dtype: int64
使用.transform()
,我们可以将这些数字替换为相应的单元格
df_train.groupby(["ord_2"])["id"].transform("count")>>0 67508.01 124239.02 142726.03 64840.04 97822.0 ... 599995 142726.0599996 84790.0599997 142726.0599998 124239.0599999 84790.0Name: id, Length: 600000, dtype: float64
可以将这种逻辑也应用于其他数学运算,应该测试最能提高模型性能的方法。
4. 从分类变量创建新的分类特征
我们一起看一下 ord_1 列和 ord_2 列
我们可以通过合并现有变量来创建新的分类变量。例如,我们可以合并 ord_1 和 ord_2 来创建一个新的特征
df_train["new_1"] = df_train["ord_1"].astype(str) + "_" + df_train["ord_2"].astype(str)df_train["new_1"]>>0 Contributor_Hot1 Grandmaster_Warm2 nan_Freezing3 Novice_Lava Hot4 Grandmaster_Cold ... 599995 Novice_Freezing599996 Novice_Boiling Hot599997 Contributor_Freezing599998 Master_Warm599999 Contributor_Boiling HotName: new_1, Length: 600000, dtype: object
这种技术几乎可以应用于任何情况。分析师应该遵循的思路是通过添加原本难以理解的信息来提高模型的性能。
5. 将 NaN 用作分类变量
经常会将空值删除。我通常不建议这样做,因为 NaN 包含了对我们的模型有用的信息。
一种解决方案是将 NaN 视为自己的一类。
让我们再次看一下 ord_2 列
df_train["ord_2"].value_counts()>>Freezing 142726Warm 124239Cold 97822Boiling Hot 84790Hot 67508Lava Hot 64840Name: ord_2, dtype: int64
现在让我们尝试将 .fillna(“NONE")
应用于看看有多少空单元格存在
df_train["ord_2"].fillna("NONE").value_counts()>>Freezing 142726Warm 124239Cold 97822Boiling Hot 84790Hot 67508Lava Hot 64840NONE 18075
作为百分比,NONE 在整个列中占约 3%。这是一个相当明显的数量。利用 NaN 更有意义,可以使用前面提到的 One Hot Encoder 来实现。
追踪罕见的类别
让我们记住 OneHotEncoder 的作用:它创建一个稀疏矩阵,其列数和行数等于所引用列中唯一类别的数量。这意味着我们还必须考虑测试集中可能存在但在训练集中可能不存在的类别。
对于 LabelEncoder,情况类似——测试集中可能存在但在训练集中不存在的类别,这可能在转换过程中会产生问题。
我们通过连接数据集来解决这个问题。这将允许我们将编码器应用于所有数据,而不仅仅是训练数据。
test["target"] = -1data = pd.concat([train, test]).reset_index(drop=True)features = [f for f in train.columns if f not in ["id", "target"]]for feature in features: le = preprocessing.LabelEncoder() temp_col = data[feature].fillna("NONE").astype(str).values data.loc[:, feature] = le.fit_transform(temp_col) train = data[data["target"] != -1].reset_index(drop=True)test = data[data["target"] == -1].reset_index(drop=True)
这种方法可以帮助我们处理测试集。如果没有测试集,当新类别成为训练集的一部分时,我们将考虑像 NONE 这样的值。
建模类别数据
现在我们开始训练一个简单的模型。我们将按照以下链接中的文章中设计和实现交叉验证的步骤进行👇
什么是机器学习中的交叉验证
了解交叉验证——构建可推广模型的基本技术
towardsdatascience.com
我们从头开始,使用Sklearn的StratifiedKFold
导入我们的数据并创建我们的折叠。
train = pd.read_csv("/kaggle/input/cat-in-the-dat-ii/train.csv")test = pd.read_csv("/kaggle/input/cat-in-the-dat-ii/test.csv")df = train.copy()df["kfold"] = -1df = df.sample(frac=1).reset_index(drop=True)y = df.target.valueskf = model_selection.StratifiedKFold(n_splits=5)for f, (t_, v_) in enumerate(kf.split(X=df, y=y)): df.loc[v_, 'kfold'] = f
这段小代码将创建一个带有5个组的Pandas数据框,以便对我们的模型进行测试。
现在让我们定义一个函数,将在每个组上测试逻辑回归模型。
def run(fold: int) -> None: features = [ f for f in df.columns if f not in ("id", "target", "kfold") ] for feature in features: df.loc[:, feature] = df[feature].astype(str).fillna("NONE") df_train = df[df["kfold"] != fold].reset_index(drop=True) df_valid = df[df["kfold"] == fold].reset_index(drop=True) ohe = preprocessing.OneHotEncoder() full_data = pd.concat([df_train[features], df_valid[features]], axis=0) print("Fitting OHE on full data...") ohe.fit(full_data[features]) x_train = ohe.transform(df_train[features]) x_valid = ohe.transform(df_valid[features]) print("Training the classifier...") model = linear_model.LogisticRegression() model.fit(x_train, df_train.target.values) valid_preds = model.predict_proba(x_valid)[:, 1] auc = metrics.roc_auc_score(df_valid.target.values, valid_preds) print(f"FOLD: {fold} | AUC = {auc:.3f}")run(0)>>Fitting OHE on full data...Training the classifier...FOLD: 0 | AUC = 0.785
我邀请感兴趣的读者阅读有关交叉验证的文章,以更详细地了解所示代码的功能。
现在让我们看看如何应用像XGBoost这样的树模型,它也适用于LabelEncoder。
def run(fold: int) -> None: features = [ f for f in df.columns if f not in ("id", "target", "kfold") ] for feature in features: df.loc[:, feature] = df[feature].astype(str).fillna("NONE") print("Fitting the LabelEncoder on the features...") for feature in features: le = preprocessing.LabelEncoder() le.fit(df[feature]) df.loc[:, feature] = le.transform(df[feature]) df_train = df[df["kfold"] != fold].reset_index(drop=True) df_valid = df[df["kfold"] == fold].reset_index(drop=True) x_train = df_train[features].values x_valid = df_valid[features].values print("Training the classifier...") model = xgboost.XGBClassifier(n_jobs=-1, n_estimators=300) model.fit(x_train, df_train.target.values) valid_preds = model.predict_proba(x_valid)[:, 1] auc = metrics.roc_auc_score(df_valid.target.values, valid_preds) print(f"FOLD: {fold} | AUC = {auc:.3f}")# 在2个组上执行for fold in range(2): run(fold)>>Fitting the LabelEncoder on the features...Training the classifier...FOLD: 0 | AUC = 0.768Fitting the LabelEncoder on the features...Training the classifier...FOLD: 1 | AUC = 0.765
结论
总之,还有其他值得一提的处理分类变量的技术:
- 基于目标的编码,其中将类别转换为与之对应的目标变量所假定的平均值
- 神经网络的嵌入,可用于表示文本实体
总之,以下是正确管理分类变量的基本步骤:
- 始终处理空值
- 根据变量类型和我们想要使用的模板应用LabelEncoder或OneHotEncoder
- 考虑NaN或NONE作为可以向模型提供信息的分类变量的变量丰富
- 对数据进行建模!
感谢您的时间,Andrea