我是一名从能源转行做数分的研二学生,看了近4个月python,之前对计算机接近0基础(c语言都考不过),首先这篇适合给那些跟我一样转行的朋友,转行不易,坚持第一!其次这篇文章也适合给那些准备面试的朋友,了解数据分析的大概流程,粗数据如何处理,这点比能多调用一个学习包更有作用~这篇文章大部分都是我从别的博客上引用的,感谢网路上的各位大神~
这是一个二分类问题,提供的特征有Pclass,Sex,SibSp,Parch,Embarked这类的离散值(包含离散的数字和文本),Age这类连续的数字以及Name,Fare,Ticket这类的文本,其中包含有缺失值;最后要对乘客进行0,1预测(1表示生还)。
print 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
Sex Survived
0 female 0.742038
1 male 0.188908
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
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个兄弟姐妹或配偶(SibSp)以及有三个子女或父母(Parch)的乘客生还几率最大,这有利于之后做特征
- 对于连续的特征,利用直方图观察其与分类间的关系,比如:
g = sns.FacetGrid(train_df, col='Survived')#sns为seaborn模块
g.map(plt.hist, 'Age', bins=4)#bins指把数值区间分成几块
plt.show()
生成结果:
由此可见:年龄与生还率有很大的关系,小孩生还的几率更大,这表明,年龄与生还率并不是简单的线性关系,这一点如果要给年龄切片做哑变量时会用到
之所以称之为简单,是因为把一些看上去没什么用的特征直接删除了,比如Ticket以及Cabin,这两者都是看上去杂乱无章的。
- 对缺失值进行填充处理,可以用0填充,也可以用平均值,中位数等等,比如:
dataset['Title'] = dataset['Title'].fillna(0)
#fillna()用来填充缺失值,此处用0取代缺失值,也可以fillna('missing')
guess_ages = np.zeros((2,3))
#根据sex与pclass来对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() #dropna()用来删除缺失数据
# if(i==1)&(j==1):
# print guess_df
# 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]
#对于age进行数字上的切割,将连续变幻的量变成离散值
train_df['AgeBand']=pd.cut(train_df['Age'],5)
print train_df[['AgeBand','Survived']].groupby(['AgeBand'],as_index=False).mean().sort_values(by='AgeBand',ascending=True)
结果如下:
AgeBand Survived
0 (-0.08, 16] 0.550000
1 (16, 32] 0.337374
2 (32, 48] 0.412037
3 (48, 64] 0.434783
4 (64, 80] 0.090909
然后就可以为不同年龄段打上标签:
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
for dataset in combine:
dataset['FamilySize']=dataset['SibSp']+dataset['Parch']+1
print train_df[['FamilySize','Survived']].groupby(['FamilySize'],as_index=False).\
mean().sort_values(by='Survived',ascending=False)
FamilySize Survived
3 4 0.724138
2 3 0.578431
1 2 0.552795
6 7 0.333333
0 1 0.303538
4 5 0.200000
5 6 0.136364
7 8 0.000000
8 11 0.000000
上图表明:新特征FamilySize为4时,生还的几率高达72%!最后根据此将乘客分为有家属和无家属两类;
最初的特征如下:
['PassengerId' 'Pclass' 'Name' 'Sex' 'Age' 'SibSp' 'Parch' 'Ticket' 'Fare' 'Cabin' 'Embarked']
最后的特征如下:
Pclass Sex Age Fare Embarked Title IsAlone Age*Class
# -*- coding: utf-8 -*-
#data analysis and wrangling
import pandas as pd
import numpy as np
import random as rnd
#visualization
import seaborn as sns
import matplotlib.pyplot as plt
#machine learning
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC,LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import Perceptron #感知机
from sklearn.linear_model import SGDClassifier #随机梯度下降分类器
from sklearn.tree import DecisionTreeClassifier
#acquire data
train_df=pd.read_csv('/Users/apple/Desktop/titanic/train.csv')
test_df=pd.read_csv('/Users/apple/Desktop/titanic/test.csv')
combine=[train_df,test_df] #生成list
#analyze by describing data
print train_df.columns.values
print '_'*40
print train_df.head() #展示头5组数据,可以用tail展示最后5组数据
print '_'*40
print test_df.head()
print '_'*40
print test_df.info()#用来查看数据的基本状态,有多少数据缺失
print '_'*40
print train_df.describe()
print '_'*40
print train_df.describe(include=['O'])#include=['O']用来描述categorical features,即都是字符串
print '_'*40
#这种表格的方法适用于分类的种类比较少的(离散的)情况,如果是连续分布的(比如说年龄),那么用图来表示最靠谱
print train_df[['Pclass','Survived']].groupby(['Pclass'],as_index=False
).mean().sort_values(by='Survived',ascending=False)
print train_df[['Sex','Survived']].groupby(['Sex'],as_index=False
).mean().sort_values(by='Survived',ascending=False)
print train_df[['SibSp','Survived']].groupby(['SibSp'],as_index=False
).mean().sort_values(by='Survived',ascending=False)
print train_df[['Parch','Survived']].groupby(['Parch'],as_index=False
).mean().sort_values(by='Survived',ascending=False)
#利用直方图(histgram)可以用来看连续特征值对于结果的影响,bins用来调整展示密度
g = sns.FacetGrid(train_df, col='Survived')
g.map(plt.hist, 'Age', bins=4)#bins指把数值区间分成几块
plt.show()
train_df = train_df.drop(['Ticket', 'Cabin'], axis=1)#默认的是删除行,axis=1表示删除列
test_df = test_df.drop(['Ticket', 'Cabin'], axis=1)
combine = [train_df, test_df]
#为了查看名称
for dataset in combine:
dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.',expand=False)#expanf=True&False似乎没什么区别
print pd.crosstab(train_df['Title'], train_df['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_df[['Title', 'Survived']].groupby(['Title'], as_index=False).mean().sort_values(by='Survived',ascending=False)
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)#fillna()用来填充缺失值,此处用0取代缺失值,也可以fillna('missing')
# print train_df.head()
#将姓名与PassengerId这两个没用的属性删去:
train_df=train_df.drop(['Name','PassengerId'],axis=1)
test_df=test_df.drop(['Name'],axis=1)
combine=[train_df,test_df]
# print train_df.shape,test_df.shape
#将一些文字的属性匹配成数字,因为大部分的算法只会处理数字,这也是在做特征工程
for dataset in combine:
dataset['Sex']=dataset['Sex'].map({'female':1,'male':0}).astype(int)
# print train_df.head()
guess_ages = np.zeros((2,3))
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() #dropna()用来删除缺失数据
# if(i==1)&(j==1):
# print guess_df
# 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)
#对于age进行数字上的切割,将连续变幻的量变成离散值
train_df['AgeBand']=pd.cut(train_df['Age'],5)
print train_df[['AgeBand','Survived']].groupby(['AgeBand'],as_index=False).mean().sort_values(by='AgeBand',ascending=True)
#将不同区间的age打上编号
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
# print train_df.head()
#删除AgeBand栏
train_df=train_df.drop(['AgeBand'],axis=1)
combine=[train_df,test_df]
# print train_df.head()
#将parch与sibsp两个特征合并,制作新的特征familysize
for dataset in combine:
dataset['FamilySize']=dataset['SibSp']+dataset['Parch']+1
# print train_df[['FamilySize','Survived']].groupby(['FamilySize'],as_index=False).mean().\
# sort_values(by='Survived',ascending=False)
#create another feature called IsAlone
for dataset in combine:
dataset['IsAlone']=0
dataset.loc[dataset['FamilySize']==1,'IsAlone']=1
# print train_df[['IsAlone','Survived']].groupby(['IsAlone'],as_index=False).mean().\
# sort_values(by='Survived',ascending=False)
#drop Parch, SibSp, and FamilySize features in favor of IsAlone
train_df=train_df.drop(['Parch','SibSp','FamilySize'],axis=1)
test_df = test_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
combine=[train_df,test_df]
# print train_df.head()
#We can also create an artificial feature combining Pclass and Age.
for dataset in combine:
dataset['Age*Class']=dataset.Age*dataset.Pclass
print train_df.loc[:,['Age*Class','Age','Pclass']].head(10)#其中:的含义是从第一个到最后一个
#补充分类特征,因为Embarked只有两个缺失,所以直接使用最常见的来填充
freq_port=train_df.Embarked.dropna().mode()[0]#mode是求众数!!!
# print freq_port
for dataset in combine:
dataset['Embarked']=dataset['Embarked'].fillna(freq_port)
# print train_df[['Embarked','Survived']].groupby(['Embarked'],as_index=False).mean().\
# sort_values(by='Survived',ascending=False)
#Converting categorical feature to numeric
for dataset in combine:
dataset['Embarked']=dataset['Embarked'].map({'S':0,'C':1,'Q':2}).astype(int)
# print train_df.head()
#对于fare采用同样的处理,fare:连续值,不完整,用剩余的中位数进行填充
test_df['Fare'].fillna(test_df['Fare'].dropna().median(),inplace=True)
train_df['Fare'].fillna(train_df['Fare'].dropna().median(),inplace=True)#inplace参数的意思就是代替原来的变量,深拷贝
# print train_df.head()
#像对于age一样,对于fare制造fareband
train_df['FareBand']=pd.qcut(train_df['Fare'],4)#qcut是按照分位数进行划分的,以解决cut无法让所划分区间数量相等的问题
# print train_df[['FareBand','Survived']].groupby(['FareBand'],as_index=False).\
# mean().sort_values(by='FareBand',ascending=True)
#将fare特征变化为离散的数字
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)
train_df = train_df.drop(['FareBand'], axis=1)
combine = [train_df, test_df]
print train_df.head()
print '_'*40
print test_df.head()
print '_'*40
X_train=train_df.drop('Survived',axis=1)
Y_train=train_df.Survived
# print X_train,Y_train
X_test=test_df.drop('PassengerId',axis=1).copy()
# print X_train.shape,Y_train.shape,X_test.shape
# Logistic Regression
logreg = LogisticRegression()
logreg.fit(X_train, Y_train)
Y_pred = logreg.predict(X_test)
acc_log = round(logreg.score(X_train, Y_train) * 100, 2)
print acc_log
使用Logistic Regression回归,训练值为81.26,测试值为76.56
penalty='l2'
penalty表示正则化,有l1与l2两种选择,默认为l2,两种结果类似:
l1(81.14,76.555)l2(81.26,76.555)
其中()前者为训练数据,后者为预测数据,本文都采用这种模式。
实际上,l1与l2两种正则化各有特点,l1可以用来降维,即将一些系数降为0。
在lg中,C=1.0
也与正则化有关, C为正则化系数λ的倒数,必须为正数,默认为1。和SVM中的C一样,值越小,说明对模型的复杂度惩罚越大,就越不会过拟合。
下面补充以下,为什么λ越大,就能防止过拟合?
上图发生了过拟合,一种解决的办法是直接将x^3和x^4删去,但如果这两者也是特征的话,这样做就代表了特征的丢失;还有一种思路是对参数3和参数4进行惩罚,并且令两个参数很小,一个简单的办法就是给原有的Cost函数加上两个略大惩罚项,例如:
这样两个参数的值会很小,就避免了过拟合。
我这里有一个疑问:
可以给单独几个特征进行正则化处理吗?由上图可见,在lg中,是对所有特征用同一个λ进行正则化,但是实际中,可能有些特征是起决定作用的,我们不希望它被正则化,那该怎么操作?
从上面的描述可以看出,newton-cg、lbfgs和sag这三种优化算法时都需要损失函数的一阶或者二阶连续导数,因此不能用于没有连续导数的L1正则化,只能用于L2正则化。而liblinear通吃L1正则化和L2正则化。
同时,sag每次仅仅使用了部分样本进行梯度迭代,所以当样本量少的时候不要选择它,而如果样本量非常大,比如大于10万,sag是第一选择。但是sag不能用于L1正则化,所以当你有大量的样本,又需要L1正则化的话就要自己做取舍了。要么通过对样本采样来降低样本量,要么回到L2正则化。
但是liblinear也有自己的弱点!我们知道,逻辑回归有二元逻辑回归和多元逻辑回归。对于多元逻辑回归常见的有one-vs-rest(OvR)和many-vs-many(MvM)两种。而MvM一般比OvR分类相对准确一些。而liblinear只支持OvR,不支持MvM,这样如果我们需要相对精确的多元逻辑回归时,就不能选择liblinear了。也意味着如果我们需要相对精确的多元逻辑回归不能使用L1正则化了。
对此,我做了以下对比:
l2_c1_liblinear(81.26,76.555)l2_c1_lbfgs(81.37,76.555)
可见,虽然改变计算方法会给提升一点点测试集的准确率,但是对于测试集上的效果却没有什么改变,至此,我觉得应该换一种机器学习的方法了,我决定使用svm!
在使用svm之前,先回答一个问题:什么是支持向量机?
由上图可以看到:两个支撑着中间的 gap 的超平面,它们到中间的纯红线separating hyper plane 的距离相等,即我们所能得到的最大的 geometrical margin,而“支撑”这两个超平面的必定会有一些点,而这些“支撑”的点便叫做支持向量Support Vector。
很显然,由于这些 supporting vector 刚好在边界上,所以它们满足,而对于所有不是支持向量的点,也就是在“阵地后方”的点,则显然有
。这一点非常重要,因为在对损失函数增加拉格朗日算子时,得到的目标函数:
注意到如果 xi 是支持向量的话,上式中红颜色的部分是等于 0 的(因为支持向量的 functional margin 等于 1 ),而对于非支持向量来说,functional margin 会大于 1 ,因此红颜色部分是大于零的,而a又是非负的,为了满足最大化,必须等于 0 。这一点对使用核函数拟合非线性时非常重要,因为分类函数为:
所谓 Supporting Vector 也在这里显示出来——事实上,所有非Supporting Vector 所对应的系数都是等于零的,因此对于新点的内积计算实际上只要针对少量的“支持向量”而不是所有的训练数据即可。
svm_linear(78.68,76.555)
发现这个结果并没有比lg回归好,失望脸;
在svm中,使用核的目的是为了解决:维度灾难,作为最常用的svm核,从理论上讲, RBF一定不比线性核函数差,但是在实际应用中,却面临着几个重要的超参数的调优问题。如果调的不好,可能比线性核函数还要差,在享受RBF对非线性数据的良好分类效果前,我们需要对主要的超参数进行选取:惩罚系数C以及RBF核函数的系数γ:
一言以蔽之:c越大,gamma越小,越容易过拟合!
# Support Vector Machines
svc = SVC(kernel='rbf', probability=True)
param_grid = {'C': [1500,1600,1700,1800,1900], 'gamma': [0.002,0.003,0.004,0.005,0.006]}
grid_search = GridSearchCV(svc, param_grid, n_jobs=-1, verbose=True)#verbose=True显示进程使用是信息
grid_search.fit(X_train, Y_train)
best_parameters = grid_search.best_estimator_.get_params()
print '_'*40
for para, val in list(best_parameters.items()):
print(para, val)
print '_'*40
model = SVC(kernel='rbf', C=best_parameters['C'], gamma=best_parameters['gamma'], probability=True)
model.fit(X_train, Y_train)
acc_svc = round(model.score(X_train, Y_train) * 100, 2)
print acc_svc
结果如下:
Fitting 3 folds for each of 25 candidates, totalling 75 fits
[Parallel(n_jobs=-1)]: Done 42 tasks | elapsed: 7.0s
[Parallel(n_jobs=-1)]: Done 75 out of 75 | elapsed: 13.3s finished
________________________________________
('kernel', 'rbf')
('C', 1500)
#set the parameter C of C-SVC,epsilon-SVR, and nu-SVR (default 1)
('verbose', False)
('probability', True)
('degree', 3)
('shrinking', True)
# whether to use the shrinkingheuristics(启发式), 0 or 1 (default 1)
('max_iter', -1)
('decision_function_shape', None)
('random_state', None)
('tol', 0.001)
('cache_size', 200)
('coef0', 0.0)
('gamma', 0.004)
#set gamma in kernel function(default 1/num_features)
('class_weight', None)
________________________________________
83.61
虽然c高达1500,但是测试结果为79.426%,比c=1时的结果(77.99)要高出近1.5%!
在下一篇中,我将更换另外的几个常用机器学习方法,最后做一些特征工程,提高准确率~