在使用sklearn.preprocessing.LabelEncoder对分类特征进行数值化编码时,一个常见的挑战是当测试集中出现训练集中未曾见过的标签时,会引发ValueError: y contains previously unseen labels。此错误的核心在于LabelEncoder的工作机制:它在fit阶段学习并映射训练数据中的所有唯一标签到整数。如果在transform阶段遇到一个在fit时未见过的标签,由于没有对应的映射规则,便会抛出此错误。与OneHotEncoder等其他编码器不同,LabelEncoder本身并没有内置的handle_unknown参数来直接处理这种情况。
问题分析:为何会出现“未知标签”错误?
在机器学习项目中,通常会将数据集划分为训练集和测试集。当对分类特征进行编码时,一个常见的误区是分别对训练集和测试集进行编码,或者仅在训练集上拟合编码器,然后直接应用于测试集。
考虑以下伪代码片段,它展示了导致问题的典型操作模式:
import pandas as pd from sklearn.preprocessing import LabelEncoder # 假设 train_data 和 test_data 是 MultiIndex DataFrame # 假设 categoricals 是需要编码的列名列表,例如 ['TRAITS', 'UNIT_1'] le = {col: LabelEncoder() for col in categoricals} # 错误的编码方式示例 # train_data 和 test_data 结构类似原始问题中的 MultiIndex # train_data[(col, sub_col)] 是一个 Series # test_data[(col, sub_col)] 是一个 Series for col_level0 in train_data.columns.levels[0]: # 例如 'TEAM ONE', 'TEAM TWO' for col_level1 in train_data.columns.levels[1]: # 例如 'TRAITS', 'UNIT_1' if col_level1 in le: # 如果当前二级列名是需要编码的分类列 # 在训练数据上拟合并转换 train_data[(col_level0, col_level1)] = le[col_level1].fit_transform( train_data[(col_level0, col_level1)].astype(str) ) # 在测试数据上直接转换 test_data[(col_level0, col_level1)] = le[col_level1].transform( test_data[(col_level0, col_level1)].astype(str) )
上述代码的问题在于,对于每个sub_col(例如TRAITS),LabelEncoder实例le[sub_col]在循环内部的train_data[(col_level0, sub_col)].fit_transform()这一行被重复拟合。这意味着,当col_level0从’TEAM ONE’变为’TEAM TWO’时,le[‘TRAITS’]会再次被拟合,其内部的标签映射会被train_data[(‘TEAM TWO’, ‘TRAITS’)]中的标签覆盖或更新。更严重的是,当处理test_data时,如果test_data[(col_level0, sub_col)]中包含任何在le[sub_col]最后一次fit操作(即在train_data[(last_col_level0, sub_col)]上拟合)中未曾见过的标签,就会触发ValueError。
理想情况下,LabelEncoder应该在看到所有可能的标签后进行一次性拟合,无论这些标签出现在训练集还是测试集中。
解决方案:在完整数据集上拟合编码器
解决ValueError的关键在于确保LabelEncoder在执行任何转换之前,已经学习了所有可能出现的标签,包括训练集和测试集中的标签。最直接有效的方法是,对于每一个需要编码的分类列,收集其在训练集和测试集中的所有唯一值,然后用这些唯一值来拟合对应的LabelEncoder。
以下是针对MultiIndex DataFrame的解决方案步骤:
- 确定需要编码的分类列。
- 为每个分类列初始化一个LabelEncoder实例。
-
对于每个分类列:
- 收集所有唯一标签: 遍历训练集和测试集中所有相关(相同二级列名)的MultiIndex列,将这些列中的所有唯一值汇集起来。
- 拟合编码器: 使用汇集到的所有唯一标签来拟合对应的LabelEncoder实例。
- 转换数据: 使用已经拟合好的编码器,分别转换训练集和测试集中的相应列。
示例代码
为了清晰地演示,我们首先创建模拟的MultiIndex DataFrame数据。
import pandas as pd from sklearn.preprocessing import LabelEncoder import numpy as np # 1. 创建模拟的 MultiIndex DataFrame 数据 # 定义需要编码的分类列(二级列名) categoricals = ['TRAITS', 'UNIT_1', 'AUGMENT_1'] # 模拟训练数据 train_data_raw = { ('TEAM ONE', 'TRAITS'): ['A', 'B', 'C', 'A', 'B'], ('TEAM ONE', 'UNIT_1'): ['X', 'Y', 'Z', 'X', 'Y'], ('TEAM ONE', 'AUGMENT_1'): ['P', 'Q', 'R', 'P', 'Q'], ('TEAM TWO', 'TRAITS'): ['A', 'C', 'D', 'A', 'C'], ('TEAM TWO', 'UNIT_1'): ['X', 'Z', 'W', 'X', 'Z'], ('TEAM TWO', 'AUGMENT_1'): ['P', 'R', 'S', 'P', 'R'], } train_df = pd.DataFrame(train_data_raw) train_df.columns = pd.MultiIndex.from_tuples(train_df.columns) # 模拟测试数据 (故意包含训练集未见的标签,如 'E', 'F', 'V', 'T', 'G', 'H', 'U', 'K') test_data_raw = { ('TEAM ONE', 'TRAITS'): ['A', 'B', 'E', 'A', 'F'], ('TEAM ONE', 'UNIT_1'): ['X', 'Y', 'V', 'X', 'Y'], ('TEAM ONE', 'AUGMENT_1'): ['P', 'Q', 'T', 'P', 'Q'], ('TEAM TWO', 'TRAITS'): ['A', 'C', 'G', 'A', 'H'], ('TEAM TWO', 'UNIT_1'): ['X', 'Z', 'U', 'X', 'Z'], ('TEAM TWO', 'AUGMENT_1'): ['P', 'R', 'K', 'P', 'R'], } test_df = pd.DataFrame(test_data_raw) test_df.columns = pd.MultiIndex.from_tuples(test_df.columns) print("原始训练数据:\n", train_df) print("\n原始测试数据:\n", test_df) # 2. 初始化 LabelEncoder 字典 le = {col: LabelEncoder() for col in categoricals} # 3. 正确的编码方法:在所有数据上拟合编码器 train_df_encoded = train_df.copy() test_df_encoded = test_df.copy() for sub_col_name in categoricals: # 收集当前二级列名在所有一级列(TEAM ONE, TEAM TWO)和所有数据集(train, test)中的所有唯一值 all_values_for_sub_col = pd.Series(dtype='object') for team_col in train_df.columns.levels[0]: # 遍历一级列名,如 'TEAM ONE', 'TEAM TWO' # 检查 MultiIndex 列是否存在,以防数据不完整 if (team_col, sub_col_name) in train_df.columns: all_values_for_sub_col = pd.concat([all_values_for_sub_col, train_df[(team_col, sub_col_name)].astype(str)]) if (team_col, sub_col_name) in test_df.columns: all_values_for_sub_col = pd.concat([all_values_for_sub_col, test_df[(team_col, sub_col_name)].astype(str)]) # 使用所有收集到的唯一值来拟合对应的 LabelEncoder # .unique() 确保只传递唯一值,提高效率 le[sub_col_name].fit(all_values_for_sub_col.unique()) # 4. 使用拟合好的编码器转换训练集和测试集 for team_col in train_df_encoded.columns.levels[0]: if (team_col, sub_col_name) in train_df_encoded.columns: train_df_encoded[(team_col, sub_col_name)] = le[sub_col_name].transform( train_df_encoded[(team_col, sub_col_name)].astype(str) ) if (team_col, sub_col_name) in test_df_encoded.columns: test_df_encoded[(team_col, sub_col_name)] = le[sub_col_name].transform( test_df_encoded[(team_col, sub_col_name)].astype(str) ) print("\n编码后的训练数据 (正确方法):\n", train_df_encoded) print("\n编码后的测试数据 (正确方法):\n", test_df_encoded) # 验证逆转换(可选) # print("\n逆转换示例 (TEAM ONE, TRAITS 在测试数据中):") # print(le['TRAITS'].inverse_transform(test_df_encoded[('TEAM ONE', 'TRAITS')]))
在上述代码中,我们首先遍历categoricals列表中的每个二级列名(例如’TRAITS’)。然后,对于’TRAITS’这个列,我们从train_df和test_df中所有一级列(’TEAM ONE’和’TEAM TWO’)下的’TRAITS’列中收集所有的唯一值。一旦收集到所有可能的标签,我们便使用le[‘TRAITS’].fit()来拟合编码器。此后,无论是对训练集还是测试集中的’TRAITS’列进行转换,le[‘TRAITS’].transform()都能正确地处理,因为它已经“见过”所有可能的标签。
注意事项与最佳实践
- 数据类型转换 (.astype(str)): 在应用LabelEncoder之前,通常建议将目标列转换为字符串类型 (.astype(str))。这可以避免因列中存在混合数据类型(例如字符串和数字)或NaN值而导致的潜在错误或意外行为。LabelEncoder默认情况下对NaN的处理可能不是预期行为,将其转换为字符串通常能统一处理。
- 数据泄漏(Data Leakage): 尽管为了LabelEncoder的目的将训练集和测试集数据合并以拟合编码器是必要的且安全的,但在其他预处理步骤(如特征缩放、缺失值填充)中,直接合并数据可能会导致数据泄漏。数据泄漏是指模型在训练阶段接触到了测试集的信息,从而导致对模型性能的乐观估计。对于LabelEncoder,由于它只学习标签的映射关系,而不是统计属性,因此这种合并通常不会导致数据泄漏。
- 替代方案: 如果在某些情况下,测试集中出现真正的“未知”标签(即这些标签在训练集和任何已知数据集中都未出现),且您希望将其视为一个特殊类别而不是抛出错误,可以考虑使用sklearn.preprocessing.OrdinalEncoder。OrdinalEncoder提供了handle_unknown=’use_encoded_value’和unknown_value参数,允许您为未知标签指定一个特定的编码值(例如-1),从而避免错误。然而,对于LabelEncoder,上述的“预先拟合所有已知标签”策略
暂无评论内容