Kaggle练习笔记之分类变量
数据来源为Housing Prices Competition for Kaggle Learn Users。
在纯练习部分其实很少会出现问题,但是这次的练习notebook提交预测结果只是optional,并没有直接给出使用测试集数据进行预测的部分,因此需要注意对测试集的预处理方式。
通用部分代码:
import pandas as pd
from sklearn.model_selection import train_test_split
# 读取数据
X = pd.read_csv('../input/train.csv', index_col='Id')
X_test = pd.read_csv('../input/test.csv', index_col='Id')
# 删除目标数据缺失的行,将目标从预测器中分离
X.dropna(axis=0, subset=['SalePrice'], inplace=True)
y = X.SalePrice
X.drop(['SalePrice'], axis=1, inplace=True)
# To keep things simple, 删除有缺失值的列(注意,并不是指定测试集中有缺失值的列)
cols_with_missing = [col for col in X.columns if X[col].isnull().any()]
X.drop(cols_with_missing, axis=1, inplace=True)
X_test.drop(cols_with_missing, axis=1, inplace=True)
# 拆分训练集和验证集
X_train, X_valid, y_train, y_valid = train_test_split(X, y,
train_size=0.8, test_size=0.2,
random_state=0)
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
# 用于比较不同处理方法的函数
def score_dataset(X_train, X_valid, y_train, y_valid):
model = RandomForestRegressor(n_estimators=100, random_state=0)
model.fit(X_train, y_train)
preds = model.predict(X_valid)
return mean_absolute_error(y_valid, preds)
Step 1:删除分类变量列
对X_train
和 X_valid
预处理,分别获取 drop_X_train
和 drop_X_valid
。
drop_X_train = X_train.select_dtypes(exclude=['object'])
drop_X_valid = X_valid.select_dtypes(exclude=['object'])
可以得到结果:
MAE from Approach 1 (Drop categorical variables):
17837.82570776256
问题1:训练集和验证集中分类变量的唯一值问题
以Condition2
列为例,训练集和验证集中的唯一值存在差异,这样会导致验证集部分数据无法按照训练集的序号编码而报错。
Unique values in 'Condition2' column in training data: ['Norm' 'PosA' 'Feedr' 'PosN' 'Artery' 'RRAe']
Unique values in 'Condition2' column in validation data: ['Norm' 'RRAn' 'RRNn' 'Artery' 'Feedr' 'PosN']
Step 2:序号编码
Part A:预处理 唯一值问题列
将序号编码器拟合到训练数据中的列会为训练数据中出现的每个唯一值创建一个相应的整数值标签。 如果验证数据包含未出现在训练数据中的值,编码器将抛出错误,因为这些值没有分配给它们的整数。而验证集中Condition2
列包含了训练集中没有的 'RRAn'
and 'RRNn'
,需要先处理。
这个通用问题有多种处理方法,例如写一个自定义的编码器处理新的分类,最简单的方法时删除这种问题列。
处理:将有问题的列存进坏标签列表,安全分类变量的列存进好标签列表。
# 训练集中的分类变量列
object_cols = [col for col in X_train.columns if X_train[col].dtype == "object"]
# 可以安全编码的列
good_label_cols = [col for col in object_cols if
set(X_valid[col]).issubset(set(X_train[col]))]
# 将被删除的问题列
bad_label_cols = list(set(object_cols)-set(good_label_cols))
Part B:序号编码
从训练集中删除坏标签列,对剩余的好标签列进行序号编码。
将处理好的数据分别设为 label_X_train
和 label_X_valid
。
from sklearn.preprocessing import OrdinalEncoder
# 删除不会被编码的列
label_X_train = X_train.drop(bad_label_cols, axis=1)
label_X_valid = X_valid.drop(bad_label_cols, axis=1)
# 应用序号编码器
ordinal_encoder = OrdinalEncoder()
label_X_train[good_label_cols] = ordinal_encoder.fit_transform(label_X_train[good_label_cols])
label_X_valid[good_label_cols] = ordinal_encoder.transform(label_X_valid[good_label_cols])
可以得到结果(比Approach 1好很多):
MAE from Approach 2 (Ordinal Encoding):
17098.01649543379
问题2:分类变量的基数问题
# Get number of unique entries in each column with categorical data
object_nunique = list(map(lambda col: X_train[col].nunique(), object_cols))
d = dict(zip(object_cols, object_nunique))
# Print number of unique entries by column, in ascending order
sorted(d.items(), key=lambda x: x[1])
每一个分类变量列的唯一值个数如下:
[('Street', 2),
('Utilities', 2),
('CentralAir', 2),
('LandSlope', 3),
('PavedDrive', 3),
('LotShape', 4),
('LandContour', 4),
('ExterQual', 4),
('KitchenQual', 4),
('MSZoning', 5),
('LotConfig', 5),
('BldgType', 5),
('ExterCond', 5),
('HeatingQC', 5),
('Condition2', 6),
('RoofStyle', 6),
('Foundation', 6),
('Heating', 6),
('Functional', 6),
('SaleCondition', 6),
('RoofMatl', 7),
('HouseStyle', 8),
('Condition1', 9),
('SaleType', 9),
('Exterior1st', 15),
('Exterior2nd', 16),
('Neighborhood', 25)]
Step 3:基数调查
Part A:基数理解
根据上方的数据,训练集中cardinality
基数大于10的列有3个,Neighborhood
变量进行one-hot
编码需要25列。
Part B:基数选择
对于具有多行的大型数据集,one-hot编码会大大扩展数据集大小。
例如一个数据集拥有10000行数据和100基数的分类变量,one-hot编码后会多出10000*100-10000个条目,而序号编码不会增加条目数。
因此,选择小于十的低基数列进行one-hot编码,高基数列直接删除或采用序号编码。
# Columns that will be one-hot encoded
low_cardinality_cols = [col for col in object_cols if X_train[col].nunique() < 10]
# Columns that will be dropped from the dataset
high_cardinality_cols = list(set(object_cols)-set(low_cardinality_cols))
Step 4:One-hot编码
将处理好的数据分别设为 OH_X_train
和 OH_X_valid
,再编码。
所有的分类变量列表:object_cols
待编码的分类变量列表: low_cardinality_cols
其他分类变量列表将被删除。
from sklearn.preprocessing import OneHotEncoder
# 将独热编码应用到分类变量的列
OH_encoder = OneHotEncoder(handle_unknown='ignore', sparse=False)
OH_cols_train = pd.DataFrame(OH_encoder.fit_transform(X_train[low_cardinality_cols]))
OH_cols_valid = pd.DataFrame(OH_encoder.transform(X_valid[low_cardinality_cols]))
# 编码时删除了索引,需要重新加上
OH_cols_train.index = X_train.index
OH_cols_valid.index = X_valid.index
# 删除分类变量列,全为原始数值列
num_X_train = X_train.drop(object_cols, axis=1)
num_X_valid = X_valid.drop(object_cols, axis=1)
# 添加独热编码后的列到数值特征
OH_X_train = pd.concat([num_X_train, OH_cols_train], axis=1)
OH_X_valid = pd.concat([num_X_valid, OH_cols_valid], axis=1)
结果如下(比Approach 2 差一些):
MAE from Approach 3 (One-Hot Encoding):
17525.345719178084
生成测试集预测
虽然从MAE结果看,序号编码效果更好,但是想先试试one-hot编码,尝试如下:
通用预测代码部分:
# 创建随机森林模型进行预测
model = RandomForestRegressor(n_estimators=100, random_state=0)
model.fit(OH_X_train, y_train)
# 进行预测
preds_test = model.predict(OH_X_test)
# Save test predictions to file
output = pd.DataFrame({'Id': X_test.index,
'SalePrice': preds_test})
output.to_csv('submission.csv', index=False)
X_test
存在缺失值,直接使用模型预测会报错:
ValueError: Input contains NaN, infinity or a value too large for dtype('float32').
尝试1:删除缺失值所在行数据
# 删除缺失值所在行数据
X_test.dropna(inplace=True)
# 直接one-hot编码
OH_cols_test = pd.DataFrame(OH_encoder.transform(X_test[low_cardinality_cols]))
OH_cols_test.index = X_test.index
num_X_test = X_test.drop(object_cols, axis=1)
OH_X_test = pd.concat([num_X_test, OH_cols_test], axis=1)
X_test
总行数为1459,删除部分行之后,提交报错
Evaluation Exception: Submission must have 1459 rows
尝试2:缺失值插补(编码后)
查看X_test的缺失值统计,数值列和分类变量列均有缺失,可以考虑一下是在one-hot编码前插补还是编码后插补。
编码后插补的情况,对于one-hot编码,中位数插补最多只有一个为1,其余全为0。
得分为16512.64549
。
# 缺失值处理
from sklearn.impute import SimpleImputer
my_imputer = SimpleImputer(strategy="median")
my_imputer.fit(OH_X_train)
imputed_OH_X_test = pd.DataFrame(my_imputer.transform(OH_X_test))
imputed_OH_X_test.columns = OH_X_test.columns
preds_test = model.predict(imputed_OH_X_test)
尝试3:缺失值插补(编码前)
编码前插补,需要区分object列和数值列
num_X_test = X_test.select_dtypes(exclude=['object'])
obj_X_test = X_test.select_dtypes('object')
对于数值列缺失值,中位数median
插补(参考Missing Values
结果)
对于分类变量列缺失值,众数most frequent
插补(需要非数值插补)
# 缺失值处理
from sklearn.impute import SimpleImputer
# 创建imputer
num_imputer = SimpleImputer(strategy='median')
obj_imputer = SimpleImputer(strategy='most_frequent')
# 使用训练集数据进行训练
num_imputer.fit(X_train.select_dtypes(exclude=['object']))
obj_imputer.fit(X_train.select_dtypes('object'))
# 对测试集缺失值进行插补
imputed_num_X_test = pd.DataFrame(num_imputer.transform(num_X_test))
imputed_obj_X_test = pd.DataFrame(obj_imputer.transform(obj_X_test))
# 补回列名
imputed_num_X_test.columns = num_X_test.columns
imputed_obj_X_test.columns = obj_X_test.columns
# 拼接数值列和非数值列
imputed_X_test = pd.concat([imputed_num_X_test, imputed_obj_X_test], axis=1)
再进行one-hot
编码:
OH_cols_test = pd.DataFrame(OH_encoder.transform(imputed_X_test[low_cardinality_cols]))
OH_cols_test.index = imputed_X_test.index
num_X_test = imputed_X_test.drop(object_cols, axis=1)
OH_X_test = pd.concat([num_X_test, OH_cols_test], axis=1)
preds_test = model.predict(OH_X_test)
得分为16513.55892
。
尝试4:序号编码
按照尝试3同样进行缺失值插补,问题在于X_test
和X_train
存在唯一值差异(参考问题2),直接忽略会造成很大的问题,看看后续有什么其他方法。