这个项目是kaggle上的练手项目,实践方面是参考于csdn上一位大佬的总结,自己对其进行了实现和理解,主要是为了解整个项目操作的流程,并且,由于篇幅过长,所以会分为两部分记录
数据导入
import re
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
- 注:这里加入
import warnings
warnings.filterwarnings('ignore')
主要就是为了美观,如果不加的话,warning一堆堆的,不怎么整洁
#导入数据
train_data = pd.read_csv('C:/Users/Youngy/Desktop/Titanic_dataset/train.csv')
test_data = pd.read_csv('C:/Users/Youngy/Desktop/Titanic_dataset/test.csv')
#查看前几行的源数据
sns.set_style('whitegrid')#设定图表的主题,其中whitegrid的主题比较简洁
train_data.head()
输出结果
#查看所有的数据信息概况
train_data.info()
test_data.info()
train的输出结果
test的输出结果
- 看过数据的概况后,可以发现,训练集中的Age,Cabin,Embarked,测试集中的Fare是有缺失值的
#绘制存活的比例
train_data['Survived'].value_counts().plot.pie(autopct = '%1.2f%%')
存活比例
缺失值处理
- 如果数据集很大,缺失值只占极小的一部分,可以直接删掉带有缺失值的行
- 如果该特征对模型的学习来说不是很重要,则可以对缺失值填充均值or众数;比如‘在哪儿登船’的这个特征共有三个登船的地点,总的数据是891,而该特征是889,缺失两个数值,就可以直接使用众数进行填充
#众数
train_data.Embarked.dropna().mode().values
#将众数赋值到源数据中
train_data.Embarked[train_data.Embarked.isnull()] = train_data.Embarked.dropna().mode().values
-
mode
是众数,train_data.Embarked.dropna()
是表示对删去缺失值后的数据,再加个mode
则表示删去缺失值后,数据的众数 - 对于标称属性,我们可以赋予一个代表缺失的值,比如‘U0’;因为缺失本身也可能代表着一些隐含信息,比如船舱号Cabin这个特征,缺失可能表示代表其没有船舱,也是有意义的
- 注:标称属性(nominal attribute)意味着‘与名称相关’,它的值是一些符号或事物的名称。每个值代表某种类别,编码或状态,因此标称属性又被看作是分类的(categorical)
train_data['Cabin'] = train_data.Cabin.fillna('U0')
- 使用线性回归、随机森林等模型来预测缺失属性的值;比如:Age是一个非常重要的特征,我们需要保证缺失值填充的准确率,这是非常重要的
- 一般来说,会采用数据较完整的条目作为模型的训练集,以此来预测缺失值
#采用随机森林来预测
from sklearn.ensemble import RandomForestRegressor
age_df = train_data[['Age','Survived','Fare', 'Parch', 'SibSp', 'Pclass']]#提取数据
age_df_notnull = age_df.loc[(train_data['Age'].notnull())]#训练集
age_df_isnull = age_df.loc[(train_data['Age'].isnull())]#测试集(即需要进行缺失值填充的数据)
X = age_df_notnull.values[:,1:]#训练集的输入X
y = age_df_notnull.values[:,0]#训练集的输出标签y
RFR = RandomForestRegressor(n_estimators=1000,n_jobs=-1)#调用随机森林,进行训练模型
RFR.fit(X,y)
predictAges = RFR.predict(age_df_isnull.values[:,1:])#预测
train_data.loc[train_data['Age'].isnull(),['Age']] = predictAges#将预测的结果赋值到源数据中
- 注意这种操作的顺序,提取notnull和isnull别搞错了就行
- 对Series进行切片,一定要指定特征后加个values再进行,不然会报错
#查看缺失值处理后的结果
train_data.info()
缺失值处理后的结果
分析数据关系
分析数据之间的关系,也就是分析特征与特征之间的关系,下面我们依次分析
性别与是否生存的关系
- 利用
groupy().count()
将sex和survived的数据统计出来
train_data.groupby(['Sex','Survived'])['Survived'].count()
统计结果
train_data[['Sex','Survived']].groupby(['Sex']).mean().plot.bar()
统计结果的柱状图
train_data[['Sex','Survived']].groupby(['Sex']).mean()
Survived
Sex
female 0.742038
male 0.188908
- 而
train_data[['Sex','Survived']].groupby(['Survived']).mean()
并不可以统计,原因是sex中的数据都是非数字(经验证,利用replace改成数字后就可以统计了)
除此之外,似乎pandas中的筛选数据的操作只能针对数字型的数据,eg:a[a.Survived > 0]
才可以,a.Survived=0
和sex='male'
都不可以
基于以上的图表,可以发现这里性别之中,女性的生存率更高,所以有相当大的关系
船舱等级和生存与否的关系
train_data.groupby(['Pclass','Survived'])['Pclass'].count()
统计结果
train_data[['Pclass','Survived']].groupby(['Pclass']).mean().plot.bar()
柱状图
- 这里的
mean
到底计算的是什么呢?- 其实前面的
count
是统计总的量,而这里的mean
是要计算全部非NA量的平均值,也就是Survived
属性中的所有值都加在一起,然后除以这个属性的值个数,即
- 其实前面的
train_data[['Sex','Pclass','Survived']].groupby(['Pclass','Sex']).mean().plot.bar()
柱状图
train_data.groupby(['Sex','Pclass','Survived'])['Survived'].count()
统计结果
由上面的图表可以看出,虽然每种船舱里都体现着女性优先,但最终,生存的情况和船舱等级有密不可分的联系
年龄与存活与否的关系
fig,axes = plt.subplots(1,2,figsize = (10,5))
sns.violinplot(x = 'Pclass' , y = 'Age' , hue = 'Survived' , data = train_data , split = True , ax = axes[0])
axes[0].set_title('Pclass and Age vs Survived')
axes[0].set_yticks(range(0,110,10))
sns.violinplot(x = 'Sex' , y = 'Age' , hue = 'Survived' , data = train_data , split = True , ax = axes[1])
axes[1].set_title('Sex and Age vs Survived')
axes[1].set_yticks(range(0,110,10))
plt.show()
琴图
- 在写这里时出了个问题,就是会显示
matplotlib
里没有subplots
属性,这里是因为前面在导入包的时候,导入的是import matplotlib as plt
,其实只要导入import matplotlib.pyplot as plt
就可以了
# 我们先用直方图和箱线图看一下所有人的年龄分布
plt.figure(figsize=(10,5))
plt.subplot(121)
train_data['Age'].hist(bins = 70)
plt.xlabel('Age')
plt.ylabel('Num')
plt.subplot(122)
train_data.boxplot(column='Age',showfliers=False)
plt.show()
直方图和箱线图
-
plt.subplot(121)
表示一行两列的图像,最后一个1表示显示第一个位置
bins
是直方图中的分类粒度大小,值越大越细
showfliers
表示是否显示异常值,而默认是显示的,所以这里showfliers = False
是让箱线图不显示异常值
# 看一下不同年龄段的生存与否的分布情况
facet= sns.FacetGrid(train_data,hue = 'Survived',aspect=3)#aspect是关于图像大小的参数
facet.map(sns.kdeplot,'Age',shade=True)
facet.set(xlim=(0,train_data['Age'].max()))
facet.add_legend()
-
FacetGrid
对象用于生成plot的网格布局,当网格创建完毕后,可以使用FacetGrid
的map
方法来指定plot类型和需要绘制的属性-
hue='Survived'
是分类参数,也可以使用col='Survived'
设定条件将数据分为多个子集,另外,aspect
是设置图像大小的参数 -
sns.kdeplot
是核密度曲线(Kernel Density Plot),其中shade=True
是显示曲线的面积,可以方便观察
-
# 再观察一下不同年龄下的平均生存率
fig,axes1 = plt.subplots(1,1,figsize=(10,5))
train_data['Age_int'] = train_data['Age'].astype(int)#源数据里的Age是float64,现在转换为int32,并且作为一个新的特征加入源数据
#做一个数据的聚合,将平均值mean的数值作为Survived属性的新数据,然后将Age_int和Survived特征放到新的数据集average_age中
average_age = train_data[['Age_int','Survived']].groupby(['Age_int'],as_index = False).mean()
sns.barplot(x='Age_int',y='Survived',data=average_age)
各个年龄的平均生存率
- 上面是先转换数据,作为一个新特征加入源数据,然后筛选属性,做一个数据的聚合,将平均值mean的数值作为Survived属性的新数据,然后将Age_int和Survived特征放到新的数据集average_age中
- 其中,
as_index=False
是保留原先的0~891的数字索引,而如果as_index=True
(该参数默认为True),列Age_int会被默认为索引列,新的dataframe中不再包含这列数据
- 其中,
# 观察年龄统计
train_data['Age'].describe()
输出
- 样本有891,平均年龄约为30岁,标准差13.5岁,最小年龄为0.42,最大年龄80.
- 根据年龄统计,我们可以将乘客划分为儿童,青少年,成年和老年,并进一步分析四个群体的生还情况
bins = [0,12,18,65,100]
train_data['Age_group']= pd.cut(train_data['Age'],bins)
by_age = train_data.groupby('Age_group')['Survived'].mean()
by_age
输出结果
-
pd.cut
是pandas自身的函数,其可以对连续型变量进行分类汇总- 其中,我们对
(0,12)
这样的区间段可以加上label
eg:train_data['Age_group']= pd.cut(train_data['Age'],bins,label=['儿童','青少年','成年','老年'])
- 其中,我们对
by_age.plot(kind = 'bar')
image.png
乘客姓名与存活与否的关系
train_data['Name'].head(5)
输出
- 通过观察名字数据,我们可以看出其中包括对乘客的称呼,如:Mr、Miss、Mrs等,称呼信息包含了乘客的年龄、性别,同时也包含了如社会地位等的称呼,如:Dr,、Lady、Major、Master等的称呼
train_data['Title'] = test_data['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
pd.crosstab(train_data['Title'],train_data['Sex'])
输出
- 这里是将姓名中的称呼用字符串str的
str.extract()
函数+正则表达式提取出来,然后利用交叉表crosstab将Title中的数据用sex特征进行统计- 注1:注意书写格式:要提取的部分正则表达式要用引号引起来。抽取多个数字或者字母的话要在后面加上'+'
- 注2:加入之后的数据并不是数值格式的(属于字符串格式的),因此不能跟正常的数值一样进行运算,需要计算的时候要进行格式的转换
-
train_data['Title'].astype(float)
转换为浮点型 -
train_data['Title'] = train_data['Title'].map(lambda x:float(x))
也可以用map和匿名函数转换格式
-
- 注3:
.str
的功能是可以使用切片器的,eg:train_data['Title'].str[:7]
train_data[['Title','Survived']].groupby(['Title']).mean().plot.bar()[图片上传中...(image.png-cfdce2-1552632604968-0)]
image.png
fig,axis1 = plt.subplots(1,1,figsize = (10,5))
train_data['Name_length'] = train_data['Name'].apply(len)
name_length = train_data[['Name_length','Survived']].groupby(['Name_length'],as_index=False).mean()
sns.barplot(x='Name_length',y='Survived',data=name_length)
image.png
在计算名字的长度时,我们用到了
pandas.apply
函数,这个函数是所有函数中自由度最高的函数
-
DataFrame.apply(func, axis=0, broadcast=False, raw=False, reduce=None, args=(), **kwds)
-
func
是函数,相当于c里的函数指针,而这个函数需要自己去实现,因为函数的传入参数是根据axis
来确定的,比如设置axis=1
,则会把一行的数据作为Series
的数据结构传入这个函数中,此时我们可以在这个函数中实现对Series
的不同属性之间的数值计算,并返回一个结果,而apply
函数则会自动遍历每一行的DataFrame
的数据,最后将所有结果组合成一个Series
数据结构返回;如果设置默认,即axis=1
,则依然会一行一行的将数值参数传入函数中,比如x+9
的简单函数,如果传入的数据中,一行有两个属性,则每个属性值都会+9
,结果会返回一个所有数值都+9
的DateFrame
- 如果我们想给自己实现的函数传递参数,就可以用的apply函数的
*args
和**kwds
参数,比如:func
的传入参数是两个,而DataFrame
只有一个数值,我们则可以用*args
和**kwds
参数传入func
中
-
有无兄弟姐妹和存活与否的关系
# 我们先将数据分为有兄弟姐妹和没兄弟姐妹两组
sibsp_df = train_data[train_data['SibSp']!=0]
np_sibsp_df = train_data[train_data['SibSp']==0]
#再对两组数据进行可视化
plt.figure(figsize = (10,5))
plt.subplot(121)
sibsp_df['Survived'].value_counts().plot.pie(labels=['No Survived','Survived'],autopct= '%1.1f%%')
plt.xlabel('sibsp')
plt.subplot(122)
sibsp_df['Survived'].value_counts().plot.pie(labels=['No Survived','Survived'],autopct= '%1.1f%%')
plt.xlabel('no_sibsp')
plt.show()
image.png
有无父母子女和存活与否的关系
parch_df = train_data[train_data['Parch']!=0]
no_parch_df = train_data[train_data['Parch']==0]
plt.figure(figsize=(10,5))
plt.subplot(121)
parch_df['Survived'].value_counts().plot.pie(labels=['No Survived','Survived'],autopct= '%1.1f%%')
plt.xlabel('parch')
plt.subplot(122)
no_parch_df['Survived'].value_counts().plot.pie(labels=['No Survived','Survived'],autopct= '%1.1f%%')
plt.xlabel('no_parch')
plt.show()
image.png
亲友的人数和存活与否的关系 SibSp & Parch
# 先看一下有兄弟姐妹、有父母这两个属性与存活之间的数据统计
fig,ax=plt.subplots(1,2,figsize=(10,5))
train_data[['Parch','Survived']].groupby(['Parch']).mean().plot.bar(ax=ax[0])
ax[0].set_title('Parch and Survived')
train_data[['SibSp','Survived']].groupby(['SibSp']).mean().plot.bar(ax=ax[1])
ax[1].set_title('SibSp and Survived')
image.png
# 再看一下SibSp & Parch在一起后与存活之间的关系
train_data['Family_Size'] = train_data['Parch'] + train_data['SibSp'] + 1
train_data[['Family_Size','Survived']].groupby(['Family_Size']).mean().plot.bar()
image.png
- 从图表中可以看出,若独自一人,那么其存活率比较低;但是如果亲友太多的话,存活率也会很低
票价分布和存活与否的关系 Fare
fig,axes = plt.subplots(1,2,figsize = (10,5))
train_data['Fare'].hist(bins=70,ax = axes[0])
train_data.boxplot(column='Fare',by='Pclass',showfliers=False,ax = axes[1])
plt.show()
image.png
train_data['Fare'].describe()
image.png
#绘制生存与否与票价均值和方差的关系
fare_not_survived = train_data['Fare'][train_data['Survived']==0]
fare_survived = train_data['Fare'][train_data['Survived']==1]
average_fare= pd.DataFrame([fare_not_survived.mean(),fare_survived.mean()])
std_fare = pd.DataFrame([fare_not_survived.std(),fare_survived.std()])
average_fare.plot(yerr=std_fare,kind='bar',legend=False)
plt.show()
image.png
- 其中,参数
yerr
是y error的简称,可以画出y的偏差,所以我们在这里传入了yerr=std_fare
船舱类型和存活与否的关系 Cabin
- 由于船舱的缺失值确实太多,有效值仅仅有204个,很难分析出不同的船舱和存活的关系,所以在做特征工程的时候,可以直接将该组特征丢弃
- 当然,这里我们也可以对其进行一下分析,将缺失的数据都分为一类。
进一步,我们可以简单地将数据分为是否有Cabin记录来作为一个新的特征,让其与生存与否进行分析
#加入一个新的特征‘Has_Cabin’
train_data.loc[train_data.Cabin.isnull(),'Cabin'] = 'U0'
train_data['Has_Cabin'] = train_data['Cabin'].apply(lambda x: 0 if x=='U0' else 1)
train_data[['Has_Cabin','Survived']].groupby(['Has_Cabin']).mean().plot.bar()
image.png
#接着对不同类型的船舱进行分析
train_data['CabinLetter'] = train_data['Cabin'].map(lambda x :
train_data['CabinLetter'] = pd.factorize(train_data['CabinLetter'])[0]
train_data[['CabinLetter','Survived']].groupby(['CabinLetter']).mean().plot.bar()
image.png
可见,不同的船舱生存率也有不同,但是差别不大。所以在处理中,我们可以直接将特征删除
港口和存活与否的关系 Embarked
- 泰坦尼克号从英国的南安普顿港出发,途径法国瑟堡和爱尔兰昆士敦,那么在昆士敦之前上船的人,有可能在瑟堡或昆士敦下船,这些人将不会遇到海难
sns.countplot('Embarked',hue = 'Survived',data = train_data)
plt.title('Embarked and Survived')
image.png
sns.factorplot('Embarked','Survived',data=train_data,size=3,aspect=2)
plt.title('Embarked and survived rate')
image.png
- 由上可以看出,在不同的港口上船,生还率不同,C最高,Q次之,S最低
- 据了解,泰坦尼克号上共有2224名乘客。本训练数据只给出了891名乘客的信息,如果该数据集是从总共的2224人中随机选出的,根据中心极限定理,该样本的数据也足够大,那么我们的分析结果就具有代表性;但如果不是随机选取,那么我们的分析结果就可能不太靠谱了。
其他可能和存活与否有关系的特征
- 对于数据集中没有给出的特征信息,我们还可以联想其他可能会对模型产生影响的特征因素。如:乘客的国籍、乘客的身高、乘客的体重、乘客是否会游泳、乘客职业等等。
- 另外还有数据集中没有分析的几个特征:Ticket(船票号)、Cabin(船舱号),这些因素的不同可能会影响乘客在船中的位置从而影响逃生的顺序。但是船舱号数据缺失,船票号类别大,难以分析规律,所以在后期模型融合的时候,将这些因素交由模型来决定其重要性
变量转换
- 变量转换的目的是将数据转换为适用于模型使用的数据,不同模型接受不同类型的数据,Scikit-learn要求数据都是数字型numeric,所以我们要将一些非数字型的原始数据转换为数字型numeric
- 所有的数据可以分为两类:
- 定量(Quantitative)变量可以以某种方式排序,Age就是一个很好的列子。
- 定性(Qualitative)变量描述了物体的某一(不能被数学表示的)方面,Embarked就是一个例子。
定性转换
Dummy Variables
- 定性变量的数据,例如性别、民族等,由于定性变量通常表示的是某种特征的有和无,所以量化方法可采用取值为0,1,2,3....等。这种变量称作虚拟变量(Dummy Variable),用D表示。它的作用是使定性数据能包括在统计模型中
- 当qualitative variable(定性变量)是一些频繁出现的几个独立变量时,Dummy Variables比较适合使用。我们以Embarked为例,Embarked只包含三个值’S’,‘C’,‘Q’,我们可以使用下面的代码将其转换为dummies
embark_dummies = pd.get_dummies(train_data['Embarked'])
train_data = train_data.join(embark_dummies)
train_data.drop(['Embarked'],axis=1,inplace=True)
-
get_dummies
是pandas里的一个函数,可以用来对定性变量进行one-hot编码
然后将编码后的数据连接到源数据中,同时又删去了‘Embarked’特征
embark_dummies = train_data[['S','C','Q']]
embark_dummies.head()
image.png
Factorizing
- dummy不好处理Cabin(船舱号)这种标称属性,因为他出现的变量比较多。所以Pandas有一个方法叫做factorize(),它可以创建一些数字,来表示类别变量,对每一个类别映射一个ID,这种映射最后只生成一个特征,不像dummy那样生成多个特征
- 疑问:这样的话会不会造成有序呢?就是会不会出现one-hot编码之前想去避免的情况呢
train_data['Cabin'][train_data.Cabin.isnull()] = 'U0'
train_data['CabinLetter'] = train_data['Cabin'].map(lambda x :
train_data['CabinLetter'] = pd.factorize(train_data['CabinLetter'])[0]
train_data['CabinLetter'].head()
image.png
定量(Quantitative)转换
scaling缩放
- Scaling可以将一个很大范围的数值映射到一个很小的范围(通常是-1 - 1,或则是0 - 1),很多情况下我们需要将数值做Scaling使其范围大小一样,否则大范围数值特征将会由更高的权重。比如:Age的范围可能只是0-100,而income的范围可能是0-10000000,在某些对数组大小敏感的模型中会影响其结果
#对Age进行缩放
from sklearn import preprocessing
assert np.size(train_data['Age']) == 891
scaler = preprocessing.StandardScaler()
train_data['Age_scaled'] = scaler.fit_transform(train_data['Age'].values.reshape(-1,1))
train_data['Age_scaled'].head()
image.png
Binning
- Binning通过观察“邻居”(即周围的值)将连续数据离散化。存储的值被分布到一些“桶”或“箱“”中,就像直方图的bin将数据划分成几块一样。下面的代码对Fare进行Binning
train_data['Fare_bin'] = pd.qcut(train_data['Fare'],5)
train_data['Fare_bin'].head()
image.png
pd.qcut
是根据这些值的频率来选择箱子的均匀间隔,即每个箱子中含有的数的数量是相同的pd.cut
将根据值本身来选择箱子均匀间隔,即每个箱子的间距都是相同的
- 在将数据Bining化后,要么将数据factorize化,要么dummies化
#factorize化
train_data['Fare_bin_id'] = pd.factorize(train_data['Fare_bin'])[0]
#dummies化
fare_bin_dummies_df = pd.get_dummies(train_data['Fare_bin']).rename(columns=lambda x: 'Fare_' + str(x))
train_data = pd.concat([train_data, fare_bin_dummies_df], axis=1)