Titanic生存预测1
背景音乐:Toca Toca - Fly Project
背景音乐有毒……
前言
前段时间玩了一下Kaggle平台上的Titanic号生存预测竞赛,这是一个非常适合入门机器学习的项目。
这场竞赛的内容是,通过一部分乘客的数据及其存活率,来预测另一部分乘客的存活率,目标是预测的准确率尽可能高。
我的最好分数是0.81339,在9606支队伍中排名300+~500+,也就是说有差不多200人拿到了这个成绩噗。对于我投入的时间而言,这个成绩还算满意,而且我相信还有一定的提升空间。
这里假设,看这篇文章的人已经参加过,但是没有拿到较好的成绩(我认为拿到0.8以上就不错了)。
我的建议
拿到0.8以上的分数后,就不要再追求排名了,而是需要看看别人的kernel学习他们的特征处理方式,理由是:
包括我在内,很多参赛选手发现,自己在测试集上有0.85左右的分数,但是一提交结果却发现只有0.78左右,这说明模型已经过拟合了。
不是你的问题,因为该项目的数据量太少,噪音也很多,因此做好模型不如做好特征工程来得重要,更不如发现数据背后的规律并善加利用重要。后者要求你对数据有敏锐的观察力,同时利用这些规律来修正你的模型的预测结果,不然我敢保证你的模型极限只会是0.83。
为什么有一些人的准确率会有0.9甚至1.0呢?理由很简单,他们在现有模型的基础上,加了很多认为设定的规则,故意让结果“过拟合”。理论上,除非数据集有问题,否则模型的准确率不可能会达到100%的。他们中的有些人可能确实发现了一个牛逼的特征,但我相信一部分人是存在作弊嫌疑的……
废话不多说,上干货。
环境:python 3.6.2
系统:macOS 10.13.1
1. 数据探索
EDA很重要,很重要,很重要!!!
但我同样假设你已经发现了一部分规律,比如:
1)女性存活率要远高于男性;
2)Embarked为S的乘客幸存率较低;
……
所以我这里跳过该部分。不了解的童鞋看这里:EDA To Prediction(DieTanic)
from sklearn.preprocessing import LabelEncoder
from sklearn import model_selection
from xgboost import XGBRegressor
import pandas as pd
path_data = '../../data/titanic/'
df_train = pd.read_csv(path_data + 'train.csv')
df_test = pd.read_csv(path_data + 'test.csv')
df_data = pd.concat([df_train, df_test])
2. 特征工程
所谓特征工程,最重要的就是通过已有的特征,去构建有预测能力的新特征。
而有预测能力的新特征,有三条基本特性(瞎掰的哈):可解释性、相关性、易获得性。
我做了4方面内容——数据清洗、构造新特征、特征选择、特征转换:
2.1 数据清洗
缺少数据的主要有4个变量,其中:
- Cabin:缺太多了,提取头字母,将缺失值和头字母数少于10的归为一类
- Embarked和Fare:都缺很少,可以用众数填补
- Age:有部分缺失,但EDA结果显示它很重要,因此我决定构造模型来预测Age。不过该部分放在最后。
df_data['Embarked'].fillna(df_data['Embarked'].mode()[0], inplace=True)
df_data['Fare'].fillna(df_data['Fare'].median(), inplace=True)
df_data['Cabin'] = df_data['Cabin'].apply(lambda x:x[0] if x is not np.nan else 'X')
cabin_counts = df_data['Cabin'].value_counts()
df_data['Cabin'] = df_data['Cabin'].apply((lambda x:'X' if cabin_counts[x] < 10 else x))
2.2 构造新特征
构造新特征其实是一件特别蛋疼的事……因为这个完全靠经验+尝试, 别无他法。
常常是拍脑瓜想出一个新特征,然后看这个特征和结果的相关性,如果还不错的话,说明有一定的预测能力,然后代入到模型中,看测试集的表现是否有提高。如果有提高,高兴啦;如果没什么变化甚至降低了,就要考虑是暂时保留还是剔除掉……
下面直接列出我用的、对结果有一定帮助的特征。
FamilySize
df_data['FamilySize'] = df_data['SibSp'] + df_data['Parch'] + 1
IsAlone
df_data['IsAlone'] = 1
df_data['IsAlone'].loc[df_data['FamilySize'] > 1] = 0
Title
拥有人数少于10的title都改成Rare
df_data['Title'] = df_data['Name'].str.split(", ", expand=True)[1].str.split(".", expand=True)[0]
title_counts = df_data['Title'].value_counts()
df_data['Title'] = list(map(lambda x:'Rare' if title_counts[x] < 10 else x, df_data['Title']))
Family_Name
部分西方国家中人名的重复度较高,而姓氏重复度较低,姓氏具有一定辨识度。
姓氏相同的乘客,可能是一家人,而一家人同时幸存或遇难的可能性较高。
df_data['Family_Name'] = df_data['Name'].apply(lambda x: str.split(x, ",")[0])
Family_Survival
此处逻辑是:
- 一个人的家庭存活率为0.5
- 再将Family_Name和Fare进行组合,认为同一姓氏且有着同一票价的人们组成一个家庭,对于一个家庭(大于1人)而言,设如果有人存活则家庭存活率为1,否则为0
- 再将Family_Name和Ticket进行组合,认为同一姓氏且有着共享一张票的人们组成一个家庭,对于一个家庭(大于1人)而言,设如果有人存活则家庭存活率为1,否则为0
DEFAULT_SURVIVAL_VALUE = 0.5
df_data['Family_Survival'] = DEFAULT_SURVIVAL_VALUE
for grp, grp_df in df_data.groupby(['Family_Name', 'Fare']):
if (len(grp_df) != 1):
for ind, row in grp_df.iterrows():
smax = grp_df.drop(ind)['Survived'].max()
smin = grp_df.drop(ind)['Survived'].min()
passID = row['PassengerId']
if (smax == 1.0):
df_data.loc[df_data['PassengerId'] == passID, 'Family_Survival'] = 1
elif (smin==0.0):
df_data.loc[df_data['PassengerId'] == passID, 'Family_Survival'] = 0
for _, grp_df in df_data.groupby('Ticket'):
if (len(grp_df) != 1):
for ind, row in grp_df.iterrows():
if (row['Family_Survival'] == 0) | (row['Family_Survival']== 0.5):
smax = grp_df.drop(ind)['Survived'].max()
smin = grp_df.drop(ind)['Survived'].min()
passID = row['PassengerId']
if (smax == 1.0):
df_data.loc[df_data['PassengerId'] == passID, 'Family_Survival'] = 1
elif (smin==0.0):
df_data.loc[df_data['PassengerId'] == passID, 'Family_Survival'] = 0
2.3 预测年龄
这一部分也属于数据清洗,但是比较高级。
在这里我用交叉验证和GridSearchCV的方式训练出一个较好的xgboost回归模型来预测年龄。
def predict_age(x_train, y_train, x_test):
param_grid = {
'learning_rate':[.001, .005, .01, .05, .1],
'max_depth':[2, 4, 6, 8],
'n_estimators':[50, 100, 300, 500, 1000],
'seed':[2018]
}
cv_split = model_selection.ShuffleSplit(n_splits = 10, test_size = .3, train_size = .6, random_state = 0)
tune_model = model_selection.GridSearchCV(XGBRegressor(nthread=-1), param_grid=param_grid,
scoring = 'neg_mean_squared_error', cv = cv_split)
tune_model.fit(x_train, y_train)
print(tune_model.best_params_)
y_test = tune_model.best_estimator_.predict(x_test)
return y_test
data_p = df_data.drop(['Cabin', 'Embarked', 'FareBin', 'Name', 'PassengerId',
'Sex', 'Survived', 'Ticket', 'Title', 'Family_Name'], 1)
x_train = data_p.loc[~data_p['Age'].isnull(), :].drop('Age', 1)
y_train = data_p.loc[~data_p['Age'].isnull(), :]['Age']
x_test = data_p.loc[data_p['Age'].isnull(), :].drop('Age', 1)
df_data.loc[df_data['Age'].isnull(), 'Age'] = predict_age(x_train, y_train, x_test)
2.4 特征转换
该部分要做的就是将分类变量处理成哑变量,因为模型只认识数字不认识字符。
要转换的特征有Sex、Embarked、Title、Cabin这4个,其中Sex属于二分类,可以用LabelEncoder处理。
label = LabelEncoder()
df_data['Sex_Code'] = label.fit_transform(df_data['Sex']) # female为0, male为1
df_data = pd.concat([df_data, pd.get_dummies(df_data[['Embarked', 'Title', 'Cabin']])], axis=1)
2.5 剔除特征
经历了上面几个部分的特征处理,最后剔除掉没用的特征,并将结果保留下来,用以训练。
drop_columns = ['Sex', 'Name', 'Embarked', 'Cabin', 'Ticket', 'Title', 'Family_Name']
df_data = df_data.drop(drop_columns, 1)
df_data.to_csv(path_data + 'fe_data.csv', index=False)
得到一份有着1309行,26列的数据集