机器学习之数据与特征工程
学习过程中梳理一些知识点,在机器学习中,对原始数据的处理和特征提取,是最为重要的,处理好原始数据,哪怕后面使用简单的算法都可以得到比较好的准确率。
这是大神们的理解
说白了,所谓的特征工程,听起来很高大上,其实就是从数据中抽取出来的对预测结构有用的信息,使得其能在机器学习算法上发挥更好的作用。
大部分算法精进和数据分析所做的工作都是:
a)跑数据, 各种map-reduce,从数据空中获取数据。
b)数据清洗,清除明显有问题的数据。(如,一个身高数据中,一个身高为3米,这种数据明显是错误的,需要清洗)。
c)分析业务,找特征。。。
d)应用在常见的机器学习算法上。
下面以一个经典的案例来介绍数据处理和特征提取。
泰坦尼克号问题,是Kaggle中的经典入门问题。泰坦尼克号的故事大家都很清除了,船要沉了,大家都想上救生船,但是救生船的数量有限,一个前提要求是【lady and kid first】。以这个为背景,给出了训练集和测试数据集,其中包含一些乘客的个人信息和存活情况,需要尝试根据训练集生成一个很是的模型,预测其他人的存活情况。
关于训练集合测试数据集,大家可以在Kaggle中下载。
这里提供一个连接,供大家下载数据集 点击打开链接
首先,我们先看看数据长得什么样
用pandas加载数据,并打印出数据的列
import pandas as pd #数据分析
import numpy as np #科学计算
from pandas import Series,DataFrame
data_train = pd.read_csv("G:\Train.csv")
data_train.columns
输出:Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'], dtype='object')我们看大概有以下这些字段
PassengerId => 乘客ID
Pclass => 乘客等级(1/2/3等舱位)
Name => 乘客姓名
Sex => 性别
Age => 年龄
SibSp => 堂兄弟/妹个数
Parch => 父母与小孩个数
Ticket => 船票信息
Fare => 票价
Cabin => 客舱
Embarked => 登船港口
我们还可以通过pandas看看一些信息
data_train.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 891 entries, 0 to 890 Data columns (total 12 columns): PassengerId 891 non-null int64 Survived 891 non-null int64 Pclass 891 non-null int64 Name 891 non-null object Sex 891 non-null object Age 714 non-null float64 SibSp 891 non-null int64 Parch 891 non-null int64 Ticket 891 non-null object Fare 891 non-null float64 Cabin 204 non-null object Embarked 889 non-null object dtypes: float64(2), int64(5), object(5) memory usage: 83.6+ KB
上面数据告诉我们一些直观的信息,训练数据中有891名乘客,但是有些属性特征的数据不全。如乘客的年龄(Age),客舱(Cabin)。这些信息显然还不够,我们再利用pandas获取展示更多具体的数据
data_train.describe()
其中mean字段表明,其中只有大概0.383838的人获救了,乘客的平均年龄是29.7岁等等。
但是上述的信息还是不能给我们提供很有用的特征,,,我们还需要进一步分析数据。
我们最终的结构是要预测是否获救,所以我们查看一个或多个数据属性与最后的Survived之前的关系
还有就是只看数据(数字)是看不出有什么的关联的。这时候,最好是进行图像的展示。
import matplotlib.pyplot as plt
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
plt.subplot2grid((2,3),(0,0)) # 在一张大图里分列几个小图f
data_train.Survived.value_counts().plot(kind='bar')# plots a bar graph of those who surived vs those who did not.
plt.title(u"获救情况 (1为获救)") # puts a title on our graph
plt.ylabel(u"人数")
plt.subplot2grid((2,3),(0,1))
data_train.Pclass.value_counts().plot(kind="bar")
plt.ylabel(u"人数")
plt.title(u"乘客等级分布")
plt.subplot2grid((2,3),(0,2))
plt.scatter(data_train.Survived, data_train.Age)
plt.ylabel(u"年龄") # sets the y axis lable
plt.grid(b=True, which='major', axis='y') # formats the grid line style of our graphs
plt.title(u"按年龄看获救分布 (1为获救)")
plt.subplot2grid((2,3),(1,0), colspan=2)
data_train.Age[data_train.Pclass == 1].plot(kind='kde') # plots a kernel desnsity estimate of the subset of the 1st class passanges's age
data_train.Age[data_train.Pclass == 2].plot(kind='kde')
data_train.Age[data_train.Pclass == 3].plot(kind='kde')
plt.xlabel(u"年龄")# plots an axis lable
plt.ylabel(u"密度")
plt.title(u"各等级的乘客年龄分布")
plt.legend((u'头等舱', u'2等舱',u'3等舱'),loc='best') # sets our legend for our graph.
plt.subplot2grid((2,3),(1,2))
data_train.Embarked.value_counts().plot(kind='bar')
plt.title(u"各登船口岸上船人数")
plt.ylabel(u"人数")
plt.show()
可以得到下面一张图
由上图可见:
1)被救人数300多点,半数不到。
2)3等仓人数非常多,遇难和获救的人的年龄段跨度都很广
3)等船港口人数中,S港远多于其他两个港口
由此可以分析:
1)不同舱位和乘客等级在最后获救概率上回有不同。
2)年龄对获救概率有一定影响,毕竟背景前提是【女士和小孩优先】
3)是不是和登船港口有关系呢,不同的登船港口和乘客的身份等级有关??
有了这些想法,试试把这些属性和最后获救参数进行统计
#看看各乘客等级的获救情况
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
Survived_0 = data_train.Pclass[data_train.Survived == 0].value_counts()
Survived_1 = data_train.Pclass[data_train.Survived == 1].value_counts()
df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0})
df.plot(kind='bar', stacked=True)
plt.title(u"各乘客等级的获救情况")
plt.xlabel(u"乘客等级")
plt.ylabel(u"人数")
plt.show()
从上面的图表中可以明显看出,1号舱获救的比例<2号舱获救的比例<三号舱获救比例
所以船舱等级会影响最后的获救的结果。
#看看各登录港口的获救情况
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
Survived_0 = data_train.Embarked[data_train.Survived == 0].value_counts()
Survived_1 = data_train.Embarked[data_train.Survived == 1].value_counts()
df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0})
df.plot(kind='bar', stacked=True)
plt.title(u"各登录港口乘客的获救情况")
plt.xlabel(u"登录港口")
plt.ylabel(u"人数")
plt.show()
这个图表中,并没有看出什么特别的
继续分析
#看看各性别的获救情况
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
Survived_m = data_train.Survived[data_train.Sex == 'male'].value_counts()
Survived_f = data_train.Survived[data_train.Sex == 'female'].value_counts()
df=pd.DataFrame({u'男性':Survived_m, u'女性':Survived_f})
df.plot(kind='bar', stacked=True)
plt.title(u"按性别看获救情况")
plt.xlabel(u"性别")
plt.ylabel(u"人数")
plt.show()
有上图标中看出,获救人员中,女性比例高于男性,看来背景中的【女士和小孩优先】还是被执行的不错。所以,性别特征可以影响最后获救的概率。
#然后我们再来看看各种舱级别情况下各性别的获救情况
fig=plt.figure()
fig.set(alpha=0.65) # 设置图像透明度,无所谓
plt.title(u"根据舱等级和性别的获救情况")
ax1=fig.add_subplot(141)
data_train.Survived[data_train.Sex == 'female'][data_train.Pclass != 3].value_counts().plot(kind='bar', label="female highclass", color='#FA2479')
ax1.set_xticklabels([u"获救", u"未获救"], rotation=0)
ax1.legend([u"女性/高级舱"], loc='best')
ax2=fig.add_subplot(142, sharey=ax1)
data_train.Survived[data_train.Sex == 'female'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='female, low class', color='pink')
ax2.set_xticklabels([u"未获救", u"获救"], rotation=0)
plt.legend([u"女性/低级舱"], loc='best')
ax3=fig.add_subplot(143, sharey=ax1)
data_train.Survived[data_train.Sex == 'male'][data_train.Pclass != 3].value_counts().plot(kind='bar', label='male, high class',color='lightblue')
ax3.set_xticklabels([u"未获救", u"获救"], rotation=0)
plt.legend([u"男性/高级舱"], loc='best')
ax4=fig.add_subplot(144, sharey=ax1)
data_train.Survived[data_train.Sex == 'male'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='male low class', color='steelblue')
ax4.set_xticklabels([u"未获救", u"获救"], rotation=0)
plt.legend([u"男性/低级舱"], loc='best')
plt.show()
从上图标中可以看出,高级船舱中(船舱为1和2的)获救比例不管男女都要比低级船舱(船舱为3号的)高。由此可以考虑组合一个船舱号和性别的特征。
对于其他属性,比如堂兄弟,乘客ID,船票,父母与小孩个数,这些属性对最终获救有什么影响呢。我们来分析一下
g = data_train.groupby(['SibSp','Survived'])
df = pd.DataFrame(g.count()['PassengerId'])
df
上面为分析堂兄弟与获救情况的表格,但是从中并没有什么明显的关联。
同样分析父母与小孩个数与获救之前的关系
g = data_train.groupby(['Parch','Survived'])
df = pd.DataFrame(g.count()['PassengerId'])
df
上面中,也没有看出明显的规律,同样先放一放
船票信息Ticket只是船的标号。和最后的是否获救没有太大的关系,不纳入考虑的特征范畴。
Cabin在测试集中,只有204个,有很多缺省的值,先看看已有参数的分布情况
data_train.Cabin.value_counts()
Cabin值不集中,而且缺失很多,完全丢失又觉得可惜。我们可以在有无Cabin信息这个粗粒度上看看获救的情况
#cabin的值计数太分散了,绝大多数Cabin值只出现一次。感觉上作为类目,加入特征未必会有效
#那我们一起看看这个值的有无,对于survival的分布状况,影响如何吧
fig = plt.figure()
fig.set(alpha=0.2) # 设定图表颜色alpha参数
Survived_cabin = data_train.Survived[pd.notnull(data_train.Cabin)].value_counts()
Survived_nocabin = data_train.Survived[pd.isnull(data_train.Cabin)].value_counts()
df=pd.DataFrame({u'有':Survived_cabin, u'无':Survived_nocabin}).transpose()
df.plot(kind='bar', stacked=True)
plt.title(u"按Cabin有无看获救情况")
plt.xlabel(u"Cabin有无")
plt.ylabel(u"人数")
plt.show()
#似乎有cabin记录的乘客survival比例稍高,那先试试把这个值分为两类,有cabin值/无cabin值,一会儿加到类别特征好了
从上面的图表中可以看到,有Cabin信息的获救概率会高一些。
毕竟有丢失的数据的属性,对下一不工作影响很大。我们可以将Cabin这个属性按照有无信息,分为Yes和No两种属性进行处理。而测试数据集里面的Age也存在数据缺失的情况。
通常遇到数据缺失的情况,我们会有集中处理方式:
1,如果缺失值的样本占总数的比例极高,我们可以直接舍弃,如果强行作为特征加入的话,可能会带来noise,影响最后的结果。
2,如果缺失值的样本适中,并且该属性为非连续值,可以参照Cabin这个属性的处理方式进行处理。
3,如果缺失值的样本适中,并且该属相为连续值,可以先对此属性就行离散化,在进行处理。
4,有些情况下,缺失的数值并不是很多。我们可以试试根据已经有的值进行拟合,补充上。对于Age这个属性就可以采用这种拟合不全的方式。
这里采用scikit-learn中的RandomForest来拟合Age的缺省数据。
from sklearn.ensemble import RandomForestRegressor
### 使用 RandomForestClassifier 填补缺失的年龄属性
def set_missing_ages(df):
# 把已有的数值型特征取出来丢进Random Forest Regressor中
age_df = df[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
# 乘客分成已知年龄和未知年龄两部分
known_age = age_df[age_df.Age.notnull()].as_matrix()
unknown_age = age_df[age_df.Age.isnull()].as_matrix()
# y即目标年龄
y = known_age[:, 0]
# X即特征属性值
X = known_age[:, 1:]
# fit到RandomForestRegressor之中
rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)
rfr.fit(X, y)
# 用得到的模型进行未知年龄结果预测
predictedAges = rfr.predict(unknown_age[:, 1::])
# 用得到的预测结果填补原缺失数据
df.loc[ (df.Age.isnull()), 'Age' ] = predictedAges
return df, rfr
def set_Cabin_type(df):
df.loc[ (df.Cabin.notnull()), 'Cabin' ] = "Yes"
df.loc[ (df.Cabin.isnull()), 'Cabin' ] = "No"
return df
data_train, rfr = set_missing_ages(data_train)
data_train = set_Cabin_type(data_train)
data_train
整理后的部分数据展示如下
891 rows × 12 columns
因为逻辑回归的时候,需要输入特征都是数值型特征,需要对类目型特征进行因子化--one-hot编码
什么叫做因子化/one-hot编码?举个例子:
以Embarked为例,原本一个属性维度,因为其取值可以是[‘S’,’C’,’Q‘],而将其平展开为’Embarked_C’,’Embarked_S’, ‘Embarked_Q’三个属性
原本Embarked取值为C的,在此处的”Embarked_C”下取值为1,在’Embarked_S’, ‘Embarked_Q’下取值为0
原本Embarked取值为Q的,在此处的”Embarked_Q”下取值为1,在’Embarked_C’, ‘Embarked_S’下取值为0
我们使用pandas的”get_dummies”来完成这个工作,并拼接在原来的”data_train”之上,如下所示。
# 因为逻辑回归建模时,需要输入的特征都是数值型特征
# 我们先对类目型的特征离散/因子化
# 以Cabin为例,原本一个属性维度,因为其取值可以是['yes','no'],而将其平展开为'Cabin_yes','Cabin_no'两个属性
# 原本Cabin取值为yes的,在此处的'Cabin_yes'下取值为1,在'Cabin_no'下取值为0
# 原本Cabin取值为no的,在此处的'Cabin_yes'下取值为0,在'Cabin_no'下取值为1
# 我们使用pandas的get_dummies来完成这个工作,并拼接在原来的data_train之上,如下所示
dummies_Cabin = pd.get_dummies(data_train['Cabin'], prefix= 'Cabin')
dummies_Embarked = pd.get_dummies(data_train['Embarked'], prefix= 'Embarked')
dummies_Sex = pd.get_dummies(data_train['Sex'], prefix= 'Sex')
dummies_Pclass = pd.get_dummies(data_train['Pclass'], prefix= 'Pclass')
df = pd.concat([data_train, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1)
df.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True)
df
部分结果显示如下
这样一看,数据处理的差不多了,其实还有一些工作要处理的,我们看到Age和Fare两个特征浮动变化太大,这种情况加入逻辑回归和与梯度下降的话,会对收敛速度造成影响,严重的导致不收敛。所以我们先用scikit-learn里面的preprocessing模块对这俩货做一个scaling,所谓scaling,其实就是将一些变化幅度较大的特征化到[-1,1]之内。
# 接下来我们要接着做一些数据预处理的工作,比如scaling,将一些变化幅度较大的特征化到[-1,1]之内
# 这样可以加速logistic regression的收敛
import sklearn.preprocessing as preprocessing
scaler = preprocessing.StandardScaler()
age_scale_param = scaler.fit(df['Age'])
df['Age_scaled'] = scaler.fit_transform(df['Age'], age_scale_param)
fare_scale_param = scaler.fit(df['Fare'])
df['Fare_scaled'] = scaler.fit_transform(df['Fare'], fare_scale_param)
df
部分显示如下
增加了两个特征。
此致,特征提取数据优化完成,然后我们需要把之前分析的特征提取出来,并转化成算法需要的矩阵形式,进行逻辑回归建模。
# 我们把需要的feature字段取出来,转成numpy格式,使用scikit-learn中的LogisticRegression建模
from sklearn import linear_model
train_df = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
train_np = train_df.as_matrix()
# y即Survival结果
y = train_np[:, 0]
# X即特征属性值
X = train_np[:, 1:]
# fit到RandomForestRegressor之中
clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
clf.fit(X, y)
clf
接下来,我们要对测试集做和训练集一样的数据处理操作
data_test = pd.read_csv("G:\Test.csv")
data_test.loc[ (data_test.Fare.isnull()), 'Fare' ] = 0
# 接着我们对test_data做和train_data中一致的特征变换
# 首先用同样的RandomForestRegressor模型填上丢失的年龄
tmp_df = data_test[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
null_age = tmp_df[data_test.Age.isnull()].as_matrix()
# 根据特征属性X预测年龄并补上
X = null_age[:, 1:]
predictedAges = rfr.predict(X)
data_test.loc[ (data_test.Age.isnull()), 'Age' ] = predictedAges
data_test = set_Cabin_type(data_test)
dummies_Cabin = pd.get_dummies(data_test['Cabin'], prefix= 'Cabin')
dummies_Embarked = pd.get_dummies(data_test['Embarked'], prefix= 'Embarked')
dummies_Sex = pd.get_dummies(data_test['Sex'], prefix= 'Sex')
dummies_Pclass = pd.get_dummies(data_test['Pclass'], prefix= 'Pclass')
df_test = pd.concat([data_test, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1)
df_test.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True)
age_scale_param = scaler.fit(df['Age'])
df_test['Age_scaled'] = scaler.fit_transform(df_test['Age'], age_scale_param)
fare_scale_param = scaler.fit(df['Fare'])
df_test['Fare_scaled'] = scaler.fit_transform(df_test['Fare'], fare_scale_param)
df_test
部分显示如下
test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
predictions = clf.predict(test)
result = pd.DataFrame({'PassengerId':data_test['PassengerId'].as_matrix(), 'Survived':predictions.astype(np.int32)})
result.to_csv("logistic_regression_predictions.csv", index=False)
生成结果文件,读取结果文件
pd.read_csv("logistic_regression_predictions.csv")
这里显示部分文件信息
最后将生成的结果文件上传的Kaggle中进行正确率的比对。对于Kaggle的一些操作这里就不多说的。这个结果大概是0.76.还能接受吧,毕竟只是简单的分析,入门的级别。
等等。。。
还没有完事。
我们还要判断一下当前的模型所处的状态(欠拟合还是过拟合还是正常)
对于处理解决欠拟合和过拟合的方法这里就不再多说了,这里只介绍判断当前模型是处在什么状态
我们采用scikit-learn里面的learning_curve来分辨当前模型的状态。
import numpy as np
import matplotlib.pyplot as plt
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):
"""
画出data在某模型上的learning curve.
参数解释
----------
estimator : 你用的分类器。
title : 表格的标题。
X : 输入的feature,numpy类型
y : 输入的target vector
ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点
cv : 做cross-validation的时候,数据分成的份数,其中一份作为cv集,其余n-1份作为training(默认为3份)
n_jobs : 并行的的任务数(默认1)
"""
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"训练样本数")
plt.ylabel(u"得分")
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"训练集上得分")
plt.plot(train_sizes, test_scores_mean, 'o-', color="r", label=u"交叉验证集上得分")
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"学习曲线", X, y)
从上图可以看出,训练集和交叉验证集上的曲线走势还是符合预期的。
现在我们的模型还不属于过拟合的状态,我们可以再增加一些特征。
要优化这个模型,在原始数据上在进行有用特征的提取。
比如说我们放弃的几个特征。Name,Ticket。能不能当做有用的特征值加入到算法中呢。
还有,最重要的Age属性。作为连续值,如果在算法模型上直接给定一个固定的参数,似乎是不存在两头照顾的情况。。
如果我们把Age离散化,按区段分作类别属性会更好一些。
而在此之前,看看现在得到的模型系数,因为系数和他们最终的判定能力强弱是正相关的
pd.DataFrame({"columns":list(train_df.columns)[1:], "coef":list(clf.coef_.T)})
我们先看看那些权重绝对值非常大的feature,在我们的模型上:
- Sex属性,如果是female会极大提高最后获救的概率,而male会很大程度拉低这个概率。
- Pclass属性,1等舱乘客最后获救的概率会上升,而乘客等级为3会极大地拉低这个概率。
- 有Cabin值会很大程度拉升最后获救概率(这里似乎能看到了一点端倪,事实上从最上面的有无Cabin记录的Survived分布图上看出,即使有Cabin记录的乘客也有一部分遇难了,估计这个属性上我们挖掘还不够)
- Age是一个负相关,意味着在我们的模型里,年龄越小,越有获救的优先权(还得回原数据看看这个是否合理)
- 有一个登船港口S会很大程度拉低获救的概率,另外俩港口压根就没啥作用(这个实际上非常奇怪,因为我们从之前的统计图上并没有看到S港口的获救率非常低,所以也许可以考虑把登船港口这个feature去掉试试)。
- 船票Fare有小幅度的正相关(并不意味着这个feature作用不大,有可能是我们细化的程度还不够,举个例子,说不定我们得对它离散化,再分至各个乘客等级上?)
我们通过交叉验证来一一验证我们的想法,之后具体的操作就不再说明了。
最后推荐一个连接,也是对这个案例的分析处理。
上一篇: 解析php根据ip查询所在地区