作者归档:feiguyun

第3章 机器学习简单示例

为更好理解机器学习中的有关概念和流程,我们以一个实际案例来说明,这个实例需求就是根据(输入数据为房子面积,目标数据为房子价格),建立一个模型对新房价进行预测。

  • 输入数据(只有一维):房子面积
  • 目标数据(只有一维):房子价格
  • 需要做的:就是根据已知的房子面积和价格进行机器学习,建立一个模型,找到它们之间的内在关系。

下面我们就一步步地进行操作。

1、获取与预处理数据

查看源数据:

system head -5 ./data/python_and_ml/prices.txt
['2104,399900', '1600,329900', '2400,369000', '1416,232000', '3000,539900']

显示前5行数据,每行数据由‘房子面积,房子价格’组成,这些数据比较大,相差也比较大,故需要对源数据进行标准化处理,使用如下公式:

其中  表示房子面积的均值、std(X)表示X的标准差,具体实现代码如下:

%matplotlib inline
#导入需要的库
import numpy as np
import matplotlib.pyplot as plt
# 定义存储输入数据、目标数据的数组
x, y = [], []
#遍历数据集,变量sample对应每个样本
for sample in open("./data/python_and_ml/prices.txt", "r"):
x1, y1 = sample.split(",")
x.append(float(x1))
y.append(float(y1))
#把读取后的数据转换为numpy数组
x, y = np.array(x), np.array(y)
# 对x数据进行标准化处理
x = (x - x.mean()) / x.std()
# 可视化原数据
plt.figure()
plt.scatter(x, y, c="g", s=20)
plt.show()

这里横轴是标准化的房子面积,纵轴为房子价格,通过数据可视化,我们能很直观了解到原数据的分布情况,接下来我们开始训练模型。

2、 选择和训练模型

从数据可视化后的结果看来,我们可以通过线性回归(Linear Regression)中的多项式拟合来得到一个模型,该模型的数学表达式为:

构造模型的损失函数

其中f(x|p;n)就是我们的模型,p,n是模型参数,p是多项式f的各项系统,n是多项式的次数。L(p;n)是模型的损失函数,这里我们采用了常用的平方损失函数。x、y分别是输入向量和目标向量,它们都是47维的向量。

在确定模型后,接下来就开始编写代码训练模型。训练一般是最小化损失函数,这里就是使L(p;n)最小,从而求出相关参数。

当然,这里我们采用数理统计中的正规方程方法。具体实现代码如下:

#在(-2,4)这个区间上取100个点作为画图的基础
x0 = np.linspace(-2, 4, 100)

#利用numpy的函数定义训练并返回多项式回归模型函数
# deg参数表示模型参数中的n
def get_model(deg):
return lambda input_x=x0: np.polyval(np.polyfit(x, y, deg), input_x)

其中polyval和polyfit都是numpy中的函数。

polyfit(x,y,deg):该函数返回使得上述损失函数  最小的参数p;

polyval:根据多项式的各项系统p和多项式中的x,返回多项式的y值。

3、 评估与可视化结果

模型训练好以后,接下来就要判断各种参数下模型的性能了,为简单起见,我们这里采用deg=1,4,10(即多项式最大次数分别为1,4,10),采用损失函数来衡量模型的性能。

# 定义损失函数
def get_cost(deg, input_x, input_y):
return 0.5 * ((get_model(deg)(input_x) - input_y) ** 2).sum()

# 设置n的几个参数值
test_set = (1, 4, 10)
for d in test_set:
print(get_cost(d, x, y))

# Visualize results
plt.scatter(x, y, c="g", s=20)
for d in test_set:
plt.plot(x0, get_model(d)(), label="degree = {}".format(d))
plt.xlim(-2, 4)
plt.ylim(1e5, 8e5)
plt.legend()
plt.show()

损失值分别为:

96732238800.4

94112406641.7

75874846680.1

结果可视化图像:

从损失值来看,n=10时,损失值最小,当从图形来看,当n=10,显然存在过拟合。

n=4的损失值小于n=1,但从图形来看,n=4也是过拟合,所以综合来看,取n=1为一个不错选择。

通过这个简单实例,我们对机器学习的大致流程有个大致了解,对机器学习相关概念也有更具体认识,对后续更复杂的机器学习应该有了一个很好的基础。

 

 

 

 

第2章 机器学习一般流程

机器学习一般流程大致分为收集数据、探索数据、预处理数据,对数据处理后,接下来开始训练模型、评估模型,然后优化模型等步骤,具体可参考下图:

 

通过这个图形我们可以比较直观了解机器学习的一般步骤或整体框架,接下来我们就各部分分别加以说明。

1 数据探索

拿到数据以后,一般不会急于创建模型、训练模型,在这之前,需要对数据、对需求或机器学习的目标进行分析,尤其对数据进行一些必要的探索,如了解数据的大致结构、数据量、各特征的统计信息、整个数据质量情况、数据的分布情况等。为了更好体现数据分布情况,数据可视化是一个不错方法。

2 数据预处理

通过对数据探索后,可能发现不少问题:如存在缺失数据、数据不规范、数据分布不均衡、存在奇异数据、有很多非数值数据、存在很多无关或不重要的数据等等。这些问题的存在直接影响数据质量,为此,数据预处理工作应该就是接下来的重点工作,数据预处理是机器学习过程中必不可少的重要步骤,特别是在生产环境中的机器学习,数据往往是原始、为加工和处理过,数据预处理常常占据整个机器学习过程的大部分时间。

数据预处理过程中,一般包括数据清理、数据转换、规范数据、特征选择等等工作。

3 选择模型并进行训练

在模型选择时,一般不存在某种对任何情况都表现很好的算法(这种现象又称为没有免费的午餐)。因此在实际选择时,一般会选用几种不同方法来训练模型,然后比较它们的性能,从中选择最优的这个。当然,在比较不同模型之前,我们需要先确认衡量性能的指标,对分类问题常用的是准确率或ROC曲线,对回归连续性目标值问题一般采用误差来评估。

训练模型前,一般会把数据集分为训练集和测试集,或对训练集再细分为训练集和验证集,从而对模型的泛化能力进行评估。

4 模型验证和使用未知数据进行预测

