Kaggle Titanic: Machine Learning from Disaster 一种思路

最近在做Kaggle中的Titanic,主要是练习Pandas和Sklearn,用了非常长的时间,下面是代码,成绩是0.80383,虽然不是很高,但基本上能想到的都实验了。下面是代码:


# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd
from sklearn import cross_validation
from sklearn.linear_model import Lasso
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier


train = pd.read_csv(r'D:\train.csv')
test = pd.read_csv(r'D:\test.csv')
train['from'] = 'train'
test['from'] = 'test'
comb_data = pd.concat([train, test], axis=0, ignore_index=True)  # 首先将train和test合并在一起,所有的处理都在comb_data中进行,需要预测的时候再分开
comb_data['Sex'].replace({'male': 0, 'female': 1}, inplace=True)  # 将Sex转换为数值型
# 少量缺失值的处理
comb_data.fillna({'Fare': 13.3}, inplace=True)  # 只有一个缺失值,缺失乘客属于3等舱(Pclass==3),因此按照3等舱的均值填充
comb_data.fillna({'Embarked': "S"}, inplace=True)  # 有两个缺失值,且船票号(Ticket)相同,将两个缺失的港口信息按照众数'S'处理
# 生成title变量,这个变量参考了其他参赛者分享的思路。由于部分title数量太少且表达含义相似,因此需要合并处理。
# 合并的原则是结合性别、年龄以及在英语世界的具体含义
comb_data['title'] = comb_data['Name'].str.split(',').str.get(1).str.split('.').str.get(0).str.strip()
comb_data.loc[comb_data['title'].isin(["Capt", "Don", "Jonkheer", "Major", "Sir"]), 'title'] = 'Mr'
comb_data.loc[comb_data['title'].isin(["Dona", "Lady", "the Countess"]), 'title'] = 'Mrs'
comb_data.loc[comb_data['title'].isin(["Mlle", "Mme", "Ms"]), 'title'] = 'Miss'
# 生成家庭规模变量,这个变量也参考了其他参赛者分享的思路。这里统计家庭规模的时候,参考的变量是last_name,也就是Name的第一项
# 通过肉眼观察,大部分比较准确。当然也有分错的
comb_data['last_name'] = comb_data['Name'].str.split(',').str.get(0)
family_name_group = comb_data[comb_data[['Parch', 'SibSp']].sum(axis=1) > 0][['last_name']]
family_name_freq = family_name_group['last_name'].value_counts()
family_name_freq = pd.DataFrame({'last_name': family_name_freq.index, 'family_size': family_name_freq.values})
comb_data = pd.merge(left=comb_data, right=family_name_freq, how='left', on='last_name', sort=False)
comb_data['family_size'] = np.where(comb_data[['Parch', 'SibSp']].sum(axis=1) == 0, 1, comb_data['family_size'])
# 生成家庭角色变量,根据title、Sex、Parch和SibSp判断在家庭中的角色,通过肉眼观察,大部分比较准确
comb_data['family_role'] = np.where((comb_data['title'] == 'Master') & (comb_data['Parch'] > 0), 'son',\
np.where((comb_data['title'] == 'Miss') & (comb_data['Parch'] > 0), 'daughter',\
np.where((comb_data['title'] != 'Miss') & (comb_data['Parch'] == 0) & (comb_data['SibSp'] > 0) & (comb_data['Sex'] == 1), 'wife',\
np.where((comb_data['title'] != 'Master') & (comb_data['Parch'] == 0) & (comb_data['SibSp'] > 0) & (comb_data['Sex'] == 0), 'husband',\
np.where((comb_data['title'] != 'Miss') & (comb_data['Parch'] > 0) & (comb_data['SibSp'] == 0) & (comb_data['Sex'] == 1), 'mother',\
np.where((comb_data['title'] != 'Master') & (comb_data['Parch'] > 0) & (comb_data['SibSp'] == 0) & (comb_data['Sex'] == 0), 'father',\
np.where((comb_data['title'] != 'Miss') & (comb_data['Parch'] > 0) & (comb_data['SibSp'] > 0) & (comb_data['Sex'] == 1), 'wife_mother',\
np.where((comb_data['title'] != 'Master') & (comb_data['Parch'] > 0) & (comb_data['SibSp'] > 0) & (comb_data['Sex'] == 0), 'husband_father', \
np.where((comb_data['title'] == 'Mrs') & (comb_data['Parch'] == 0) & (comb_data['SibSp'] == 0), 'wife_only', 'other')))))))))
# 生成婚否变量,与家庭变量类似,判断是否已婚,主要针对女性。因为title为"Mrs"的女士基本可以判断已婚,男性则没找到合理的判断方法
comb_data['marry_flag'] = np.where(comb_data['family_role'].isin(['son', 'daughter']) | \
((comb_data['family_role'] == 'other') & (comb_data['Sex'] == 1) & (comb_data['title'] == 'Miss')), 'un_marry',\
np.where(comb_data['family_role'].isin(['wife', 'husband', 'mother', 'father', 'wife_mother', 'husband_father', 'wife_only']), 'married', 'unknown'))
# 生成昵称变量。如果Name变量中有英文双引号,则打标签1,否则为0。其实我也不是特别清楚,名字中的英文双引号的具体含义
comb_data['nick_name_flag'] = np.where(comb_data['Name'].str.find('"') == -1, 0, 1)
# Ticket处理。有的Ticket是纯数字,有的带有英文字母,不了解其中的具体含义,只是将数字和字母的长度分别生成了两个变量
# 因为Ticket分类也很多,因此只提取了Ticket的第一个字母作为一个变量
comb_data['ticket_pos'] = comb_data['Ticket'].str[0]
comb_data['ticket_alpha_len'] = comb_data['Ticket'].str.split(' ').str[0].str.len()
comb_data['ticket_num_len'] = comb_data['Ticket'].str.split(' ').str[1].str.len()
comb_data['ticket_num_len'].fillna(0, inplace=True)
# Cabin处理,将Cabin的首字母作为单独的变量。根据其他参赛者分享的代码,貌似Cabin反映了在船中的位置。我一直单纯的理解为船舱的门牌号
comb_data['cabin_no'] = comb_data['Cabin'].str[0]
comb_data['cabin_no'] = comb_data['cabin_no'].replace({"T": np.NaN})  # 将唯一的“T”替换成缺失值,通过算法填充
comb_data['cabin_cnt'] = comb_data['Cabin'].str.count(" ") + 1  # 有的Cabin具有连续多个值,猜测是船客同时买了多个船舱,将这个数量记录下来
comb_data['fare_avg'] = np.where(comb_data['cabin_cnt'].notnull(), comb_data['Fare'] / comb_data['cabin_cnt'], comb_data['Fare'])  # 为了准确判断单个仓的价格,所以用Fare/cabin_cnt,做一个平均值代替Fare


