Numpy常用操作

numpy简介

Python中用列表(list)可以用来当作数组使用,不过由于列表的元素可以是任何对象,因此列表中所保存的是对象的指针。这样为了保存一个简单的[1,2,3],需要有3个指针和三个整数对象。对于数值运算来说这种结构显然比较浪费内存和CPU计算时间。
此外python还提供了一个array模块,array对象和列表不同,它直接保存数值,和C语言的一维数组比较类似。但是由于它不支持多维,也没有各种运算函数,因此也不适合做数值运算。

NumPy的诞生弥补了这些不足,NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则是能够对数组进行处理的函数。Numpy内部运算是通过C语言实现的,所以性能方面也很不错。

1、生成numpy的ndarray的几种方式

numpy提供了ndarray和matrix两种类型的数据,numpy的二维数组能够很好地实现矩阵的各种功能,而且比matrix要灵活,速度也更快。
numpy有ndarray和matix,性能方面ndarray优于matix,所以一般使用ndarray。
我们可以通过给array函数传递Python的序列对象创建数组,如果传递的是多层嵌套的序列,将创建多维数组。
(1)创建numpy的array几种方法

import numpy as np
a = np.array([1, 2, 3, 4])
b = np.array((5, 6, 7, 8))
##b的结果如下
array([5, 6, 7, 8])
c = np.array([[1, 2, 3, 4],[4, 5, 6, 7], [7, 8, 9, 10]])
##c的结果如下
array([[ 1, 2, 3, 4],
[ 4, 5, 6, 7],
[ 7, 8, 9, 10]])

(2)利用numpy内置函数创建

zeros = np.zeros((2,2)) ###生成一个全为0的数组或矩阵
结果为:
array([[ 0., 0.],
[ 0., 0.]])

e = np.eye(2) ###生成一个单位矩阵
##结果如下:
array([[ 1., 0.],
[ 0., 1.]])

(3) 数组的大小可以通过其shape属性获得

c.ndim ##获取数组或矩阵维数
c.shape ##获取矩阵各维长度
c.shape[0] ##获取矩阵行数,shape[1]获取矩阵的列数
c.size ##获取矩阵的元素个数
c.dtype ##查看数组数据类型

(4)修改其形状
数组c的shape有两个元素,因此它是二维数组,其中第0轴的长度为3,第1轴的长度为4。还可以通过修改数组的shape属性,在保持数组元素个数不变的情况下,改变数组每个轴的长度。

c.reshape(4,3) ##把原来3x4矩阵,转换为4x3矩阵
##结果为:array([[ 1, 2, 3],
[ 4, 4, 5],
[ 6, 7, 7],
[ 8, 9, 10]])
c.reshape(2,-1) ###当某个轴的元素为-1时,将根据数组元素的个数自动计算 ###此轴的长度

(5)arange函数类似于python的range函数,通过指定开始值、终值和步长来创建一维数组,注意数组不包括终值:
np.arange(0,1,0.1)
##结果为:array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

2、存取元素

(1)数组元素的存取方法和Python的标准方法相同

a = np.arange(10)
a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a[5] # 用整数作为下标可以获取数组中的某个元素
5
a[3:5] # 用范围作为下标获取数组的一个切片,包括a[3]不包括a[5]
##结果为:array([3, 4])
a[:-1] # 下标可以使用负数,表示从数组后往前数
结果为:array([0, 1, 2, 3, 4, 5, 6, 7, 8])
a[2:4] = 100,101 # 下标还可以用来修改元素的值
a
#结果为:array([ 0, 1, 40, 41, 4, 5, 6, 7, 8, 9])
a[1:-1:2] # 范围中的第三个参数表示步长,2表示隔一个元素取一个元素
#结果为:array([ 1, 41, 5, 7])

(2) 和Python的列表序列不同,通过下标范围获取的新的数组是原始数组的一个视图。它与原始数组共享同一块数据空间。

b = a[3:7] # 通过下标范围产生一个新的数组b,b和a共享同一块数据空间
b[2] = -10 # 将b的第2个元素修改为-10
#a结果如下
array([ 0, 1, 40, 41, 4, -10, 6, 7, 8, 9])
#b的结果如下
array([ 41, 4, -10, 6])

3、多维数组

创建一个6x6的多维数组或矩阵

a = np.arange(0, 60, 10).reshape(-1, 1) + np.arange(0, 6)
#运行结果如下:
array([[ 0, 1, 2, 3, 4, 5],
[10, 11, 12, 13, 14, 15],
[20, 21, 22, 23, 24, 25],
[30, 31, 32, 33, 34, 35],
[40, 41, 42, 43, 44, 45],
[50, 51, 52, 53, 54, 55]])

a[3:, [0, 2, 5]]
##下标中的第0轴是一个范围,它选取第3行之后的所有行; ##第1轴是整数序列,它选取第0, 2, 5三列
#运行结果为:
array([[30, 32, 35],
[40, 42, 45],
[50, 52, 55]])

a[2::2,::2]
##第0轴,从第2行开始,步长为2;第1轴,从第0行开始,步长为2
##运行结果为:
array([[20, 22, 24],
[40, 42, 44]])

4、矩阵操作

(1)以下介绍矩阵操作,包括两个矩阵间操作如矩阵乘法等。

A = np.array([[1, 2], [-1, 4]])
B = np.array([[2, 0], [3, 4]])
###对应元素相乘
A*B
##结果如下:
array([[ 2, 0],
[-3, 16]])

####矩阵乘法
np.dot(A, B) # 或者 A.dot(B)
##运行结果如下
array([[ 8, 8],
[10, 16]])

(2)线性代数运算
对矩阵进行线性代数运行,如求转置、特征向量等。求A的转置

##求A的转置
A.transpose() ##或A.T
##运行结果
array([[ 1, -1],
[ 2, 4]])

## 求A的逆矩阵
linalg.inv(A)
##运行结果
array([[ 0.66666667, -0.33333333],
[ 0.16666667, 0.16666667]])

# 求A的特征值和特征向量
eigenvalues, eigenvectors = linalg.eig(A)
##其中eigenvalues为 特征值,eigenvectors为特征向量

(3)调整坐标顺序
transpose的参数为坐标,正常顺序为(0, 1, 2, ... , n - 1),
现在传入的为(1, 0)代表C[x][y]转换为C[y][x],第0个和第1个坐标互换。

C.transpose((1,0)) ###第0个和第1个坐标互换
##结果如下:
array([[ 0.68752896, -0.11705268, 0.49078462, -0.48826679, -1.26943352,
-0.97029925],
[ 1.01686837, -1.55073073, -1.40240593, -0.98632156, 0.80378005,
0.33703986],
[ 0.95644284, -0.19360605, 1.82482162, -0.45383782, 0.26197213,
0.9131711 ]])

(4)在矩阵或数组上运用数学和统计方法

import numpy as np
import numpy.random as np_random

print('求和,求平均')
arr = np.random.randn(5, 4)
print(arr)
print(arr.mean())
print(arr.sum())
print(arr.mean(axis = 1)) # 对每一行的元素求平均
print(arr.sum(0)) # 对每一列元素求和,axis可以省略。

(5)向量或矩阵运算与循环运算性能比较

import time as tm  
import numpy as np
 
dim = 100000#数据长度(包含的元素个数)  
x1 = np.ones(dim)  
x2 = np.ones(dim)  
yFor = np.ones(dim)  

tStart = tm.clock()#开始计时  
#for循环解算x1*x2(对应元素相乘)  
for i in range(dim):  
    yFor[i] = x1[i]*x2[i]  
tEnd=tm.clock()#停止计时  
tFor = tEnd-tStart#计算用时  

tStart = tm.clock()#开始计时  
#向量计算x1*x2(对应元素相乘)  
yVector = x1*x2  
tEnd = tm.clock()#停止计时  
tVector = tEnd-tStart#计算用时  

print ('for循环用时tFor=',tFor)  
print ('向量运算用时tVector=',tVector)

5、 数据合拼与拆分

import numpy as np
import numpy.random as np_random

print('连接两个二维数组')
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
print(np.concatenate([arr1, arr2], axis = 0)) # 按行连接
print(np.concatenate([arr1, arr2], axis = 1)) # 按列连接

##或通过以下命令进行连接,效果一样
print('垂直stack与水平stack')
print(np.vstack((arr1, arr2))) # 垂直堆叠
print(np.hstack((arr1, arr2))) # 水平堆叠

6、numpy上的通用函数(ufunc)