使用训练数据构建模型后,通常使用测试数据对模型进行测试,测试模型对新数据的

测试。如果我们对模型的测试结果满意,就可以用此模型对以后的进行预测;如果我们测试结果不满意,我们可以优化模型,优化的方法很多,其中网格搜索参数是一种有效方法,当然我们也可以采用手工调节参数等方法。如果出现过拟合,尤其是回归类问题,我们可以考虑正则化方法来降低模型的泛化误差。

第1章 机器学习简介

大数据、人工智能是目前大家谈论比较多的话题,它们的应用也越来越广泛、与我们的生活关系也越来越密切,影响也越来越深远,其中很多已进入寻常百姓家,如无人机、网约车、自动导航、智能家电、电商推荐、人机对话机器人等等。

大数据是人工智能的基础,而使大数据转变为知识或生产力,离不开机器学习(Machine Learning),可以说机器学习是人工智能的核心,是使机器具有类似人的智能的根本途径。

本章主要介绍机器有关概念、与大数据、人工智能间的关系、机器学习常用架构及算法等,具体如下:

  • 机器学习的定义
  • 大数据与机器学习
  • 机器学习与、人工智能及深度学习
  • 机器学习的基本任务
  • 如何选择合适算法
  • Spark在机器学习方面的优势

1.1机器学习的定义

机器学习是什么?是否有统一或标准定义?目前好像没有,即使在机器学习的专业人士,也好像没有一个被广泛认可的定义。在维基百科上对机器学习有以下几种定义:

“机器学习是一门人工智能的科学,该领域的主要研究对象是人工智能,特别是如何在经验学习中改善具体算法的性能”。

“机器学习是对能通过经验自动改进的计算机算法的研究”。

“机器学习是用数据或以往的经验,以此优化计算机程序的性能标准。”

一种经常引用的英文定义是:A computer program is said to learn from experience (E) with respect to some class of tasks( T) and performance(P) measure , if its performance at tasks in T, as measured by P, improves with experience E。

可以看出机器学习强调三个关键词:算法、经验、性能,其处理过程如下图所示。                

图1.1 机器学习处理流程

图1.1表明机器学习是使数据通过算法构建出模型,然后对模型性能进行评估,评估后的指标,如果达到要求就用这个模型测试新数据,如果达不到要求就要调整算法重新建立模型,再次进行评估,如此循环往复,最终获得满意结果。

1.2大数据与机器学习

我们已进入大数据时代,产生数据的能力空前高涨,如互联网、移动网、物联网、成千上万的传感器、穿戴设备、GPS等等,存储数据、处理数据等能力也得到了几何级数的提升,如Hadoop、Spark技术为我们存储、处理大数据提供有效方法。
数据就是信息、就是依据,其背后隐含了大量不易被我们感官识别的信息、知识、规律等等,如何揭示这些信息、规则、趋势,正成为当下给企业带来高回报的热点。其中数据是重要考量,在某个方面来说,数据比算法重要,数据犹如经验。

1.3 机器学习、人工智能及深度学习

人工智能和机器学习这两个科技术语如今已经广为流传,已成为当下的热词,
然而,他们间有何区别?又有哪些相同或相似的地方?虽然人工智能和机器学习高度相关,但却并不尽相同。
人工智能是计算机科学的一个分支,目的是开发一种拥有智能行为的机器,目前
很多大公司都在努力开发这种机器学习技术。他们都在努力让电脑学会人类的行为模式,
以便推动很多人眼中的下一场技术革命——让机器像人类一样“思考”。
过去10年,机器学习已经为我们带来了无人驾驶汽车、实用的语音识别、有效的网络搜索等等。接下来人工智能将如何改变我们的生活?在哪些领域最先发力?我们拭目以待。
对很多机器学习来说,特征提取不是一件简单的事情。在一些复杂问题上,
要想通过人工的方式设计有效的特征集合,往往要花费很多的时间和精力。

 

图1.2 机器学习与深度学习流程对比

深度学习解决的核心问题之一就是自动地将简单的特征组合成更加复杂的特征,并利用这些组合特征解决问题。深度学习是机器学习的一个分支,它除了可以学习特征和任务之间的关联以外,还能自动从简单特征中提取更加复杂的特征。图1.2 中展示了深度学习和传统机器学习在流程上的差异。如图1.2 所示,深度学习算法可以从数据中学习更加复杂的特征表达,使得最后一步权重学习变得更加简单且有效。

前面我们分别介绍了机器学习、人工智能及深度学习,它们间的关系如何?

图1.3 人工智能、机器学习与深度学习间的关系

人工智能、机器学习和深度学习是非常相关的几个领域。图1.3说明了它们之间大致关系。人工智能是一类非常广泛的问题,机器学习是解决这类问题的一个重要手段,深度学习则是机器学习的一个分支。在很多人工智能问题上,深度学习的方法突破了传统机器学习方法的瓶颈,推动了人工智能领域的快速发展。

1.4 机器学习的基本任务

机器学习基于数据,并以此获取新知识、新技能。它的任务有很多,分类是其基本任务之一,分类就是将新数据划分到合适的类别中。分类一般用于目标特征为类别型,如果目标特征为连续型,我们往往采用回归方法,回归对新进行预测,回归是机器学习中使用非常广泛的方法之一。

分类和回归,都是先根据标签值或目标值建立模型或规则,然后利用这些带有目标值的数据形成的模型或规则,对新数据进行识别或预测。这两种方法都属于监督学习。与监督学习相对是无监督学习,无监督学习不指定目标值或预先无法知道目标值,它可以将把相似或相近的数据划分到相同的组里,聚类就是解决这一类问题的方法之一。

除了监督学习、无监督学习这两种最常见的方法外,还有半监督学习、强化学习等方法,这里我们就不展开了,下图形为这些基本任务间的关系。

 

图1.4  机器学习基本任务

1.5 如何选择合适算法

在讲如何选择机器学习算法之前,我们先简单介绍一下机器常用方法,重点介绍算法核心思想、优缺点及模式图示等方面的内容,具体可参考下图:

当我们接到一个数据分析或挖掘的任务或需求时,如果希望用机器学习来处理,首要任务是根据任务或需求选择合适算法,选择哪种算法较合适?分析的一般步骤为:

