跳至主要内容

Kaggle 机器学习:特征工程

了解在 Kaggle 构建机器学习模型时,特征工程如何助您更进一步:创建新列、转换变量等!
· 14分钟 阅读

使用 AI 探索

在前两个 Kaggle 教程中,您已经学习了如何通过探索性数据分析和基线机器学习模型将数据整理为可用于构建第一个机器学习模型的形式。接着,您成功构建了第一个机器学习模型——决策树分类器。您将这些模型都提交到了 Kaggle,并对其准确率进行了分析。

在第三个教程中,您将进一步学习特征工程。它是一个利用您对数据的领域知识,创建额外且相关的特征,从而提升算法预测能力并让您的机器学习模型表现更佳的过程!

更具体地说,

开始上手!

在正式开始之前,像上一期教程那样,先完成所有导入,使用 IPython 魔法命令确保图形在 Jupyter Notebook 内联显示,并设置可视化样式。接下来导入数据,并确保将训练数据中的目标变量安全地单独存放。之后,将训练集和测试集合并(但不包含 df_train 的 'Survived' 列),并将结果存入 data。

请记住,这样做是为了确保您对数据进行的任意预处理,都能同时体现在训练集和测试集中!

最后,使用 .info() 方法查看数据概况:

# Imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import re
import numpy as np
from sklearn import tree
from sklearn.model_selection import GridSearchCV

# Figures inline and set visualization style
%matplotlib inline
sns.set()

# Import data
df_train = pd.read_csv('data/train.csv')
df_test = pd.read_csv('data/test.csv')

# Store target variable of training data in a safe place
survived_train = df_train.Survived

# Concatenate training and test sets
data = pd.concat([df_train.drop(['Survived'], axis=1), df_test])

# View head
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 11 columns):
PassengerId    1309 non-null int64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1046 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1308 non-null float64
Cabin          295 non-null object
Embarked       1307 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 122.7+ KB

为什么要做特征工程?

进行特征工程是为了从数据中提取更多信息,以便在构建模型时更胜一筹。

泰坦尼克号乘客的称谓

我们通过一个示例来看看这意味着什么。使用 .tail() 方法查看 'Name' 列,看看数据最后五行是什么样的:

# View head of 'Name' column
data.Name.tail()
413              Spector, Mr. Woolf
414    Oliva y Ocana, Dona. Fermina
415    Saether, Mr. Simon Sivertsen
416             Ware, Mr. Frederick
417        Peter, Master. Michael J
Name: Name, dtype: object

您会突然注意到不同的称谓!换句话说,这一列包含了包含称谓的字符串或文本,例如“Mr”“Master”和“Dona”。

这些称谓当然能反映社会地位、职业等信息,而这些最终可能与生存有关。

乍一看,将姓名与称谓分开似乎很难,但别慌!记住,您可以轻松用正则表达式提取称谓,并将其存入新列 'Title':

# Extract Title from Name, store in column and plot barplot
data['Title'] = data.Name.apply(lambda x: re.search(' ([A-Z][a-z]+)\.', x).group(1))
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

bar chart

注意,这个新的 'Title' 列实际上就是您数据集的新特征!

小贴士:想进一步了解正则表达式,请查看我对上一次FB Live 实时编程活动的记录,或参阅 DataCamp 的Python 正则表达式教程。

从上面的图可以看到,有不少不同的称谓,而且很多出现频次并不高。因此,将它们合并为更少的类别是有意义的。

例如,您或许希望将 'Mlle' 和 'Ms' 替换为 'Miss',将 'Mme' 替换为 'Mrs',因为这些是法语称谓,而理想情况下,您希望所有数据使用同一种语言。接下来,再把一些暂时无法立即分类的称谓合到一个叫 'Special' 的桶中。

小贴士:可以多尝试不同分组方式,看看算法表现如何随之变化!

然后,使用 .countplot() 方法绘制结果的柱状图:

data['Title'] = data['Title'].replace({'Mlle':'Miss', 'Mme':'Mrs', 'Ms':'Miss'})
data['Title'] = data['Title'].replace(['Don', 'Dona', 'Rev', 'Dr',
                                            'Major', 'Lady', 'Sir', 'Col', 'Capt', 'Countess', 'Jonkheer'],'Special')
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

bar chart

这就是新构造的特征 'Title' 的样子!

现在,确认已经有了 'Title' 列,并再次使用 .tail() 方法查看数据:

