7天从入门到运用机器学习 (一) -- 数据探索与预处理

前言

这个系列主要是面向做工程的同事做一些分享,旨在让大家都可以应用机器学习来解决问题,而不仅仅是看看理论浅尝辄止。

机器学习是一门包含多方面知识的学科,想要几天掌握是不太可能的。但是如果把它当做一个工具来使用,不追本溯源,其实不需要花费太多的时间。

这一系列分享的目的在于,希望全部完成以后,任何一个会写代码但对机器学习还不了解的同学,都可以上手运用机器学习的工具来完成一些预测任务,如分类或回归。

机器学习包含了有监督学习与无监督学习,但是由于时间有限,这一系列分享的内容仅包含有监督学习,因此下文的机器学习特指有监督学习

概括

机器学习也就是让机器从已知数据中自动的学习出规律,从而再对未知的数据进行预测。如何让机器从数据中学习知识?从什么样的数据才能学习到知识?解决这两个问题是有套路可用的,下面就来讲如何去套路

如何让机器从数据中学习?

现在已经有非常多种成熟的机器学习算法,如KNN,逻辑回归,决策树,SVM,各种ensemble方法,以及深度学习的神经网络。不同的算法适用于不同的情况。学习这些算法需要花费大量的时间与精力,但是可以把这些算法全部看成一个API来进行调用。比如对于二分类的任务,不管用什么算法,输入输出都是一样的,即训练时输入样本集,输出训练好的模型。预测时,输入未知样本,输出类别标签,通常为0/1。

把不同的算法抽象成API以后,就可以暂且不需要知道背后的原理也能开心的应用了。

从什么样的数据才能学习到知识?

有了算法API接下来就是准备好数据,输入的数据应该长什么样呢?输入的数据应该是结构化的表格数据。训练模型的输入数据也叫做训练集,可以看成数据库的一张表,每行记录为一个样本,每个字段为一个特征。其中有一个字段标示每个样本的类别或者是某个连续值。由于机器学习模型都是数学模型,每一个特征都需要转换为数值型数据。

归纳一下:只需要准备好一个二维数组,每个维度都是数值型数据,可以是连续也可以是离散的,就可以调用算法API进行学习了。

第一讲就围绕如何准备机器学习的数据来进行讲解,包括了数据探索以及一些数据预处理的技巧。

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt


%config InlineBackend.figure_format = 'png' #set 'png' here when working on notebook
%matplotlib inline

这里的案例数据采用Kaggle Titanic数据集,每行代表一个乘客,每列为乘客的特征。标签是存活与否,即这是一个二分类任务。

下面是几个字段的说明:
- survival 生还与否 (0 = No; 1 = Yes)
- pclass 乘客乘坐的舱位级别 (1 = 1st; 2 = 2nd; 3 = 3rd)
- name 姓名
- sex 性别
- age 年龄
- sibsp 船上兄弟姐妹的数量
- parch 船上父母或子女的数量
- ticket 票号
- fare 票价
- cabin 客舱
- embarked 登船港口 (C = Cherbourg; Q = Queenstown; S = Southampton)

#载入数据,案例数据使用Kaggle Titanic数据集
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')

train.head(10)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
5 6 0 3 Moran, Mr. James male NaN 0 0 330877 8.4583 NaN Q
6 7 0 1 McCarthy, Mr. Timothy J male 54.0 0 0 17463 51.8625 E46 S
7 8 0 3 Palsson, Master. Gosta Leonard male 2.0 3 1 349909 21.0750 NaN S
8 9 1 3 Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) female 27.0 0 2 347742 11.1333 NaN S
9 10 1 2 Nasser, Mrs. Nicholas (Adele Achem) female 14.0 1 0 237736 30.0708 NaN C

观察数据,处理数据