图1.5 选择算法的一般步骤

充分了解数据及其特性,有助于我们更有效地选择机器学习算法。采用以上步骤在一定程度上可以缩小算法的选择范围,使我们少走些弯路,但在具体选择哪种算法方面,一般并不存在最好的算法或者可以给出最好结果的算法,在实际做项目的过程中,这个过程往往需要多次尝试,有时还要尝试不同算法。不过先用一种简单熟悉的方法,然后,在这个基础上不断优化,时常能收获意想不到的效果。

1.6机器学习常用术语

为更好说明机器学习中一些常用术语,我们以一个预测天气的示例来说明,通过具体数据来介绍一些概念,可能比单纯定义来得更具体一些。

假如我们有一组天气数据,是来自全世界不同国家和地区的每日天气,内容包括最高温度、最低温度、平均湿度、风速之类的相关数据,例如数据的一部分是这样的:

在这组数据中,我们将称A市、B市、C市、D市等以及温度、湿度等情况的总和称为数据集(data set)。表格中的每一行,也就是某城市和它的对应的情况被称为一个样本(sample/instance)。表格中的每一列,例如最高温度、最低温度,被称为特征(feature/attribute),而每一列中的具体数值,例如36℃ 、28℃,被称为属性值(attribute value)。数据中也可能会有缺失数据(missing data),例如B市的某时刻风速,我们会将它视作缺失数据。

如果我们想预测城市的天气,例如是晴朗还是阴雨天,这些数据是不够的,除了特征以外,我们还需要每个城市的具体天气情况,也就是通常语境下的结果。在机器学习中,它会被称为标签(label),用于标记数据。值得注意的是,数据集中不一定包含标签信息,而这种区别会引起方法上的差别。我们可以给上述示例加上一组标签:


在机器学习中,数据集又可以分为三类:训练集(Training Set )、测试集(Testing Set)和交叉验证集(Cross-Validation Set)。 顾名思义,训练集在机器学习的过程中,用来训练我们模型的部分;而测试集用评估、测试模型泛化能力的部分;交叉验证集它是用来调整模型具体参数的数据集。
根据数据有没有标签,我们可以把机器学习分类为监督学习(Supervised Learning)、无监督学习(Unsupervised Learning)和强化学习(Reinforcement  Learning)。

监督学习是学习给定标签的数据集,比如说有一组气候数据,给出他们的详细资料,将它们作为判断或预测天气是晴天还是阴雨等标签的依据,然后预测明天是否会下雨,就是一种典型的监督学习。监督学习中也有不同的分类,如果我们训练的结果是阴雨、晴天之类离散的类型,则称为分类(Classification),如果只有两种类型的话可以进一步称为二分类(Binary Classification);如果我们训练的结果是下雨的概率为0.87之类连续的数字,则称为回归(Regression)。

无监督学习是学习没有标签的数据集,比如在分析大量语句之后,训练出一个模型将较为接近的词分为一类,而后可以根据一个新的词在句子中的用法(和其他信息)将这个词分入某一类中。其中比较微妙的地方在于,这种问题下使用聚类(Clustering)(方法)所获得的簇(Cluster)(结果),有时候是无法人为地观察出其特征的,但是在得到聚类后,可能会对数据集有新的启发。

强化学习是构建一个系统,在与环境交互的过程中提高系统的性能。环境的当前状态信息中通常包含一个反馈信号,这个反馈值不是一个确定的类标或连续类型的值,而是通过反馈函数产生的对当前系统行为的评价。通过探索性的试错或者借助精心设计的激励系统使得正向反馈最大化。象棋或围棋对弈就是一个常见的强化学习例子。下图为强化学习系统原理示意图:

 

对模型性能的评估,或对其泛化能力的评估,往往是一项重要内容。对分类问题我们通常利用准确率、ROC曲线等指标进行评估;对回归类模型,我们通常均方误差(MSE)、均方根误差(RMSE)等指标来确定模型的精确性。

但值得注意的是,模型并不是误差越小就一定越好,因为如果仅仅基于误差,我们可能会得到一个过拟合(Overfitting)的模型;但是如果不考虑误差,我们可能会得到一个欠拟合(Underfitting)的模型,用图像来说的话大致可以这样理解:

如果模型十分简单,往往会欠拟合,对于训练数据和测试数据的误差都会很大;但如果模型太过于复杂,往往会过拟合,那么训练数据的误差可能相当小,但是测试数据的误差会增大。所以需要“驰张有度”,找到最好的那个平衡点。

如果出现过拟合或欠拟合,有哪些解决方法呢?

1、对于欠拟合,一般可考虑提高数据质量、规范特征、增加新特征或训练数据量等方法;采用交叉验证及网格搜索参数等方法调优超参数(不是通过算法本身学习出来的参数,如迭代步数、树高度、步长等);采用其它算法如集成算法等等。

2、对于过拟合问题,可以考虑引入正则化,正则化指修改算法,使其降低泛化误差(而非降低训练误差);对于维数较大的情况,采用PCA降维也是选项之一。

在模型训练过程中,泛化误差不会随着模型复杂度趋于0,相反它一般呈现U型曲线,但训练误差一般训练复杂度增强而变小,直至趋于0。具体关系我们可以参考下图:

聚类问题的标准一般基于距离:簇内距离(Intra-cluster Distance)和簇间距离(Inter-cluster Distance)。根据常识而言,簇内距离是越小越好,也就是簇内的元素越相似越好;而簇间距离越大越好,也就是说簇间(不同簇)元素越不相同越好。

