这是 Kaggle Playground Series 在24年8月的一场比赛:
赛题:Binary Prediction of Poisonous Mushrooms
比赛的内容较为简单,是有关蘑菇毒性的二元分类问题。比赛的难点在于数据集样本的特征中存在较多缺失值,同时,特征的描述为符号语言,也及到对文本特征进行编码的问题。这场比赛是学习数据预处理的良好契机,推荐 kaggle beginner 积极尝试。
数据概览 拿到数据后,我们首先读取数据,打印并观察数据集特征维数、含义等基础信息。
1 2 3 4 train_data = pd.read_csv("/kaggle/input/playground-series-s4e8/train.csv" ) test_data = pd.read_csv("/kaggle/input/playground-series-s4e8/test.csv" ) print (train_data.head(5 ), test_data.head(5 ))
注意到数据除了特征之外,有列名为 id
的一列,这是数据集样本本身的编号,对分类问题没有直接帮助,我们将其 drop
掉。
1 2 train_data = train_data.drop(columns=['id' ]) test_data = test_data.drop(columns=['id' ])
完成后,我们打印 DataFrame 的 info,查看特征标签和对应数据类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <class 'pandas.core.frame.DataFrame'> RangeIndex: 3116945 entries, 0 to 3116944 Data columns (total 21 columns): # Column Dtype --- ------ ----- 0 class object 1 cap-diameter float64 2 cap-shape object 3 cap-surface object 4 cap-color object 5 does-bruise-or-bleed object 6 gill-attachment object 7 gill-spacing object 8 gill-color object 9 stem-height float64 10 stem-width float64 11 stem-root object 12 stem-surface object 13 stem-color object 14 veil-type object 15 veil-color object 16 has-ring object 17 ring-type object 18 spore-print-color object 19 habitat object 20 season object dtypes: float64(3), object(18) memory usage: 499.4+ MB
对于数据较多的 DataFrame ,更推荐使用 describe
方法查看数据的统计特征。
观察到其中大多数特征都是 object
数据类型,一般是字符串或者空值,这些会在后续处理。
我们将这些特征为 object
类型的特征名提取出来,查看他们的成员情况。
1 2 3 4 categorical_columns = test_data.select_dtypes(include=['object' ]).columns unique_values = {col: test_data[col].nunique() for col in categorical_columns} for col, unique_count in unique_values.items(): print (f"{col} : {unique_count} unique values" )
output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class: 2 unique values cap-shape: 74 unique values cap-surface: 83 unique values cap-color: 78 unique values does-bruise-or-bleed: 26 unique values gill-attachment: 78 unique values gill-spacing: 48 unique values gill-color: 63 unique values stem-root: 38 unique values stem-surface: 60 unique values stem-color: 59 unique values veil-type: 22 unique values veil-color: 24 unique values has-ring: 23 unique values ring-type: 40 unique values spore-print-color: 32 unique values habitat: 52 unique values season: 4 unique values
同理查看测试集上的 unique values,这有助于我们对数据集整体结构有个大概的认知。
查看特征集合。
1 print (train_data.columns, test_data.columns)
output:
1 2 3 4 5 6 7 8 9 10 11 12 (Index(['class', 'cap-diameter', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color', 'stem-height', 'stem-width', 'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color', 'habitat', 'season'], dtype='object'), Index(['cap-diameter', 'cap-shape', 'cap-surface', 'cap-color', 'does-bruise-or-bleed', 'gill-attachment', 'gill-spacing', 'gill-color', 'stem-height', 'stem-width', 'stem-root', 'stem-surface', 'stem-color', 'veil-type', 'veil-color', 'has-ring', 'ring-type', 'spore-print-color', 'habitat', 'season'], dtype='object'))
对数据集有个大致的了解后,我们着手开始处理缺失值。
缺失值处理 我们查看训练集和测试集中每一个特征的缺失值占比,并打印其中大于 0 的内容。
1 2 3 4 5 6 7 8 missing_train = train_data.isna().mean() * 100 missing_test = test_data.isna().mean() * 100 print ("percentage of missing train_data:" )print (missing_train[missing_train > 0 ])print ("percentage of missing test_data:" )print (missing_test[missing_test > 0 ])
output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 percentage of missing train_data: cap-diameter 0.000128 cap-shape 0.001283 cap-surface 21.528227 cap-color 0.000385 does-bruise-or-bleed 0.000257 gill-attachment 16.809280 gill-spacing 40.373988 gill-color 0.001829 stem-root 88.452732 stem-surface 63.551362 stem-color 0.001219 veil-type 94.884350 veil-color 87.936970 has-ring 0.000770 ring-type 4.134818 spore-print-color 91.425482 habitat 0.001444 dtype: float64 percentage of missing test_data: cap-diameter 0.000337 cap-shape 0.001492 cap-surface 21.506821 cap-color 0.000626 does-bruise-or-bleed 0.000481 gill-attachment 16.834796 gill-spacing 40.404694 gill-color 0.002358 stem-height 0.000048 stem-root 88.452543 stem-surface 63.595327 stem-color 0.001011 veil-type 94.878689 veil-color 87.880445 has-ring 0.000914 ring-type 4.148051 spore-print-color 91.417224 habitat 0.001203 dtype: float64
可以看到,有些列的缺失值占比达到百分之五十以上,更有甚者达到百分之94+。
对于缺失值太多的特征,插值往往不会有很好的效果,我们设置一个阈值,将缺失比例高于阈值的特征列进行 drop.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 missing_threshold = 0.95 high_missing_columns = train_data.columns[train_data.isnull().mean() > missing_threshold] train_data = train_data.drop(columns=high_missing_columns) test_data = test_data.drop(columns=high_missing_columns) target = 'class' for column in train_data.columns: if train_data[column].isnull().any (): if train_data[column].dtype == 'object' : mode_value = train_data[column].mode()[0 ] train_data[column].fillna(mode_value, inplace=True ) test_data[column].fillna(mode_value, inplace=True ) else : median_value = train_data[column].median() train_data[column].fillna(median_value, inplace=True ) test_data[column].fillna(median_value, inplace=True )
在这些代码中,我们丢弃了缺失值高于 missing_threshold
的特征列。然后,为了保持训练集和测试集插值的统一性,我们以训练集为参考对象,每次选取一个包含缺失值的特征列,如果该列的数据类型为 object
,我们提取其中出现次数最多的元素作为插值元素,并使用 fillna
方法,对训练集与测试集的该特征进行插值。
到这里我们完成了对缺失值的初步处理,需要注意的是,训练集和测试集中有缺失值的特征不一定相同,我们以训练集为插值参考对象插值后,测试集可能仍然存在缺失值特征没有被插值,这一类情况在后续讨论。
K近邻插值 是一种基于实例的学习方法,通过找到与缺失值最接近的 K 个邻居数据点,并根据这些邻居的特征估算缺失值。我们使用 KNN 插值对数据集做进一步处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from sklearn.impute import KNNImputerdef knn_impute (df, n_neighbors=5 ): df_encoded = df.copy() for col in df_encoded.select_dtypes(include='object' ).columns: df_encoded[col] = df_encoded[col].astype('category' ).cat.codes knn_imputer = KNNImputer(n_neighbors=n_neighbors) df_imputed = pd.DataFrame(knn_imputer.fit_transform(df_encoded), columns=df_encoded.columns) for col in df.select_dtypes(include='object' ).columns: df_imputed[col] = df_imputed[col].round ().astype(int ).map ( dict (enumerate (df[col].astype('category' ).cat.categories)) ) return df_imputed train_data_imputed = knn_impute(train_data, n_neighbors=5 ) test_data_imputed = knn_impute(test_data, n_neighbors=5 ) train_data_imputed.head(5 ), test_data_imputed.head(5 )
到这一步,我们已经填充了训练集和测试集所有的缺失值,是时候对特征列进行编码表示了。
特征向量编码 我们将缺失值处理后的训练数据、测试数据数据框中的类别型特征转换为数值型。
首先,我们选择类别型特征,同时过滤掉 class
,后面单独编码类标签。
1 2 cat_cols_train = train_data_imputed.select_dtypes(include=['object' ]).columns cat_cols_train = cat_cols_train[cat_cols_train != 'class' ]
Ordinal Encoding 通过给每个类别分配唯一整数,将分类数据转换为数值数据。它的有点事保持特征空间紧凑,像 One-Hot-Encoding 则会显著增加数据集的维度。
1 2 ordinal_encoder = OrdinalEncoder(handle_unknown='use_encoded_value' , unknown_value=-1 )
我们在训练集,测试集上应用转换。
1 2 train_data_imputed[cat_cols_train] = ordinal_encoder.fit_transform(train_data_imputed[cat_cols_train].astype(str )) test_data_imputed[cat_cols_train] = ordinal_encoder.transform(test_data_imputed[cat_cols_train]).astype(float )
fit_transform 计算数据的统计信息,并将这些信息应用于数据的转换,而 transform 将之前计算得到的统计信息应用到新数据上,而不重新计算参数
上述操作保持了训练集和测试集的一致性。
到这一步,我们完成了对特征向量的数值编码,可以查看一下数据类型来确认。
1 train_data_imputed.info()
output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <class 'pandas.core.frame.DataFrame'> RangeIndex: 3116945 entries, 0 to 3116944 Data columns (total 21 columns): # Column Dtype --- ------ ----- 0 class object 1 cap-diameter float64 2 cap-shape float64 3 cap-surface float64 4 cap-color float64 5 does-bruise-or-bleed float64 6 gill-attachment float64 7 gill-spacing float64 8 gill-color float64 9 stem-height float64 10 stem-width float64 11 stem-root float64 12 stem-surface float64 13 stem-color float64 14 veil-type float64 15 veil-color float64 16 has-ring float64 17 ring-type float64 18 spore-print-color float64 19 habitat float64 20 season float64 dtypes: float64(20), object(1) memory usage: 499.4+ MB
将处理好的数据赋值给原始数据框。
1 2 train_data = train_data_imputed test_data = test_data_imputed
在建模之前,我们还需要将训练集的类标签转换为数值编码:
1 2 le = LabelEncoder() train_data['class' ] = le.fit_transform(train_data['class' ])
到这里,我们已经完成了所有的数据预处理工作,可以开始建模。
模型建立 训练集-测试集划分 1 2 3 4 5 6 7 8 from sklearn.model_selection import train_test_splity = train_data['class' ] X = train_data.drop(['class' ], axis=1 ) train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=0.2 , random_state=42 , stratify=y)
马修斯相关系数 使用马修斯相关系数作为模型性能的评价指标。
马修斯相关系数(Matthews Correlation Coefficient,MCC)是一种用于评估二分类模型性能的指标。它考虑了混淆矩阵中的所有四个类别(真正例、真负例、假正例、假负例)的计数,并对它们进行综合评估。
MCC 的值范围从 -1 到 1,其中”
1 表示完美预测
0 表示模型性能等同于随机预测
-1 表示模型预测与真实情况完全相反
1 2 3 4 5 6 7 from sklearn.metrics import matthews_corrcoefdef mcc_metric (y_pred, dmatrix ): y_true = dmatrix.get_label() y_pred = (y_pred > 0.5 ).astype(int ) mcc = matthews_corrcoef(y_true, y_pred) return 'mcc' , mcc
XGBoost 一篇介绍 XGBoost 的好博客 [link]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from xgboost import XGBClassifiermodel = XGBClassifier( alpha=0.1 , subsample=0.8 , colsample_bytree=0.6 , objective="binary:logistic" , max_depth=14 , min_child_weight=7 , gamma=1e-6 , random_state=42 , n_estimators=100 ) XGB = model.fit( train_X, train_y, eval_set=[(test_X, test_y)], eval_metric=mcc_metric )
sckit-learn 提供了 XGBoost 实现,我们可以通过简单的调用 api 来便捷地训练一个拟合现有数据的梯度提升决策树。
预测结果 预测标签的数值编码:
1 test_pred_prob = XGB.predict(test_data)
还原到原始文本标签:
1 test_pred_class = le.inverse_transform(test_pred_prob)
将数据导出在 csv 中:
1 2 3 4 df_sub = pd.read_csv('/kaggle/input/playground-series-s4e8/sample_submission.csv' ) df_sub['class' ] = test_pred_class df_sub.to_csv('submission.csv' , index = False )
到这里,我们已经将结果输出在 kaggle kernel 的 output 空间,可以提交结果看看评分啦。