# View head of data
data.tail()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitle
41313053Spector, Mr. WoolfmaleNaN00A.5. 32368.0500NaNSMr
41413061Oliva y Ocana, Dona. Ferminafemale39.000PC 17758108.9000C105CSpecial
41513073Saether, Mr. Simon Sivertsenmale38.500SOTON/O.Q. 31012627.2500NaNSMr
41613083Ware, Mr. FrederickmaleNaN003593098.0500NaNSMr
41713093Peter, Master. Michael JmaleNaN11266822.3583NaNCMaster

乘客舱位

当您载入并检查数据时,会看到 'Cabin' 列中有不少 NaN 或缺失值。

我们有理由假设,这些 NaN 表示乘客没有舱位,这可能与 'Survival' 有关。因此,创建一个新的 'Has_Cabin' 列来编码此信息,指示乘客是否有舱位。

注意,下面的代码中使用了 .isnull() 方法,如果乘客没有舱位则返回 True,否则为 False。但由于要将结果存入 'Has_Cabin' 列,您需要将结果取反:当乘客有舱位时返回 True。这就是使用波浪号 ~ 的原因。

# Did they have a Cabin?
data['Has_Cabin'] = ~data.Cabin.isnull()

# View head of data
data.head()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitleHas_Cabin
013Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNSMrFalse
121Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85CMrsTrue
233Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNSMissFalse
341Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123SMrsTrue
453Allen, Mr. William Henrymale35.0003734508.0500NaNSMrFalse

接下来,您需要删除一些不再包含有效信息(或尚不清楚如何处理)的列。在本例中,可以考虑 ['Cabin', 'Name', 'PassengerId', 'Ticket'],原因如下:

  • 关于乘客是否拥有舱位的信息,已在新增的 'Has_Cabin' 列中提取;
  • 您也已经从 'Name' 列中提取了称谓;
  • 此外,'PassengerId' 和 'Ticket' 列大概率无法进一步解释乘客的生存情况,因此也可以删除。

小贴士:'Cabin' 列中可能还蕴含更多信息,但在本教程中,我们假设没有!

要在实际的 data DataFrame 中删除这些列,请在 .drop() 方法中使用 inplace 参数并设为 True:

# Drop columns and view head
data.drop(['Cabin', 'Name', 'PassengerId', 'Ticket'], axis=1, inplace=True)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

恭喜!您已经成功构造了 'Title'、'Has_Cabin' 等新特征,并且移除了对机器学习模型不再提供有用信息的特征!

接下来,您将处理缺失值、对数值数据分箱,并再次使用 .get_dummies() 将所有特征转换为数值变量。最后,您将构建本教程的最终模型。请继续查看下一节了解具体步骤!

处理缺失值

在对原始 data DataFrame 做了以上修改后,最好用 .info() 检查是否还有缺失值:

data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1046 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1308 non-null float64
Embarked     1307 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB

从结果可知,'Age'、'Fare' 和 'Embarked' 存在缺失值。

请记住,可以先看总条目数(1309),再核对 .info() 列出的各列非空值数量是否相等。此处 'Age' 只有 1046 个非空值,意味着有 263 个缺失;类似地,'Fare' 只有 1 个缺失,'Embarked' 有 2 个缺失。

与上一期教程一样,您将使用 .fillna() 来填补缺失值:

注意,这里依然用中位数填充 'Age' 和 'Fare' 列,因为中位数对异常值更稳健。其他填补方式包括使用均值(将所有数据点相加再除以数量)或众数(出现频率最高的值)。

对于 'Embarked' 列的两个缺失值,用 'S'(南安普敦)填充,因为该值是该列中最常见的值。

小贴士:您可以通过进一步的探索性数据分析来复核这一点!

# Impute missing values for Age, Fare, Embarked
data['Age'] = data.Age.fillna(data.Age.median())
data['Fare'] = data.Fare.fillna(data.Fare.median())
data['Embarked'] = data['Embarked'].fillna('S')
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1309 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1309 non-null float64
Embarked     1309 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

数值数据分箱

接下来,您需要对数值数据进行分箱,因为年龄和票价都有一定范围。然而,这些数值中可能存在不反映数据模式的波动,即噪声。因此,您会将落在某个年龄或票价范围内的人分到同一箱。可以使用 pandas 的 qcut() 来对数值数据分箱:

# Binning numerical columns
data['CatAge'] = pd.qcut(data.Age, q=4, labels=False )
data['CatFare']= pd.qcut(data.Fare, q=4, labels=False)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_CabinCatAgeCatFare
03male22.0107.2500SMrFalse00
11female38.01071.2833CMrsTrue33
23female26.0007.9250SMissFalse11
31female35.01053.1000SMrsTrue23
43male35.0008.0500SMrFalse21

注意,您传入的是 Series(data.Age 和 data.Fare),然后指定分位数个数 q=4。最后将 labels 设为 False,以数值编码各个箱。