Theano是Python的一个库,为开源项目,在2008年,由Yoshua Bengio领导的加拿大蒙特利尔理工学院LISA实验室开发。对于解决大量数据的问题,使用Theano可能获得与手工用C实现差不多的性能。另外通过利用GPU,它能获得比CPU上的快很多数量级的性能。
Theano把计算机代数系统(CAS)和优化的编译器结合在一起。 它也可以对许多数学操作生成自定义的C代码。这种CAS和优化编译的组合对于有复杂数学表达式重复的被求值并且求值速度很关键的问题是非常有用的。对于许多不同的表达式只求值一次的场景,Theano也能最小化编译/分析的次数,但是仍然可以提供诸如自动差分这样的符号计算的特性。
Theano主要特点:
第一个特点:
使用“符号计算图”来描述模型表达式的开源架构,当前很多优秀的开源工具库或深度学习框架,如TensorFlow、Keras等,都借鉴了Theano的设计风格及其底层设计,因此,了解Theano的特点,尤其是符合图的机制对我们学好其它开源工具非常有帮助。
第二个特点:Theano针对多维数组(或张量),能够高效实现、编译和评估数学表达式,它同时还可以使得代码在GPU上执行。它没有专门提供深度学习相关的API,因此,用户构建模型时需要从最基本的网络层开始构建。
1、符号变量
Theano的变量类型称为符号变量,用TensorVariable表示,又称为张量,它是Theano表达式和运算操作的基本单位。创建符号变量的方式有如下几种:
 使用内置的变量类型创建
目前Theano支持7种内置的变量类型,分别是标量(scalar)、向量(vector)、行(row)、列(col)、矩阵(matrix)、tensor3、tensor4等。其中标量是0阶张量,向量为1阶张量,矩阵为二阶张量等,以下为创建内置变量的实例:

import theano
from theano import tensor as T
data=T.vector(name='data',dtype='float64')

其中,
name指定变量名字
dtype指变量的数据类型。
以下我们通过Theano的tensor模块中scalar来计算一维数据样本点x的输入z,权重假设为w,偏移量为b,即它们间的关系为:
z=w*x+b
实现这个表达式的代码如下:

import theano
from theano import tensor as T
#初始化
x=T.scalar()
w=T.scalar()
b=T.scalar()

z=w*x+b
#编译
net_input=theano.function(inputs=[w,x,b],outputs=z)
#执行
print('net input: %.2f' %net_input(3.0,2.0,0.1))

运算结果为:net input: 6.10
 自定义变量类型
内置的变量类型只能处理4维及以下的变量,如果需要处理更高维的数据时,我们可以使用Theano的自定义变量类型,具体通过TensorType方法来实现:

import theano
from theano import tensor as T

mytype=T.TensorType('float64',broadcastable=(),name=None,sparse_grad=False)

其中broadcastable是True或False的布尔类型元组,元组的大小等于变量的维度,如果为True,表示变量在对应维度上的数据可以进行广播,否则数据不能广播。
广播机制(broadcast)是一种重要机制,有了这种机制,就可以方便对不同维的张量进行运算,否则,就要手工把低维数据变成高维,利用广播机制系统自动利用复制等方法把低维数据补齐,numpy也有这种机制。以下我们通过一个实例来说明广播机制原理:

上图矩阵与向量相加的具体代码如下:

import theano
import numpy as np
import theano.tensor as T
r = T.row()
r.broadcastable
# (True, False)

mtr = T.matrix()
mtr.broadcastable
# (False, False)

f_row = theano.function([r, mtr], [r + mtr])
R = np.arange(1,3).reshape(1,2)
R
#array([[1, 2]])

M = numpy.arange(1,7).reshape(3, 2)
M
#array([[1, 2],
# [3, 4],
# [5, 6]])

f_row(R, M)
#[array([[ 2., 4.],
# [ 4., 6.],
# [ 6., 8.]])]

 将Python类型变量或者Numpy类型变量转化为Theano共享变量
共享变量是Theano实现变量更新的重要机制,后面我们会详细讲解。要创建一个共享变量,只要把一个Python对象或Numpy对象传递给shared函数即可。

import theano
import numpy as np
import theano.tensor as T

data=np.array([[1,2],[3,4]])
shared_data=theano.shared(data)
type(shared_data)

2.符号计算图模型
要定义一个符号表达式,首先创建表达式所需的变量,然后通过操作符(op)把这些变量结合在一起。
Theano处理符号表达式时通过把符号表达式转换为一个计算图(graph)来处理(TensorFlow也使用了这种方法,等到我们介绍TensorFlow时,大家可对比一下),符号计算图的节点有:variable、type、apply和op
variable节点:即符号的变量节点,符号变量是符号表达式存放信息的数据结构,可以分为输入符号和输出符号。
type节点:当定义了一种具体的变量类型以及变量的数据类型时,Theano为其指定数据存储的限制条件。
apply节点:把某一种类型的符号操作符应用到具体的符号变量中,与variable不同,apply节点无须由用户指定,一个apply节点包括3个字段:op、inputs、outputs。
op节点:即操作符节点,定义了一种符号变量间的运算,如+、-、sum()、tanh()等。
Theano是将符号表达式的计算表示成graphs。这些graphs是由将Apply 和 Variable节点内连接而组成的,它们是分别与函数的应用和数据相连接的。 操作是由 Op 实例表示,而数据类型是由 Type 实例表示。下面有一段代码和一个图表,该图表用来说明由这些代码所构建的结构。借助这个图或许有助于您理解如何将这些片拟合到一起:

import theano
import numpy as np
import theano.tensor as T

x = T.dmatrix('x')
y = T.dmatrix('y')
z = x + y

图中箭头表示指向python对象的引用。这里的蓝色盒子是一个 Apply 节点。红色盒子是 Variable 节点。绿色圆圈是Ops。紫色盒子是 Types。
在创建 Variables 之后,对它们应用 Apply Ops 从而得到更多的变量,并得到一个二分、有向、无环图。变量指向 Apply 节点的过程是用来表示函数通过它们的owner 域来生成它们 。这些Apply节点是通过它们的inputs和outputs域来得到它们的输入和输出变量的。
x 和 y 的owner 域的指向都是None是因为它们不是另一个计算的结果。如果它们中的一个是另一个计算的结果,那么owner域将会指向另一个的蓝色盒。
3.函数
函数是Theano的一个核心设计模块,我们了解了Theano如何把一个符号表达式转化为符号计算图,函数的功能则是提供一个接口,把函数计算图编译为可调用的函数对象。
3.1 Theano的编程风格
在theano中,我们一般是先声明自变量x(不需要赋值),然后编写函数方程结束后;最后在为自变量赋值,计算出函数的输出值y。如下示例:

import theano

#声明一个int类型的变量x
x=theano.tensor.iscalar('x')
#定义y=x^3
y=theano.tensor.pow(x,3)
#定义函数的自变量为x(输入),因变量为y(输出)
f=theano.function([x],y)
#计算当x=2的时候,函数f(x)的值
print (f(2))
#计算当x=4时,函数f(x)=x^3的值
print (f(4))
8
64

