<audio id="audio" title="19丨决策树（下）：泰坦尼克乘客生存预测" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/24/c0/2488ceea1b379251231440e4f03b50c0.mp3"></audio>

在前面的两篇文章中，我给你讲了决策树算法。决策树算法是经常使用的数据挖掘算法，这是因为决策树就像一个人脑中的决策模型一样，呈现出来非常直观。基于决策树还诞生了很多数据挖掘算法，比如随机森林（Random forest）。

今天我来带你用决策树进行项目的实战。

决策树分类的应用场景非常广泛，在各行各业都有应用，比如在金融行业可以用决策树做贷款风险评估，医疗行业可以用决策树生成辅助诊断，电商行业可以用决策树对销售额进行预测等。

在了解决策树的原理后，今天我们用sklearn工具解决一个实际的问题：泰坦尼克号乘客的生存预测。

## sklearn中的决策树模型

首先，我们需要掌握sklearn中自带的决策树分类器DecisionTreeClassifier，方法如下：

```
clf = DecisionTreeClassifier(criterion='entropy')

```

到目前为止，sklearn中只实现了ID3与CART决策树，所以我们暂时只能使用这两种决策树，在构造DecisionTreeClassifier类时，其中有一个参数是criterion，意为标准。它决定了构造的分类树是采用ID3分类树，还是CART分类树，对应的取值分别是entropy或者gini：

<li>
entropy: 基于信息熵，也就是ID3算法，实际结果与C4.5相差不大；
</li>
<li>
gini：默认参数，基于基尼系数。CART算法是基于基尼系数做属性划分的，所以criterion=gini时，实际上执行的是CART算法。
</li>

我们通过设置criterion='entropy’可以创建一个ID3决策树分类器，然后打印下clf，看下决策树在sklearn中是个什么东西？

```
DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,
            max_features=None, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, presort=False, random_state=None,
            splitter='best')

```

这里我们看到了很多参数，除了设置criterion采用不同的决策树算法外，一般建议使用默认的参数，默认参数不会限制决策树的最大深度，不限制叶子节点数，认为所有分类的权重都相等等。当然你也可以调整这些参数，来创建不同的决策树模型。

我整理了这些参数代表的含义：

<img src="https://static001.geekbang.org/resource/image/ea/0c/ead008e025a039c8731884ce2e29510c.jpg" alt="">

在构造决策树分类器后，我们可以使用fit方法让分类器进行拟合，使用predict方法对新数据进行预测，得到预测的分类结果，也可以使用score方法得到分类器的准确率。

下面这个表格是fit方法、predict方法和score方法的作用。

<img src="https://static001.geekbang.org/resource/image/3c/f8/3c7057b582b8078129c8342cde709ef8.png" alt="">

## Titanic乘客生存预测

**问题描述**