实际的工作中,我们可能要从多个数据源收集数据,最终再汇总成一个结构化的表格。这里原始的输入数据已经是结构化的表格数据,减少了很多ETL的工作。但是这些数据还不能直接作为输入数据给机器学习算法,因为有很多字段还存在非数值型数据,以及缺失数据NaN。接下来先简单的把这些数据都转换为数值型数据。

#1. 丢弃认为对分类没有必要的特征,如有大量缺失值的Cabin。Ticket为票号,比较散乱无序,暂且认为与乘客的生还与否没有关系。
train = train.drop(['Ticket','Cabin'], axis=1)
test = test.drop(['Ticket','Cabin'], axis=1)
# 'Embarked' , 'Sex', 'Pclass'都算是标签型的特征, 先看一下它们都有哪些数值, 是否含有缺失值, 并统计一下频数

print 'Embarked'
print train['Embarked'].unique()
print train['Embarked'].value_counts()
print '-----------------------------'

print 'Sex'
print train['Sex'].unique()
print train['Sex'].value_counts()
print '-----------------------------'

print 'Pclass'
print train['Pclass'].unique()
print train['Pclass'].value_counts()
print '-----------------------------'

print 'SibSp'
print train['SibSp'].unique()
print train['SibSp'].value_counts()
print '-----------------------------'

print 'Parch'
print train['Parch'].unique()
print train['Parch'].value_counts()
print '-----------------------------'
    Embarked
    ['S' 'C' 'Q' nan]
    S    644
    C    168
    Q     77
    Name: Embarked, dtype: int64
    -----------------------------
    Sex
    ['male' 'female']
    male      577
    female    314
    Name: Sex, dtype: int64
    -----------------------------
    Pclass
    [3 1 2]
    3    491
    1    216
    2    184
    Name: Pclass, dtype: int64
    -----------------------------
    SibSp
    [1 0 3 4 2 5 8]
    0    608
    1    209
    2     28
    4     18
    3     16
    8      7
    5      5
    Name: SibSp, dtype: int64
    -----------------------------
    Parch
    [0 1 2 5 3 4 6]
    0    678
    1    118
    2     80
    5      5
    3      5
    4      4
    6      1
    Name: Parch, dtype: int64
    -----------------------------
#将训练集与测试集除类别标签外的字段合并,统一进行数据预处理
#选取认为有用的几个特征,Name暂时不用
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

all_data = pd.concat((train[features], test[features]))

all_data.head()
Pclass Sex Age SibSp Parch Fare Embarked
0 3 male 22.0 1 0 7.2500 S
1 1 female 38.0 1 0 71.2833 C
2 3 female 26.0 0 0 7.9250 S
3 1 female 35.0 1 0 53.1000 S
4 3 male 35.0 0 0 8.0500 S
#进行缺失值填充

# 类别的缺失值采用最多的值填充
all_data['Embarked'] = all_data['Embarked'].fillna('S')

# 数值型缺失值采用中位数填充
all_data = all_data.fillna(all_data.median())

all_data.describe()
Pclass Age SibSp Parch Fare
count 1309.000000 1309.000000 1309.000000 1309.000000 1309.000000
mean 2.294882 29.503186 0.498854 0.385027 33.281086
std 0.837836 12.905241 1.041658 0.865560 51.741500
min 1.000000 0.170000 0.000000 0.000000 0.000000
25% 2.000000 22.000000 0.000000 0.000000 7.895800
50% 3.000000 28.000000 0.000000 0.000000 14.454200
75% 3.000000 35.000000 1.000000 0.000000 31.275000
max 3.000000 80.000000 8.000000 9.000000 512.329200
from sklearn.preprocessing import LabelEncoder
### 只做简单特征处理

le_embarked = LabelEncoder()
le_embarked.fit(all_data['Embarked'])
all_data['Embarked'] = le_embarked.transform(all_data['Embarked'])

all_data['Sex'] = all_data['Sex'].map(lambda x:1 if x == 'male' else 0)