一旦我们定义了f(x)=x^3,这个时候,我们就可以输入我们想要的x值,然后计算出x的三次方。

3.2 函数的定义
Theano函数定义语法为:
theano.function(inputs, outputs, mode=None, updates=None, givens=None, no_default_updates=False, accept_inplace=False, name=None,rebuild_strict=True, allow_input_downcast=None, profile=None, on_unused_input='raise')。
参数看起来很多,但常用的一般只用到三个,inputs表示自变量、outputs表示函数的因变量(也就是函数的返回值),还有另外一个比较常用的是updates这个参数,这个一般用于神经网络共享变量参数更新,通常以字典或元组列表的形式指定;givens是一个字典或元组列表,记为[(var1,var2)],表示在每一次函数调用时,在符号计算图中,把符号变量var1节点替换为var2节点,该参数常用来指定训练数据集的batch大小。
我们看一个有多个自变量、同时又有多个因变量的函数定义例子:

import theano
x, y =theano.tensor.fscalars('x', 'y')
z1= x + y
z2=x*y
f =theano.function([x,y],[z1,z2])#定义x、y为自变量,z1、z2为函数返回值(因变量)
print(f(2,3))#返回当x=2,y=3的时候,函数f的因变量z1,z2的值
[array(5.0, dtype=float32), array(6.0, dtype=float32)]

3.3 重要函数
Theano有个很好用的函数,就是求函数的偏导数theano.grad(),比如下面S函数:

我们要求当x=3的时候,s函数的导数,代码如下:

import theano
x =theano.tensor.fscalar('x')#定义一个float类型的变量x
y= 1 / (1 + theano.tensor.exp(-x))#定义变量y
dx=theano.grad(y,x)#偏导数函数
f= theano.function([x],dx)#定义函数f,输入为x,输出为s函数的偏导数
print(f(3))#计算当x=3的时候,函数y的偏导数
0.045176658779382706

3.4 更新共享变量参数
在theano.function函数中,有个非常重要的参数updates,updates是一个包含两个元素的列表或tuple,updates=[old_w,new_w],当函数被调用的时候,这个会用new_w替换old_w,具体看下面这个例子

import theano
w= theano.shared(1)#定义一个共享变量w,其初始值为1
x=theano.tensor.iscalar('x')
f=theano.function([x], w, updates=[[w, w+x]])#定义函数自变量为x,因变量为w,当函数执行完毕后,更新参数w=w+x
print(f(3))#函数输出为w
print(w.get_value())#这个时候可以看到w=w+x为4
1
4

在求梯度下降的时候,经常用到updates这个参数。比如updates=[w,w-α*(dT/dw)],其中dT/dw就是我们梯度下降的时候,损失函数对参数w的偏导数,α是学习率。为便于大家的更全面的了解Theano函数一些使用,下面我们通过一个逻辑回归的完整实例来说明:

import numpy as np
import theano
import theano.tensor as T
rng = np.random

# 我们为了测试,自己生成10个样本,每个样本是3维的向量,然后用于训练
N = 10
feats = 3
D = (rng.randn(N, feats).astype(np.float32), rng.randint(size=N, low=0, high=2).astype(np.float32))

# 声明自变量x、以及每个样本对应的标签y(训练标签)
x = T.matrix("x")
y = T.vector("y")

#随机初始化参数w、b=0,为共享变量
w = theano.shared(rng.randn(feats), name="w")
b = theano.shared(0., name="b")

#构造损失函数
p_1 = 1 / (1 + T.exp(-T.dot(x, w) - b)) # s激活函数
xent = -y * T.log(p_1) - (1-y) * T.log(1-p_1) # 交叉商损失函数
cost = xent.mean() + 0.01 * (w ** 2).sum()# 损失函数的平均值+L2正则项以防过拟合,其中权重衰减系数为0.01
gw, gb = T.grad(cost, [w, b]) #对总损失函数求参数的偏导数

prediction = p_1 > 0.5 # 大于0.5预测值为1,否则为0.

train = theano.function(inputs=[x,y],outputs=[prediction, xent],updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))#训练所需函数
predict = theano.function(inputs=[x], outputs=prediction)#测试阶段函数

#训练
training_steps = 1000
for i in range(training_steps):
pred, err = train(D[0], D[1])
print (err.mean())#查看损失函数下降变化过程

4、共享变量
共享变量是实现机器学习算法参数更新的重要机制。对于一般的符号变量,没有赋予初始值。在编写深度学习程序时,需要对权重参数进行初始化,此时需要带有初始值的符号变量,这种带有初始值的符号变量称为共享变量,共享变量的初始值由Numpy数据指定。
创建共享变量有两种模型:深拷贝和浅拷贝。深拷贝复制Numpy数组,浅拷贝只是复制Numpy的指针,后续对Numpy的修改将影响浅拷贝的共享变量值。这两种模式通过borrow参数来设置,默认borrow=False(即深拷贝),下面我们通过一个实例来说明:

import numpy as np
import theano
np_array=np.ones(2,dtype='float32')
s_default=theano.shared(np_array)
s_f=theano.shared(np_array,borrow=False)
s_t=theano.shared(np_array,borrow=True)
np_array的值为:array([ 1., 1.], dtype=float32)

下面我们修改np_array的值,查看对不同模式的共享变量影响。

np_array+=2
np_array
# array([ 3., 3.], dtype=float32)
s_default.get_value()
# array([ 1., 1.], dtype=float32)
s_f. get_value()
# array([ 1., 1.], dtype=float32)
s_t. get_value()
# array([ 3., 3.], dtype=float32)