# Age有大量缺失值,所以为了让结果更准确一些,需要使用算法预测Age, 本例中使用Lasso
for_age = comb_data[['Age', 'Embarked', 'Fare', 'Parch', 'Pclass', 'Sex', 'SibSp', 'title', \
'family_size', 'family_role', 'marry_flag', 'nick_name_flag']].copy()
for_age = pd.get_dummies(for_age, columns=['Embarked', 'Pclass', 'title', 'family_role', 'marry_flag'])  # 将分类变量处理成哑变量
for_age_train = for_age[for_age['Age'].notnull()]
X_age = for_age_train.drop(['Age'], axis=1)
y_age = for_age_train['Age']
las = Lasso(alpha=0.055)
X_train_age, X_test_age, y_train_age, y_test_age = cross_validation.train_test_split(X_age, y_age, test_size=0.2, random_state=56894)
las.fit(X_train_age, y_train_age)
for_age_predict = for_age[for_age['Age'].isnull()].drop(['Age'], axis=1)
for_age_predict_index = for_age_predict.index
predict_age = las.predict(for_age_predict)
predict_age = pd.DataFrame(predict_age, index=for_age_predict_index, columns=["Age"])
comb_data = comb_data.combine_first(predict_age)


# 预测Cabin,使用K-means。这里只使用了两个变量:Pclass和fare_avg。因为凭感觉,船舱位置和仓的等级以及仓的价格比较相关
# 这里的处理方式是,分别针对每个Pclass都做了一次K-means, 这样处理应该会相对准确一些
for_cabin = comb_data[['cabin_no', 'fare_avg', 'Pclass']].copy()
unique_cabin_no = np.unique(for_cabin['cabin_no'].dropna(axis=0))
cabin_no_dict = {}
cabin_no_back_dict = {}
cnt = 1
for item in unique_cabin_no:
    cabin_no_dict[item] = cnt
    cabin_no_back_dict[cnt] = item
    cnt += 1
