训练和测试数据是一些乘客的个人信息以及存活状况。这是一个二分类问题
# python 2.7 python 2.7 python 2.7 import pandas as pd #数据分析 import numpy as np #科学计算 import matplotlib.pyplot as plt import seaborn as sns #color = sns.color_palette() %matplotlib inline
train = pd.read_csv("./data/titanic_train.csv") train.head()
字段有:Passengerld => 乘客ID , Pclass => 乘客等级(1/2/3等舱位) , Name => 乘客姓名 , Sex => 性别 , Age => 年龄 ,SibSp => 堂兄弟/妹个数 , Parch => 父母与小孩个数 , Ticket => 船票信息 , Fare => 票价 , Cabin => 客舱 , Embarked => 登船港口
#数据集基本信息 train.info()
训练数据总共有891名乘客,但是很不幸,我们有些属性的数据不全,Age(年龄)属性只有714名乘客有记录 Cain(客舱)只有204名乘客是已知的; 测试数据中总共有481名乘客,有更多缺失值Age(年龄)属性只有332名乘客有记录,Fare属性缺1位乘客的信息,Cabin(客舱)只有91名乘客是已知的;
train.describe()
mean字段告诉我们,大概0.383838的人最后获救了
fig = plt.figure() # Get current size fig_size = plt.rcParams["figure.figsize"] # Set figure width to 12 and height to 9 #fig_size[0] = fig_size[0]*3 #fig_size[1] = fig_size[1]*3 #plt.rcParams["figure.figsize"] = fig_size fig.set(alpha=0.2) # 设定图表颜色alpha参数 plt.subplot2grid((2,3),(0,0)) # 在一张大图里分列几个小图 sns.countplot(train.Survived); plt.xlabel('Survived'); plt.ylabel('Number of people'); plt.subplot2grid((2,3),(0,1)) sns.countplot(train.Pclass); plt.xlabel('Pclass'); plt.ylabel('Number of people'); plt.subplot2grid((2,3),(0,2)) plt.scatter(train.Survived, train.Age) plt.ylabel("Age") # sets the y axis lable plt.grid(b=True, which='major', axis='y') # formats the grid line style of our graphs plt.title("Age vs. Survived") plt.subplot2grid((2,3),(1,0), colspan=2) train.Age[train.Pclass == 1].plot(kind='kde') # plots a kernel desnsity estimate of the subset of the 1st class passanges's age train.Age[train.Pclass == 2].plot(kind='kde') train.Age[train.Pclass == 3].plot(kind='kde') plt.xlabel(u"Age")# plots an axis lable plt.ylabel(u"pdf") plt.title(u"Age of Pclasses") plt.legend((u'1', u'2',u'3'),loc='best') # sets our legend for our graph. plt.subplot2grid((2,3),(1,2)) sns.countplot(train.Embarked); plt.xlabel('Embarked Port'); plt.ylabel('Number of people'); plt.show()
3等舱乘客非常多;遇难和获救的人年龄似乎跨度都很广,3个不同的舱年龄总体趋势似乎也一致,2等舱和3等舱乘客20岁多点的人最多,1等舱40岁左右的最多(财富和年龄的分配?)登船港口人数按照S,C,Q递减,而且S远多于另外俩港口; 1.不同舱位/乘客等级可能和财富/地位有关系,最后获救概率可能会不一样 2.年龄对获救概率也一定是有影响的,副船长说"小孩和女士先走" 3.和登船港口是不是有关系呢?
#看看各乘客等级的获救情况 fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_0 = train.Pclass[train.Survived == 0].value_counts() Survived_1 = train.Pclass[train.Survived == 1].value_counts() df=pd.DataFrame({u'Survived':Survived_1, u'Not Survived':Survived_0}) df.plot(kind='bar', stacked=True) plt.title(u"Survived vs. Pclass") plt.xlabel(u"Pclass") plt.ylabel(u"Number of People")
1等舱的乘客获救的概率最高,3等舱的最低
#看看各登录港口的获救情况 fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_0 = train.Embarked[train.Survived == 0].value_counts() Survived_1 = train.Embarked[train.Survived == 1].value_counts() df=pd.DataFrame({u'Survived':Survived_1, u'Not Survived':Survived_0}) df.plot(kind='bar', stacked=True) plt.title(u"Survived vs. Embarked") plt.xlabel(u"Embarked") plt.ylabel(u"Number of People") plt.show()
#看看各性别的获救情况 fig = plt.figure() fig.set(alpha=0.2) # 设定图表颜色alpha参数 Survived_m = train.Survived[train.Sex == 'male'].value_counts() Survived_f = train.Survived[train.Sex == 'female'].value_counts() df=pd.DataFrame({u'Male':Survived_m, u'Female':Survived_f}) df.plot(kind='bar', stacked=True) plt.title(u"Survived vs. Sex") plt.xlabel(u"Sex") plt.ylabel(u"Number of People") plt.show()
女士的获救概率远大于男士
# 兄弟姐妹 g = train.groupby(['SibSp','Survived']) df = pd.DataFrame(g.count()['PassengerId']) df
# 家庭成员 g = train.groupby(['Parch','Survived']) df = pd.DataFrame(g.count()['PassengerId']) df
通常遇到缺值的情况,我们会有几种常见的处理方式 1.如果缺值的样本占总数比例极高,我们可能就直接舍弃了,作为特征加入的话,可能反倒带入噪声 1)如果缺值的样本适中,而该属性非连续值特征属性(比如说类目属性),那就把NaN作为一个新类别,加到类别特征中 2)如果缺值的样本适中,而该属性为连续值特征属性,有时候我们会考虑给定一个setp(比如这里的age,我们可以考虑每隔2,3岁为一个步长),然后把它离散化,之后把NaN作为一个type加到属性类目中; 3)有些情况下,缺失的值个数并不是特别多,那我们也可以试着根据已有的值,拟合一下数据,补充上; 本例中,后两种处理方式应该都是可行的,我们先试试拟合补全(话说没有特别多的背景可供我们拟合,不一定是一个多好的选择) 这里用scikit-learn中的RandomForest来拟合一下缺失的年龄数据
train.shape
#ticket是船票编号,应该是唯一的,和最后的结果没有太大的关系,不纳入考虑的特征范畴 #cabin只有204个乘客有值,缺失值太多,丢弃 #PassengerId和Name与目标关系似乎不大,丢弃 train = train.drop(["PassengerId" , "Ticket" , "Cabin" , "Name"] , axis = 1) y = train['Survived'] train = train.drop('Survived' , axis = 1)
train.info()
#其实Age用均值代替就好,这里示范一下如何用回归来拟合 from sklearn.ensemble import RandomForestRegressor ### 使用 RandomForestClassifier 填补缺失的年龄属性 def set_missing_ages(df): #把已有的数值型特征取出来丢进Random Forest Regressor中 age_df = df[['Age' , 'Fare' , 'Parch' , 'SibSp' , 'Pclass']] #乘客分成已知年龄和未知年龄两部分 know_age = age_df[age_df.Age.notnull()].as_matrix() unknow_age = age_df[age_df.Age.isnull()].as_matrix() #y即目标年龄 y = know_age[: , 0] # X即特征属性值 X = know_age[: , 1:] #fit到RandomForestRegressor之中 rfr = RandomForestRegressor(random_state = 0 , n_estimators = 2000 , n_jobs = -1) rfr.fit(X , y) #用得到的模型进行未知年龄结果预测 predictedAges = rfr.predict(unknow_age[: , 1::]) #用得到的预测结果填补原缺失数据 df.loc[ (df.Age.isnull()) , 'Age' ] = predictedAges return df , rfr train , rfr = set_missing_ages(train) train
有些模型,如Logisstic回归或xgboost,需要输入的特征都是数值型特征,我们通常会先对类别型特征进行one-hot编码。以Embarked为例,原来一个属性维度,因为其取值可以是['S' , 'C' , 'Q'],而将其展开为'Embarked_C' , 'Embarked_S','Embarked_Q'三个属性,当Embarked取值为S时,为Embarked_S = 1 , Embarked_C = 0 , Embarked_Q = 0 我们使用pandas的get_dummies来完成这个工作,并拼接在原来的train之上
dummies_Embarked = pd.get_dummies(train['Embarked'] , prefix = 'Embarked') dummies_Sex = pd.get_dummies(train['Sex'] , prefix = 'Sex') dummies_Pclass = pd.get_dummies(train['Pclass'] , prefix = 'Pclass') train = pd.concat([train , dummies_Embarked , dummies_Sex , dummies_Pclass] , axis = 1) train.drop(['Pclass' , 'Sex' , 'Embarked'] , axis = 1 , inplace = True) train
#对数值型特征做预处理,标准化 #对类似正则化的Logistic回归模型等很重要,对xgboost等基于树的模型不是必须 from sklearn.preprocessing import StandardScaler # 分别初始化标准化器 ss_age = StandardScaler() ss_fare = StandardScaler() train['Age'] = ss_age.fit_transform(train['Age'].reshape(-1 , 1)) train['Fare'] = ss_fare.fit_transform(train['Fare'].reshape(-1 , 1)) train
#我们把需要的feature字段取出来,转成numpy格式,使用scikit-learn中的LogisticRegression建模 from sklearn import linear_model X = train.values[:]#将需要的特征字段取出来,转成numpy格式 #fit到RandomForestRegressor之中 clf = linear_model.LogisticRegression(C = 1.0 , penalty = 'l1' , tol = 1e-6) clf.fit(X , y) clf
train.columns
我们可以通过系数的绝对值看出特征的重要性: 1)Sex属性,如果是female会极大提高最后获救的概率,而male会很大程度拉低这个概率; 2)Pclass属性,1等舱乘客最后获救的概率会上升,而乘客等级为3会极大的拉低这个概率; 3)Age是一个负相关,意味着在我们的模型里,年龄越小,越有获救的优先权; 4)有一个登船港口S会很大程度拉低获救的概率,另外俩港口压根就没啥作用,所以也许可以考虑把登船港口这个特征去掉试试; 5)船票Fare有小幅度的正相关,也许将其离散化,再分至各个乘客等级上?
对测试集做和训练集一样的操作
test= pd.read_csv("./data/titanic_test.csv") #保存结果用 PassengerId = test["PassengerId"] test = test.drop(["PassengerId","Ticket", "Cabin", "Name"], axis=1) test.loc[ (test.Fare.isnull()), 'Fare' ] = 0 # 接着我们对test做和train中一致的特征变换 # 首先用同样的RandomForestRegressor模型填上丢失的年龄 tmp_df = test[['Age','Fare', 'Parch', 'SibSp', 'Pclass']] null_age = tmp_df[test.Age.isnull()].as_matrix() # 根据特征属性X预测年龄并补上 temp_age = null_age[:, 1:] predictedAges = rfr.predict(temp_age) test.loc[ (test.Age.isnull()), 'Age' ] = predictedAges #对类别型变量编码 dummies_Embarked = pd.get_dummies(test['Embarked'], prefix= 'Embarked') dummies_Sex = pd.get_dummies(test['Sex'], prefix= 'Sex') dummies_Pclass = pd.get_dummies(test['Pclass'], prefix= 'Pclass') test = pd.concat([test, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1) test.drop(['Pclass', 'Sex', 'Embarked'], axis=1, inplace=True) #Age 和Fare标准化 test['Age'] = ss_age.transform(test['Age'].reshape(-1, 1)) test['Fare'] = ss_fare.transform(test['Fare'].reshape(-1, 1))
predictions = clf.predict(test) result = pd.DataFrame({'PassengerId':PassengerId, 'Survived':predictions.astype(np.int32)}) result.to_csv("logistic_regression_predictions.csv", index=False)
有一个很可能发生的问题是,我们不断地做feature engineering,产生的特征越来越多,用这些特征去训练模型,会对我们的训练集拟合得越来越好,同时也可能在逐步丧失泛化能力,从而在待预测的数据上,表现不佳,也就是发生过拟合问题。
从另一个角度上说,如果模型在待预测的数据上表现不佳,除掉上面说的过拟合问题,也有可能是欠拟合问题,也就是说在训练集上,其实拟合的也不是那么好。
对过拟合而言,做一下feature selection,挑出较好的feature的subset来做training
而对于欠拟合而言,我们通常需要更多的feature,更复杂的模型来提高准确度。
我们用scikit-learn里面的learning_curve来帮我们分辨我们模型的状态。举个例子,这里我们一起画一下我们最先得到的baseline model的learning curve。 样本数为横坐标,训练和交叉验证集上的准确率作为纵坐标
estimator : 分类器 title : 表格的标题 X : 输入的feature,numpy类型 y : 输入的target vector ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点 cv : 做cross-validation的时候,数据分成的份数,缺省为3
from sklearn.learning_curve import learning_curve # 用sklearn的learning_curve得到training_score和cv_score,使用matplotlib画出learning curve def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, n_jobs=1, train_sizes=np.linspace(.05, 1., 20), verbose=0, plot=True): train_sizes, train_scores, test_scores = learning_curve( estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, verbose=verbose) train_scores_mean = np.mean(train_scores, axis=1) train_scores_std = np.std(train_scores, axis=1) test_scores_mean = np.mean(test_scores, axis=1) test_scores_std = np.std(test_scores, axis=1) if plot: plt.figure() plt.title(title) if ylim is not None: plt.ylim(*ylim) plt.xlabel(u"train samples") plt.ylabel(u"score") plt.gca().invert_yaxis() plt.grid() plt.fill_between(train_sizes, train_scores_mean - train_scores_std, train_scores_mean + train_scores_std, alpha=0.1, color="b") plt.fill_between(train_sizes, test_scores_mean - test_scores_std, test_scores_mean + test_scores_std, alpha=0.1, color="r") plt.plot(train_sizes, train_scores_mean, 'o-', color="b", label=u"train") plt.plot(train_sizes, test_scores_mean, 'o-', color="r", label=u"CV") plt.legend(loc="best") plt.draw() plt.gca().invert_yaxis() plt.show() midpoint = ((train_scores_mean[-1] + train_scores_std[-1]) + (test_scores_mean[-1] - test_scores_std[-1])) / 2 diff = (train_scores_mean[-1] + train_scores_std[-1]) - (test_scores_mean[-1] - test_scores_std[-1]) return midpoint, diff plot_learning_curve(clf, u"learning curve", X, y)
在实际数据上看,我们得到的learning curve没有理论推导的那么光滑 但是可以大致看出来,训练集和交叉验证集上的得分曲线走势还是符合预期的。
目前的曲线看来,我们的model并不处于overfitting的状态(overfitting的表现一般是训练集上得分高,而交叉验证集上要低很多,中间的gap比较大)。 因此我们可以再做些feature engineering的工作,添加一些新产出的特征或者组合特征到模型中。
接下来,我们就该看看如何优化baseline系统了 我们还有些特征可以再挖掘挖掘 1)Name可以抽取出Title,对分类也许有用;如男性中带某些字眼的(‘Capt’, ‘Don’, ‘Major’, ‘Sir’)可以统一到一个Title,女性也一样 2)以我们的日常经验,小朋友和老人可能得到的照顾会多一些,这样我们把年龄离散化,按区段分作类别属性会更合适 3)Pclass和Sex俩太重要了,我们试着用它们去组出一个组合属性来试试 train['SexPclass'] = train.Sex + "" + train.Pclass.map(str) 4)单加一个Child字段,Age<=12的,设为1,其余为0 5)如果名字里面有『Mrs』,而Parch>1的,我们猜测她可能是一个母亲,应该获救的概率也会提高,因此可以多加一个Mother字段,此种情况下设为1,其余情况下设为0 6)登船港口可以考虑先去掉试试(Q和C本来就没权重,S有点诡异) 7)把堂兄弟/兄妹 和 Parch删除,增加一个是否有Family字段 train['Family'] = train["Parch"] + train["SibSp"] train['Family'].loc[train['Family'] > 0] = 1 train['Family'].loc[train['Family'] == 0] = 0 8)Sex其实可以只留一维特征: Sex_map = {'female': 1, 'male': 0} train['Sex'] = train['Sex'].apply(lambda x: Sex_map[x]) test['Sex'] = test['Sex'].apply(lambda x: Sex_map[x])
还有一种方式是将性别和年龄一起编码为:孩子、老人、男性和女性(老人和小孩是否存和是不考虑性别的)4个哑变量
要做交叉验证监控系统状态 可以用scikit-learn的cross_validation来完成这个工作。。。