通过以上代码运行结果可以看到,修改numpy的np_array值,对深拷贝的共享变量s_default,s_f变量没有影响,对s_t共享变量有影响。
5、配置Theano
现在maeOS、Linux或windows,大都使用64位内存寻址方式。Numpy和Theano默认情况下,也都使用双精度浮点格式(float64),但是,如果想要使用GPU加速计算,一般依赖32位的内存寻址方式,这是目前Theano唯一支持的计算框架。CPU在32位和64位都可以。
修改Theano的配置,可以通过以下两种方法,优先级从高到底:
(1)通过设置THEANO_FLAGS环境变量;
(2)通过.theanorc文件来设置。
5.1通过THEANO_FLAGS配置
通过设置THEANO_FLAGS环境变量来修改Theano的配置,这种方式可以是全局的,也可以是针对某个脚本文件。THEANO_FLAGS以字符串的形式显示,字符串以逗号分隔,如下所示:
THEANO_FLAGS='floatX=float32,device=gpu0'
具体修改可以直接修改环境变量或在命令行下修改或在脚本中修改。在脚本中修改时,需要在导入theano之前修改,否则可能导致修改无效。如下示例:

import os
os.environ["THEANO_FLAGS"]= 'floatX=float32,device=cpu'
import theano
print(theano.config.floatX)
float64

5.2通过.theanorc文件配置
.theanorc文件一般在$HOME目录下,编辑这个文件添加如下内容即可:

[global]
device=cpu
floatX=float64

1、 数据可视化
无论是大数据、还是小数据、也不管通过统计还是挖掘或机器学习,人们最终想看到的数据,越直观越好,所以这个就涉及到一个数据的可视化问题,而python或pandas的数据可视化功能很强大,可画的种类多,也非常便捷,这是一般数据库软件和开发工具目前所欠缺的。以下我们通过两个实例来说明利用python的matplotlib或pandas实现数据的可视化。
下例利用matplotlib实现数据的可视化

In [1]: %paste
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif']=['SimHei'] ###显示中文
plt.rcParams['axes.unicode_minus']=False ##防止坐标轴上的-号变为方块
x = np.linspace(0, 10, 100)
y = np.sin(x)
y1 = np.cos(x)
##绘制一个图,长为10,宽为6(默认值是每个单位80像素)
plt.figure(figsize=(10,6))
###在图列中自动显示$间内容
plt.plot(x,y,label="$sin(x)$",color="red",linewidth=2)
plt.plot(x,y1,"b--",label="$cos(x^2)$") ###b(blue),--线形
plt.xlabel(u"X值") ##X坐标名称,u表示unicode编码
plt.ylabel(u"Y值")
plt.title(u"三角函数图像") ##t图名称
plt.ylim(-1.2,1.2) ##y上的max、min值
plt.legend() ##显示图例
plt.savefig('fig01.png') ##保持到当前目录
plt.show()

以下是运行结果:


(图 1-1 matplotlib数据可视化)

2、数据地图

下例通过matplotlib及mpl_toolkits.basemap实现地图数据的可视化。

# -*- coding: utf-8 -*-

from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams['font.sans-serif']=['SimHei']
#============================================# read data
names = []
pops = []
lats = []
lons = []
countries = []
for line in file("/home/hadoop/data/bigdata_map/china_city_jobs_stat.csv"):
info = line.split(',')
names.append(info[0])
pops.append(float(info[1]))
lat = float(info[2][:-1])
if info[2][-1] == 'S': lat = -lat
lats.append(lat)
lon = float(info[3][:-1])
if info[3][-1] == 'W': lon = -lon + 360.0
lons.append(lon)
country = info[4]
countries.append(country)

#============================================
#lat0 = 35;lon0 = 120;change = 25;
lat0 = 30;lon0 = 120;change = 26;
lllat=lat0-change; urlat=lat0+change;
lllon=lon0-change; urlon=lon0+change;

map = Basemap(ax=None,projection='stere',lon_0=(urlon + lllon) / 2,lat_0=(urlat + lllat) / 2,llcrnrlat=lllat, urcrnrlat=urlat,llcrnrlon=lllon,urcrnrlon=urlon,resolution='f')
# draw coastlines, country boundaries, fill continents.
map.drawcoastlines(linewidth=0.25)
map.drawcountries(linewidth=0.25)
# draw the edge of the map projection region (the projection limb)
map.drawmapboundary(fill_color='#689CD2')
# draw lat/lon grid lines every 30 degrees.
#map.drawmeridians(np.arange(0,360,30))
#map.drawparallels(np.arange(-90,90,30))
# Fill continent wit a different color
map.fillcontinents(color='green',lake_color='#689CD2',zorder=0)
# compute native map projection coordinates of lat/lon grid.
shapefilepath = '/home/hadoop/data/bigdata_map/map/map'
map.readshapefile(shapefilepath,'city') #添加街道数据
x, y = map(lons, lats)

max_pop = max(pops)
# Plot each city in a loop.
# Set some parameters
size_factor = 80.0
y_offset = 15.0
rotation = 30
for i,j,k,name in zip(x,y,pops,names):
size = size_factor*k/max_pop
cs = map.scatter(i,j,s=size,marker='o',color='yellow')
plt.text(i,j+y_offset,name,rotation=rotation,fontsize=1)

plt.title(u'中国大数据主要城市需求分布图(2017-03-17)')
plt.show()

运行结果如下图:


(图1-2中国大数据需求分别图)

【几点说明】
为了在图形中显示中文,做了如下处理:
(1)、把simhei.ttf文件上传到如下目录:
~/ anaconda2/lib/python2.7/site-packages/matplotlib/mpl-data/fonts/ttf
(2)、删除~/.cache/matplotlib下的缓存文件 ##删除matplotlib使用缺省字体
(3)、然后配置:

plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False ##防止坐标轴上负号(-)变为小正方形。

3、下例为使用pandas的plot函数画图非常简便。

df=DataFrame(np.random.randn(10,4).cumsum(0),columns=['A','B','C','D'], index=np.arange(0,100,10))
df.plot()

(图1-3使用pandas的plot函数画图)

第1章 pandas基础

1.1 pandas简介

“Pandas经过几个版本的更新,目前已经成为数据清洗、处理和分析的不二选择。”
前面我们介绍了NumPy,它提供了数据处理功能,但需要写很多命令,是否有更加便捷、直观、有效的方法?Pandas就是为此而诞生的。Pandas提供了众多更高级、更直观的数据处理功能,尤其是它的DataFrame数据结构,将给你全新的体验,可以用处理数据库表或电子表格的方式来处理分析数据。
Pandas基于NumPy构建的,它提供的结构或工具,让以NumPy为中心的数据处理、数据分析变得更加简单和高效。
Pandas中两个最常用的对象是Series和DataFrame。使用pandas前,需导入以下内容:

In [1]: import numpy as np
In [2]: from pandas import Series,DataFrame
In [3]: import pandas as pd

1.2 pandas数据结构

Pandas主要采用Series和DataFrame两种数据结构。Series是一种类似一维数据的数据结构,由数据(values)及索引(indexs)组成,而DataFrame是一个表格型的数据结构,它有一组有序列,每列的数据可以为不同类型(NumPy数据组中数据要求为相同类型),它既有行索引,也有列索引。

a1=np.array([1,2,3,4])
a2=np.array([5,6,7,8])
a3=np.array(['a','b','c','d'])
df=pd.DataFrame({'a':a1,'b':a2,'c':a3})
print df

1.3 Series

上章节我们介绍了多维数组(ndarray),当然,它也包括一维数组,Series类似一维数组,为啥还要介绍Series呢?或Series有哪些特点?
Series一个最大特点就是可以使用标签索引,序列及ndarray也有索引,但都是位置索引或整数索引,这种索引有很多局限性,如根据某个有意义标签找对应值?切片时采用类似[2:3]的方法,只能取索引为2这个元素等等,无法精确定位。
Series的标签索引(它位置索引自然保留)使用起来就方便多了,而且定位也更精确,不会产生歧义。举例说明。

In [1]: import numpy as np
In [2]: from pandas import Series,DataFrame
In [3]: import pandas as pd

In [4]: s1=Series([1,3,6,-1,2,8])

In [5]: s1
Out[5]:
0 1
1 3
2 6
3 -1
4 2
5 8
dtype: int64

In [6]: s1.values
Out[6]: array([ 1, 3, 6, -1, 2, 8])

In [7]: s1.index
Out[7]: RangeIndex(start=0, stop=6, step=1)
###创建Series时,自定义索引或称为标签索引
In [8]: s2=Series([1,3,6,-1,2,8],index=['a','c','d','e','b','g'])

In [9]: s2
Out[9]:
a 1
c 3
d 6
e -1
b 2
g 8
dtype: int64

In [10]: s2['a'] ###根据标签索引找对应值
Out[10]: 1
In [11]: s2[['a','e']] ###根据标签索引找对应值
Out[11]:
a 1
e -1
dtype: int64

当然,Series除了标签索引外,还有其它很多优点,如运算的简洁:

In [15]: s2[s2>1]
Out[15]:
c 3
d 6
b 2
g 8
dtype: int64

In [16]: s2*10
Out[16]:
a 10
c 30
d 60
e -10
b 20
g 80
dtype: int64

1.4 DataFrame

DataFrame除了索引有位置索引也有标签索引,而且其数据组织方式与MySQL的表极为相似,除了形式相似,很多操作也类似,这就给我们操作DataFrame带来极大方便。这些是DataFrame特色的一小部分,它还有比数据库表更强大的功能,如强大统计、可视化等等。
DataFrame几要素:index、columns、values等,columns就像数据库表的列表,index是索引,当然values就是值了。

In [18]:
####自动生成一个3行4列的DataFrame,并定义其索引(如果不指定,缺省为整数索引)####及列名
d1=DataFrame(np.arange(12).reshape((3,4)),index=['a','b','c'],columns=['a1','a2','a3','a4'])

In [19]: d1
Out[19]:
a1 a2 a3 a4
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11

In [20]: d1.index ##显示索引
Out[20]: Index([u'a', u'b', u'c'], dtype='object')

In [21]: d1.columns ##显示列名
Out[21]: Index([u'a1', u'a2', u'a3', u'a4'], dtype='object')

In [22]: d1.values ##显示值
Out[22]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])

1.4.1 生成DataFrame

生成DataFrame有很多,比较常用的有导入等长列表、字典、numpy数组、数据文件等。

In [33]: data={'name':['zhanghua','liuting','gaofei','hedong'],'age':[40,45,50,46],'addr':['jianxi','pudong','beijing','xian']}

In [34]: d2=DataFrame(data)

In [35]: d2
Out[35]:
addr age name
0 jianxi 40 zhanghua
1 pudong 45 liuting
2 beijing 50 gaofei
3 xian 46 hedong

In [36]: d3=DataFrame(data,columns=['name','age','addr'],index=['a','b','c','d'])

In [37]: d3
Out[37]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian

1.4.2 获取数据

获取DataFrame结构中数据可以采用obj[]操作、查询、obj.ix[]等命令。

In [8]: data={'name':['zhanghua','liuting','gaofei','hedong'],'age':[40,45,50,46],'addr':['jianxi','pudong','beijing','xian']}
###把字典数据转换为DataFrame,并指定索引
In [9]: d3=DataFrame(data,columns=['name','age','addr'],index=['a','b','c','d'])

In [10]: d3
Out[10]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian

In [11]: d3[['name','age']] ##选择列
Out[11]:
name age
a zhanghua 40
b liuting 45
c gaofei 50
d hedong 46

In [12]: d3['a':'c'] ##选择行
Out[12]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing

In [13]: d3[1:3] ##选择行(利用位置索引)
Out[13]:
name age addr
b liuting 45 pudong
c gaofei 50 beijing
In [14]: d3[d3['age']>40] ###使用过滤条件
Out[14]:
name age addr
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian
obj.ix[indexs,[columns]]可以根据列或索引同时进行过滤,具体请看下例:
In [16]: d3.ix[['a','c'],['name','age']]
Out[16]:
name age
a zhanghua 40
c gaofei 50

In [17]: d3.ix['a':'c',['name','age']]
Out[17]:
name age
a zhanghua 40
b liuting 45
c gaofei 50

In [18]: d3.ix[0:3,['name','age']]
Out[18]:
name age
a zhanghua 40
b liuting 45
c gaofei 50

1.4.3 修改数据

我们可以像操作数据库表一样操作DataFrame,删除数据,插入数据、修改字段名、索引名、修改数据等,以下通过一些实例来说明。

In [9]: data={'name':['zhanghua','liuting','gaofei','hedong'],'age':[40,45,50,46],'addr':['jianxi','pudong','beijing','xian']}

In [10]: d3=DataFrame(data,columns=['name','age','addr'],index=['a','b','c','d'])