all_data.head()
Pclass Sex Age SibSp Parch Fare Embarked
0 3 1 22.0 1 0 7.2500 2
1 1 0 38.0 1 0 71.2833 0
2 3 0 26.0 0 0 7.9250 2
3 1 0 35.0 1 0 53.1000 2
4 3 1 35.0 0 0 8.0500 2

此时输入的数据已经全部转换为数值型,可以输入给机器学习算法进行学习与预测。因为这一节不讲解具体的算法,因此选用一个基本的逻辑回归

from sklearn.linear_model import LogisticRegressionCV
from sklearn.model_selection import cross_val_score

def accuracy_cv(model):
    """
    计算交叉验证的准确率
    """
    return cross_val_score(model, X_train, y, scoring='accuracy', cv=3)
# 生成训练数据
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']

X_train.head()
Pclass Sex Age SibSp Parch Fare Embarked
0 3 1 22.0 1 0 7.2500 2
1 1 0 38.0 1 0 71.2833 0
2 3 0 26.0 0 0 7.9250 2
3 1 0 35.0 1 0 53.1000 2
4 3 1 35.0 0 0 8.0500 2
#定义模型与交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()
print '简单的特征处理准确率为 %f' % accuracy
简单的特征处理准确率为 0.787879

小结

截至上面的步骤,已经完成了一次完整的机器学习,并且交叉验证的分类准确率达到79%,且生还与未生还的样本分布并不算倾斜,这个结果比随机猜测要高多了,说明这么简单的几步就让机器学到了一些规律。把机器学习当做一个黑箱子工具来使用,也不是完全不靠谱的。

但是机器学习是一个不断优化的过程,在商业价值高的任务上,如CTR预估,0.1%的提高都能带来显著的收益提升。因此不能满足于一个看似满意的结果,还需要不断的提升。下面的内容就是通过数据探索与处理来提升最终的准确率。

# 从原始训练、测试数据重新载入数据,此时使用'Name'特征
features = ['Name','Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

all_data = pd.concat((train[features], test[features]))

# 填充'Embarked'的缺失值
all_data['Embarked'] = all_data['Embarked'].fillna('S')

# 填充'Fare'缺失值,使用中位数填充
all_data['Fare'] = all_data['Fare'].fillna(all_data['Fare'].median())

# 填充'Age'缺失值,使用中位数填充
#all_data['Age'] = all_data['Age'].fillna(all_data['Age'].mean())

all_data.head()
Name Pclass Sex Age SibSp Parch Fare Embarked
0 Braund, Mr. Owen Harris 3 male 22.0 1 0 7.2500 S
1 Cumings, Mrs. John Bradley (Florence Briggs Th… 1 female 38.0 1 0 71.2833 C
2 Heikkinen, Miss. Laina 3 female 26.0 0 0 7.9250 S
3 Futrelle, Mrs. Jacques Heath (Lily May Peel) 1 female 35.0 1 0 53.1000 S
4 Allen, Mr. William Henry 3 male 35.0 0 0 8.0500 S

数据探索

数据探索就是从已有的数据里面,再挖掘出一些有助于提升最终结果的特征,这方面没有固定的套路,但是很有意思,依赖对数据的理解,对业务的理解

新增一个家庭特征, 反映该乘客是单人乘船还是随家庭乘船 , 从交叉验证的结果来看,这个特征是失败的


all_data['Family'] = all_data['SibSp'] + all_data['Parch']
all_data['Family'].loc[all_data['Family'] > 0] = 1
all_data['Family'].loc[all_data['Family'] == 0] = 0

把这些变成标签型特征再进行one hot编码, 从交叉验证结果来看这个尝试是失败的

all_data['SibSp'] = all_data['SibSp'].map(lambda x:str(x))
all_data['Parch'] = all_data['Parch'].map(lambda x:str(x))

将每个乘客的Title作为一个特征 , 从结果看这是一个显著的特征

def get_title(x):
    if 'Mr' in x:
        return 'Mr'
    elif 'Mrs' in x:
        return 'Mrs'
    elif 'Miss' in x:
        return 'Miss'
    elif 'Master' in x:
        return 'Master'
    else:
        return 'Rare'

all_data['Title'] = all_data['Name'].map(get_title)

年龄的缺失数据, 这个方法也有助于提升结果

all_data['Age'].loc[(all_data['Title'] == 'Mr') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Mr')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Mrs') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Mrs')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Miss') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Miss')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Master') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Master')].mean()
all_data['Age'].loc[(all_data['Title'] == 'Rare') & (all_data['Age'].isnull())] = all_data['Age'].loc[(all_data['Title'] == 'Rare')].mean()

