返回

测试集NaN值处理:策略与实践指南

python

测试集中 NaN 值的处理:策略与实践

数据预处理是机器学习模型开发的关键步骤,其中一个常见挑战便是处理数据中的缺失值,通常表示为 NaN (Not a Number)。 特别是在构建训练和测试集时,NaN 值的处理方式会显著影响模型性能。本文将探讨在测试集中是否应保留 NaN 值,以及应该如何有效处理。

问题剖析:为什么测试集会有 NaN 值?

在实际场景中,测试集中存在 NaN 值的原因多种多样:

  1. 数据采集问题 :传感器故障、人工录入错误、API 返回异常等,均会导致数据集中出现 NaN。
  2. 数据转换丢失 :在数据处理和清洗流程中,某些数据变换可能产生 NaN 值。 例如,字符串转数值、缺失值填充算法引入错误等。
  3. 与训练数据不一致 : 训练集和测试集来自不同的数据分布,有可能训练集没有出现缺失值,但是测试集中出现了。

出现 ValueError: Found unknown categories [nan] in column ... during transform 通常表明你的训练数据在执行 One-Hot Encoding (独热编码) 时没有 NaN 类别,而在测试集转换的时候出现了 NaN,这意味着模型未学习如何处理 NaN 这一个“类别”,自然会抛出错误。 这凸显了一个核心问题, 测试集的数据特征和格式要尽可能和训练集一致,特别是分类特征。

解决思路:多种方法处理 NaN

面对测试集中的 NaN,以下是一些处理方案。

1. 训练集测试集同步 Imputation(填充)

方法 : 在训练阶段,通过某种策略对训练集中的缺失值进行填充。 测试集使用相同的填充策略,以确保测试集中没有缺失值, 且处理方式与训练集一致。
操作步骤
* 选择一种合适的 Imputation 方法:如均值填充、中位数填充、众数填充、KNN 填充、特定值填充。 选择方法需要参考数据本身特性以及业务场景需求。
* 在训练集上使用.fit() 计算所需的统计量(如均值、中位数)。
* 训练集和测试集使用相同的 .transform() 填充。

代码示例 (Python/pandas)
```python
import pandas as pd
from sklearn.impute import SimpleImputer

# 模拟训练集和测试集
train_data = {'feature1': [1, 2, float('nan'), 4, 5],
            'feature2': ['A', 'B', float('nan'), 'A', 'C']}
test_data  = {'feature1': [6, float('nan'), 8, 9, 10],
            'feature2': ['B', 'C', float('nan'), 'A', 'B']}

train_df = pd.DataFrame(train_data)
test_df = pd.DataFrame(test_data)


# 数值特征填充中位数
numerical_imputer = SimpleImputer(strategy='median')
train_df[['feature1']] = numerical_imputer.fit_transform(train_df[['feature1']])
test_df[['feature1']]  = numerical_imputer.transform(test_df[['feature1']])

 # 分类特征填充众数
categorical_imputer = SimpleImputer(strategy='most_frequent')
train_df[['feature2']] = categorical_imputer.fit_transform(train_df[['feature2']])
test_df[['feature2']]  = categorical_imputer.transform(test_df[['feature2']])


print("填充后的训练集:\n", train_df)
print("填充后的测试集:\n", test_df)
```

**操作步骤** 

1. 使用pandas 创建一个简单的含有 `NaN` 的DataFrame 用来模拟训练集和测试集。
2. 使用 sklearn 提供的 `SimpleImputer` 对象创建不同的 Imputer (填充器),数值特征采用中位数填充,分类特征使用众数填充。
3. 在训练集 `feature1` 列 使用 `fit_transform` 方法完成计算并填充, 并用相同的imputer 使用 `transform` 方法处理测试集的`feature1`。`fit_transform` 等效于先 `fit()` 然后再`transform()`。 `fit()` 计算需要的统计值, `transform()` 根据统计值完成填充。 分类特征同理。
4. 输出填充后的训练集和测试集。

注意: 需确保Imputation 的填充逻辑的一致性, 使用训练集的数据信息 fit() 计算,测试集使用相同的逻辑transform()填充。

2. 引入新的“未知”类别

方法 :对于类别型特征,当测试集中出现训练集未出现的 NaN 时,可以将其视为一个新的类别,例如 “Unknown” 或 “Missing”。 在进行 One-Hot Encoding 前,将测试集和训练集的缺失值都替换成同一个特定字符串。
操作步骤
* 将训练集和测试集的 NaN 值替换为 “Unknown” 或类似的占位符。
* 使用训练集执行One-Hot Encoding时,需保留缺失值的这个类别。
* 使用训练好的 One-Hot Encoding 模型在测试集上执行 transform.

代码示例(Python/pandas):