In [11]: d3
Out[11]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian

In [12]: d3.drop('d',axis=0) ###删除行,如果欲删除列,使axis=1即可
Out[12]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
In [13]: d3 ###从副本中删除,原数据没有被删除
Out[13]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian
###添加一行,注意需要ignore_index=True,否则会报错
In [14]: d3.append({'name':'wangkuan','age':38,'addr':'henan'},ignore_index=True)
Out[14]:
name age addr
0 zhanghua 40 jianxi
1 liuting 45 pudong
2 gaofei 50 beijing
3 hedong 46 xian
4 wangkuan 38 henan

In [15]: d3 ###原数据未变
Out[15]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian
###添加一行,并创建一个新DataFrame
In [16]: d4=d3.append({'name':'wangkuan','age':38,'addr':'henan'},ignore_index=True)

In [17]: d4
Out[17]:
name age addr
0 zhanghua 40 jianxi
1 liuting 45 pudong
2 gaofei 50 beijing
3 hedong 46 xian
4 wangkuan 38 henan

In [18]: d4.index=['a','b','c','d','e'] ###修改d4的索引

In [19]: d4
Out[19]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian
e wangkuan 38 henan
In [20]: d4.ix['e','age']=39 ###修改索引为e列名为age的值

In [21]: d4
Out[21]:
name age addr
a zhanghua 40 jianxi
b liuting 45 pudong
c gaofei 50 beijing
d hedong 46 xian
e wangkuan 39 henan

1.4.4 汇总统计

Pandas有一组常用的统计方法,可以根据不同轴方向进行统计,当然也可按不同的列或行进行统计,非常方便。
常用的统计方法有:


(表1-1 Pandas统计方法)
统计方法 说明
count 统计非NA的数量
describe 统计列的汇总信息
min、max 计算最小值和最大值
sum 求总和
mean 求平均数
var 样本的方差
std 样本的标准差
以下通过实例来说明这些方法的使用

from pandas import DataFrame
import numpy as np
import pandas as pd
inputfile = '/home/hadoop/data/stud_score.csv'
data = pd.read_csv(inputfile)
#其他参数,
###header=None 表示无标题,此时缺省列名为整数;如果设为0,表示第0行为标题
###names,encoding,skiprows等
#读取excel文件,可用 read_excel
In [7]: df=DataFrame(data)

In [8]: df.head(3) ###显示前3行
Out[8]:
stud_code sub_code sub_nmae sub_tech sub_score stat_date
0 2.015101e+09 10101.0 数学分析 NaN 90.0 NaN
1 2.015101e+09 10102.0 高等代数 NaN 88.0 NaN
2 2.015101e+09 10103.0 大学物理 NaN 67.0 NaN

In [9]: df.count()
Out[9]:
stud_code 121
sub_code 121
sub_nmae 121
sub_tech 0
sub_score 121
stat_date 0
dtype: int64

In [10]: df['sub_score'].describe() ##汇总学生各科成绩
Out[10]:
count 121.000000
mean 78.561983
std 12.338215
min 48.000000
25% 69.000000
50% 80.000000
75% 89.000000
max 98.000000
Name: sub_score, dtype: float64

In [11]: df['sub_score'].std() ##求学生成绩的标准差
Out[11]: 12.338214729032906

注:DataFrame数据结构的函数或方法有很多,大家可以通过df.[Tab键]方式查看,具体命令的使用方法,如df.count(),可以在Ipython命令行下输入:?df.count() 查看具体使用,退出帮助界面,按q即可。

1.4.5 应用函数及映射

我们知道数据库中有很多函数可用作用于表中元素,DataFrame也可将函数(内置或自定义)应用到各列或行上,而且非常方便和简洁,具体可用通过DataFrame的apply,使或applymap或map,也可以作用到元素级。以下通过实例说明具体使用。

In [23]: d1=DataFrame(np.arange(12).reshape((3,4)),index=['a','b','c'],columns=['a1','a2','a3','a4'])

In [24]: d1
Out[24]:
a1 a2 a3 a4
a 0 1 2 3
b 4 5 6 7
c 8 9 10 11

In [25]: d1.apply(lambda x:x.max()-x.min(),axis=0) ###列级处理
Out[25]:
a1 8
a2 8
a3 8
a4 8
dtype: int64

In [26]: d1.applymap(lambda x:x*2) ###处理每个元素
Out[26]:
a1 a2 a3 a4
a 0 2 4 6
b 8 10 12 14
c 16 18 20 22
In [27]: d1.ix[1]
Out[27]:
a1 4
a2 5
a3 6
a4 7
Name: b, dtype: int64
In [28]: d1.ix[1].map(lambda x:x*2) ###处理每行数据
Out[28]:
a1 8
a2 10
a3 12
a4 14
Name: b, dtype: int64

1.4.6 时间序列

pandas最基本的时间序列类型就是以时间戳(时间点)(通常以python字符串或datetime对象表示)为索引的Series:

dates = ['2017-06-20','2017-06-21','2017-06-22','2017-06-23','2017-06-24']
ts = pd.Series(np.random.randn(5),index = pd.to_datetime(dates))
ts
####ts结果为
2017-06-20 -1.360504
2017-06-21 -0.966608
2017-06-22 0.754748
2017-06-23 0.832451
2017-06-24 -0.307611
dtype: float64
索引为日期的DataFrame数据的索引、选取以及子集构造
ts.index
###显示结果
DatetimeIndex(['2017-06-20', '2017-06-21', '2017-06-22', '2017-06-23',
'2017-06-24'],
dtype='datetime64[ns]', freq=None)

#传入可以被解析成日期的字符串
ts['2017-06-21']
###显示结果
-0.96660788809762366
#传入年或年月
ts['2017-06']
###显示结果
2017-06-20 -1.360504
2017-06-21 -0.966608
2017-06-22 0.754748
2017-06-23 0.832451
2017-06-24 -0.307611
dtype: float64

#时间范围进行切片
ts['2017-06-20':'2017-06-22']
###显示结果
2017-06-20 -1.360504
2017-06-21 -0.966608
2017-06-22 0.754748
dtype: float64

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语言级别的循环计算。

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章 分析京东客户评价数据

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