1. 引言
检验学习成果最快的方式就是去实战,kaggle上提供了各式各样练手和比赛的数据集,"Titanic: Machine Learning from Disaster"就是最经典的入门比赛,既适合经验丰富的Data scientist去深入分析争取top3%的成绩,也适合新手应用数据集对所学习的分类算法来练手。
应用机器学习,千万不要一上来就试图做到完美,先撸一个baseline的model出来,再进行后续的分析步骤,一步步提高。—— Andrew Ng
本篇就是基于决策树模型快速的撸一个baseline model。
选择决策树的原因:模型对数据的要求不高,一般原始数据简单的预处理就能让模型跑起来(如果要得到更高的分数,数据预处理特征工程还是不能少)
2. 泰坦尼克号背景介绍
泰坦尼克号的沉没是历史上最臭名昭著的沉船之一,泰坦尼克号在首航中撞上冰山沉没,2224名乘客和船员中1502人遇难。这一耸人听闻的悲剧震惊了国际社会,并导致了对船舶更严格的安全规定。
我们的任务是运用机器学习的工具,分析船上人员的信息来预测什么样的人能够从船难中活下来。
3. 数据集分析
3.1 特征介绍
Variable | Definition | Key |
---|---|---|
survival | 是否生存 | 0 = No, 1 = Yes |
pclass | 乘客等级(1/2/3等舱位) | 1 = 1st, 2 = 2nd, 3 = 3rd |
sex | 性别 | |
Age | 年龄 | |
sibsp | 堂兄弟/妹个数 | |
parch | 父母与小孩个数 | |
ticket | 船票信息 | |
fare | 票价 | |
cabin | 客舱 | |
embarked | 登船港口 | C = Cherbourg, Q = Queenstown, S = Southampton |
3.2 查看缺失值和特征类型
In [1]:
#导入数据
import pandas as pd
data = pd.read_csv('Taitanic data/data.csv')
data.info()
out [1]:
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
通过data.info()
和data.head()
,我们可以观察出有多少乘客、特征的数据类型和缺失值。
In [2]:
#观察前5行数据
data.head()
out [2]:
PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
决策树模型的输入要求就是特征必须为数值型的数据,且sklearn无法自动的处理缺失值,因此为了尽快的撸出baseline,我们就不过多的进行分析,先把数据调整成符合模型要求的样子。
4. 数据预处理
4.1 特征选择
一个正确的数学模型应当在形式上是简单的 —— 吴军,《数学之美》
特征选择的目的是为了去掉不含信息量或是信息量较少的特征,特征选择和方法很多,如果特征上百个可以选择降维、相关系数分析、卡方检验等方法,既然是暴力的撸出一个baseline,就直接肉眼观察剔除不重要的特征。
- PassengerId?Name? 剔除
- Ticket 观察一下每张船票都不一样,就跟条形码一样是无用特征
- Cabin 缺失值严重,剔除
In [3]:
#特征选择
data.drop(['Cabin','Name','Ticket','PassengerId']
,inplace=True
,axis=1
)
4.2 缺失值处理
由于模型本身没有处理缺失值的能力,我们需要人工的处理缺失值。
缺失值处理的方法常见的有均值填充、中位数填充、归为一类新的特征甚至可以用随机森林或者K-means来预测,还是那句话,先撸出一个model来,怎么快怎么来!
Age大部分数据还是完整的(714/891),因此直接上均值填充填充
In [4]:
#处理缺失值
data['Age'] = data['Age'].fillna(data['Age'].mean())
In [5]:
data.info()
Out [5]:
RangeIndex: 891 entries, 0 to 890
Data columns (total 8 columns):
Survived 891 non-null int64
Pclass 891 non-null int64
Sex 891 non-null object
Age 891 non-null float64
SibSp 891 non-null int64
Parch 891 non-null int64
Fare 891 non-null float64
Embarked 889 non-null object <--缺失值
dtypes: float64(2), int64(4), object(2)
memory usage: 55.8+ KB
Embarked 的缺失记录只有2条,怎么快怎么来——直接把那两条记录删掉!
In [6]:
#删除含有空值的记录
data = data.dropna(axis=0)
In [7]:
data.info() #再次观察数据
Out [7]:
Int64Index: 889 entries, 0 to 890
Data columns (total 8 columns):
Survived 889 non-null int64
Pclass 889 non-null int64
Sex 889 non-null object
Age 889 non-null float64
SibSp 889 non-null int64
Parch 889 non-null int64
Fare 889 non-null float64
Embarked 889 non-null object
dtypes: float64(2), int64(4), object(2)
memory usage: 62.5+ KB
非常干净了,缺失值的处理到此为止!
4.3 数据转换
数据转换的目的就是把人看的数据转换成计算机看得懂的数据。
sklearn的模型无法识别male和female,我们需要用0/1来代替
In [8]:
#男性为1(True),女性为0(False)
data['Sex'] = (data['Sex'] == 'male').astype('int')
再看看Embarked,官方数据集高速我们总共有三个港口分别是C、Q、S
同样的方式处理,映射成0,1,2
In [9]:
data['Embarked'] = data['Embarked'].map({'S':0,'C':1,'Q':2})
再看看现在数据是什么样子
In [10]:
data.head()
Survived | Pclass | Sex | Age | SibSp | Parch | Fare | Embarked | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 3 | 1 | 22.0 | 1 | 0 | 7.2500 | 0 |
1 | 1 | 1 | 0 | 38.0 | 1 | 0 | 71.2833 | 1 |
2 | 1 | 3 | 0 | 26.0 | 0 | 0 | 7.9250 | 0 |
3 | 1 | 1 | 0 | 35.0 | 1 | 0 | 53.1000 | 0 |
4 | 0 | 3 | 1 | 35.0 | 0 | 0 | 8.0500 | 0 |
到这里一个简单的数据预处理就结束了,没有过多的数据分析,仅仅是把数据处理成模型能够处理的格式。
5. 决策树分类
到了这里就是真正的运用机器学习算法了。
第一步,把数据调整成sklearn能够传入的格式:
sklearn的模型都是把特征和标签分别传入训练,否则一整个数据集模型也无法得知哪个才是特征哪个是标签
In [11]:
X = data.iloc[:,data.columns != "Survived"]
y = data.iloc[:,data.columns == "Survived"]
第二步,划分训练集和测试集:
我们把训练集和测试集按7:3 进行划分
In [12]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.3)
#test_size是测试集占总数据集的比例
第三步,导入模型,粗略跑一下查看结果:
sklearn的模型运用基本上分为三步:调用模型、训练模型、评价模型。三行代码如下。
In [13]:
#1.声明分类树模型
clf = DecisionTreeClassifier()
#2.传入训练集训练模型
clf = clf.fit(Xtrain, Ytrain)
#3.传入测试集评价模型
score_ = clf.score(Xtest, Ytest)
score = cross_val_score(clf,X,y,cv=10).mean() #交叉验证集准确度
print('测试集准确度:{}\n交叉验证集准确度:{}'.format(score_,score))
Out [13]:
测试集准确度:0.7715355805243446
交叉验证集准确度:0.7717058222676201
以上,一个非常粗略的baseline就撸出来了。
6. 模型参数调整
上面那个粗略的分类树模型都是用默认参数,简单方便但是效果确不是很好,至少调整一个合适的参数还是能够继续提高准确度。
6.1 DecisionTreeClassifier参数介绍
调参,我们首先要知道有哪些参数以及参数的含义。这里就先列出分类树常用的参数
参数=默认 | 介绍 |
---|---|
criterion=‘gini’ | gini/entropy 划分节点的指标 |
splitter=‘best’ | 节点分支策略 |
max_depth=‘None’ | 树最大深度 |
min_samples_split=2 | 一个中间节点分支需要的最少样本(<min_samples_split就不分枝) |
min_samples_leaf=1 | 分支后叶节点至少需要的最少样本 |
random_state | 随机数种子 |
可以通过试不同的变量来确定一部分参数
6.2 学习曲线调整参数
In [14]:
#分别记录不同参数在测试集和训练集下准确度
import matplotlib.pyplot as plt
tr_entropy = []
te_entropy = []
tr_gini = []
te_gini = []
#尝试深度从1~10
for i in range(10):
clf = DecisionTreeClassifier(random_state=25
,max_depth=i+1
,criterion='entropy' #尝试信息增益
)
clf.fit(Xtrain,Ytrain)
score_tr = clf.score(Xtrain,Ytrain)
score_te = cross_val_score(clf,X,y,cv=10).mean()
tr_entropy.append(score_tr)
te_entropy.append(score_te)
clf = DecisionTreeClassifier(random_state=25
,max_depth=i+1
,criterion='gini' #尝试基尼系数
)
clf.fit(Xtrain,Ytrain)
score_tr = clf.score(Xtrain,Ytrain)
score_te = cross_val_score(clf,X,y,cv=10).mean()
tr_gini.append(score_tr)
te_gini.append(score_te)
fig, (ax0, ax1) = plt.subplots(1,2, figsize=(18, 6))
ax0.plot(range(1,11),tr_entropy,color='r',label='train')
ax0.plot(range(1,11),te_entropy,color='blue',label='test')
ax0.set_xticks(range(1,11))
ax0.set_title('entropy')
ax0.legend()
ax1.plot(range(1,11),tr_gini,color='r',label='train')
ax1.plot(range(1,11),te_gini,color='blue',label='test')
ax1.set_xticks(range(1,11))
ax0.set_title('gini')
ax1.legend()
print('entropy上的最好准确度为{}\njini上的最好准确度为{}'.format(max(te_entropy),max(te_gini)))
Out [14]:
entropy上的最好准确度为0.8177860061287026
jini上的最好准确度为0.8177987742594486
比起默认参数,经过参数的粗略调整后,模型在测试集上的准确度得到了明显提升
可以观察出当最大深度为3时,拟合效果较好,且两种划分情况准确度都十分相近
6.3 网格搜索调整参数
如果参数的取值范围很大,参数个数也很多,这么一个个参数人为的去慢慢尝试是非常消耗时间的,因此我们可以调用sklearn的GridSearchCV来帮助我们寻找合适的参数。
网格参数搜索的本质其实就是把每个参数的取值排列组合一个个帮我们尝试,并且返回交叉验证准确度最好的一组参数。
在调用网格参数搜索前最好先确定参数的大致范围,否则相当消耗时间
In [15]:
#网格搜索:能够帮助我们调整多个参数的技术---枚举
#网格搜索:能够帮助我们调整多个参数的技术---枚举
import numpy as np
from sklearn.model_selection import GridSearchCV
gini_threholds = np.linspace(0,0.5,20)
parameters = {'criterion':('gini','entropy')
,'splitter':('best','random')
,'max_depth':[*range(2,5)]
,'min_samples_leaf':[*range(1,10,2)]
# ,'min_impurity_decrease':np.linspace(0,0.5,20)
}
clf = DecisionTreeClassifier(random_state=25)
gs = GridSearchCV(clf,parameters,cv=10)
gs.fit(Xtrain,Ytrain)
In [16]:
gs.best_params_ #我们输入参数和参数取值中,最佳组合
Out [16]:
{'criterion': 'gini',
'max_depth': 4,
'min_samples_leaf': 1,
'splitter': 'random'}
用训练的参数导入模型
In [17]:
clf = DecisionTreeClassifier(random_state=20
,criterion='gini'
,max_depth=4
,min_samples_leaf=1
,splitter='random'
)
clf = clf.fit(Xtrain, Ytrain)
cross_val_score(clf,X,y,cv=10).mean()
Out [17]:
0.806511746680286
比之前稍差了点,这其实是因为GridSearchCV在评判的参数好坏的标准是把传入的训练集又分为了训练集和测试集,并通过交叉验证求平均找出准确率最好的参数组合;而之前的算法的准确率是直接用训练集训练并用全部数据集交叉验证的结果,因此两者在评判对象上有所不同,如果两者准确率相差不大,那就任选即可。
如果上面的解释没看懂,那就记住如果自己调试的参数和网格搜索结果相差不大,那说明你已经逼近了调参结果的上限,任选一个就好了。
7. 上传到kaggle查看得分
把官方的测试数据集进行预测并上传到官网
刚刚的模型是训练集经过处理才能使用的,因此测试集也要做同样处理。
In [18]:
test = pd.read_csv('Taitanic data/test.csv')
test.info()
Out [18]:
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
PassengerId 418 non-null int64
Pclass 418 non-null int64
Name 418 non-null object
Sex 418 non-null object
Age 332 non-null float64 <----缺失
SibSp 418 non-null int64
Parch 418 non-null int64
Ticket 418 non-null object
Fare 417 non-null float64 <----缺失
Cabin 91 non-null object
Embarked 418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
发现和训练集不同的是‘Fare’特征有一个缺失值,这需要小心不能忘了处理
In [19]:
#把测试集预处理操作封装
def clean_data(data):
data = data.drop(['Cabin','Name','Ticket','PassengerId']
,axis=1
)
data['Age'] = data['Age'].fillna(data['Age'].mean())
data['Fare'] = data['Fare'].fillna(data['Fare'].mean()) #
data = data.dropna(axis=0)
data['Sex'] = (data['Sex'] == 'male').astype('int')
data['Embarked'] = data['Embarked'].map({'S':0,'C':1,'Q':2})
return data
In [20]:
test_data = clean_data(test)
res = pd.concat([test['PassengerId'],pd.DataFrame(clf.predict(test_data))],axis=1)
res.columns = ['PassengerId','Survived']
res.to_csv("result.csv",sep=',',index=False)
提交结果查看得分,top20%的baseline,还行
8 总结
完成了一次完整的kaggle还是很有成就感的,不过依然有很多瑕疵。
kaggle最重要的特征工程几乎被我一笔带过,数据没有经过严密的统计分析,有句话叫“特征工程决定了最后结果的上限,而机器学习算法只是在逼近这个上限”。特征上还有很多事情可以做,例如:
- Age可以尝试Random forest、SVM等算法预测填充
- Cabin可以保留,把缺失值当作一类,非缺失值当作一类
- sibsp,parch也可以推测出一个人的年龄区间
- sibsp,parch两个特征可以用一个新的特征“家庭成员数量”代替试试
- … …
甚至尝试不同的模型,对于不同的模型又会有不同的数据处理方式,例如降为、归一化、One-hot编码等,如果把泰坦尼克号数据集的内容吃透对于其他数据集也能得心应手了。