1、概述
正式入坑kaggle竞赛,为了学习,做了Titanic: Machine Learning from Disaster,原文地址:https://www.kaggle.com/c/titanic
经过一段时间的学习,对数据分析和机器学习有了更深的了解和认知。在机器学习中,特征工程及其重要,如果特征表达的好,一个复杂的问题用一个简单的分类器可能也能做的很好。sklearn中的算法集成的很好,每个算法接口都是一样的,做实验的时候很简单,只需要简单替换即可。
初步拿到这个题目的时候,没有看那些大牛的方法整理,按照自己的理解,使用自己仿照《机器学习实战》的Adaboost做分类得到了0.78的成绩,再难上去了,后来也试了一些其他的分类算法,效果并不明显,甚至比这个得分还差。特征表达上,对于类别特征,将他们用自然数编号,对于连续的特征,如年龄和票价,则将他们分为几个区间然后再编号,这样就得到了全部是数字的特征,最后用Adaboost进行分类。
一开始还尝试了朴素贝叶斯,用的是sklearn的BernoulliNB,特征表达上也不一样,不是对特征编号,而是对他们做因子化处理,转换为01序列,最后的特征很长,这个的得分为0.76。期间还出现了一些不稳定性因素,比如受性别的影响很大。
后来看了一下Tutorial和网上一些人的分享,觉得那些高分的人的特征工程做的真的是很好,比如存活率竟然跟名字的长度、船票的编号之类有关,实在是难以理解。
2、数据分析
数据是csv格式,能用excel打开,所有拿到数据的时候都是直接用excel打开,还可以做一些分析,比如用excel的筛选,可以统计不同Embarked的人的存活率。
拿到数据首先要理解每一个columns的含义,不再赘述。用excel的筛选也可以快速看到那个column有空白值,以及该culumn有几个种类都一目了然。按照常理,存活率应该跟性别、年龄、亲朋好友人数和在哪里上船有关,而跟名字、社会等级、船票编号、票价关系不大。不过经过分析发现,存活率跟社会等级关系很大,可能是社会地位高的人资源更丰富吧;名字里面含有人的头衔,比如Mr.,Miss.,Master.等等,这些与社会地位、男女等有关,因此与存活率也有关系,比如Miss的存活率要比Mr的高;后来发现票价与存活率也有关,票价高的存活率较高。而cabin这个列空白值太多,直接丢弃,当然实际中存活率与住在哪个船舱肯定有关。或许也可以分两种情况来预测,一种是Cabin不为空,一种是空。没有尝试该方式。
下面使用Pandas对数据进行分析,首先:
import numpy as np import pandas as pd from pandas import Series, DataFrame from matplotlib import pyplot as plt import seaborn as sns
然后将训练数据和测试数据读入
train_csv = pd.read_csv("D:\\leran\\titanic\\train.csv") test_csv = pd.read_csv("D:\\leran\\titanic\\test.csv")
(1)数据一览
在训练集和测试集中,Age、Fare、Cabin、Embarked有NaN数据,Age、Fare、Embarked是需要使用的,后面需要想办法填充NaN数据
可以看到:训练集中有38.38的人存活了,平均年龄是29岁左右,平均票价是32块钱,但最高票价却达到了512块钱,差太多了,可以考虑到是否团体票、家庭票。
可以看到,每个人的名字都不一样,Sex中,出现最多的是male,人数为577人,Tickcet竟然有7个人的票号相同,说明肯定是团体票了,Embarked中出现最多的是S,多达644个,后面将用这个S来填充测试集中Embarked的唯一一个NaN值
(2)Pclass
下面分析Pclass与存活的关系train_csv.groupby(["Pclass"])["Survived"].count()
Out[11]:
Pclass
1 216
2 184
3 491
Name: Survived, dtype: int64
train_csv.groupby(["Pclass"])["Survived"].sum()
Out[12]:
Pclass
1 136
2 87
3 119
Name: Survived, dtype: int64
train_csv.groupby(["Pclass"])["Survived"].mean()
Out[13]:
Pclass
1 0.629630
2 0.472826
3 0.242363
Name: Survived, dtype: float64
可以清晰看到,Pclass越小存活率越高,Pclass为1中有62.9%的人都活下来了,因此即便是测试数据中,我们有充分理由认为社会等级高的人存活率更大。
(3)Age
g = sns.FacetGrid(train_csv, col='Survived')
g.map(plt.hist, 'Age', bins=20)
这样看的不是很直观,不是很好分辨那个年龄段的人存活的概率高
下面统计每个年龄的存活率
train_csv["Age_int"] = train_csv["Age"].dropna().astype("int")
train_csv[["Age_int", "Survived"]].groupby(["Age_int"]).mean().plot.bar()
年龄太多,看着分布感觉年龄小的和年龄老的存活率最高。按照常识,可以将年龄分段
从上图中,可以将年龄分为以下几段:
bins=[0, 10, 20, 47, 100]
train_csv["Age_cut"] = pd.cut(train_csv["Age"], bins,labels=[1,2,3,4])
train_csv.groupby(["Age_cut"])["Survived"].mean()
Out[47]:
Age_cut
1 0.593750
2 0.382609
3 0.383408
4 0.415730
Name: Survived, dtype: float64
画出了看看也行
train_csv.groupby(["Age_cut"])["Survived"].mean().plot.bar()
当然,年龄也可以更加细分一点。
(4)Sex
train_csv.groupby("Sex")["Survived"].mean()
Out[5]:
Sex
female 0.742038
male 0.188908
Name: Survived, dtype: float64
train_csv.groupby("Sex")["Survived"].count()
Out[6]:
Sex
female 314
male 577
Name: Survived, dtype: int64
train_csv.groupby("Sex")["Survived"].sum()
Out[7]:
Sex
female 233
male 109
Name: Survived, dtype: int64
太可怕了,男人死了很多。所以如果是女人,存活几率更大
(5)Fare
重新限制一下x轴:
可以看到船票价格高于20元左右的人的存活率较高
train_csv.boxplot(column="Fare", by="Pclass", showfliers=False)
社会等级高的人票价也高,因此Fare和Pclass具有较强的相关性
(6)Embarked
train_csv.groupby("Embarked")["Survived"].mean()
Out[23]:
Embarked
C 0.553571
Q 0.389610
S 0.336957
Name: Survived, dtype: float64
train_csv.groupby("Embarked")["Survived"].sum()
Out[24]:
Embarked
C 93
Q 30
S 217
Name: Survived, dtype: int64
train_csv.groupby("Embarked")["Survived"].count()
Out[26]:
Embarked
C 168
Q 77
S 644
Name: Survived, dtype: int64
S上来的人最多,但C上料的人存活率最高
sns.countplot("Embarked", hue="Survived", data=train_csv)
Out[37]: <matplotlib.axes._subplots.AxesSubplot at 0x1cca7197eb8>
plt.title("Embarked and Survived")
Out[38]: <matplotlib.text.Text at 0x1cc9d110668>
或者:
sns.factorplot("Embarked", "Survived", data=train_csv)
Out[40]: <seaborn.axisgrid.FacetGrid at 0x1cca7ff8208>
(7)Cabin
Cabin空白项太多了,不使用该特征
(8)Name
首先分析名字的长度与存活率的关系
train_csv["Name_Len"] = train_csv["Name"].apply(len)
facet=sns.FacetGrid(train_csv, hue="Survived", aspect=4)
facet.map(sns.kdeplot, "Name_Len", shade=True)
Out[47]: <seaborn.axisgrid.FacetGrid at 0x1cca84c4518>
plt.title("Name_Len and Survived")
Out[48]: <matplotlib.text.Text at 0x1cca821f908>
可见,名字长的存活率高
另外,名字中含有每个人的称号,比如Mr,Mrs,Miss等,这些称号与社会地位、男女等有关,应该与生存几率也有关系,一开始没有注意到这个。
有很多只有一个人,还有很多相似的,需要合并处理一下
train_csv['Title'] = train_csv['Title'].replace(['Lady', 'Countess', 'Capt', 'Col',
'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
train_csv['Title'] = train_csv['Title'].replace('Mlle', 'Miss')
train_csv['Title'] = train_csv['Title'].replace('Ms', 'Miss')
train_csv['Title'] = train_csv['Title'].replace('Mme', 'Mrs')
train_csv.groupby("Title")["Survived"].count()
Out[58]:
Title
Master 40
Miss 185
Mr 517
Mrs 126
Rare 23
Name: Survived, dtype: int64
下面求一下titl和存活率的关系
train_csv.groupby("Title")["Survived"].sum()
Out[59]:
Title
Master 23
Miss 130
Mr 81
Mrs 100
Rare 8
Name: Survived, dtype: int64
train_csv.groupby("Title")["Survived"].mean()
Out[60]:
Title
Master 0.575000
Miss 0.702703
Mr 0.156673
Mrs 0.793651
Rare 0.347826
Name: Survived, dtype: float64
train_csv.groupby("Title")["Survived"].mean().sort_values()
Out[61]:
Title
Mr 0.156673
Rare 0.347826
Master 0.575000
Miss 0.702703
Mrs 0.793651
Name: Survived, dtype: float64
由此可见,存活率高的都是女人和地位高的。title的分类可以更详细一些,有的头衔不太懂是什么
(9)亲朋好友数量
这两个都没有像Sex和Pclass之类的能够明显分出是否存活,我们把两者加起来,叫做家庭
train_csv["Family"] = train_csv["SibSp"] + train_csv["Parch"]
可以看到,有3个亲朋好友的存活率还是很高的,2个和1个的都超过了存活率均值,因此具有一定的参考意义
3、特征表示
(1)Pclass
Pclass不用处理,直接使用
(2)Sex
# 处理Sex特征,Sex在训练和测试中都没有NaN项 for dataset in combine: dataset['Sex'] = dataset['Sex'].map({'female': 1, 'male': 0}).astype(int)直接映射
(3)Fare
# 处理船价格 _test['Fare'] = _test['Fare'].fillna(_test['Fare'].dropna().median()) _train['FareBand'] = pd.qcut(_train['Fare'], 4) print(_train[['FareBand', 'Survived']].groupby(['FareBand'], as_index=False).mean().sort_values(by='FareBand', ascending=True)) 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)
Fare只有一个空白值,所以用中位数填充一下
然后按照qcut的结果将Fare划分区间,重新label一下
(4)Embarked
# 处理Embarked, 这个字段对处理结果好像影响不大,反而对某些算法产生干扰 freq_port = _train["Embarked"].dropna().mode()[0] for dataset in combine: dataset['Embarked'] = dataset['Embarked'].fillna(freq_port) print(_train[['Embarked', 'Survived']].groupby(['Embarked'], as_index=False).mean().sort_values(by='Survived', ascending=False)) for dataset in combine: dataset['Embarked'] = dataset['Embarked'].map({'S': 0, 'C': 1, 'Q': 2}).astype(int) _test = _test.drop(["Embarked"], axis=1) _train = _train.drop(["Embarked"], axis=1) combine = [_train, _test]
使用众数来填充NaN值,NaN值也不多,然后直接编号
(5)NameLen
dataset["Name_Len"] = dataset["Name"].apply(len)
放在Title一起处理
(6)Title
# 处理title特征 for dataset in combine: dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False) # print("title vs sex:\r\n", pd.crosstab(_train['Title'], _train['Sex'])) 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') # print(_train[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()) 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) dataset["Name_Len"] = dataset["Name"].apply(len)首先提取Title,然后将少见的合并为Rare,最后Label
(7)Age
处理一:
# 处理Age,存在部分NaN项,根据Pclass和性别估计Age,用相同Pclass和Sex的人的Age中位数填充NaN项 # train和test是分别处理的,得到的结果并不一样 guess_ages = np.zeros((2, 3)) for dataset in combine: for i in range(0, 2): # 上面已经将Sex转为0和1 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) # # print(combine[0].info()) # print("guess_ages: ", guess_ages) # 将年龄分段,相当于幼儿,少年,青年,中年,老年 _train['AgeBand'] = pd.cut(_train['Age'], 5) print(_train[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True)) 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 = _train.drop(["AgeBand"], axis=1) combine = [_train, _test]
处理二:
使用已知的特征预测每个NaN人的年龄
classers = ['Fare', 'Pclass', 'Title', 'Sex', 'Embarked', 'Family', 'Name_Len'] etr = RandomForestRegressor(n_estimators=200) X_train = full[classers][full['Age'].notnull()] Y_train = full['Age'][full['Age'].notnull()] X_test = full[classers][full['Age'].isnull()] etr.fit(X_train, np.ravel(Y_train)) age_preds = etr.predict(X_test) full['Age'][full['Age'].isnull()] = age_preds _train = full[0:891].copy() _test = full[891:].copy() combine = [_train, _test]
4、模型选择和训练
刚刚拿到这个题目路的时候,做了简单的数据分析,用了朴素贝叶斯来做,当然用朴素贝叶斯需要将特征全部因子化,最后得到的分数在76-78分左右,后来改用Adaboost,分数也差不多,难以上升。
再后来参考了网上的人的特征工程,对原有特征提取做了一定的改善,不过最后分数也并没有明显的提高,甚至下降了,十分不解。后来在Kaggle上参考了一人的特征分析,做的很复杂,也很难理解,不过他的分数达到了82分。或许还有很多值得学习的地方。先留着
特征工程后得到了按照我们意图来的训练集和测试集的特征表达方式,直接使用sklearn可以很方便地进行测试。不过要先简单处理一下输入数据:
train_x = _train.drop(["Survived", "PassengerId"], axis=1) train_y = _train["Survived"] test_x = _test.drop(["PassengerId", "Survived"], axis=1).copy() tx, validate_x, ty, validate_y = train_test_split(train_x, train_y, train_size=.6, random_state=1) # train_x.info() predict_x = test_x vx = validate_x vy = validate_y丢弃了一些训练中没用的数据,并且划分了训练和验证数据集
svc = SVC() svc.fit(tx, ty) y_pred_svc = svc.predict(predict_x) acc_svc = round(svc.score(tx, ty) * 100, 2) acc_svc1 = round(svc.score(vx, vy) * 100, 2) print("svc acc: ", acc_svc, acc_svc1) liner_svc = LinearSVC() liner_svc.fit(tx, ty) y_pred_linear_svc = liner_svc.predict(predict_x) acc_svc = round(liner_svc.score(tx, ty) * 100, 2) acc_svc1 = round(liner_svc.score(vx, vy) * 100, 2) print("liner_svc acc: ", acc_svc, acc_svc1) knn = KNeighborsClassifier(n_neighbors=3) knn.fit(tx, ty) y_pred_knn = knn.predict(predict_x) acc_svc = round(knn.score(tx, ty) * 100, 2) acc_svc1 = round(knn.score(vx, vy) * 100, 2) print("knn acc: ", acc_svc, acc_svc1) gaussian = GaussianNB() gaussian.fit(tx, ty) y_pred_gaussian = gaussian.predict(predict_x) acc_svc = round(gaussian.score(tx, ty) * 100, 2) acc_svc1 = round(gaussian.score(vx, vy) * 100, 2) print("gaussian acc: ", acc_svc, acc_svc1) sgd = SGDClassifier() sgd.fit(tx, ty) y_pred_sgd = sgd.predict(predict_x) acc_svc = round(sgd.score(tx, ty) * 100, 2) acc_svc1 = round(sgd.score(vx, vy) * 100, 2) print("sgd acc: ", acc_svc, acc_svc1) dtc = DecisionTreeClassifier() dtc.fit(tx, ty) y_pred_dtc = dtc.predict(predict_x) acc_svc = round(dtc.score(tx, ty) * 100, 2) acc_svc1 = round(dtc.score(vx, vy) * 100, 2) print("dtc acc: ", acc_svc, acc_svc1) rnd_forest = RandomForestClassifier(n_estimators=300, min_samples_split=4, class_weight={0: 0.745, 1: 0.255}) rnd_forest.fit(tx, ty) y_pred_rnd_forest = rnd_forest.predict(predict_x) acc_svc = round(rnd_forest.score(tx, ty) * 100, 2) acc_svc1 = round(rnd_forest.score(vx, vy) * 100, 2) print("rnd_forest acc: ", acc_svc, acc_svc1) perceptron = Perceptron() perceptron.fit(tx, ty) y_pred_perceptron = gaussian.predict(predict_x) acc_svc = round(perceptron.score(tx, ty) * 100, 2) acc_svc1 = round(perceptron.score(vx, vy) * 100, 2) print("perceptron acc: ", acc_svc, acc_svc1)
这里用了多个分类器进行测试,输出:
SVM方法表现很差劲,不是很理解为什么,最好的是随机森林。
所以最后输出随机森林的结果来提交:
y_pred = y_pred_rnd_forest submission = pd.DataFrame({"PassengerId": _test.PassengerId, "Survived": y_pred.astype(int)}) submission.to_csv("D:\\leran\\titanic\\submission.csv", index=False)