判断是否是小孩

all_data['Child'] = all_data['Age']
all_data['Child'].loc[all_data['Child'] < 10] = 1
all_data['Child'].loc[all_data['Child'] >= 10] = 0

处理数值型倾斜数据,数据倾斜指的是,单维度的数据不呈正态分布,有可能左偏有可能右边,而对数据进行log变换,可以减小其倾斜度。

matplotlib.rcParams['figure.figsize'] = (12.0, 6.0)

fare = pd.DataFrame({'Fare':all_data['Fare'], 'log(Fare+1)':np.log1p(all_data['Fare'])})

fare.hist()

# 对倾斜数据做log变换 , 这一步在这个数据集上对结果的提升并没有帮助
print 'skew before log trick: %f ' %  all_data['Fare'].skew()
all_data['Fare'] = np.log1p(all_data['Fare'])
print 'skew after log trick: %f' % all_data['Fare'].skew()
skew before log trick: 4.369510 
skew after log trick: 0.542617

不进行One Hot编码,直接标注0,1,2,3

le_title = LabelEncoder()
le_title.fit(all_data['Title'])
all_data['Title'] = le_title.transform(all_data['Title'])

le_embarked = LabelEncoder()
le_embarked.fit(all_data['Embarked'])
all_data['Embarked'] = le_embarked.transform(all_data['Embarked'])

all_data['Sex'] = all_data['Sex'].map(lambda x:1 if x == 'male' else 0)

all_data = all_data.drop('Name', axis=1)

# 生成训练集
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']

# 训练模型并进行交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()

print '未使用One-Hot编码准确率为 %f' % accuracy
0.810325476992

对标签特征进行序数编码是不太妥当的,因为大多数算法接受数值型输入,是假设这些数值都是连续值,也就是有序的。而标签本身是无序的、不存在大小关系的,因此需要使用另一种编码方式,也就是One-Hot编码。

# 对标签特征进行One Hot编码,由于Pclass是被标示为数值型,为了使用get_dummies API,先转换为字符串
all_data['Pclass'] = all_data['Pclass'].map(lambda x:str(x))

all_data = pd.get_dummies(all_data.drop(['Name'], axis=1))

# 生成训练集
X_train = all_data[:train.shape[0]]
X_test = all_data[train.shape[0]:]
y = train['Survived']

# 训练模型并进行交叉验证
model_lr = LogisticRegressionCV()
accuracy = accuracy_cv(model_lr).mean()

print '使用One-Hot编码准确率为 %f' % accuracy
使用One-Hot编码准确率为 0.822671

总结

  1. 机器学习算法的输入样本数据为结构化、数值型数据
  2. 对于标签型特征可以简单的使用0,1,2,3…转化为数值型(使用sklearn.preprocessing.LabelEncoder) ,但不太合理
  3. 处理标签型特征最好的方法为One-Hot编码。(使用pandas.get_dummies或sklearn.preprocessing.OneHotEncoder)
  4. 缺失值填充可以使用该列的总体平均数,最好可以根据更精细的特征进行推断,如头衔为Mrs与Miss的年龄缺失值应该填充不同的值
  5. 学会从看似无用的特征里挖掘有用的信息,如抽取Name中的头衔作为一个特征。
  6. 理解数据,多做尝试

你可能感兴趣的:(机器学习)