泰坦尼克海难是著名的十大灾难之一，究竟多少人遇难，各方统计的结果不一。现在我们可以得到部分的数据，具体数据你可以从GitHub上下载：[https://github.com/cystanford/Titanic_Data](https://github.com/cystanford/Titanic_Data)

（完整的项目代码见：[https://github.com/cystanford/Titanic_Data/blob/master/titanic_analysis.py](https://github.com/cystanford/Titanic_Data/blob/master/titanic_analysis.py) 你可以跟着学习后自己练习）

其中数据集格式为csv，一共有两个文件：

<li>
train.csv是训练数据集，包含特征信息和存活与否的标签；
</li>
<li>
test.csv: 测试数据集，只包含特征信息。
</li>

现在我们需要用决策树分类对训练集进行训练，针对测试集中的乘客进行生存预测，并告知分类器的准确率。

在训练集中，包括了以下字段，它们具体为：

<img src="https://static001.geekbang.org/resource/image/2c/0a/2c14e00b64a73f40d75451a180c57b0a.png" alt=""><br>
**生存预测的关键流程**

我们要对训练集中乘客的生存进行预测，这个过程可以划分为两个重要的阶段：

<img src="https://static001.geekbang.org/resource/image/6c/7f/6c08ca83a20a969100e5ceddee0ab27f.jpg" alt="">

<li>
**准备阶段**：我们首先需要对训练集、测试集的数据进行探索，分析数据质量，并对数据进行清洗，然后通过特征选择对数据进行降维，方便后续分类运算；
</li>
<li>
**分类阶段**：首先通过训练集的特征矩阵、分类结果得到决策树分类器，然后将分类器应用于测试集。然后我们对决策树分类器的准确性进行分析，并对决策树模型进行可视化。
</li>

下面，我分别对这些模块进行介绍。

**模块1：数据探索**

数据探索这部分虽然对分类器没有实质作用，但是不可忽略。我们只有足够了解这些数据的特性，才能帮助我们做数据清洗、特征选择。

那么如何进行数据探索呢？这里有一些函数你需要了解：

<li>
使用info()了解数据表的基本情况：行数、列数、每列的数据类型、数据完整度；
</li>
<li>
使用describe()了解数据表的统计情况：总数、平均值、标准差、最小值、最大值等；
</li>
<li>
使用describe(include=[‘O’])查看字符串类型（非数字）的整体情况；
</li>
<li>
使用head查看前几行数据（默认是前5行）；
</li>
<li>
使用tail查看后几行数据（默认是最后5行）。
</li>

我们可以使用Pandas便捷地处理这些问题：

```
import pandas as pd
# 数据加载
train_data = pd.read_csv('./Titanic_Data/train.csv')
test_data = pd.read_csv('./Titanic_Data/test.csv')
# 数据探索
print(train_data.info())
print('-'*30)
print(train_data.describe())
print('-'*30)
print(train_data.describe(include=['O']))
print('-'*30)
print(train_data.head())
print('-'*30)
print(train_data.tail())

```

运行结果：

```
&lt;class 'pandas.core.frame.DataFrame'&gt;
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB
None
------------------------------
       PassengerId    Survived     ...           Parch        Fare
count   891.000000  891.000000     ...      891.000000  891.000000
mean    446.000000    0.383838     ...        0.381594   32.204208
std     257.353842    0.486592     ...        0.806057   49.693429
min       1.000000    0.000000     ...        0.000000    0.000000
25%     223.500000    0.000000     ...        0.000000    7.910400
50%     446.000000    0.000000     ...        0.000000   14.454200
75%     668.500000    1.000000     ...        0.000000   31.000000
max     891.000000    1.000000     ...        6.000000  512.329200

[8 rows x 7 columns]
------------------------------
                                          Name   Sex   ...       Cabin Embarked
count                                      891   891   ...         204      889
unique                                     891     2   ...         147        3
top     Peter, Mrs. Catherine (Catherine Rizk)  male   ...     B96 B98        S
freq                                         1   577   ...           4      644

[4 rows x 5 columns]
------------------------------
   PassengerId  Survived  Pclass    ...        Fare Cabin  Embarked
0            1         0       3    ...      7.2500   NaN         S
1            2         1       1    ...     71.2833   C85         C
2            3         1       3    ...      7.9250   NaN         S
3            4         1       1    ...     53.1000  C123         S
4            5         0       3    ...      8.0500   NaN         S

[5 rows x 12 columns]
------------------------------
     PassengerId  Survived  Pclass    ...      Fare Cabin  Embarked
886          887         0       2    ...     13.00   NaN         S
887          888         1       1    ...     30.00   B42         S
888          889         0       3    ...     23.45   NaN         S
889          890         1       1    ...     30.00  C148         C
890          891         0       3    ...      7.75   NaN         Q

[5 rows x 12 columns]

```

**模块2：数据清洗**

通过数据探索，我们发现Age、Fare和Cabin这三个字段的数据有所缺失。其中Age为年龄字段，是数值型，我们可以通过平均值进行补齐；Fare为船票价格，是数值型，我们也可以通过其他人购买船票的平均值进行补齐。

具体实现的代码如下：

```
# 使用平均年龄来填充年龄中的nan值
train_data['Age'].fillna(train_data['Age'].mean(), inplace=True)
test_data['Age'].fillna(test_data['Age'].mean(),inplace=True)
# 使用票价的均值填充票价中的nan值
train_data['Fare'].fillna(train_data['Fare'].mean(), inplace=True)
test_data['Fare'].fillna(test_data['Fare'].mean(),inplace=True)

```

Cabin为船舱，有大量的缺失值。在训练集和测试集中的缺失率分别为77%和78%，无法补齐；Embarked为登陆港口，有少量的缺失值，我们可以把缺失值补齐。

首先观察下Embarked字段的取值，方法如下：

```
print(train_data['Embarked'].value_counts())

```

结果如下：

```
S    644
C    168
Q     77

```

我们发现一共就3个登陆港口，其中S港口人数最多，占到了72%，因此我们将其余缺失的Embarked数值均设置为S：

```
# 使用登录最多的港口来填充登录港口的nan值
train_data['Embarked'].fillna('S', inplace=True)
test_data['Embarked'].fillna('S',inplace=True)

```

**模块3：特征选择**

特征选择是分类器的关键。特征选择不同，得到的分类器也不同。那么我们该选择哪些特征做生存的预测呢？

通过数据探索我们发现，PassengerId为乘客编号，对分类没有作用，可以放弃；Name为乘客姓名，对分类没有作用，可以放弃；Cabin字段缺失值太多，可以放弃；Ticket字段为船票号码，杂乱无章且无规律，可以放弃。其余的字段包括：Pclass、Sex、Age、SibSp、Parch和Fare，这些属性分别表示了乘客的船票等级、性别、年龄、亲戚数量以及船票价格，可能会和乘客的生存预测分类有关系。具体是什么关系，我们可以交给分类器来处理。

因此我们先将Pclass、Sex、Age等这些其余的字段作特征，放到特征向量features里。

```
# 特征选择
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
train_features = train_data[features]
train_labels = train_data['Survived']
test_features = test_data[features]

```

特征值里有一些是字符串，这样不方便后续的运算，需要转成数值类型，比如Sex字段，有male和female两种取值。我们可以把它变成Sex=male和Sex=female两个字段，数值用0或1来表示。

同理Embarked有S、C、Q三种可能，我们也可以改成Embarked=S、Embarked=C和Embarked=Q三个字段，数值用0或1来表示。

那该如何操作呢，我们可以使用sklearn特征选择中的DictVectorizer类，用它将可以处理符号化的对象，将符号转成数字0/1进行表示。具体方法如下：

```
from sklearn.feature_extraction import DictVectorizer
dvec=DictVectorizer(sparse=False)
train_features=dvec.fit_transform(train_features.to_dict(orient='record'))

```

你会看到代码中使用了fit_transform这个函数，它可以将特征向量转化为特征值矩阵。然后我们看下dvec在转化后的特征属性是怎样的，即查看dvec的feature_names_属性值，方法如下：

```
print(dvec.feature_names_)

```

运行结果：

```
['Age', 'Embarked=C', 'Embarked=Q', 'Embarked=S', 'Fare', 'Parch', 'Pclass', 'Sex=female', 'Sex=male', 'SibSp']

```

你可以看到原本是一列的Embarked，变成了“Embarked=C”“Embarked=Q”“Embarked=S”三列。Sex列变成了“Sex=female”“Sex=male”两列。

这样train_features特征矩阵就包括10个特征值（列），以及891个样本（行），即891行，10列的特征矩阵。

**模块4：决策树模型**

刚才我们已经讲了如何使用sklearn中的决策树模型。现在我们使用ID3算法，即在创建DecisionTreeClassifier时，设置criterion=‘entropy’，然后使用fit进行训练，将特征值矩阵和分类标识结果作为参数传入，得到决策树分类器。

```
from sklearn.tree import DecisionTreeClassifier
# 构造ID3决策树
clf = DecisionTreeClassifier(criterion='entropy')
# 决策树训练
clf.fit(train_features, train_labels)

```

**模块5：模型预测&amp;评估**

在预测中，我们首先需要得到测试集的特征值矩阵，然后使用训练好的决策树clf进行预测，得到预测结果pred_labels：

```
test_features=dvec.transform(test_features.to_dict(orient='record'))
# 决策树预测
pred_labels = clf.predict(test_features)

```

在模型评估中，决策树提供了score函数可以直接得到准确率，但是我们并不知道真实的预测结果，所以无法用预测值和真实的预测结果做比较。我们只能使用训练集中的数据进行模型评估，可以使用决策树自带的score函数计算下得到的结果：

```
# 得到决策树准确率
acc_decision_tree = round(clf.score(train_features, train_labels), 6)
print(u'score准确率为 %.4lf' % acc_decision_tree)

```

运行结果：

```
score准确率为 0.9820

```

你会发现你刚用训练集做训练，再用训练集自身做准确率评估自然会很高。但这样得出的准确率并不能代表决策树分类器的准确率。

这是为什么呢？

因为我们没有测试集的实际结果，因此无法用测试集的预测结果与实际结果做对比。如果我们使用score函数对训练集的准确率进行统计，正确率会接近于100%（如上结果为98.2%），无法对分类器的在实际环境下做准确率的评估。

那么有什么办法，来统计决策树分类器的准确率呢？

这里可以使用K折交叉验证的方式，交叉验证是一种常用的验证分类准确率的方法，原理是拿出大部分样本进行训练，少量的用于分类器的验证。K折交叉验证，就是做K次交叉验证，每次选取K分之一的数据作为验证，其余作为训练。轮流K次，取平均值。

K折交叉验证的原理是这样的：

<li>
将数据集平均分割成K个等份；
</li>
<li>
使用1份数据作为测试数据，其余作为训练数据；
</li>
<li>
计算测试准确率；
</li>
<li>
使用不同的测试集，重复2、3步骤。
</li>

在sklearn的model_selection模型选择中提供了cross_val_score函数。cross_val_score函数中的参数cv代表对原始数据划分成多少份，也就是我们的K值，一般建议K值取10，因此我们可以设置CV=10，我们可以对比下score和cross_val_score两种函数的正确率的评估结果：

```
import numpy as np
from sklearn.model_selection import cross_val_score
# 使用K折交叉验证 统计决策树准确率
print(u'cross_val_score准确率为 %.4lf' % np.mean(cross_val_score(clf, train_features, train_labels, cv=10)))

```

运行结果：

```
cross_val_score准确率为 0.7835

```

你可以看到，score函数的准确率为0.9820，cross_val_score准确率为 0.7835。

这里很明显，对于不知道测试集实际结果的，要使用K折交叉验证才能知道模型的准确率。

**模块6：决策树可视化**

sklearn的决策树模型对我们来说，还是比较抽象的。我们可以使用Graphviz可视化工具帮我们把决策树呈现出来。

<img src="https://static001.geekbang.org/resource/image/4a/a0/4ae6016c6c7d1371586c5be015247da0.png" alt=""><br>
安装Graphviz库需要下面的几步：

<li>
安装graphviz工具，这里是它的下载地址；[http://www.graphviz.org/download/](http://www.graphviz.org/download/)
</li>
<li>
将Graphviz添加到环境变量PATH中；
</li>
<li>
需要Graphviz库，如果没有可以使用pip install graphviz进行安装。
</li>

这样你就可以在程序里面使用Graphviz对决策树模型进行呈现，最后得到一个决策树可视化的PDF文件，可视化结果文件Source.gv.pdf你可以在GitHub上下载：[https://github.com/cystanford/Titanic_Data](https://github.com/cystanford/Titanic_Data)

## 决策树模型使用技巧总结

今天我用泰坦尼克乘客生存预测案例把决策树模型的流程跑了一遍。在实战中，你需要注意一下几点：

<li>
特征选择是分类模型好坏的关键。选择什么样的特征，以及对应的特征值矩阵，决定了分类模型的好坏。通常情况下，特征值不都是数值类型，可以使用DictVectorizer类进行转化；
</li>
<li>
模型准确率需要考虑是否有测试集的实际结果可以做对比，当测试集没有真实结果可以对比时，需要使用K折交叉验证cross_val_score；
</li>
<li>
Graphviz可视化工具可以很方便地将决策模型呈现出来，帮助你更好理解决策树的构建。
</li>

<img src="https://static001.geekbang.org/resource/image/f0/ea/f09fd3c8b1ce771624b803978f01c9ea.png" alt=""><br>
我上面讲了泰坦尼克乘客生存预测的六个关键模块，请你用sklearn中的决策树模型独立完成这个项目，对测试集中的乘客是否生存进行预测，并给出模型准确率评估。数据从GitHub上下载即可。

最后给你留一个思考题吧，我在构造特征向量时使用了DictVectorizer类，使用fit_transform函数将特征向量转化为特征值矩阵。DictVectorizer类同时也提供transform函数，那么这两个函数有什么区别？

欢迎你在评论区留言与我分享你的答案，也欢迎点击“请朋友读”把这篇文章分享给你的朋友或者同事，一起交流一下。