现在有了分箱信息,您可以安全地删除 'Age' 和 'Fare' 列。别忘了查看数据前五行!

data = data.drop(['Age', 'Fare'], axis=1)
data.head()
 PclassSexSibSpParchEmbarkedTitleHas_CabinCatAgeCatFare
03male10SMrFalse00
11female10CMrsTrue33
23female00SMissFalse11
31female10SMrsTrue23
43male00SMrFalse21

同船家属人数

接下来可以创建一个新列,表示在泰坦尼克号上的同船家属人数。在本教程中,我们不将其纳入,并观察模型在没有该特征时的表现。若您想看看加入该列后模型表现如何,请运行以下代码:

# Create column of number of Family members onboard
data['Fam_Size'] = data.Parch + data.SibSp

目前,我们将直接从 DataFrame 中删除 'SibSp' 和 'Parch' 列:

# Drop columns
data = data.drop(['SibSp','Parch'], axis=1)
data.head()
 PclassSexEmbarkedTitleHas_CabinCatAgeCatFare
03maleSMrFalse00
11femaleCMrsTrue33
23femaleSMissFalse11
31femaleSMrsTrue23
43maleSMrFalse21

将变量转换为数值变量

现在,您已经构造了更多特征(如 'Title' 和 'Has_Cabin'),并处理了缺失值、完成了数值数据分箱,是时候将所有变量转换为数值型了。这是因为机器学习模型通常接收数值输入。

如之前所做,使用 .get_dummies() 完成转换:

# Transform into binary variables
data_dum = pd.get_dummies(data, drop_first=True)
data_dum.head()
 PclassHas_CabinCatAgeCatFareSex_maleEmbarked_QEmbarked_STitle_MissTitle_MrTitle_MrsTitle_Special
03False001010100
11True330000010
23False110011000
31True230010010
43False211010100

一切就绪,可以开始构建最终模型了!

用新数据集构建模型!

与之前一样,先将 data 划分回训练集和测试集。然后将它们转换为数组:

# Split into test.train
data_train = data_dum.iloc[:891]
data_test = data_dum.iloc[891:]

# Transform into arrays for scikit-learn
X = data_train.values
test = data_test.values
y = survived_train.values

现在,您将在全新特征工程后的数据集上构建决策树。为了选择超参数 max_depth,您将使用一种基于训练/测试划分的变体——“交叉验证”。

cross validation

首先把数据集分成 5 组或称为折(fold)。然后将第一折作为测试集,使用其余四折拟合模型,在测试集上预测并计算指标。接着将第二折作为测试集,用其余数据训练,在测试集上预测并计算指标。第三、第四和第五折以此类推。

最终您会得到五个准确率值,并可据此计算感兴趣的统计量,如中位数和/或均值以及 95% 置信区间。

对每个待调优的超参数的每个取值都这样做,并选择表现最佳的一组超参数。这就叫做网格搜索。

原理就说到这里,开始动手吧!

下面将使用交叉验证和网格搜索为新特征工程数据集选择最佳的 max_depth:

# Setup the hyperparameter grid
dep = np.arange(1,9)
param_grid = {'max_depth' : dep}

# Instantiate a decision tree classifier: clf
clf = tree.DecisionTreeClassifier()

# Instantiate the GridSearchCV object: clf_cv
clf_cv = GridSearchCV(clf, param_grid=param_grid, cv=5)

# Fit it to the data
clf_cv.fit(X, y)

# Print the tuned parameter and score
print("Tuned Decision Tree Parameters: {}".format(clf_cv.best_params_))
print("Best score is {}".format(clf_cv.best_score_))
Tuned Decision Tree Parameters: {'max_depth': 3}
Best score is 0.8103254769921436

现在,您可以在测试集上进行预测,创建新的 'Survived' 列并将预测结果存入其中。别忘了将 df_test 的 'PassengerId' 与 'Survived' 两列保存为 .csv 文件并提交到 Kaggle!

Y_pred = clf_cv.predict(test)
df_test['Survived'] = Y_pred
df_test[['PassengerId', 'Survived']].to_csv('data/predictions/dec_tree_feat_eng.csv', index=False)

kaggle

您本次提交的准确率为 78.9。

后续步骤

看看能否进行更多特征工程,并尝试新的模型来进一步提升成绩。本笔记本与前两个一并发布在 GitHub,期待大家改进这些模型。

还有许多预处理内容值得学习,例如数据缩放。您也会发现 scikit-learn 的流水线非常有用。请查看我们的scikit-learn 监督学习课程和 scikit-learn 文档以获取更多信息。

主题