ufunc是universal function的缩写,它是一种能对数组的每个元素进行操作的函数。NumPy内置的许多ufunc函数都是在c语言级别实现的,因此它们的计算速度非常快。让我们来看一个例子:

import time
import math
import numpy as np

x = [i * 0.001 for i in np.arange(1000000)]
start = time.clock()
for i, t in enumerate(x):
x[i] = math.sin(t)
print ("math.sin:", time.clock() - start )

x = [i * 0.001 for i in np.arange(1000000)]
x = np.array(x)
start = time.clock()
np.sin(x)
print ("numpy.sin:", time.clock() - start )

运行结果如下:
math.sin: 0.5793389999999974
numpy.sin: 0.06916299999999964
说明,numpy.sin比math.sin快10倍多。这得利于numpy.sin在C语言级别的循环计算。

利用Python抓取并分析京东商品评论数据

2.1 内容简介
本章主要介绍如何利用Python抓取京东商城商品评论信息,并对这些评论信息进行分析和可视化。下面是要抓取的商品信息,一款女士文胸。这个商品共有红色,黑色和肤色等颜色, 70B到90D共18个尺寸,以及超过500条的购买评论。
2.2 获取页面源码信息
京东商品评论信息是由JS动态加载的,所以直接抓取商品详情页的URL并不能获得商品评论的信息。因此我们需要先找到存放商品评论信息的文件。这里我们使用Chrome浏览器里的开发者工具进行查找。
具体方法是在商品详情页(请参考图2-1)点击鼠标右键,选择检查(请参考图2-2),在弹出的开发者工具界面(可参考图16-3)中选择Network,设置为禁用缓存(Disable cache)和只查看JS文件。然后刷新页面。页面加载完成后向下滚动鼠标找到商品评价部分,等商品评价信息显示出来后,在下面Network界面的左侧筛选框中输入productPageComments,这时下面的加载记录中只有一条信息,这里包含的就是商品详情页的商品评论信息。点击这条信息,在右侧的Preview界面中可以看到其中包含了当前页面中的评论信息。

图2-1 商品详情页

图2-2 检查详情页面

图2-3 开发者工具页面

复制这条信息,并把URL地址放在浏览器中打开,里面包含了当前页的商品评论信息。这就是我们要抓取的URL地址。https://club.jd.com/comment/productPageComments.action?callback=fetchJSON_comment98vv8&productId=10809260839&score=0&sortType=5&page=0&pageSize=10&isShadowSku=0&fold=1
仔细观察这条URL地址可以发现,其中productId=10809260839是当前商品的商品ID。与商品详情页URL中的ID一致。而page=0是页码。如果我们要获取这个商品的所有评论,只需要更改page后面的数字即可。
在获得了商品评论的真实地址以及URL地址的规律后,我们开始使用python抓取这件商品的500+条评论信息。并对这些信息进行处理和分析。
2.3 抓取信息前的准备工作
设置完请求的头文件和Cookie信息后,我们开始抓取京东商品评论的信息。在URL中包含两个重要的信息,一个是商品ID,另一个是页码。这里我们只抓取一个商品的评论信息,因此商品ID不需要更改。但这个商品的评论有500+条,也就是有近40页需要抓取,因此页码不是一个固定值,需要在0-40之间变化。这里我们将URL分成两部分,通过随机生成页码然后拼接URL的方式进行抓取。
导入必要的库

#为显示中文图标
%matplotlib inline
import matplotlib.font_manager as fm
myfont = fm.FontProperties(fname='/home/hadoop/anaconda3/lib/python3.6/site-packages/matplotlib/mpl-data/fonts/ttf/simhei.ttf')
#导入requests库(请求和页面抓取)
import requests
#导入time库(设置抓取Sleep时间)
import time
#导入random库(生成乱序随机数)
import random
#导入正则库(从页面代码中提取信息)
import re
#导入数值计算库(常规计算)
import numpy as np
#导入科学计算库(拼表及各种分析汇总)
import pandas as pd
#导入绘制图表库(数据可视化)
import matplotlib.pyplot as plt
#导入结巴分词库(分词)
import jieba as jb
#导入结巴分词(关键词提取)
import jieba.analyse

2.4将爬虫伪装成浏览器
导入完库文件后,还不能直接进行抓取,因为这样很容易被封。我们还需要对爬虫进行伪装,是爬虫看起来更像是来自浏览器的访问。这里主要的两个工作是设置请求中的头文件信息以及设置Cookie的内容。
头文件信息很容易找到,在Chrome的开发者工具中选择Network,刷新页面后选择Headers就可以看到本次访问的头文件信息,里面包含了一些浏览器的技术参数和引荐来源信息。将这些信息直接添加到代码中就可以,这里我们将头部信息保存在headers中。

