深入解析那些高赞回答(一)
谨以此篇文章记录小菜鸡作者向数据分析大佬的仰望。如有冒犯,纯属无意
这次主要分析的是Titanic Data Science Solutions这一篇文章
正如文章开头所说的
The notebook walks us through a typical workflow for solving data science competitions at sites like Kaggle.
这篇文章对于我这种刚刚入门的小白来讲还是很有帮助的。
下面我就主要结合自己的理解并带上自己刚入门的疑问来“翻译下下”大佬对此类问题的解决建议叭~
如果文章有些菜鸡的地方,希望大佬们不要介意~
工作流程
解决一般的类似KAGGLE竞赛,主要有七步。
- 问题定义的理解
- 下载并导入训练集和测试集
- 对数据进行预处理(如数据清洗、对空值的处理等)
- 分析数据中的关系
- 建模并解决问题
- 可视化,完成问题解决步骤,呈现最终答案
- 提交结果
当然,这只是一般的流程。也会有一些特殊情况,需要将这些步骤打乱或者某一步骤重复使用。
下面将逐一介绍这些步骤
题目定义与分析
Titanic竞赛的题目是和数据集一起给出来的,并且偶尔还会在题目的周围给一些辅助说明。Titanic的题目说明在这里.
Knowing from a training set of samples listing passengers who survived or did not survive the Titanic disaster, can our model determine based on a given test dataset not containing the survival information, if these passengers in the test dataset survived or not.
之后的那一部分主要讲述了数据处理流程中一步步需要解决的问题。小编我能力有限,之后真正理解了数据分析的精髓再补充叭~
notebook构建
这一部分主要介绍了作者的这一篇notebook结合Titanic题目真正想要达到的目的。
将训练集和测试集的数据结合起来进行操作
正确观察-将近30%的乘客有兄弟姐妹和/或配偶在船上。
原谅我还没能够真正理解这句话的意思正确解释逻辑回归系数(这在文章的后面会提到)
在画图的时候设置画板大小并注释图例
尽快对数据的特征进行相关性分析,对特征进行相关性分析有助于对数据进行降维,减少操作数据
尽量将多个数据图展现在一个面板中,增强数据的可读性
下面就到了真正操作的部分了
首先先导入python强大的外部库
# 数据分析的模块
import pandas as pd
import numpy as np
import random as rnd
# 数据可视化模块
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
# 机器学习模块
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import Perceptron
from sklearn.linear_model import SGDClassifier
from sklearn.tree import DecisionTreeClassifier
导入数据
pandas是一个处理数据集十分顺手的python包,首先将官方提供的数据放到pandas的dataframe中,再统一对数据进行处理。
下载下来之后有三个文件,有个叫 gender_submission 的文件目前不用管它~
这个文件按照官方的说法,是用来提供提交数据样本的。
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')
combine = [train_df, test_df]
上述代码不要直接复制吼,这是用的相对路径读取文件袋的方式,如果不懂的话希望自己可以查一下呢~
执行完上述代码后,combine应该是list类型的
通过数据本身描述分析
依旧是用pandas对数据进行描述性分析~打开数据我们会发现,train数据每一行有12个特征,test数据每一行有11个数据。
这么多的特征,所以我们就要判断那些特征属于冗余特征,也就是对数据进行降维。官方对数据每个特征的描述在这里。
接着来查看一下这些数据
print(train_df.columns.values)
类别型数据特征
这类的数据特征是可以作为依据对总体数据进行分类的。并且,这类数据有助于对数据进行可视化。
这之中又可分为以下两类:
- 没有大小关系: Survived, Sex, and Embarked.
- 有大小关系:Pclass.
数字型数据特征
这类数据特征的值随着样本的不同而发生变化,又可分为以下两类:
- 离散的:Age, Fare
- 连续的:SibSp, Parch
混合型数据特征
这类数据特征的值是数字和字母类型的组合,也是需要处理的数据。其中Ticket和Cabin就是这种类型的数据
值有误的数据特征
对于大型数据集而言,这类数据特征的值很难处理,就比如姓名的值就容易出现错字。
包含空值的数据特征
train_df.info()
print('_'*40)
test_df.info()
通过上述代码可以看到在test和train数据集中,均有部分值为空的数据特征。
样本中数字性数据特征的分布特点
train_df.describe()
通过对结果的观察,可以得到以下几条结论:
- 样本数据有891个
- 样本的存活率大约为38%
- 75%以上的乘客没有和父母、孩子一起
- 30%以上的乘客是和兄妹一起来的
非数字型数据特征描述
#注意这里是大写的英文字母O,不是0
train_df.describe(include=['O'])
活了这么久第一次用到describe中的函数,哈哈,还是自己吃的盐太少了
又可以得到一些有趣的信息
- 游轮上没有重名的游客
- 男性游客占了65%左右
根据数据分析做出一些猜测
根据之前的分析,做出下述假设,并在之后会对假设进行验证~
- 部分特征与存活率关联性较强,应在作业初期找出这些特征,减少工作量
- 含有空值的特征可能对是否存活有较强的影响,就比如年龄,因此,需要对空值进行一些操作
- “Cabin”这一特征由于空值太多,可能会被舍弃
- “Ticket”这一特征由于重复值没有什么规律,并且感觉应该和存活率没有什么关系,也应被舍弃。同样“Name”和“PassengerId”也应该被舍弃
- 创建一个新的特征叫“Family”,将“Parch”和“SibSp”的值加起来
- 创建将年龄、船票进行分段的特征
- 女人、小孩可能更容易获救
- 船舱等级越高的乘客越容易获救
数据特征独立分析
先对一些没有空值的数据特征进行分析,如:
- Pclass
train_df[['Pclass', 'Survived']].groupby(
['Pclass'], as_index=False).mean().sort_values(
by='Survived', ascending=False)
实现的功能是先只保留train_df数据集中的[‘Pclass’,‘Survived’]这两列(这么说其实不太好。应该说,这个集合中的这两个特征为了体现为什么用两个中括号),按照Pclass这一特征进行分组(as_index=False是为了不将Pclass这一特征作为索引),有一点点数据库基础的同学应该就会比较熟悉groupby的用法,后面接一个聚合函数(mean,sum,agg等),就可以得到另一个dataframe,后面的函数就是一个按照Survived属性大小的降序排列函数了。结果如下图。
可以看出来船舱越高级,存活率越高。
之后对剩下的特征做同样的操作,可以发现,Sex对存活率有较大的影响,而SibSp和Parch就不是了。因此,关于特征降维的选取就有了初步打算。
数据可视化分析
通过对数据的可视化来判断不同特征之间的关联性,还有其他特征和survived之间的关系
数字型特征
其中直方图有利于分析连续性数字特征,如Age。
g = sns.FacetGrid(train_df, col='Survived')
g.map(plt.hist, 'Age', bins=20)
若想在数据集的子集内可视化变量的分布或多个变量之间的关系时,用到的是seaborn中的FacetGrid类,下面简要介绍这个类的用法。
-
sns.FacetGrid() 画出轮廓
-
map() 填充内容
对map()中的部分参数进行说明 -
plt.hist是绘图的类型,是直方图的形式,也可以替换成plt.scatter(散点图)、sns.distplot(sns直方图)等其他图像类型。
-
Age是x轴的数据,后面也可以再跟y轴的数据。
-
bins代表从x轴的最小值到最大值有几个条。
根据得到的直方图,可以得到以下信息。 -
婴儿(年龄小于等于4岁的有较高的存活率)。
-
多数游客年龄在15-35岁之间。
-
在15-25岁的游客死亡率较高。
-
年龄最大(80)的旅客活了下来。
可以看出来,年龄是一个十分重要的特征,所以可以的出以下结论:
- 将年龄中的空值补全
- 将年龄进行分组
数字型特征和序数型特征之间的相关性
可以在一个图中祝贺多个特征显示关联性,这可以通过数字型特征和具有数字型的分类型特征表示(Pclass)。
有两种展示形式
#形式1
grid = sns.FacetGrid(train_df, col='Survived', row='Pclass', size=2.2, aspect=1.6)
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
#形式2
grid = sns.FacetGrid(train_df, col='Pclass', hue='Survived')
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend()
- col代表列的分类
- row代表行的分类
- size和aspect共同组成图片的大小
- alpha大小(小于1)决定条形的透明度
形式一:
形式二:
根据直方图可以得到以下信息:
- Pclass3乘客最多,存活率最低
- Age和Pclass之间有一定的关系
- Pclass1中的乘客大多数活了下来
- 不同的Pclass中Age和存活率的关系不一样
所以Pclass也是十分重要的一个特征
类别型特征之间的关联性
由之前的信息可以知道,类别型数据特征有四个:Pclass、Sex、Survived、Embarked。将这四个特征放在一起进行。
grid = sns.FacetGrid(train_df, col='Embarked')
grid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex',palette='deep')
grid.add_legend()
这次是将Embarked作为底图的分类。图像的类型是连点图。
根据图像可以得到一下信息:
- 女性存活率远高于男性。
- 在Embarked=C中男性存活率高于女性,也不一定证明Embarked和Survived之间有直接的关系。
可以得到以下结论:
- 将Sex特征加到模型训练中。
- 将Embarked特征的空值补全,加到模型训练中。
类别型特征和数字型特征之间的关联性
将类别型特征(不具有数字值),和数字型特征相关联。Embarked (不具有数字值的类别型特征), Sex (不具有数字值的类别型特征), Fare (连续性数字型特征), with Survived (有数字值的类别型特征)。
grid = sns.FacetGrid(train_df, col='Embarked', hue='Survived', palette={0: 'b', 1: 'r'})
grid.map(sns.barplot, 'Sex', 'Fare', alpha=.5, ci=None)
grid.add_legend()
- hue代表子图中的分类
- sns.barplot是条形图
可以得到以下信息: - Fare和Survived有关,Fare越高存活率越大
- Embarked和Survived有关
得到以下结论
1.将Fare分组作为训练模型的特征
数据处理与分析
根据前文提到的假设和结论,对数据进行处理和分析,得到最终的预测结果。
舍弃无用的特征
根据上面的初步分析,打算舍弃的数据特征有:Cabin、Name、Ticket。结合之前的非数值的类别型数据特征描述图
可知,舍弃的这三种特征都是类别较多的。也加快了之后数据的分析速度。 但是!由于,有的时候名字也可以体现一个人的某种特性,看下图
有人是Mr,有人是Miss,有人是Mrs,有人是Master,极有可能之后对名字进行拆分,就得到另一种数据特征。
之后的数据处理尽量将训练集和验证集同时处理。
train_df = train_df.drop(['Ticket', 'Cabin'], axis=1)
test_df = test_df.drop(['Ticket', 'Cabin'], axis=1)
combine = [train_df, test_df]
创建新的数据特征
首先,就是对Name这一特征创建新的特征,来寻找与Survived之间的关联性。
因为是对字符串做处理,所以,应用正则表达式的方式。观察之前Name的内容,提取与第一个 . 匹配的单词。
正则表达式确实有一点难理解呢~不是很懂的读者可以看看菜鸟教程中关于正则表达式的部分.
for dataset in combine:
dataset['Title'] = dataset.Name.str.extract(' ([A-Z][a-z]+)\.', expand=False)
pd.crosstab(train_df['Title'], train_df['Sex'])
- 这里combine是一个list,有两个dataframe,一个是train_df另一个是test_df。str.extract()是Series使用正则表达式抽取匹配数据的方法。第一个参数是正则表达式,第二个参数决定不返回dataframe,而是以原本的形式存在。
- pd.crosstab是引用交叉表。是统计分组频率的特殊透视表
title的值很多,需要分组,并查看分组与Survived的关系。
for dataset in combine:
dataset['Title'] = dataset['Title'].replace(['Lady', 'Countess','Capt', 'Col',
'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss')
dataset['Title'] = dataset['Title'].replace('Ms', 'Miss')
dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs')
train_df[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()
其中,Mlle是Miss的法语表达,Ms是Miss的另一种英文表达。Mme是Mrs是法语表达。
利用之前可视化的方法查Age和Title和Survived之间的关系。结果如下。
可以得到如下信息:
- 有时Title和Age的分组有关,如Master
- Title和Survived也有关
所以:应该把Title也作为一个重要的特征加入到模型的训练中
将Title用更简单的形式表示出来。
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
for dataset in combine:
dataset['Title'] = dataset['Title'].map(title_mapping)
dataset['Title'] = dataset['Title'].fillna(0)
最后一句是将没有上述Title值的置为0,也就是补空值,检查一下是否出现空值。
然后就可以丢弃不用的数据特征了。
train中的 ‘Name’ 和 ‘PassengerId’
test中的 ‘Name’
由于在最后对test进行判断的时候,需要用到 ‘PassengerId’ 所以不可以省去。
train_df = train_df.drop(['Name', 'PassengerId'], axis=1)
test_df = test_df.drop(['Name'], axis=1)
combine = [train_df, test_df]
转换类别型特征
将包含字符串的特征转换为数值型,因为在之后训练模型时没有办法对字符串进行训练的。
首先是 ‘Sex’ 中的 ‘female’ 是1,‘male’ 是0。
for dataset in combine:
dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)
补全数字型特征的空值
首先对年龄这一特征进行空值的填补。
有三种办法:
- 利用均值和标准差产生随机数
- 利用其它的数据特征,如(Pclass、Sex),所以可用Pclass和Sex的组合来猜测Age,取对应区间的中位数。
- 结合前两种方法
由于1和3犯法会产生随机数,可能偏离,所以,用第二种方法。
先可视化Pclass、Sex和Age的关系。
grid = sns.FacetGrid(train_df, col='Pclass', hue='Sex')
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend()
可以看出来,Pclas和Sex的不同取值,对Age的分布还是有一定影响的。
用一个2*3(Sex=0、1;Pclass=1、2、3)的的数组来表示每一个填补空白的值。
guess_ages = np.zeros((2,3))#创建数组
for dataset in combine:
for i in range(0, 2):
for j in range(0, 3):
guess_df = dataset[(dataset['Sex'] == i) &
(dataset['Pclass'] == j+1)]['Age'].dropna()
# age_mean = guess_df.mean()
# age_std = guess_df.std()
# age_guess = rnd.uniform(age_mean - age_std, age_mean + age_std)
age_guess = guess_df.median()
# Convert random age float to nearest .5 age
guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5
for i in range(0, 2):
for j in range(0, 3):
dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) &
(dataset.Pclass == j+1),'Age'] = guess_ages[i,j]
dataset['Age'] = dataset['Age'].astype(int)
第一个for循环是为了算出对每一个分组的 预测数 也就是算出来中位数(或用均值和方差的算法)。
guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5
这句话的作用,是将之前得到的中位数进行估值类似于四舍五入的感觉,分成了三个值:0,0.5,1。
第二个for循环是对空值进行赋值。
最后一句话是将AGE转换为int类型~
小编感觉这就是一个四舍五入的操作,为什么不在之前加上0.5再int取整嘞~
之后再检查空值,就发现:
只有 Embarked有空值 了。
之后就是将Age分段然后确定与Survived之间的关系啦~
train_df['AgeBand'] = pd.cut(train_df['Age'], 5)
train_df[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True)
将Age分成五段,查看每一段和Survived之间的关系。
之后将年龄段进行替换,替换成Age的顺序数,并丢掉训练集中的AgeBand:
for dataset in combine:
dataset.loc[ dataset['Age'] <= 16, 'Age'] = 0
dataset.loc[(dataset['Age'] > 16) & (dataset['Age'] <= 32), 'Age'] = 1
dataset.loc[(dataset['Age'] > 32) & (dataset['Age'] <= 48), 'Age'] = 2
dataset.loc[(dataset['Age'] > 48) & (dataset['Age'] <= 64), 'Age'] = 3
dataset.loc[ dataset['Age'] > 64, 'Age'] = 4
train_df = train_df.drop(['AgeBand'], axis=1)
combine = [train_df, test_df]
因为测试集没有生成AgeBand,所以只用丢掉训练集的AgeBand
根据已知特征创建新的数据特征
将Parch和SibSp合并成一个特征叫FamilySize。
for dataset in combine:
dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1
train_df[['FamilySize', 'Survived']].groupby(['FamilySize'], as_index=False).mean().sort_values(by='Survived', ascending=False)
然后观察到:
似乎没有什么标准,于是将有亲人与否进行了分类,创建特征IsAlone
for dataset in combine:
dataset['IsAlone'] = 0
dataset.loc[dataset['FamilySize'] == 1, 'IsAlone'] = 1
train_df[['IsAlone', 'Survived']].groupby(['IsAlone'], as_index=False).mean()
再观察结果:
这就有点意思了~于是之留下来这IsAlone这一个特征就好啦
也可以将Age和Pclass相乘,创建一个新的特征。
train_df = train_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
test_df = test_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
combine = [train_df, test_df]
for dataset in combine:
dataset['Age*Class'] = dataset.Age * dataset.Pclass
train_df[['Age*Class', 'Survived']].groupby(['Age*Class'],as_index=False).mean()
结果如下:
还有一定道理的~
类别型数据特征的空值处理
由于在上文中可以知道,训练集中,Embarked只缺少两个值,所以,用 出现最多 的Embarked值(也就是用众数)填补空值。
freq_port = train_df.Embarked.dropna().mode()[0]
#众数是S
for dataset in combine:
dataset['Embarked'] = dataset['Embarked'].fillna(‘S’)
dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)
同时将Embarked的值进行数值化。
数字型数据特征空值处理
现在,经过查看,只有测试集中的Fare有空值,可以用众数、平均数、中位数代替。用的是中位数。
test_df['Fare'].fillna(test_df['Fare'].dropna().median(), inplace=True)
然后将Fare分段
train_df['FareBand'] = pd.qcut(train_df['Fare'], 4)
train_df[['FareBand', 'Survived']].groupby(['FareBand'], as_index=False).mean().sort_values(by='FareBand', ascending=True)
这个qcut和cut的区别在于:
- qcut是根据频率分段
- cut是根据间隔分段
具体区别可见 这篇文章.
qcut中的参数4,是不断尝试出来的,这个样子,Survived在不同的分段中,值最大限度的有差异。
之后将Fare的分段更简单表示
for dataset in combine:
dataset.loc[ dataset['Fare'] <= 7.91, 'Fare'] = 0
dataset.loc[(dataset['Fare'] > 7.91) & (dataset['Fare'] <= 14.454), 'Fare'] = 1
dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 31), 'Fare'] = 2
dataset.loc[ dataset['Fare'] > 31, 'Fare'] = 3
dataset['Fare'] = dataset['Fare'].astype(int)
train_df = train_df.drop(['FareBand'], axis=1)
combine = [train_df, test_df]
终于数据算是处理完啦~
看一眼最后的数据是什么样子吧
建立模型并预测解决问题
在sklearn中有相当多的模型可以选择,但是选择什么模型是通过观察问题和数据得到的。这个问题是一个分类问题,判断测试集是否存活。并且训练集已经打上标签了。所以,可选择的模型如下:
- Logistic Regression 逻辑回归
- KNN or k-Nearest Neighbors
- Support Vector Machines(SVM)支持向量机
- Naive Bayes classifier 朴素贝叶斯分类器
- Decision Tree 决策树
- Random Forest 随机森林
- Perceptron 感知器
- Artificial neural network 人工神经网络
- RVM or Relevance Vector Machine 相关向量机
在sklearn中所有的模型都有四个固定且常用的方法:
# 拟合模型
model.fit(X_train, y_train)
# 模型预测
model.predict(X_test)
# 获得这个模型的参数
model.get_params()
# 为模型进行打分
model.score(data_X, data_y)
emmm,小编似乎对这篇文章中模型的选择与训练部分还没有搞得很太懂,大家可以看一看KAGGLE下方的评论区,关于模型的处理方法,下一篇文章再来说叭~