本节书摘来异步社区《Python机器学习——预测分析核心算法》一书中的第2章,第2.2节,作者:【美】Michael Bowles(鲍尔斯),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.2 分类问题:用声纳发现未爆炸的水雷
此小节将介绍在分类问题上首先需要做的工作。首先是简单的测量:数据的规模、数据类型、缺失的数据等。接着是数据的统计特性、属性之间的关系、属性与标签之间的关系。本节的数据集来自UC Irvine 数据仓库(见参考文献1)。数据来源于实验:测试声纳是否可以用于检测在港口军事行动后遗留下来的未爆炸的水雷。声纳信号又叫作啁啾信号(chirped signal),即信号在一个脉冲期间频率会增加或降低。此数据集的测量值代表声纳接收器在不同地点接收到的返回信号,其中在大约一半的例子中,返回的声纳信号反映的是岩石的形状,而另一半是金属圆筒的形状(水雷)。下文就用“岩石vs.水雷”来代表这个数据集。
2.2.1 “岩石vs.水雷”数据集的物理特性
对新数据集所做的第一件事就是确定数据集的规模。代码清单2-1为获取“岩石vs. 水雷”数据集规模的代码。在本章的后续内容,将多次遇到此数据集,主要用来作为介绍算法的例子,此数据集来源于UC Irvine数据仓库。在此例中,确定数据集的行数、列数的代码十分简单。数据集文件是由逗号分割的,一次实验数据占据文本的一行。文件处理十分简单:读入一行,对数据按逗号进行分割,将结果列表存入输出列表即可。
代码清单2-1 确定新数据集规模-rockVmineSummaries.py
(输出:outputRocksVMinesSummaries.txt)
__author__ = 'mike_bowles'
import urllib2
import sys
#read data from uci data repository
target_url = ("https://archive.ics.uci.edu/ml/machine-learning-"
"databases/undocumented/connectionist-bench/sonar/sonar.all-data")
data = urllib2.urlopen(target_url)
#arrange data into list for labels and list of lists for attributes
xList = []
labels = []
for line in data:
#split on comma
row = line.strip().split(",")
xList.append(row)
sys.stdout.write("Number of Rows of Data = " + str(len(xList)) + '\n')
sys.stdout.write("Number of Columns of Data = " + str(len(xList[1])))
Output:
Number of Rows of Data = 208
Number of Columns of Data = 61```
如代码输出所示,此数据集为208行,61列(每行61个字段)。这有什么影响吗?数据集的规模(行数、列数)至少在以下几个方面会影响你对数据的处理。首先,根据数据的规模可以大致判断训练所需的时间。对于像“岩石vs.水雷”这种小数据集,训练时间会少于1分钟,这有利于在训练过程中不断调整和迭代。如果数据集规模增加到1 000×1 000,惩罚线性回归训练时间将不到一分钟,而集成方法训练时间需要几分钟。如果数据集的行、列增加到万级规模,则惩罚线性回归的训练时间将达到3~4小时,而集成方法则长达12~24小时。更长的训练时间将会影响你的开发进度,因为通常需要迭代几次来对算法进行调整或优化。
另外一个重要的观察是如果数据集的列数远远大于行数,那么采用惩罚线性回归的方法则有很大的可能获得最佳的预测,反之亦然。在第3章有实际的例子,这会加深对这个结论的理解。
根据应做事项清单,下一步要做的就是确定哪些列是数值型的,哪些列是类别型的。代码清单2-2为针对“岩石vs.水雷”数据集完成上述分析的代码。代码依次检查每一列,确定数值型(整型或浮点型)的条目数量、非空字符串的条目数量、内容为空的条目数量。分析的结果是:前60列都是数值型,最后一列都是字符串。这些字符串值是标签。通常类别型变量用字符串表示,如此例所示。在某些情况下,二值类别变量可以表示成0和1。
代码清单2-2 确定每个属性的特征-rockVmineContents.py
(输出:outputRocksVMinesContents.txt)
author = 'mike_bowles'
import urllib2
import sys
read data from uci data repository
target_url = ("https://archive.ics.uci.edu/ml/machine-learning-"
"databases/undocumented/connectionist-bench/sonar/sonar.all-data")
data = urllib2.urlopen(target_url)
arrange data into list for labels and list of lists for attributes
xList = []
labels = []
for line in data:
#split on comma
row = line.strip().split(",")
xList.append(row)
nrow = len(xList)
ncol = len(xList[1])
type = [0]*3
colCounts = []
for col in range(ncol):
for row in xList:
try:
a = float(row[col])
if isinstance(a, float):
type[0] += 1
except ValueError:
if len(row[col]) > 0:
type[1] += 1
else:
type[2] += 1
colCounts.append(type)
type = [0]*3
sys.stdout.write("Col#" + 't' + "Number" + 't' +
"Strings" + '\t ' + "Other\n")
iCol = 0
for types in colCounts:
sys.stdout.write(str(iCol) + '\t\t' + str(types[0]) + '\t\t' +
str(types[1]) + '\t\t' + str(types[2]) + "\n")
iCol += 1
Output:
Col# Number Strings Other
0 208 0 0
1 208 0 0
2 208 0 0
3 208 0 0
4 208 0 0
5 208 0 0
6 208 0 0
7 208 0 0
8 208 0 0
9 208 0 0
10 208 0 0
11 208 0 0
. . . .
. . . .
. . . .
54 208 0 0
55 208 0 0
56 208 0 0
57 208 0 0
58 208 0 0
59 208 0 0
60 0 208 0`
2.2.2 “岩石vs.水雷”数据集统计特征
确定哪些属性是类别型,哪些是数值型之后,下一步就是获得数值型属性的描述性统计信息和类别型属性具体类别的数量分布。代码清单2-3为这两个处理过程的实例代码。
代码清单2-3 数值型和类别型属性的统计信息-rVMSummaryStats.py
(输出:outputSummaryStats.txt)
__author__ = 'mike_bowles'
import urllib2
import sys
import numpy as np
#read data from uci data repository
target_url = ("https://archive.ics.uci.edu/ml/machine-learning-"
"databases/undocumented/connectionist-bench/sonar/sonar.all-data")
data = urllib2.urlopen(target_url)
#arrange data into list for labels and list of lists for attributes
xList = []
labels = []
for line in data:
#split on comma
row = line.strip().split(",")
xList.append(row)
nrow = len(xList)
ncol = len(xList[1])
type = [0]*3
colCounts = []
#generate summary statistics for column 3 (e.g.)
col = 3
colData = []
for row in xList:
colData.append(float(row[col]))
colArray = np.array(colData)
colMean = np.mean(colArray)
colsd = np.std(colArray)
sys.stdout.write("Mean = " + '\t' + str(colMean) + '\t\t' +
"Standard Deviation = " + '\t ' + str(colsd) + "\n")
#calculate quantile boundaries
ntiles = 4
percentBdry = []
for i in range(ntiles+1):
percentBdry.append(np.percentile(colArray, i*(100)/ntiles))
sys.stdout.write("\nBoundaries for 4 Equal Percentiles \n")
print(percentBdry)
sys.stdout.write(" \n")
#run again with 10 equal intervals
ntiles = 10
percentBdry = []
for i in range(ntiles+1):
percentBdry.append(np.percentile(colArray, i*(100)/ntiles))
sys.stdout.write("Boundaries for 10 Equal Percentiles \n")
print(percentBdry)
sys.stdout.write(" \n")
#The last column contains categorical variables
col = 60
colData = []
for row in xList:
colData.append(row[col])
unique = set(colData)
sys.stdout.write("Unique Label Values \n")
print(unique)
#count up the number of elements having each value
catDict = dict(zip(list(unique),range(len(unique))))
catCount = [0]*2
for elt in colData:
catCount[catDict[elt]] += 1
sys.stdout.write("\nCounts for Each Value of Categorical Label \n")
print(list(unique))
print(catCount)
Output:
Mean = 0.053892307 Standard Deviation = 0.046415983
Boundaries for 4 Equal Percentiles
[0.0057999999999999996, 0.024375000000000001, 0.044049999999999999,
0.064500000000000002, 0.4264]
Boundaries for 10 Equal Percentiles
[0.00579999999999, 0.0141, 0.022740000000, 0.0278699999999,
0.0362200000000, 0.0440499999999, 0.050719999999, 0.0599599999999,
0.0779400000000, 0.10836, 0.4264]
Unique Label Values
set(['R', 'M'])
Counts for Each Value of Categorical Label
['R', 'M']
[97, 111]```
代码第一部分读取数值型数据的某一列,然后产生它的统计信息。第一步计算此属性的均值和方差。了解这些统计信息可以加强在建立预测模型时的直观感受。
第二部分代码主要是为了找到异常值。基本过程如下:假设在下面数值列表[0.1,0.15,0.2,0.25,0.3,0.35,0.4,4]中确定是否有异常值,显然最后一个数“4”是异常值。
发现这种异常值的一种方法是:将一组数字按照百分位数进行划分。例如,第25百分位数是含有最小的25%的数,第50百分位数是含有最小的50%的数。把这种分组可视化最简单的方法是假想把这些数据按顺序排列。上述的例子已经按顺序排好,这样就可以很容易地看到百分位数的边界。一些经常用到的百分位数通常被赋予特殊的名字。将数组按照1/4、1/5、1/10划分的百分位数通常分别叫作四分位数(quartiles,按顺序排列的一组数据被划分为4个相等部分的分割点的数值)、五分位数(quintiles)和十分位数(deciles)。
上述的数组很容易定义出四分位数,因为此数组已按顺序排好,共有8个元素。第一个四分位数含有0.1和0.15,以下的以此类推。可以注意到这些四分位数的跨度。第一个是0.05(0.15~0.1)。第二个四分位数的跨度也大致相同。然而最后一个四分位数的跨度却是3.6,这个是其他四分位数跨度的几十倍。
代码清单2-3中四分位数边界的计算过程与之类似。程序计算四分位数,然后显示最后一个四分位数的跨度要比其他的宽很多。为了更加准确,又计算了十分位数,同样证明了最后一个十分位数的跨度要远远大于其他的十分位数。有些情况下的最后一个分位数变宽是正常的,因为通常数据的分布在尾部会变稀疏。
####2.2.3 用分位数图展示异常点
更具体地研究异常点(异常值)的一个方法就是画出数据的分布图,然后与可能的分布进行比较,判断相关的数据是否匹配。代码清单2-4展示如何使用Python的probplot函数来帮助确认数据中是否含有异常点。分布图展示了数据的百分位边界与高斯分布的同样百分位的边界对比。如果此数据服从高斯分布,则画出来的点应该是一条直线。来自“岩石vs.水雷”数据集的第4列(第4属性)的一些点远离这条直线,如图2-1所示。这说明此数据集尾部的数据要多于高斯分布尾部的数据。
代码清单2-4 “岩石vs. 水雷”数据集的第4列的分位数图-qqplotAttribute.py
author = 'mike bowles'
import numpy as np
import pylab
import scipy.stats as stats
import urllib2
import sys
target_url = ("https://archive.ics.uci.edu/ml/machine-learning-"
"databases/undocumented/connectionist-bench/sonar/sonar.all-data")
data = urllib2.urlopen(target_url)
arrange data into list for labels and list of lists for attributes
xList = []
labels = []
for line in data:
#split on comma
row = line.strip().split(",")
xList.append(row)
nrow = len(xList)
ncol = len(xList[1])
type = [0]*3
colCounts = []
generate summary statistics for column 3 (e.g.)
col = 3
colData = []
for row in xList:
colData.append(float(row[col]))
stats.probplot(colData, dist="norm", plot=pylab)
pylab.show()
<div style="text-align: center"><img src="https://yqfile.alicdn.com/55c85d987d43d66785f4db1ea559518b0e0a6369.png" width="" height="">
</div>
那么如何利用这些信息?异常点在建模或预测期间都会带来麻烦。基于此数据集训练完一个模型后,可以查看此模型预测错误的情况,然后确认此错误是否与这些异常点有关。如果确实是这样的话,可以采取步骤进行校正。例如,可以复制这些预测模型表现不好的例子,以加强这些例子在数据集中的比重。也可以把这些不好的例子分离出来,然后单独训练。如果认为预测模型在真正部署时不会遇到此类异常数据,则也可以把这些例子排除出数据集。一个可行办法是在对数据集进行探究阶段,先产生四分位数边界,然后看看潜在的异常点的规模对后续建模及预测可能的影响。这样在分析错误时,可以通过分位数图(quantile-quantile,Q-Q)确定哪些数据可以称为异常点。
####2.2.4 类别属性的统计特征
上述的分析过程只适用于数值属性。那么类别属性呢?你可能想知道一共可以分为几类、每类数据的数目。想获得这些信息主要是基于以下原因:性别属性有两个值(男、女),但是如果属性是美国的州,则有50个可能的值。随着属性数目的增加,处理的复杂度也在增加。绝大多数二元决策树算法(集成方法的基础)对于其可以处理的类别数是有限制的。由Breiman和Cutler(此算法的发明人)写的流行的随机森林算法包支持32个类别。如果一个属性超过32个类别,则需要合并。
有时在训练过程中会随机抽取数据集的一个子集,然后在此子集上训练一系列的模型。例如,如果类别属性就是美国的州,其中爱达荷州只出现了两次。一个随机抽取的训练用数据子集中很可能不含有爱达荷州的样本。你需要在这些问题发生前就预见到可能会出现这样的情况,然后再着手进行处理。以两个爱达荷州的样本为例,可以把它与蒙大纳州或怀俄明州合并,也复制这两个样本(增加其所占的比例),或者控制随机取样保证抽取到含有爱达荷州的样本,这个过程叫作分层抽样(stratified sampling)。
####2.2.5 利用Python Pandas对“岩石vs.水雷”数据集进行统计分析
Python Pandas工具包可以帮助自动化数据统计分析的过程,已经被证实在数据预处理阶段特别有用。Pandas工具包可以将数据读入一种特定的数据结构,叫作数据框(data frame)。数据框是依据CRAN-R数据结构建模的。
注意
Pandas工具包的安装可能会有困难,主要原因是它有一系列的依赖,每个依赖必须安装正确的版本,而且相互之间要匹配,或者诸如此类的问题。绕过此类障碍的一个简单的方法就是直接安装Anaconda Python Distribution分发包,此分发包可以直接从Continuum Analytics处下载。安装过程十分简单,只要按指令依次进行就可以安装好数据分析、机器学习所需的大量软件包。
你可以把数据框当成一个表格或者类似矩阵的数据结构,如表2-1所示。数据框定义行代表一个实例(一次实验、一个例子、一次测量等),列代表一个特定的属性。此结构像矩阵,但又不是矩阵,因为每列的元素很可能是不同类型的。形式上矩阵里的所有元素都是来自一个域的(如实数、二进制数、复数等)。但对于统计学来说,矩阵的限制太严格了,因为统计方面的一个样本往往是多个不同类型的值的混合。
表2-1样例中的第1个属性列是实数,第两个属性列是类别变量(属性),第3个属性列是整数。在一个列内,所有元素的取值都是同一类型,但是列与列之间是不同的。通过数据框,可以通过索引(index)的方式访问具体某个元素,类似Python中访问一个Numpy数组或二维数组中的元素(element)。类似地,采用索引切片(index slicing)可以访问整行或整列,而且在Pandas数据框中,可以通过名字来访问行或列。这对于小规模或中等规律的数据是十分方便的(搜索“Pandas introduction”会找到关于使用Pandas的入门指导的链接)。
如何从UC Irvine 数据仓库网站读取“岩石vs.水雷”数据的CSV文件如代码清单2-5所示。这里的输出只是完整输出中的一部分。自行运行代码就可以获得完整输出。
代码清单2-5 用Python Pandas 读入数据、分析数据- pandasReadSummarizer.py
author = 'mike_bowles'
import pandas as pd
from pandas import DataFrame
import matplotlib.pyplot as plot
target_url = ("https://archive.ics.uci.edu/ml/machine-learning-"
"databases/undocumented/connectionist-bench/sonar/sonar.all-data")
read rocks versus mines data into pandas data frame
rocksVMines = pd.read_csv(target_url,header=None, prefix="V")
print head and tail of data frame
print(rocksVMines.head())
print(rocksVMines.tail())
print summary of data frame
summary = rocksVMines.describe()
print(summary)
Output (truncated):
V0 V1 V2 ... V57 V58 V59 V60
0 0.0200 0.0371 0.0428 ... 0.0084 0.0090 0.0032 R
1 0.0453 0.0523 0.0843 ... 0.0049 0.0052 0.0044 R
2 0.0262 0.0582 0.1099 ... 0.0164 0.0095 0.0078 R
3 0.0100 0.0171 0.0623 ... 0.0044 0.0040 0.0117 R
4 0.0762 0.0666 0.0481 ... 0.0048 0.0107 0.0094 R
[5 rows x 61 columns]
V0 V1 V2 ... V57 V58 V59 V60
203 0.0187 0.0346 0.0168 ... 0.0115 0.0193 0.0157 M
204 0.0323 0.0101 0.0298 ... 0.0032 0.0062 0.0067 M
205 0.0522 0.0437 0.0180 ... 0.0138 0.0077 0.0031 M
206 0.0303 0.0353 0.0490 ... 0.0079 0.0036 0.0048 M
207 0.0260 0.0363 0.0136 ... 0.0036 0.0061 0.0115 M
[5 rows x 61 columns]
V0 V1 ... V58 V59
count 208.000000 208.000000 ... 208.000000 208.000000
mean 0.029164 0.038437 ... 0.007941 0.006507
std 0.022991 0.032960 ... 0.006181 0.005031
min 0.001500 0.000600 ... 0.000100 0.000600
25% 0.013350 0.016450 ... 0.003675 0.003100
50% 0.022800 0.030800 ... 0.006400 0.005300
75% 0.035550 0.047950 ... 0.010325 0.008525
max 0.137100 0.233900 ... 0.036400 0.043900
读入数据后,程序第一部分首先打印头数据和尾数据。注意到所有的头数据都有R标签,所有的尾数据都有M标签。对于这个数据集,第一部分是R标签的(岩石),第二部分是M标签的(水雷)。在分析数据时首先要注意到此类信息。在后续章节中会看到,确定模型的优劣有时需要对数据进行取样。那么取样就需要考虑到数据的存储结构。最后的代码打印输出实数属性列的统计信息。