for_cabin['cabin_no_trans'] = for_cabin['cabin_no'].replace(cabin_no_dict)  # 将字符转换成数字
for i in range(1, 4):
    temp = for_cabin[for_cabin['Pclass'] == i][['fare_avg', 'cabin_no_trans']]
    temp_train = temp[temp['cabin_no_trans'].notnull()]
    temp_predict = temp[temp['cabin_no_trans'].isnull()].drop(['cabin_no_trans'], axis=1)
    temp_predict_index = temp_predict.index
    nobs = temp_train.shape[0]
    knn = KNeighborsClassifier(n_neighbors=1)
    X_cabin = temp_train['fare_avg'].copy().reshape(-1, 1)
    y_cabin = temp_train['cabin_no_trans'].copy()
    knn.fit(X_cabin, y_cabin)
    temp_predict_cabin = knn.predict(temp_predict)
    temp_predict_cabin = pd.DataFrame(temp_predict_cabin, index=temp_predict_index, columns=['cabin_no_trans'])
    if i != 1:
        predict_cabin = pd.concat([predict_cabin, temp_predict_cabin], axis=0)
    else:
        predict_cabin = temp_predict_cabin.copy()
    pass
predict_cabin['cabin_no'] = predict_cabin['cabin_no_trans'].replace(cabin_no_back_dict)  # 再将数字转换成字符
predict_cabin.drop(['cabin_no_trans'], axis=1, inplace=True)
comb_data = comb_data.combine_first(predict_cabin)


# 最后处理一些小问题
comb_data['cabin_cnt'].fillna(1, inplace=True)  # 这里的缺失值实际上都是1,可以在前面一并处理
# 由于ticket_pos中'5'和'8'的数量非常少,所以将他们合并进来。合并没有特别的依据,仅是分配进距离最近的ticket_pos中
comb_data['ticket_pos'] = np.where(comb_data['ticket_pos'] == '5', '6',
                                   np.where(comb_data['ticket_pos'] == '8', '7', comb_data['ticket_pos']))
comb_data = pd.get_dummies(comb_data, columns=['Embarked', 'Pclass', 'cabin_no', 'ticket_pos', 'title', 'family_role', 'marry_flag'])


# 将数据集还原为train和test,下面开始预测
final_train = comb_data[comb_data['from'] == 'train'].drop(['Cabin', 'Fare', 'Name', 'Ticket', 'last_name', 'from'], axis=1)
final_predict = comb_data[comb_data['from'] == 'test'].drop(['Cabin', 'Fare', 'Name', 'Ticket', 'Survived', 'last_name', 'from'], axis=1)
final_predict_index = final_predict.index
# 使用随机森林
X = final_train.drop(['Survived', 'PassengerId'], axis=1)
y = final_train['Survived']
X_train, X_test, y_train, y_test = cross_validation.train_test_split(X, y, test_size=0.2, random_state=54684)
rf = RandomForestClassifier(n_estimators=100, max_features=8, min_samples_leaf=5, random_state=54684)
rf.fit(X_train, y_train)
rf_result = rf.predict(final_predict.drop(['PassengerId'], axis=1)).astype(int)
rf_result = pd.DataFrame(rf_result, index=final_predict_index, columns=['Survived'])
# 结果输出
rf_output_result = pd.concat([final_predict['PassengerId'], rf_result['Survived']], axis=1)
rf_output_result.to_csv(r'D:\result.csv', index=False)

上面的代码仅仅是完整的运行代码,中间省略了分析过程代码。


做这个用了很长时间,收获也比较大:

1.之前一直没什么合适的项目练习Pandas和Sklearn,通过这个算是熟悉一些了;

2.通过每次提交的成绩,了解自己和其他参赛者的差距;

3.通过看其他参赛者的代码和思路,可以极大扩展自己的思路;

4.通过测试不同的算法,选择不同的参数,可以获得对算法更直观的感受;

5.最大的感受是特征工程非常重要,一方面取决于专注数据的程度,另一方面也需要具有一些特定的知识。例如在本例中,我一开始并没有留意到title其实包含了很重要的信息,因此将其忽略了。

你可能感兴趣的:(数据挖掘,Kaggle,Titanic,Python,Pandas,Sklearn)