开始Kaggle练习,最先进行的当然是Kaggle的“Hello World”---“Titanic: Machine Learning from Disaster",即预测哪些泰坦尼克的旅客幸存。自己最开始写的很乱,包括数据处理、分析、建模等。参考了Kaggle的一篇[1]再整理一遍,清晰很多。实验并没有得到一个很好的结果,但是规范了整个流程,因此写下笔记 。
本文约6.2k字,预计阅读15分钟
竞赛解决方案的流程主要分为以下7个步骤:
问题的定义;
获取训练集和测试集;
整理、准备、清洗数据;
分析、探索数据;
建模、预测和解决问题;
可视化、报告和呈现问题解决步骤和最终解决方案;
提交结果;
当然这个只是一般的步骤,3、4可以进行交叉交换,可视化也可以应用到多个阶段。
泰坦尼克数据集的机器学习研究可以说是Kaggle的“Hello world”。问题的直观定义很简单:使用机器学习创建一个模型,预测哪些乘客在泰坦尼克号沉船中幸存。
其他的信息条件:
1912年4月15日,被广泛认为“永不沉没”的皇家邮轮“泰坦尼克号”在处女航中撞上一座冰山后沉没。不幸的是,船上没有足够的救生艇,导致2224名乘客和船员中有1502人死亡。(存活率为:32.46%)
虽然在生存中有一些运气因素,但似乎有些群体的人比其他人更有可能生存下来。
数据集的获取很简单,使用pandas包即可。
import pandas as pd train_df = pd.read_csv('../input/titanic/train.csv') test_df = pd.read_csv('../input/titanic/test.csv')赛题中Data部分给出了数据特征,也可以通过pandas进行查看:
train_df.columns.values Out: array(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'], dtype=object)结合赛题描述,我们可以对上述特征进行归类:
分类特征:这里指的是可以对数据集按照该特征可以明确的划分为一系列相似的样本。上述属于分类特征的有:
Pclass(阶级)、Survived(是否存活)、Sex(性别)、Embarked(出发地);
数值型特征:值会随着样本的变化而变化,可以是连续的、离散的或基于时间序列的,这里有:
Age(年龄)、Fare(票价)、SibSp(船上兄弟姐妹数量)、Parch(船上父母孩子数量)【注:后两者属于数值型特征是因为当不知道数据集时无法确定特征的取值范围,是随着样本而变化】
其他特征:很难找到有效的的规律,这里还剩下:
Name(姓名)、Ticket(船票号)、Cabin(船舱号);
在对训练集的首尾数据进行查看:
train_df.head() train_df.tail()通过数据中的信息,可以进行得到以下结论:
Name特征中,虽然是字符串类型,但包含了一些性别特征,如(Mr、Mrs、Miss);
Ticket特征,包含了数字和字母的组合,我们很难从中找出一些规律;
我们发现,Cabin特征中存在缺失值,我们需要进一步进行判断,并查看其他特征是否也有缺失;
Passengerld特征为序号,对模型建立应该并无影响;
首先最为关键的是数据缺失问题,Pandas可以通过很多种方式进行判断每个特征是否存在缺失值。最简单的是直接查看数据集的全局信息。
train_df.info() print('-' * 40) test_df.info() <class 'pandas.core.frame.DataFrame'> RangeIndex: 891 entries, 0 to 890 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 PassengerId 891 non-null int64 1 Survived 891 non-null int64 2 Pclass 891 non-null int64 3 Name 891 non-null object 4 Sex 891 non-null object 5 Age 714 non-null float64 6 SibSp 891 non-null int64 7 Parch 891 non-null int64 8 Ticket 891 non-null object 9 Fare 891 non-null float64 10 Cabin 204 non-null object 11 Embarked 889 non-null object dtypes: float64(2), int64(5), object(5) memory usage: 83.7+ KB ________________________________________ <class 'pandas.core.frame.DataFrame'> RangeIndex: 418 entries, 0 to 417 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 PassengerId 418 non-null int64 1 Pclass 418 non-null int64 2 Name 418 non-null object 3 Sex 418 non-null object 4 Age 332 non-null float64 5 SibSp 418 non-null int64 6 Parch 418 non-null int64 7 Ticket 418 non-null object 8 Fare 417 non-null float64 9 Cabin 91 non-null object 10 Embarked 418 non-null object dtypes: float64(2), int64(4), object(5) memory usage: 36.0+ KB通过上述数据集的信息我们可以分析:
数据集中有7个为数值型,5个为object类型;
训练集共891个样本,Age特征缺失177(即19.87%),Cabin特征缺失687(即77.10%),Embarked特征缺失2(即0.22%);
测试集共418个样本,Age特征缺失86(即20.57%),Cabin特征缺失326(即67.78%),Fare特征缺失1(即0.21%);
可以通过Pandas包的describe方法快速查看整个数据的分布情况【这里只观察训练集】:
train_df.describe()结果如图所示:
我们可以得到以下结论:
在训练集中,存活率为38.38%;
社会经济地位(SES)是低的人占的比例最大;
船上大多数没有和父母孩子一起(至少75%);
船上有兄弟姐妹的关系的旅客为25%~50%;
年老的乘客很少;
票价分布不均匀,差异很大,极少数人花费超过了500美元;
上述缺少了部分特征(数据类型不为数值),查看数据分布,可以添加参数include=['O'],即包含object类型:
train_df.describe(include=['O'])可以发现:
游客出发的港口有3个;
船舱有部分重复(147个不相同),某些游客可能共用一个船舱;
票据特征有一定的重复率(22%);
上述分析的是数据集整体的结构与分布,接下来我们将探索单个特征与是否存活的相关性,这是非常有意义的。这里我们指分析不含有缺失值的特征:Pclass、Sex、SibSp、Parch。
(1)Pclass:
train_df[['Pclass', 'Survived']].groupby('Pclass', as_index=False).mean().sort_values(by='Survived', ascending=False) Pclass Survived 0 1 0.629630 1 2 0.472826 2 3 0.242363结论:社会地位高的人存活率最高。
(2)Sex:
train_df[['Sex', 'Survived']].groupby('Sex', as_index=False).mean().sort_values(by='Survived', ascending=False) Sex Survived 0 female 0.742038 1 male 0.188908结论:女性比男性的存活率高。
(3)SibSP:
train_df[['SibSp', 'Survived']].groupby('SibSp', as_index=False).mean().sort_values(by='Survived', ascending=False) SibSp Survived 1 1 0.535885 2 2 0.464286 0 0 0.345395 3 3 0.250000 4 4 0.166667 5 5 0.000000 6 8 0.000000结论:有少数(1~3)或无兄弟姐妹或的旅客存活率较高。
(4)Parch:
rain_df[['Parch', 'Survived']].groupby('Parch', as_index=False).mean().sort_values(by='Survived', ascending=False) Parch Survived 3 3 0.600000 1 1 0.550847 2 2 0.500000 0 0 0.343658 5 5 0.200000 4 4 0.000000 6 6 0.000000结论:与上述结论类似。
(1)首先我们可视化年龄与存活的关系:
import matplotlib.pyplot as plt plt.figure(dpi=100) plt.hist(train_df['Age'], label='All', bins=20) plt.hist(train_df.loc[train_df.Survived == 1, 'Age'], label='Survived', bins=20) plt.xlabel('Age') plt.ylabel('People Number') plt.legend()如图所示:
结论:模型建模应该考虑年龄;0~8岁的小孩和75~80的老人存活率偏高;需要对年龄进行分段处理,并填充缺失值
(2)对上述直方图我们再加入Pclass特征:
plt.clf() fig, axes = plt.subplots(1, 3, dpi=100) plt.subplots_adjust(left=0, bottom=None, right=2.5, top=None, wspace=None, hspace=None) def subplot(Pclass, index): ax_i = axes[index] ax_i.hist(train_df.loc[train_df.Pclass == Pclass, 'Age'], label='All', bins=20) ax_i.hist(train_df.loc[(train_df.Survived == 1) & (train_df.Pclass == Pclass), 'Age'], label='Survived', bins=20) ax_i.set_xlabel('age') ax_i.set_title('Pclass=' + str(Pclass)) ax_i.set_ylabel('People Number') for i in range(3): subplot(i+1, i) plt.legend()如图:
结论:Pclass=1的旅客存活率最高,Pclass=2其次,Pclass=3最低;Pclass=2和3中年龄为0~10的旅客存活率较高;故Pclass是一个重要的特征,建模需要加入
(3)可视化出发地、性别、社会地位(Pclass)与存活率的关系:
import seaborn as sns grid = sns.FacetGrid(train_df, row='Embarked', height=2.2, aspect=1.6) grid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette='deep') grid.add_legend()如图【用了seaborn】:
结论:在三个出发港口中,港口C男性各个阶级的存活率远高于其他两个,且比女性高;对于社会地位高的男性在不同的港口出发,存活率也各不相同;故性别(Sex)与出发地(Embarked)都是重要的特征,建模需要加入
(4)可视化票价与存活率的关系:
plt.figure(dpi=100) plt.hist(train_df['Fare'], label='All', bins=50) plt.hist(train_df.loc[train_df.Survived == 1, 'Fare'], label='Survived', bins=50) plt.xlabel('Fare') plt.ylabel('People Number') plt.legend()如图:
结论:票价高的旅客存活率更高;由于票价分布差异较大,因此可以对该特征进行分段处理;故应将Fare加入进行建模
经过上述对11个特征(除去Survived作为标签)进行数据分析,结论如下:
PassengerId为ID,Ticket特征很难找到规律,Cabin缺失太多,因此都需要进行删除;
Name特征可以提取部分性别、已婚未婚信息、身份等【Name特征本身需要删除】;
Sex、Pclass、Age、Embarked、Fare是非常重要的特征,且特征之间存在着一定的关系;
SibSp、Parch特征关于存活率的分布类似,且两者相加能反应一个家族的人员情况。可以尝试相加;
Age、Embarked、Fare存在着缺失数据,需要对其进行填充;
Sex、Emabrked需要转化为分类的数值型特征;
Age、Fare根据之前的分析进行合理的分段;
经上述分析,删除Passengerld【可进行保存】、Ticket、Cabin特征:
data_id = [] train_df = train_df.drop(['Ticket', 'Cabin'], axis=1) test_df = test_df.drop(['Ticket', 'Cabin'], axis=1) data_id.append(train_df.pop('PassengerId')) data_id.append(test_df.pop('PassengerId')) train_df.shape, test_df.shape # ((891, 9), (418, 8))关于Name特征,一般称呼上"."前面表示一种身份,例如“Miss”、“Dr“等,可以将其用正则表达式进行提取:
train_df['Title'] = train_df.Name.str.extract('([A-Za-z]+)\.', expand=False) test_df['Title'] = test_df.Name.str.extract('([A-Za-z]+)\.', expand=False) pd.crosstab(train_df['Title'], train_df['Sex']) Sex female male Title Capt 0 1 Col 0 2 Countess 1 0 Don 0 1 Dr 1 6 Jonkheer 0 1 Lady 1 0 Major 0 2 Master 0 40 Miss 182 0 Mlle 2 0 Mme 1 0 Mr 0 517 Mrs 125 0 Ms 1 0 Rev 0 6 Sir 0 1我们发现某些类别特别少,所占比例不均匀,且“Mlle”、“Ms”都是“Miss”,“Mme”应该为“Mrs”,可以对其进行替换,对于数量少的可以进行整合为一个类别“Rare”。
combine = [train_df, test_df] 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() Title Survived 0 Master 0.575000 1 Miss 0.702703 2 Mr 0.156673 3 Mrs 0.793651 4 Rare 0.347826再对所有的类别进行映射:
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)删除Name特征:
train_df = train_df.drop('Name', axis=1) test_df = test_df.drop('Name', axis=1) combine = [train_df, test_df]由于性别是字符串表示,我们可以将其用数值型代表:
for dataset in combine: dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)1、Age
最简单的填充方式就是取所有旅客的均值和标准差之间进行随机选择。但是,之前我们分析,Age和Pclass、Gender存在一定的关联。因此,我们可以选择取Pclass和Gender匹配下的Age的均值与标准差的随机选择。
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 = np.random.uniform(age_mean - age_std, age_mean + age_std) dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1),\ 'Age'] = age_guess dataset['Age'] = dataset['Age'].astype(int)由于Age为连续性数值,根据分析,我们可以将其进行分段【10段加以区分】:
train_df['AgeBand'] = pd.cut(train_df['Age'], 10) train_df[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True) AgeBand Survived 0 (-0.08, 8.0] 0.666667 1 (8.0, 16.0] 0.413043 2 (16.0, 24.0] 0.264706 3 (24.0, 32.0] 0.429245 4 (32.0, 40.0] 0.463768 5 (40.0, 48.0] 0.325843 6 (48.0, 56.0] 0.466667 7 (56.0, 64.0] 0.375000 8 (64.0, 72.0] 0.000000 9 (72.0, 80.0] 0.500000将其转化为分类数值型:
train_df['Age'] = pd.cut(train_df['Age'], bins=10, labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) test_df['Age'] = pd.cut(test_df['Age'], bins=10, labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) train_df['Age'] = train_df['Age'].astype('int64') test_df['Age'] = test_df['Age'].astype('int64') combine = [train_df, test_df]2、Embarked
由于训练集中只有两个缺失值,因此我们采用最简单的最常用填充:
for dataset in combine: dataset['Embarked'] = dataset['Embarked'].fillna(dataset.Embarked.dropna().mode()[0])然后再将其转化为分类数值型特征:
for dataset in combine: dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)3、Fare
测试集中有一个样本缺失Fare特征,我们简单的填充为中位数:
test_df['Fare'].fillna(test_df['Fare'].dropna().median(), inplace=True)将不同的Fare进行分段处理,和Age处理方式不同的是,由于票价差距过大,我们不能采用等分,且票价基本和Pclass对应,因此这里使用pd.qcut划分为3份:
train_df['Fare'] = pd.qcut(train_df['Fare'], 3, labels=[0, 1, 2]) test_df['Fare'] = pd.qcut(test_df['Fare'], 3, labels=[0, 1, 2]) train_df['Fare'] = train_df['Fare'].astype('int64') test_df['Fare'] = test_df['Fare'].astype('int64') combine = [train_df, test_df]1、将SibSp与Parch组合,得到Family特征
for dataset in combine: dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1模型完成的是分类问题,因此有很多个机器学习模型可供选择:
Logistic Regression;
KNN;
Naive Bayes Classifier;
Decision Tree;
Support Vector Machine;
MLP;
首先对数据进行处理:
X_train = np.array(train_df.drop("Survived", axis=1)) Y_train = np.array(train_df["Survived"]) X_test = np.array(test_df) X_train.shape, Y_train.shape, X_test.shape导入模型包:
from sklearn.model_selection import KFold from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression from sklearn.naive_bayes import GaussianNB from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC from sklearn.neural_network import MLPClassifier模型训练,采用五折交叉验证:
def train(clf): kf = KFold(n_splits=5) score = list() for train_index, test_index in kf.split(range(len(X_train))): train_X, test_X = X_train[train_index], X_train[test_index] train_y, test_y = Y_train[train_index], Y_train[test_index] clf.fit(train_X, train_y) score.append(clf.score(test_X, test_y)) avg_score = np.mean(score)) print(avg_score) return avg_score1、Logistic 回归
clf = LogisticRegression() lr = train(clf) 0.8047078023978406我们可以查看模型中各个特征的权重,权重的绝对值越大,说明对模型的影响也越大:
def correlation(clf): coeff_df = pd.DataFrame(train_df.columns.delete(0), columns=['Feature']) coeff_df["Correlation"] = pd.Series(clf.coef_[0]) print(coeff_df.sort_values(by='Correlation', ascending=False)) correlation(clf) Feature Correlation 1 Sex 2.119560 7 Title 0.486584 5 Fare 0.318108 6 Embarked 0.118401 4 Parch -0.027359 3 SibSp -0.202700 8 FamilySize -0.228518 2 Age -0.291479 0 Pclass -0.958525我们发现Sex与Pclass特征的影响最大,我们通过特征工程构造的FamilySize也比原来的特征作用更大;【这里我们可以调整特征,例如删除一些影响小的特征,如Parch等;
2、决策树(CART)
clf = DecisionTreeClassifier() cart = train(clf) 0.82491996735923673、KNN
clf = KNeighborsClassifier(n_neighbors=3) knn = train(clf) 0.81143682129182084、朴素贝叶斯
clf = GaussianNB() nb = train(clf) 0.78343481262946465、SVM
clf = SVC(kernel='rbf') svm = train(clf) 0.82714205009101756、MLP
clf = MLPClassifier(max_iter=1000, learning_rate='adaptive') mlp = train(clf) 0.812591802146758排序:
models = pd.DataFrame({ 'Model': ['Logistic Regression', 'Decision Tree', 'KNN', 'Naive Bayes', 'Support Vector Machines', 'MLP', ], 'Score': [lr, cart, knn, nb, svm, mlp]}) models.sort_values(by='Score', ascending=False)结果:
Model Score 4 Support Vector Machines 0.827142 1 Decision Tree 0.824920 5 MLP 0.812592 2 KNN 0.811437 0 Logistic Regression 0.806955 3 Naive Bayes 0.783435我们使用SVM模型进行预测(这里简单考虑,不做模型融合,其他的特征工程暂不考虑)
clf = SVC(kernel='rbf') svm = train(clf) pred = clf.predict(X_test) pred = np.c_[data_id[1], pred] pred_df = pd.DataFrame(pred, columns=['PassengerId', 'Survived']) pred_df.to_csv('/kaggle/working/Submission.csv', index=False)最后预测的结果为:0.78947(5116名/结果并不好,还需继续进行调整)
[1] https://www.kaggle.com/startupsci/titanic-data-science-solutions
往期精彩回顾
通过比赛整理出的8条Numpy实用技巧【你知道如何频数统计和按某列进行排序么?】
Pandas笔记---深入Groupby,它的功能没有你想的这么简单
Pandas笔记---通过比赛整理出的10条Pandas实用技巧
机器学习笔记---从极大似然估计的角度看待Logistic回归
机器学习笔记---正则化为什么可以抑制过拟合?
机器学习笔记---给“过拟合”下一个准确且规范的定义
扫码关注更多精彩