```python
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

# 模拟训练集和测试集
train_data = {'feature': ['A', 'B', float('nan'), 'A']}
test_data = {'feature': ['B', 'C', float('nan'], 'A', float('nan')]}

train_df = pd.DataFrame(train_data)
test_df = pd.DataFrame(test_data)

#  缺失值替换为Unknown
train_df['feature'].fillna("Unknown", inplace=True)
test_df['feature'].fillna("Unknown", inplace=True)


# 创建OneHotEncoder 对象
ohe = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
train_encoded = ohe.fit_transform(train_df[['feature']]) # 先训练
test_encoded = ohe.transform(test_df[['feature']]) #  在测试集上 transform


print("编码后的训练集:\n", train_encoded)
print("编码后的测试集:\n", test_encoded)


```
**操作步骤** 

  1. 创建模拟的包含 `NaN`的dataframe 作为训练集和测试集。
  2. 使用 `pandas.fillna` 将训练集和测试集中的缺失值 `NaN` 统一替换为 `Unknown`。
  3. 使用`OneHotEncoder` 创建 One-Hot 编码器对象,并指定`handle_unknown='ignore'` 处理未知类别(训练集未出现的类别)。 `sparse_output=False` 设置输出为 `numpy.array`而非稀疏矩阵。
  4. 使用 `fit_transform` 在训练集上学习转换,同时输出训练集编码后的结果。
  5.  在测试集上直接使用 `transform` 完成测试集的编码。
  6.  输出训练集和测试集的编码结果。

**注意:**  设置 `handle_unknown='ignore'` 在执行`transform()`操作时会忽略掉训练集不存在的类别, 可能会导致一些信息丢失,但可以保证程序顺利执行。另外可以使用 `ohe.categories_` 查看 `fit()` 得到的类别。

3. 特征工程: 增加指示缺失的特征

方法描述: 除直接替换或忽略,还可以使用一个额外二进制特征,标识该条数据在这个特征上是否存在缺失,然后在后续进行 Imputation 或者保留缺失值不处理。 这种方法,可以让模型知道那些地方是缺失值。
操作步骤:

*    为包含缺失值的特征添加一个指示器列(0 代表未缺失,1 代表缺失)。
*     针对新添加的缺失指示器特征做独热编码(如有需要)
*      按照既有的方式, 对缺失特征做 imputation 或 保留

代码示例(Python/pandas):

```python
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

# 模拟训练集和测试集
train_data = {'feature1': [1, 2, float('nan'), 4, 5],
              'feature2': ['A', 'B', float('nan'), 'A', 'C']}
test_data = {'feature1': [6, float('nan'), 8, 9, 10],
             'feature2': ['B', 'C', float('nan'), 'A', float('nan')]}

train_df = pd.DataFrame(train_data)
test_df = pd.DataFrame(test_data)

for feature in ['feature1','feature2']:

   train_df[feature + '_missing'] = train_df[feature].isnull().astype(int) # 生成指示特征
   test_df[feature + '_missing']  = test_df[feature].isnull().astype(int)   #生成指示特征

  #对缺失指示器独热编码
ohe = OneHotEncoder(sparse_output = False)
ohe.fit(pd.concat([train_df[[col + '_missing' for col in ['feature1','feature2'] ]], test_df[[col + '_missing' for col in ['feature1','feature2'] ]]], axis=0))

train_indicator_encoded = ohe.transform(train_df[[col + '_missing' for col in ['feature1','feature2'] ]])
test_indicator_encoded  = ohe.transform(test_df[[col + '_missing' for col in ['feature1','feature2'] ]])


train_df = pd.concat([train_df,pd.DataFrame(train_indicator_encoded)], axis = 1)
test_df = pd.concat([test_df, pd.DataFrame(test_indicator_encoded)],axis = 1 )


print("包含指示特征的训练集:\n", train_df)
print("包含指示特征的测试集:\n", test_df)
```

操作步骤:
1. 创造模拟含有NaN 的DataFrame 用来表示训练集和测试集。
2. 对训练集和测试集中存在的特征 feature1feature2, 增加对应的缺失指示列(形如: feature1_missing) 。 使用 pandas的 isnull() 判断缺失,使用astype(int)bool 类型转换成 int, 方便后续处理。
3. 使用 OneHotEncoder 对所有数据里的缺失指示列做 OneHotEncode
4. 将独热编码后的新列合并到原始的数据集。
5. 打印训练集和测试集

**注意:**  指示列本身也变成模型的一个特征,允许模型学习到缺失对结果可能带来的影响。 使用 `ohe.fit()`的时候一定要把训练集和测试集拼接起来, 确保 `OneHotEncoder` 的行为一致。

结论

在机器学习流程中,处理测试集的 NaN 值需要综合考虑多个方面。 没有一种通用的最佳方法,最佳实践往往依赖于具体的数据、模型、以及业务场景。通常,同步 Imputation, 新增未知类别, 添加缺失指示特征, 这三种方法都是常见的应对测试集中出现 NaN 的策略。处理数据需要保持耐心细致,根据实际情况灵活调整处理方法,保证模型学习效果的最大化。