#设置请求中头文件的信息
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html;q=0.9,*/*;q=0.8',
'Accept-Charset':'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
'Connection':'close',
'Referer':'https://item.jd.com/10809260821.html'
}

在查看头文件信息的旁边还有一个Cookies标签(如图16-3),点击进去就是本次访问的Cookies信息。这里的Cookies信息与前面头文件中的Cookie信息一致,不过这里更加清晰。把Request Cookies信息复制到代码中即可,这里我们将Request Cookies信息保存在Cookie中。

#设置Cookie的内容
cookie={'TrackID':'1mJoJegxxximdOIuMj1L78NM9IEUYBloQE8lNf5Kr0SN4bLXqWbNQGsuWLT7VSxXgBrnuOwGj9xdFUbqz1sLwrpxkzkjTA-HSsgVP9iJhv-g',
'__jda':'122270672.413567069.1502329865.1505359716.1505377343.17',
'__jdb':'122270672.4.413567069|17.1505377343',
'__jdc':'122270672',
'__jdu':'413567069',
'__jdv':'122270672|p.egou.com|t_36378_879430_c|tuiguang|5191ffe38de346c28750ae3309faf11a|1505288084897',
'areaId':'2',
'cn':'17',
'ipLoc-djd':'2-2830-51811-0.137557061',
'ipLocation':'%u4E0A%u6D77',
'mx':'0_X',
'rkv':'V0800',
'user-key':'acce01e8-1533-4aaa-bcea-34ac9b6d6a53',
'xtest':'4657.553.d9798cdf31c02d86b8b81cc119d94836.b7a782741f667201b54880c925faec4b'}

2.5抓取商品评论信息
设置完请求的头文件和Cookie信息后,我们开始抓取京东商品评论的信息。前面分析URL的时候说过,URL中包含两个重要的信息,一个是商品ID,另一个是页码。这里我们只抓取一个商品的评论信息,因此商品ID不需要更改。但这个商品的评论有500+条,也就是有近40页需要抓取,因此页码不是一个固定值,需要在0-40之间变化。这里我们将URL分成两部分,通过随机生成页码然后拼接URL的方式进行抓取。

#设置URL的第一部分
url1='https://club.jd.com/comment/productPageComments.action?callback=fetchJSON_comment98vv0&productId=10809260821&score=0&sortType=5&page='
#设置URL的第二部分
url2='&pageSize=10&isShadowSku=0&fold=1'
#乱序输出0-40的唯一随机数
ran_num=random.sample(range(40), 40)

为了使抓取过程看起来更加随机,我们没有从第1页一直抓取到第40页。而是使用random生成0-40的唯一随机数,也就是要抓取的页码编号。然后再将页码编号与两部分URL进行拼接。这里我们只知道商品有500+的评论,但并不知道具体数字,所以抓取范围定位从0-40页。
下面是具体的抓取过程,使用for循环每次从0-40的随机数中找一个生成页码编号,与两部分的URL进行拼接。生成要抓取的URL地址并与前面设置好的头文件信息和Cookie信息一起发送请求获取页面信息。将获取到的页面信息进行汇总。每次请求间休息5秒针,避免过于频繁的请求导致返回空值。

#拼接URL并乱序循环抓取页面
for i in ran_num:
a = ran_num[0]
if i == a:
i=str(i)
url=(url1+i+url2)
r=requests.get(url=url,headers=headers,cookies=cookie)
html=r.content
else:
i=str(i)
url=(url1+i+url2)
r=requests.get(url=url,headers=headers,cookies=cookie)
html2=r.content
html = html + html2
time.sleep(5)
print("当前抓取页面:",url,"状态:",r)

在抓取的过程中输出每一步抓取的页面URL以及状态。通过下面的截图可以看到,在page参数后面的页码是随机生成的并不连续。抓取完40个页面后,我们还需要对页面进行编码。完成编码后就可以看到其中所包含的中文评论信息了。后面大部分工作就是要对这些评论信息进行不断提取和反复的清洗。

#对抓取的页面进行编码
html=str(html, encoding = "GBK")

这里建议将抓取完的数据存储在本地,后续工作可以直接从本地打开文件进行清洗和分析工作。避免每次都要重新抓取数据。这里我们将数据保存在桌面的jd_page.txt文件中。

#将编码后的页面输出为txt文本存储
file = open("./jd_page.txt", "w")
file.write(html)
file.close()
#读取存储的txt文本文件
html = open('./jd_page.txt', 'r').read()

2.6提取信息并进行数据清洗
京东的商品评论中包含了很多有用的信息,我们需要将这些信息从页面代码中提取出来,
整理成数据表以便进行后续的分析工作。这里应该就是整个过程中最苦逼的数据提取和清洗工作了。
我们使用正则对每个字段进行提取。对于特殊的字段在通过替换等方式进行提取和清洗。
下面是提取的第一个字段userClient,也就是用户发布评论时所使用的设备类型,这类的字段提取还比较简单,
一行代码搞定。查看一下提取出来的字段还比较干净。使用同样的方法我们分别提取了以下这些字段的内容。

#使用正则提取userClient字段信息
userClient=re.findall(r',"usefulVoteCount".*?,"userClientShow":(.*?),',html)
#使用正则提取userLevel字段信息
userLevel=re.findall(r'"referenceImage".*?,"userLevelName":(.*?),',html)
#使用正则提取productColor字段信息
productColor=re.findall(r'"creationTime".*?,"productColor":(.*?),',html)
#使用正则提取recommend字段信息
recommend=re.findall(r'"creationTime".*?,"recommend":(.*?),',html)
#使用正则提取nickname字段信息
nickname=re.findall(r'"creationTime".*?,"nickname":(.*?),',html)
#使用正则提取userProvince字段信息
userProvince=re.findall(r'"referenceImage".*?,"userProvince":(.*?),',html)
#使用正则提取usefulVoteCount字段信息
usefulVoteCount=re.findall(r'"referenceImage".*?,"usefulVoteCount":(.*?),',html)
#使用正则提取days字段信息
days=re.findall(r'"usefulVoteCount".*?,"days":(.*?)}',html)
#使用正则提取score字段信息
score=re.findall(r'"referenceImage".*?,"score":(.*?),',html)
#使用正则提取isMobile字段信息
isMobile=re.findall(r'"usefulVoteCount".*?,"isMobile":(.*?),',html)

使用for循环将字段中所有的}替换为空。替换完成后字段看起来干净多了。

#替换掉最后的}
mobile=[]
for m in isMobile:
n=m.replace('}','')
mobile.append(n)

productSize字段中包含了胸围和杯罩两类信息,为了获得独立的杯罩信息需要进行二次提取,将杯罩信息单独保存出来。

#使用正则提取productSize字段信息
productSize=re.findall(r'"creationTime".*?,"productSize":(.*?),',html)
#使用for循环将productSize中的第三个字符杯罩信息提取出来,并保持在cup字段中。
#提取杯罩信息
cup=[]
for s in productSize:
s1=s[3]
cup.append(s1)
##提取天数
days1=[]
for d in table['days']:
s1=d[0][0]
s1=int(s1)
days1.append(s1)

创建评论的日期信息仅依靠正则提取出来的信息还是比较乱,无法直接使用。因此也需要进行二次提取。下面是使用正则提取出的结果。

#使用正则提取时间字段信息
creationTime1=re.findall(r'"creationTime":(.*?),"referenceName',html)

日期和时间信息处于前20个字符,在二次提取中根据这个规律直接提起每个条目的前20个字符即可。将日期和时间单独保存为creationTime。

#提取日期和时间
creationTime=[]
for d in creationTime1:
date=d[1:20]
creationTime.append(date)

在上一步日期和时间的基础上,我们再进一步提取出单独的小时信息,方法与前面类似,提取日期时间中的第11和12个字符,就是小时的信息。提取完保存在hour字段以便后续的分析和汇总工作。

#提取小时信息
hour=[]
for h in creationTime:
date=h[10:13]
hour.append(date)

最后要提取的是评论内容信息,页面代码中包含图片的评论信息是重复的,因此在使用正则提取完后还需要对评论信息进行去重。

#使用正则提取评论信息
content=re.findall(r'"guid".*?,"content":(.*?),',html)
#使用if进行判断,排除掉所有包含图片的评论信息,已达到评论去重的目的。

#对提取的评论信息进行去重
content_1=[]
for i in content:
if not "img" in i:
content_1.append(i)

完成所有字段信息的提取和清洗后,将这些字段组合在一起生成京东商品评论数据汇总表。下面是创建数据表的代码。数据表生成后还不能马上使用,需要对字段进行格式设置,例如时间和日期字段和一些包含数值的字段。具体的字段和格式设置依据后续的分析过程和目的。这里我们将creationTime设置为时间格式,并设置为数据表的索引列。将days字段设置为数值格式。

#将前面提取的各字段信息汇总为table数据表,以便后面分析
table=pd.DataFrame({'creationTime':creationTime,'hour':hour,'nickname':nickname,'productColor':productColor,'productSize':productSize,'cup':cup,'recommend':recommend,'mobile':mobile,'userClient':userClient,'userLevel':userLevel,'userProvince':userProvince,'usefulVoteCount':usefulVoteCount,'content_1':content_1,'days':days1,'score':score})
#将creationTime字段更改为时间格式
table['creationTime']=pd.to_datetime(table['creationTime'])
#设置creationTime字段为索引列
table = table.set_index('creationTime')
#设置days字段为数值格式
#table['days']=table['days'].astype(np.int64)

查看通过数据清理后的数据

#查看整理完的数据表
table.head()


保存清洗和预处理完的数据表。我们这里将数据表保存为csv格式。到了这一步可以选择在Excel
中完成后续的数据分析和可视化过程,也可以继续在python中完成。我们这里选择继续在python中完成后续的数据分析和可视化工作。

#保存table数据表
table.to_csv('./jd_table.csv')

2.7 数据分析及可视化
2.7.1分月评论数据变化趋势
首先查看京东商品评论的时间变化趋势情况,大部分用户在购买商品后会在10天以内进行评论,因此我们可以近似的认为在一个月的时间维度中评论时间的变化趋势代表了用户购买商品的变化趋势。按月的维度对数据表进行汇总,并提取每个月的nickname的数量。下面是具体的代码和分月数据。

#对数据表按月进行汇总并生成新的月度汇总数据表
table_month=table.resample('M',how=len)
#提取按月汇总的nickname
month=table_month['nickname']

数据范围从2016年06月到2017年08月。使用柱状图对分月数据进行可视化。从图表中可以看到2016年10月是评论的高峰,也可以近似的认为这个时间段是用户购买该商品的高峰(10月18日是京东促销活动)。排除2016年6月和11月数据,整齐趋势中夏、冬季评论量较高,夏季较底。这是由于该商品的季节属性导致的。

#绘制分月评论数量变化趋势图
plt.rc('font', family='SimHei', size=9)
a=np.array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])
plt.bar([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],month,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('月份',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('分月评论数量变化趋势',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.xticks(a,('16-06','07','08','09','10','11','12','17-01','02','03','04','05','06','07','08'))
plt.show()


通过筛选将数据表分为使用移动设备和未使用移动设备两个表格,再分别查看和对比评论变化趋势。

#在table表中筛选使用移动设备的条目并创建新表
mobile_t=table.loc[table["mobile"] == "true"]
#在table中筛选没有使用移动设备的条目并创建新表
mobile_f=table.loc[table["mobile"] == "false"]
#按月汇总使用移动设备的数据
mobile_t_m=mobile_t.resample('M',how=len)
#按月汇总不使用移动设备的数据
mobile_f_m=mobile_f.resample('M',how=len)
#提取使用移动设备的按月汇总nickname
mobile_y=mobile_t_m['nickname']
#提取没有使用移动设备的按月汇总nickname
mobile_n=mobile_f_m['nickname']

从结果中可以看出使用PC设备进行评论的用户在所有的时间段中都要稍高于使用移动设别的用户。

plt.subplot(2, 1, 1)
plt.plot(mobile_y,'go',mobile_y,'g-',color='#99CC01',linewidth=3,markeredgewidth=3,markeredgecolor='#99CC01',alpha=0.8)
plt.ylabel('移动设备评论数量',fontproperties=myfont,size=12)
plt.title('PC与移动设备评论数量变化趋势',fontproperties=myfont,size=12)
plt.subplot(2, 1, 2)
plt.plot(mobile_n,'go',mobile_n,'g-',color='#99CC01',linewidth=3,markeredgewidth=3,markeredgecolor='#99CC01',alpha=0.8)
plt.xlabel('月份',fontproperties=myfont,size=12)
plt.ylabel('PC评论数量',fontproperties=myfont,size=12)
plt.show()


2.7.2 24小时评论数量变化趋势
按小时维度对评论数据进行汇总,查看用户在24小时中的评论变化趋势。这里需要说明的是24小时趋势只能反映用户登录京东商城的趋势,并不能近似推断用户购买商品的时间趋势。

#按24小时分别对table表中的nickname进行计数
hour_group=table.groupby('hour')['nickname'].agg(len)

从24小时评论趋势图来看,发布商品评论的趋势与作息时间一致,并且每日的闲暇时间是发布评论的高峰。如早上的9点,15点和晚上的21点,是一天24小时中的三个评论高峰点。

#汇总24小时评论数量变化趋势图
plt.rc('font', family='STXihei', size=9)
a=np.array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21])
plt.bar([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21],hour_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('24小时',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('24小时评论数量变化趋势',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.xticks(a,('0','1','2','5','7','8','9','10','11','12''13','14','15','16','17','18','19','20','21','22','23'))
plt.show()


将24小时的评论数量分为移动设备和未使用移动设备,查看并对比这两者的变化趋势情况。

#在使用移动设备的表中按24小时对nickname进行计数
mobile_y_h=mobile_t.groupby('hour')['nickname'].agg(len)
#在没有使用移动设备的表中按24小时对nickname进行计算
mobile_n_h=mobile_f.groupby('hour')['nickname'].agg(len)
移动设备的评论数量在24小时中的各个时间段与PC的评论数量相当,并且在9点及晚间8点左右比较活跃。
#汇总PC与移动设备24小时评论数量变化趋势
plt.subplot(2, 1, 1)
plt.plot(mobile_y_h,'go',mobile_y_h,'g-',color='#99CC01',linewidth=3,markeredgewidth=3,markeredgecolor='#99CC01',alpha=0.8)
plt.ylabel('移动设备评论数量',fontproperties=myfont,size=12)
plt.title('PC与移动设备24小时评论数量变化趋势',fontproperties=myfont,size=12)
plt.subplot(2, 1, 2)
plt.plot(mobile_n_h,'go',mobile_n_h,'g-',color='#99CC01',linewidth=3,markeredgewidth=3,markeredgecolor='#99CC01',alpha=0.8)
plt.xlabel('24小时',fontproperties=myfont,size=12)
plt.ylabel('PC评论数量',fontproperties=myfont,size=12)
plt.show()


2.7.3 用户客户端分布情况
前面的分析中,我们看到使用移动设备进行评论的用户要远高于PC端的用户,下面我们对用户所使用的设备分布情况进行统计。首先在数据表中按用户设备(userClient)对nickname字段进行计数汇总。

#在table表中按userClient对数据进行汇总
userClient_group=table.groupby('userClient')['nickname'].agg(len)

从用户客户端分布情况来看,移动端的设备占大多数,其中使用iphone的用户要底于Android用户。由于微信购物和QQ购物单独被分了出来,无法确定设备,因此单独进行对比。使用微信购物渠道的用户要底于QQ购物。

#汇总用户客户端分布情况
plt.rc('font', family='STXihei', size=9)
a=np.array([1,2,3,4,5])
plt.bar([1,2,3,4,5],userClient_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('客户端分布',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('用户客户端分布情况',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.ylim(0,300)
plt.xticks(a,('other','Android','iPhone','微信购物','QQ购物'),fontproperties=myfont,size=12)
plt.show()


2.7.4 购买后评论天数分布
在购买后评论天数方面,我们将用户发布评论与购买的时间间隔分为7组,分别为购买后1-5天内,5-10天内,10-15天内,
15-20天内,20-25天内,25-30天内,以及大于30天。然后统计并对比用户在不同时间区间内发布评论的数量情况。

#设置分组条件,并对table表中的days字段进行分组
bins = [0, 5, 10, 15, 20, 25, 30, 92]
day_group = ['5天', '10天', '15天', '20天', '25天','30天','大于30天']
table['day_group'] = pd.cut(table['days'], bins, labels=day_group)
#按新设置的分组对数据进行汇总
days_group=table.groupby('day_group')['nickname'].agg(len)

从图表中看出,购买后5天到10天以内是用户发布评论的高峰,也就我们之前推测评论时间趋势近似于购买时间的依据。随着时间的增加评论数量逐渐下降。

#绘制用户购买后评论天数分布图
plt.rc('font', family='STXihei', size=9)
a=np.array([1,2,3,4,5,6,7])
plt.bar([1,2,3,4,5,6,7],days_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('购买后天数',fontproperties=myfont,size=12)
plt.ylabel('发布评论数量',fontproperties=myfont,size=12)
plt.title('购买后评论天数分布',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.ylim(0,300)
plt.xticks(a,('5天','10天','15天','20天','25天','30天','大于30天'),fontproperties=myfont,size=12)
plt.show()


2.7.5商品评分分布情况
京东商城对商品按5星评分划分为好评,中评和差评三个等级。我们这里来看下用户5星评分的分布情况。
在数据表中score字段中的值表示了用户对胸罩产品的打分情况。我们按打分情况对数据进行汇总。
商品评分分布情况
京东商城对商品按5星评分划分为好评,中评和差评三个等级。我们这里来看下用户5星评分的分布情况。在数据表中score字段中的值表示了用户对胸罩产品的打分情况。我们按打分情况对数据进行汇总。

#在table表中按score对数据进行汇总
score_group=table.groupby('score')['nickname'].agg(len)
#绘制用户评分分布情况图
plt.rc('font', family='STXihei', size=9)
a=np.array([1,3,4,5])
plt.bar([1,3,4,5],score_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('评分分布',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('用户评分分布情况',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='best',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.ylim(0,500)
plt.xticks(a,('1星','3星','4星','5星'),fontproperties=myfont,size=12)
plt.show()


从图表中可以看出,大部分用户对商品的评分是5星。4星以下的几乎没有。但从另一个维度来看,
在用户对最有用评论的投票(usefulVoteCount)中得票最多的是一个1星的评论。

2.7.6 用户胸罩尺码分布情况
在胸罩的尺寸方面包含两个信息,一个是胸围尺寸,另一个是罩杯。我们在前面的清洗过程中对杯罩创建了单独的字段。下面只对这个字段进行汇总统计。

#在table 表中按cup对数据进行汇总
cup_group=table.groupby('cup')['nickname'].agg(len)

从图表中可以看出,评论用户中最多的是B杯罩,其次为A杯罩,C的用户数量较少。

#绘制用户胸罩尺码分布图
plt.rc('font', family='STXihei', size=9)
a=np.array([1,2,3])
plt.bar([1,2,3],cup_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('尺码',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('用户胸罩尺码分布情况',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.ylim(0,350)
plt.xticks(a,('A','B','C'))
plt.show()


2.7.7胸罩颜色偏好分布
这款胸罩共分为三个颜色,红色,肤色和黑色。我们按颜色对评论数据进行汇总,查看用户对不同胸罩颜色的偏好情况。

#在table表中按productColor对数据进行汇总
color_group=table.groupby('productColor')['nickname'].agg(len)
从不同颜色的评论数量上来看,大部分用户购买的是红色。
#绘制用户颜色选择分布图
plt.rc('font', family='STXihei', size=9)
a=np.array([1,2,3,4,5,6,7,8,9,10,11,12,13,14])
plt.bar([1,2,3,4,5,6,7,8,9,10,11,12,13,14],color_group,color='#99CC01',alpha=0.8,align='center',edgecolor='white')
plt.xlabel('颜色分布',fontproperties=myfont,size=12)
plt.ylabel('评论数量',fontproperties=myfont,size=12)
plt.title('用户颜色选择分布',fontproperties=myfont,size=12)
plt.legend(['评论数量'], loc='upper right',prop=myfont)
plt.grid(color='#95a5a6',linestyle='--', linewidth=1,axis='y',alpha=0.4)
plt.ylim(0,600)
plt.xticks(a,('宝蓝色','宝蓝色单件','杏色单件','浅紫色','红色','肤色','西瓜红','酒红色','酒红色单件','银灰色','香槟色','黄色','黑色','黑色' ),rotation=30,fontproperties=myfont,size=12)
plt.show()


2.7.8 胸罩评论内容语义分析
前面我们分别对数据表中的字段进行了统计和分析,文章最后我们对商品的评论内容进行语义分析,看看大家在这700+条评论中都在说些什么。
好好先生购买比例
在人工查看了一些评论内容后,我们发现一些有意思的信息。有一部分评论是老公或男朋友发的,这说明一些好好先生会帮老婆或女友购买胸罩。那么这部分用户的比例有多少呢?
我们把评论中包含有关键词“老婆”和“女朋友”的评论单独保存在出来。

#筛选包含”老婆”和”女朋友”的评论
content_2=[]
for i in content_1:
if "老婆"in i or "女朋友"in i:
content_2.append(i)

查看这些包含关键词的评论内容,确实是老公和男朋友来购买胸罩并且发布的评论。

#查看评论内容
content_2
['"老婆说还不错……"',
'"物流很快。内衣老婆很喜欢,款式很好看,穿起来很挺。"',
'"老婆穿起来很好看,穿上之后就特别性感了,手感特别好,好享受好舒服,太棒了。"',
'"已经是老顾客了 第二次购买 效果确实很棒 老婆很喜欢 价格实惠 五分好评"']

经过计算,在这款胸罩产品的评论中,由老公或男朋友购买的比例仅为2.0%

#计算老公或男朋友购买胸罩的比例
len(content_2)/len(content_1)*100

2.7.9 商品评论关键词分析
回归到商品评论分析,我们使用结巴分词对所有胸罩的评论信息进行了分词,并提取了权重最高的关键词列表。

#文本数据格式转换
word_str = ''.join(content_1)
#提取文字关键词
word_rank=jieba.analyse.extract_tags(word_str, topK=20, withWeight=True, allowPOS=())
#转化为数据表
word_rank = pd.DataFrame(word_rank,columns=['word','rank'])
#查看关键词及权重
word_rank.sort('rank',ascending=False)


从高权重关键词列表来看,用户评论以正面信息为主,”不错”,”舒服”,”喜欢”等主观感受的正面评论权重较高。
2.8 结语
本章我们从商品评论信息的抓取,清洗到分析和数据可视化实现了一个完整的闭环。整个过程中数据的清洗和预处理是最为复杂也是耗时最多的工作。由于抓取的数据量较少,只有500+条数据。因此里面的一些结论可能没有代表性,结论也未必准确,仅供参考。

爬虫

第1章 Scrapy爬虫基础

1.1 Scrapy运行所依赖的组件
Scrapy框架是一个快速高效地抓取并解析网页内容的爬虫工具。它支持批量方式、多线程获取网页内容,采用简单易用地提取元素规则,并且还支持把抓取结果输出到多种结果集中。
目前最新版本为1.2,本课件中演示使用的版本是0.14。Scrapy可以运行在windows和Linux平台上,且都需要Python支持,可以使用Python 2.7以上版本来运行本课件中的例子。

scrapy模块组成图及运行过程

Scrapy在Linux平台上运行需要依赖很多包,以下列出在Ubuntu上安装Scrapy所需要事先安装的包,这些包之间的安装顺序存在依赖关系。
依次安装以下模块:
1. setuptools-0.6c11
2. zope.interface-4.0.1
3. 重新安装Python2.7开发版
4. Twisted-12.1.0
5. w3lib-1.2
6. libxml2和 libxml2-dev
7. libxslt1-dev和libxslt-dev
8. python2.7-mysqldb
9. Scrapy 0.14.4

1.2 手动创建一个Scrapy爬虫项目,并抓取P2P网贷信息
安装Scrapy后,可以通过命令行来测试是否安装成功,如下图:

手动创建并运行一个爬虫项目,通常需要以下几个步骤:
① 创建一个空的scrapy项目
② 定义要抓取的网址,并解析网页中特定标签中的内容
③ 定义item类来规范抓取后的内容
④ 编写pipeline类实现item信息落地
1.3创建一个空的scrapy项目
Scrapy中的爬虫是属于某个项目的。首先可以通过在命令行输入“scrapystartproject”命令来创建一个空的项目。比如现在要创建一个名为mydemo的项目,操作如下:

该命令会在硬盘上创建文件夹mydemo,并自动生成子文件夹mydemo和一些文件,其中mydemo/spiders/目录是用来存放具体爬虫文件的目录。爬虫文件需要用户手动创建,一个目录下可以同时存在多个爬虫文件

1.4 定义要抓取的网址,并解析网页中特定标签中的内容
在mydemo/spiders/下创建Python文件mydemo_page.py,内容如下:

# encoding: utf-8
#!/usr/bin/python
fromscrapy.spider import BaseSpider
classMyDemoSpider(BaseSpider):
name = "p2p_page"
start_urls = ["http://www.pingledai.com/td/tz/jkinfo?jkid=364&sh=1"]
def parse(self, response):
print(response.body)

• name属性:用来给爬虫起名字。它也是运行“scrapy crawl”命令时所要提供的参数值。
• start_urls属性:指明了爬虫要抓取的URL地址,类型是字符串数组,可以给爬虫指定一个抓取列表。
• parse方法:是对抓取得到的内容进行解析的方法。参数中的response是抓取后获得的HTML页面内容

创建mydemo_spider.py文件后,我们就可以启动爬虫了。只需要输入命令行
scrapy crawl p2p_page <回车>,就可以看到日志信息,爬虫已经启动,并且最后打印出目标页面对应的HTML源码来。

HTML源码中有我们需要的网贷信息,如何提取出这些信息,就需要在parse方法中继续编码了。Scrapy使用一种XPath Selector机制来解析HTML中的数据,它是基于XPath表达式语法的(有关XPath的详细内容,可参见http://www.w3.org/TR/xpath/)。
XPath Selector在Scrapy中内置了两种创建方式,分别是HtmlXPathSelector和XmlPathSelector用来解析Html和Xml代码。在parse方法中可以对HTML利用XPath配合css代码来提取所需的内容。XPath选择器有三个方法:
1. select(xpath): 返回一个相对于当前选中节点的选择器列表(一个XPath可能选到多个节点)
2. extract(): 返回选择器(列表)对应的节点的字符串(列表)
3. re(regex): 返回正则表达式匹配的字符串(分组匹配)列表

parse方法里面使用Xpath方式来逐个解析需要提取出来的要素信息

def parse(self, response):
hxs = HtmlXPathSelector(response)
#标题
title=hxs.select('//span[@id="tzjkcap"]/text()').extract()[0].encode('utf-8')
print(title)
data = hxs.select('//div[@class="pull-left"]/text()').extract()
#借款金额
amount = data[0].encode('utf-8')
print(amount)
#利率
interest = data[1].encode('utf-8')
print(interest)
#借款进度
progress = data[5].encode('utf-8')
progress = progress.replace(' ','')
progress = progress.replace('\n','')
print(progress)
#还款方式
payway = data[6].encode('utf-8')
print(payway)
#期限
term = data[8].encode('utf-8')
print(term)

解析出P2P网贷信息后,同样使用启动爬虫的命令
scrapy crawl p2p_page <回车>,就可以在控制台看到打印出来的内容了。

1.5 定义item类来规范抓取后的内容
页面上要抓取的内容,通常是一条完整的记录,譬如一个帖子、一条职位信息或是一条QQ动态。在Scrapy中可使用item类来封装这条记录,item.py文件在spiders的同级目录下。

fromscrapy.item import Item, Field
classMydemoItem(Item):
# define the fields for your item here like:
# name = Field()
pass

Item和Field类都是Scrapy框架提供的。Field类使用Python内置的字典(Python dict)对象,用来存放不同类型的属性值。MydemoItem类继承自Item类,pass空语句代表本程序不执行相应的动作。

在MydemoItem类中定义p2p招标信息的每个属性,类型均为Field类型,并增加一个打印所有属性的方法

# encoding: utf-8
fromscrapy.item import Item, Field
classMydemoItem(Item):
# define the fields for your item here like:
# name = Field()
#标题
title=Field()
#期限
term=Field()
#利率
interest=Field()
#还款方式
payway=Field()
#借款金额
amount=Field()
#借款进度
progress=Field()
#打印出所有属性值
defprintProps(self):
#return self['progress']
return "[标题] %s, [期限] %s, [利率] %s, [还款方式] %s, [借款进度] %s, [借款金额] %s " %(self['title'], self['term'], self['interest'], self['payway'], self['progress'], self['amount'])

在spiders/mydemo_page.py中的parse方法里面创建MydemoItem实例,把HTML中抓取的内容赋值给实例中的每个属性。

def parse(self, response):
hxs = HtmlXPathSelector(response)
#标题
title=hxs.select('//span[@id="tzjkcap"]/text()').extract()[0].encode('utf-8')
#print(title)
... ...
... ...
p2pitem = items.MydemoItem()
p2pitem['title'] = title
p2pitem['term'] = term
p2pitem['interest'] = interest
p2pitem['payway'] = payway
p2pitem['amount'] = amount
p2pitem['progress'] = progress
print(p2pitem.printProps())

最后运行scrapy crawl p2p_page命令,查看运行结果

1.6 如何把抓取后信息放入文件中
1. 定义一个管道类来实现抓取内容落地
parse方法返回的MyDemoItem对象,可以在pipelines.py文件的process_item方法中得到继续处理,把MyDemoItem中的属性写入文件。示例代码如下,把抓取下来的p2p网贷信息项写入p2poutput.dat文件中。FilePipelines.py文件可以从pipelines.py复制得到。

# encoding: utf-8
importos
classMydemoPipeline(object):
defprocess_item(self, item, spider):
#把解析后的内容放入文件中
fname = "p2poutput.dat"
f_handle = open(fname,'w')
f_handle.write(item.printProps())
f_handle.close()
return item

2. 配置管道类的执行顺序
除了定义FilePipelines文件之外,还需要让scrapy知道有这个文件存在,所以需要在mydemo/settings.py文件中增加配置项,如下红色的代码行,在里面增加“目录名.文件名.类名”这样的配置内容。

BOT_NAME = 'mydemo'
BOT_VERSION = '1.0'
SPIDER_MODULES = ['mydemo.spiders']
NEWSPIDER_MODULE = 'mydemo.spiders'
USER_AGENT = '%s/%s' % (BOT_NAME, BOT_VERSION)
ITEM_PIPELINES = {'mydemo.FilePipelines.MydemoPipeline':1}

3. 执行p2p_page爬虫
再次运行scrapy crawl p2p_page命令,查看运行结果。会发现在mydemo目录下多出来一个p2poutput.dat文件,就是管道文件生成的爬取结果文件。

以上完成了一个最基本的爬虫抓取数据的功能,后续课程还会陆续介绍如何把抓取的内容写入到数据库?如何让两个爬虫配合工作及如何抓取图片文件。

第2章 分析京东客户评价数据

Python与机器学习

第1章、机器学习基础
第2章、机器学习流程
第3章、机器学习实例
第4章、SKlearn简介
第5章、预处理方法
第6章、模型评估与参数优化
第7章、集成学习
第8章、客户价值分析实例
第9章、情感分析实例
第10章、聚类分析实例
第11章、神经网络基础
第12章、神经网络人脸识别实例

第4章 特征提取、转换和选择


本章数据集下载

第4章 特征提取、转换和选择

在实际机器学习项目中,我们获取的数据往往是不规范、不一致、有很多缺失数据,甚至不少错误数据,这些数据有时又称为脏数据或噪音,在模型训练前,务必对这些脏数据进行处理,否则,再好的模型,也只能脏数据进,脏数据出。
这章我们主要介绍对数据处理涉及的一些操作,主要包括:
特征提取
特征转换
特征选择

4.1 特征提取

特征提取一般指从原始数据中抽取特征。

4.1.1 词频-逆向文件频率(TF-IDF)

词频-逆向文件频率(TF-IDF)是一种在文本挖掘中广泛使用的特征向量化方法,它可以体现一个文档中词语在语料库中的重要程度。
在下面的代码段中,我们以一组句子开始。首先使用分解器Tokenizer把句子划分为单个词语。对每一个句子(词袋),我们使用HashingTF将句子转换为特征向量,最后使用IDF重新调整特征向量。这种转换通常可以提高使用文本特征的性能。

4.1.2 Word2Vec

Word2vec是一个Estimator,它采用一系列代表文档的词语来训练word2vecmodel。 在下面的代码段中,我们首先用一组文档,其中每一个文档代表一个词语序列。对于每一个文档,我们将其转换为一个特征向量。此特征向量可以被传递到一个学习算法。

4.1.3 计数向量器

计数向量器(Countvectorizer)和计数向量器模型(Countvectorizermodel)旨在通过计数来将一个文档转换为向量。
以下用实例来说明计数向量器的使用。
假设有以下列id和texts构成的DataFrame:

每行text都是Array [String]类型的文档。调用fit,CountVectorizer产生CountVectorizerModel含词汇(a,b,c)。转换后的输出列“向量”包含:
调用的CountVectorizer产生词汇(a,b,c)的CountVectorizerModel,转换后的输出向量如下:

每个向量代表文档的词汇表中每个词语出现的次数。

4.2 特征转换

在机器学习中,数据处理是一件比较繁琐的事情,需要对原有特征做多种处理,如类型转换、标准化特征、新增衍生特征等等,需要耗费大量的时间和精力编写处理程序,不过,自从Spark推出ML后,情况大有改观,Spark ML包中提供了很多现成转换器,例如:StringIndexer、IndexToString、OneHotEncoder、VectorIndexer,它们提供了十分方便的特征转换功能,这些转换器类都位于org.apache.spark.ml.feature包下。

4.2.1分词器

分词器(Tokenization)将文本划分为独立个体(通常为单词)。

4.2.2 移除停用词

停用词为在文档中频繁出现,但未承载太多意义的词语,它们不应该被包含在算法输入中,所以会用到移除停用词(StopWordsRemover)。
示例:
假设我们有如下DataFrame,有id和raw两列

通过对raw列调用StopWordsRemover,我们可以得到筛选出的结果列如下

其中,“I”, “the”, “had”以及“a”被移除。
实现以上功能的详细代码:

4.2.3 n-gram

一个n-gram是一个长度为整数n的字序列。NGram可以用来将输入转换为n-gram。

4.2.4 二值化

二值化,通过设置阀值,将连续型的特征转化为两个值。大于阀值为1,否则为0。
注:以下规范化操作一般是针对一个特征向量(dataFrame中的一个colum)来操作的。

4.2.5 主成分分析

主成分分析被广泛应用在各种统计学、机器学习问题中,是最常见的降维方法之一。
PCA在Spark2.0用法比较简单,只需要设置:
.setInputCol(“features”)//保证输入是特征值向量
.setOutputCol(“pcaFeatures”)//输出
.setK(3)//主成分个数
注意:PCA前一定要对特征向量进行规范化(标准化)!!!

4.2.6 多项式展开

多项式展开(PolynomialExpansion)即通过产生n维组合将原始特征将特征扩展到多项式空间。下面的示例会介绍如何将你的特征集拓展到3维多项式空间。

4.2.7 离散余弦变换

离散余弦变换(DCT)是与傅里叶变换相关的一种变换,它类似于离散傅立叶变换,但是只使用实数。

4.2.8 字符串-索引变换

字符串—索引变换(StringIndexer)是将字符串列编码为标签索引列。示例数据为一个含有id和category两列的DataFrame
id | category
----|----------
0 | a
1 | b
2 | c
3 | a
4 | a
5 | c

category是有3种取值的字符串列(a、b、c),使用StringIndexer进行转换后我们可以得到如下输出,其中category作为输入列,categoryIndex作为输出列:
id | category | categoryIndex
----|----------|---------------
0 | a | 0.0
1 | b | 2.0
2 | c | 1.0
3 | a | 0.0
4 | a | 0.0
5 | c | 1.0
a获得索引0,因为它是最频繁的,随后是具有索引1的c和具有索引2的b。
如果测试数据集中比训练数据集多了一个d类:
id | category
----|----------
0 | a
1 | b
2 | c
3 | d
如果您没有设置StringIndexer如何处理未看见的标签(默认值)或将其设置为“错误”,则会抛出异常。 但是,如果您调用了setHandleInvalid(“skip”),d类将不出现,结果为以下数据集:
id | category | categoryIndex
----|----------|---------------
0 | a | 0.0
1 | b | 2.0
2 | c | 1.0
以下是使用StringIndexer的一个示例:

4.2.9 索引-字符串变换

与StringIndexer对应,索引—字符串变换(IndexToString)是将指标标签映射回原始字符串标签。
id | categoryIndex
----|---------------
0 | 0.0
1 | 2.0
2 | 1.0
3 | 0.0
4 | 0.0
5 | 1.0
应用IndexToString,将categoryIndex作为输入列,将originalCategory作为输出列,我们可以检索我们的原始标签(它们将从列的元数据中推断):
id | categoryIndex | originalCategory
----|---------------|-----------------
0 | 0.0 | a
1 | 2.0 | b
2 | 1.0 | c
3 | 0.0 | a
4 | 0.0 | a
5 | 1.0 | c
以下是以上整个过程的一个实例:

4.2.10 独热编码

独热编码(OneHotEncoder)将标签指标映射为二值向量,其中最多一个单值。

【说明】
1、OneHotEncoder缺省状态下将删除最后一个分类或把最后一个分类作为0.
//示例

显示结果如下:
+----+---+-----+
| x| c|c_idx|
+----+---+-----+
| 1.0| a| 0.0|
| 1.5| a| 0.0|
|10.0| b| 2.0|
| 3.2| c| 1.0|
| 3.8| c| 1.0|
+----+---+-----+
最后一个分类为b,通过OneHotEncoder变为向量后,已被删除。

显示结果如下:
+----+---+-----+-------------+
| x| c|c_idx| c_idx_vec|
+----+---+-----+-------------+
| 1.0| a| 0.0|(2,[0],[1.0])|
| 1.5| a| 0.0|(2,[0],[1.0])|
|10.0| b| 2.0| (2,[],[])|
| 3.2| c| 1.0|(2,[1],[1.0])|
| 3.8| c| 1.0|(2,[1],[1.0])|
+----+---+-----+-------------+
与其他特征组合为特征向量后,将置为0,请看下例

显示结果如下:
+----+---+-----+-------------+------------------+
|x |c |c_idx|c_idx_vec |features |
+----+---+-----+-------------+------------------+
|1.0 |a |0.0 |(2,[0],[1.0])|[1.0,0.0,1.0,0.0] |
|1.5 |a |0.0 |(2,[0],[1.0])|[1.5,0.0,1.0,0.0] |
|10.0|b |2.0 |(2,[],[]) |[10.0,2.0,0.0,0.0]|
|3.2 |c |1.0 |(2,[1],[1.0])|[3.2,1.0,0.0,1.0] |
|3.8 |c |1.0 |(2,[1],[1.0])|[3.8,1.0,0.0,1.0] |
+----+---+-----+-------------+------------------+
如果想不删除最后一个分类,可添加setDropLast(False)。

显示结果如下:
+----+---+-----+-------------+
| x| c|c_idx| c_idx_vec|
+----+---+-----+-------------+
| 1.0| a| 0.0|(3,[0],[1.0])|
| 1.5| a| 0.0|(3,[0],[1.0])|
|10.0| b| 2.0|(3,[2],[1.0])|
| 3.2| c| 1.0|(3,[1],[1.0])|
| 3.8| c| 1.0|(3,[1],[1.0])|
+----+---+-----+-------------+
与其他特征向量结合后,情况如下:

显示结果如下:
+----+---+-----+-------------+----------------------+
|x |c |c_idx|c_idx_vec |features |
+----+---+-----+-------------+----------------------+
|1.0 |a |0.0 |(3,[0],[1.0])|(5,[0,2],[1.0,1.0]) |
|1.5 |a |0.0 |(3,[0],[1.0])|(5,[0,2],[1.5,1.0]) |
|10.0|b |2.0 |(3,[2],[1.0])|[10.0,2.0,0.0,0.0,1.0]|
|3.2 |c |1.0 |(3,[1],[1.0])|[3.2,1.0,0.0,1.0,0.0] |
|3.8 |c |1.0 |(3,[1],[1.0])|[3.8,1.0,0.0,1.0,0.0] |
+----+---+-----+-------------+----------------------+
2、如果分类中出现空字符,需要进行处理,如设置为"None",否则会报错。

4.2.11 向量-索引变换

在下面的例子中,我们读取一个标记点的数据集,然后使用VectorIndexer来决定哪些特征应该被视为分类。我们将分类特征值转换为它们的索引。这个变换的数据然后可以被传递到诸如DecisionTreeRegressor的处理分类特征的算法。

4.2.12交互式

例子,假设我们有以下DataFrame的列“id1”,“vec1”和“vec2”
id1|vec1 |vec2
---|--------------|--------------
1 |[1.0,2.0,3.0] |[8.0,4.0,5.0]
2 |[4.0,3.0,8.0] |[7.0,9.0,8.0]
3 |[6.0,1.0,9.0] |[2.0,3.0,6.0]
4 |[10.0,8.0,6.0]|[9.0,4.0,5.0]
5 |[9.0,2.0,7.0] |[10.0,7.0,3.0]
6 |[1.0,1.0,4.0] |[2.0,8.0,4.0]
应用与这些输入列的交互,然后interactionedCol作为输出列包含:
id1|vec1 |vec2 |interactedCol
---|--------------|--------------|------------------------------------------------------
1 |[1.0,2.0,3.0] |[8.0,4.0,5.0] |[8.0,4.0,5.0,16.0,8.0,10.0,24.0,12.0,15.0]
2 |[4.0,3.0,8.0] |[7.0,9.0,8.0] |[56.0,72.0,64.0,42.0,54.0,48.0,112.0,144.0,128.0]
3 |[6.0,1.0,9.0] |[2.0,3.0,6.0] |[36.0,54.0,108.0,6.0,9.0,18.0,54.0,81.0,162.0]
4 |[10.0,8.0,6.0]|[9.0,4.0,5.0] |[360.0,160.0,200.0,288.0,128.0,160.0,216.0,96.0,120.0]
5 |[9.0,2.0,7.0] |[10.0,7.0,3.0]|[450.0,315.0,135.0,100.0,70.0,30.0,350.0,245.0,105.0]
6 |[1.0,1.0,4.0] |[2.0,8.0,4.0] |[12.0,48.0,24.0,12.0,48.0,24.0,48.0,192.0,96.0]
以下是实现以上转换的具体代码:

4.2.13正则化

以下示例演示如何加载libsvm格式的数据集,然后将每行标准化为具有单位L1范数和单位L∞范数。

4.2.14规范化(StandardScaler)

以下示例演示如何以libsvm格式加载数据集,然后规范化每个要素的单位标准偏差。

4.2.15最大值-最小值缩放

下面的示例展示如果读入一个libsvm形式的数据以及调整其特征值到[0,1]之间。
调用示例:

显示结果如下:
Features scaled to range: [0.0, 1.0]
+--------------+--------------+
| features|scaledFeatures|
+--------------+--------------+
|[1.0,0.1,-1.0]| [0.0,0.0,0.0]|
| [2.0,1.1,1.0]| [0.5,0.1,0.5]|
|[3.0,10.1,3.0]| [1.0,1.0,1.0]|
+--------------+--------------+

4.2.16最大值-绝对值缩放

以下示例演示如何加载libsvm格式的数据集,然后将每个特征重新缩放到[-1,1]。

运行结果如下:
+--------------+----------------+
| features| scaledFeatures|
+--------------+----------------+
|[1.0,0.1,-8.0]|[0.25,0.01,-1.0]|
|[2.0,1.0,-4.0]| [0.5,0.1,-0.5]|
|[4.0,10.0,8.0]| [1.0,1.0,1.0]|
+--------------+----------------+

4.2.17离散化重组

以下示例演示如何将双列列存储到另一个索引列的列中。

运行结果如下:
+--------+----------------+
|features|bucketedFeatures|
+--------+----------------+
| -999.9| 0.0|
| -0.5| 1.0|
| -0.3| 1.0|
| 0.0| 2.0|
| 0.2| 2.0|
| 999.9| 3.0|
+--------+----------------+

4.2.18元素乘积

下面的示例演示了如何使用变换向量值来变换向量

运行结果如下:
+---+-------------+-----------------+
| id| vector|transformedVector|
+---+-------------+-----------------+
| a|[1.0,2.0,3.0]| [0.0,2.0,6.0]|
| b|[4.0,5.0,6.0]| [0.0,5.0,12.0]|
+---+-------------+-----------------+

4.2.19 SQL转换器

假设我们有以下DataFrame和列id,v1和v2
id | v1 | v2
----|-----|-----
0 | 1.0 | 3.0
2 | 2.0 | 5.0
这是SQLTransformer "SELECT *, (v1 + v2) AS v3, (v1 * v2) AS v4 FROM __THIS__":语句的输出。
id | v1 | v2 | v3 | v4
----|-----|-----|-----|-----
0 | 1.0 | 3.0 | 4.0 | 3.0
2 | 2.0 | 5.0 | 7.0 |10.0
以下是实现以上结果的具体代码:

4.2.20向量汇编

例子
假设我们有一个带有id,hour,mobile,userFeatures和clicked列的DataFrame:
id | hour | mobile | userFeatures | clicked
----|------|--------|------------------|---------
0 | 18 | 1.0 | [0.0, 10.0, 0.5] | 1.0
userFeatures是一个包含三个用户特征的向量列。我们希望将hour,mobile和userFeatures合并成一个称为特征的单一特征向量,并使用它来预测是否被点击。如果我们将VectorAssembler的输入列设置为hour,mobile和userFeatures,并将列输出到特征,则在转换后,我们应该得到以下DataFrame:
id | hour | mobile | userFeatures | clicked | features
----|------|--------|------------------|---------|-----------------------------
0 | 18 | 1.0 | [0.0, 10.0, 0.5] | 1.0 | [18.0, 1.0, 0.0, 10.0, 0.5]
以下是实现上述功能的代码:

4.2.21分位数离散化

示例:
假设我们有如下DataFrame包含id,hour:
id | hour
----|------
0 | 18.0
----|------
1 | 19.0
----|------
2 | 8.0
----|------
3 | 5.0
----|------
4 | 2.2
hour是Double类型的连续特征。我们希望将连续特征变成一个分级特征。给定numBuckets = 3,我们可得到以下DataFrame:
id | hour | result
----|------|------
0 | 18.0 | 2.0
----|------|------
1 | 19.0 | 2.0
----|------|------
2 | 8.0 | 1.0
----|------|------
3 | 5.0 | 1.0
----|------|------
4 | 2.2 | 0.0
实现以上功能的scala代码如下:

4.3 特征选择

特征选择(Feature Selection)是从特征向量中选择那些更有效的特征,组成新的、更简单有效的特征向量的过程。它在数据分析中常用使用,尤其在高维数据分析中,可以剔除冗余或影响不大的特征,提升模型的性能。

4.3.1 向量机

假设我们有一个DataFrame与列userFeatures:
userFeatures
------------------
[0.0, 10.0, 0.5]
userFeatures是一个包含三个用户特征的向量列。假设userFeature的第一列全部为零,因此我们要删除它并仅选择最后两列。 VectorSlicer使用setIndices(1,2)选择最后两个元素,然后生成一个名为features的新向量列:
userFeatures | features
------------------|-----------------------------
[0.0, 10.0, 0.5] | [10.0, 0.5]
假设userFeatures有输入属性,如[“f1”,“f2”,“f3”],那么我们可以使用setNames(“f2”,“f3”)来选择它们。
userFeatures | features
------------------|-----------------------------
[0.0, 10.0, 0.5] | [10.0, 0.5]
["f1", "f2", "f3"] | ["f2", "f3"]
以下是实现向量选择的一个scala代码示例

运行结果:
+--------------------+-------------+
|userFeatures |features |
+--------------------+-------------+
|(3,[0,1],[-2.0,2.3])|(2,[0],[2.3])|
|[-2.0,2.3,0.0] |[2.3,0.0] |
+--------------------+-------------+

4.3.2 R公式

示例:
假设我们有一个DataFrame含有id,country, hour和clicked四列:
id | country |hour | clicked
---|---------|------|---------
7 | "US" | 18 | 1.0
8 | "CA" | 12 | 0.0
9 | "NZ" | 15 | 0.0
如果我们使用RFormula公式clicked ~ country+ hour,则表明我们希望基于country和hour预测clicked,通过转换我们可以得到如下DataFrame:
id | country |hour | clicked | features | label
---|---------|------|---------|------------------|-------
7 | "US" | 18 | 1.0 | [0.0, 0.0, 18.0] | 1.0
8 | "CA" | 12 | 0.0 | [0.0, 1.0, 12.0] | 0.0
9 | "NZ" | 15 | 0.0 | [1.0, 0.0, 15.0] | 0.0
以下是实现上述功能的scala代码:

4.3.3 卡方特征选择

示例:
假设我们有一个DataFrame含有id,features和clicked三列,其中clicked为需要预测的目标:
id | features | clicked
---|-----------------------|---------
7 | [0.0, 0.0, 18.0, 1.0] | 1.0
8 | [0.0, 1.0, 12.0, 0.0] | 0.0
9 | [1.0, 0.0, 15.0, 0.1] | 0.0
如果我们使用ChiSqSelector并设置numTopFeatures为1,根据标签clicked,features中最后一列将会是最有用特征:
id | features | clicked | selectedFeatures
---|-----------------------|---------|------------------
7 | [0.0, 0.0, 18.0, 1.0] | 1.0 | [1.0]
8 | [0.0, 1.0, 12.0, 0.0] | 0.0 | [0.0]
9 | [1.0, 0.0, 15.0, 0.1] | 0.0 | [0.1]
使用ChiSqSelector的scala代码示例:

结果显示:
+---+------------------+-------+----------------+
| id| features|clicked|selectedFeatures|
+---+------------------+-------+----------------+
| 7|[0.0,0.0,18.0,1.0]| 1.0| [18.0]|
| 8|[0.0,1.0,12.0,0.0]| 0.0| [12.0]|
| 9|[1.0,0.0,15.0,0.1]| 0.0| [15.0]|
+---+------------------+-------+----------------+

4.4 小结

本章主要介绍了对数据特征或变量的一些常用操作,包括特征提取,特征转换以及特征选择等方法,这些任务在实际项目中往往花费大量时间和精力,尤其要自己编写这方面的代码或函数,更是如此,Spark ML目前提供了很多现成函数,有效使用这些函数将有助于提供我们开发效率,同时使我们有更多时间优化或提升模型性能。下一章我们将介绍优化或提升模型性能一些方法。

第3章 ML Pipelines

第3章 ML Pipelines原理与实战

Spark MLlib 是Spark的重要组成部分,也是最早推出库之一, 其基于RDD的API,算法比较丰富,也比较稳定,也比较好用。但是如果目标数据集结构复杂需要多次处理,或者是对新数据需要结合多个已经训练好的单个模型进行综合计算时,使用 MLlib 将会让程序结构复杂,甚至难于理解和实现。为改变这一局限性,从Spark 1.2 版本之后引入的 ML Pipeline,经过多个版本的发展,目前Spark ML功能齐全、性能稳定,Spark ML克服了MLlib在处理复杂机器学习问题的一些不足(如工作比较复杂,流程不清晰等),向用户提供基于DataFrame 之上的更加高层次的 API 库,以更加方便的构建复杂的机器学习工作流式应用,使整个机器学习过程变得更加易用、简洁、规范和高效。Spark的Pipeline与Scikit中Pipeline功能相近、理念相同。本章主要介绍Spark ML中Pipelines的有关内容。
本章主要介绍ML Pipeline相关内容,包括:
 Pipeline简介
 DataFrame
 构成Pipeline的一些组件
 介绍pipeline的一般原理
 使用pipeline的几个实例

3.1Pipeline简介

3.2DataFrame

以下通过一个实例来说明DataFrame的创建、操作等内容:

3.3 Pipeline组件

Pipeline组件主要包括Transformer和Estimator。
1. Transformer
2.Estimator

3.4 Pipeline原理

要构建一个Pipeline,首先需要定义Pipeline中的各个Stage,如指标提取和转换模型训练等。有了这些处理特定问题的Transformer和Estimator,我们就可以按照具体的处理逻辑来有序的组织Stages并创建一个Pipeline。

图3-1 pipeline在训练数据上的流程

整个流水线是一个估计器。所以当流水线的fit()方法运行后,会产生一个流水线模型,流水线模型是转换器。流水线模型会在测试时被调用,下面的图示说明用法。

图3-2 pipeline在测试数据上的流程

上面的图示中,流水线模型和原始流水线有同样数目的阶段,然而原始流水线中的估计器此时变为了转换器。当流水线模型的transform()方法被调用于测试数据集时,数据依次经过流水线的各个阶段。每个阶段的transform()方法更新数据集,并将之传到下个阶段。
流水线和流水线模型有助于确认训练数据和测试数据经过同样的特征处理流程。
以上两图如果合并为一图,可用如下图形表达:

 

图3-3 Spark pipeline 流程图

其中Pipeline及LogisticRegression都Estimator,Tokenizer,HashingTF,LogisticRegression Model为Transformer。

3.5Pipeline实例

3.5.1使用Estimator, Transformer, and Param实例

机器学习整个过程中,特征转换、特征选择、派生特征等工作,一般需要占据大部分时间,现在ML提供了很多Transformer,如OneHotEncoder、StringIndexer、PCA、Bucketizer、Word2vec等,利用这些函数可极大提高工作效率。
以下通过实例说明如何使用ML库中Estimaor、Transformer和Param等。