Merge branch 'jackfrued:master' into master

This commit is contained in:
fanbiao 2022-01-22 08:14:35 +08:00 committed by GitHub
commit bac8c75f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 9518 additions and 4811 deletions

View File

@ -10,7 +10,7 @@
4. 2000年10月16日Python 2.0发布,增加了完整的[垃圾回收](https://zh.wikipedia.org/wiki/%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8)),提供了对[Unicode](https://zh.wikipedia.org/wiki/Unicode)的支持。与此同时Python的整个开发过程更加透明社区对开发进度的影响逐渐扩大生态圈开始慢慢形成。
5. 2008年12月3日Python 3.0发布它并不完全兼容之前的Python代码不过因为目前还有不少公司在项目和运维中使用Python 2.x版本所以Python 3.x的很多新特性后来也被移植到Python 2.6/2.7版本中。
目前我使用的Python 3.7.x的版本是在2018年发布的Python的版本号分为三段形如A.B.C。其中A表示大版本号一般当整体重写或出现不向后兼容的改变时增加AB表示功能更新出现新功能时增加BC表示小的改动例如修复了某个Bug只要有修改就增加C。如果对Python的历史感兴趣可以阅读名为[《Python简史》](http://www.cnblogs.com/vamei/archive/2013/02/06/2892628.html)的网络文章。
目前我使用的Python 3.7.x的版本是在2018年发布的Python的版本号分为三段形如A.B.C。其中A表示大版本号一般当整体重写或出现不向后兼容的改变时增加AB表示功能更新出现新功能时增加BC表示小的改动例如修复了某个Bug只要有修改就增加C。如果对Python的历史感兴趣可以阅读名为[《Python简史》](http://www.cnblogs.com/vamei/archive/2013/02/06/2892628.html)的网络文章。
#### Python的优缺点
@ -218,7 +218,7 @@ pip3 install ipython
#### PyCharm - Python开发神器
PyCharm的安装、配置和使用在[《玩转PyCharm》](../玩转PyCharm.md)进行了介绍,有兴趣的读者可以选择阅读。
PyCharm的安装、配置和使用在[《玩转PyCharm》](../番外篇/玩转PyCharm.md)进行了介绍,有兴趣的读者可以选择阅读。
![](./res/python-pycharm.png)

View File

@ -193,7 +193,7 @@ c = (f - 32) / 1.8
print('%.1f华氏度 = %.1f摄氏度' % (f, c))
```
> **说明**:在使用`print`函数输出时,也可以对字符串内容进行格式化处理,上面`print`函数中的字符串`%1.f`是一个占位符,稍后会由一个`float`类型的变量值替换掉它。同理,如果字符串中有`%d`,后面可以用一个`int`类型的变量值替换掉它,而`%s`会被字符串的值替换掉。除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串,其中`{f:.1f}`和`{c:.1f}`可以先看成是`{f}`和`{c}`,表示输出时会用变量`f`和变量`c`的值替换掉这两个占位符,后面的`:.1f`表示这是一个浮点数小数点后保留1位有效数字。
> **说明**:在使用`print`函数输出时,也可以对字符串内容进行格式化处理,上面`print`函数中的字符串`%.1f`是一个占位符,稍后会由一个`float`类型的变量值替换掉它。同理,如果字符串中有`%d`,后面可以用一个`int`类型的变量值替换掉它,而`%s`会被字符串的值替换掉。除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串,其中`{f:.1f}`和`{c:.1f}`可以先看成是`{f}`和`{c}`,表示输出时会用变量`f`和变量`c`的值替换掉这两个占位符,后面的`:.1f`表示这是一个浮点数小数点后保留1位有效数字。
>
> ```Python
> print(f'{f:.1f}华氏度 = {c:.1f}摄氏度')

View File

@ -168,7 +168,7 @@ y = int(input('y = '))
if x > y:
# 通过下面的操作将y的值赋给x, 将x的值赋给y
x, y = y, x
# 从两个数中较的数开始做递减的循环
# 从两个数中较的数开始做递减的循环
for factor in range(x, 0, -1):
if x % factor == 0 and y % factor == 0:
print('%d和%d的最大公约数是%d' % (x, y, factor))

View File

@ -200,7 +200,7 @@ from module1 import foo
foo()
```
需要说明的是,如果我们导入的模块除了定义函数之外还有可以执行代码那么Python解释器在导入这个模块时就会执行这些代码事实上我们可能并不希望如此因此如果我们在模块中编写了执行代码最好是将这些执行代码放入如下所示的条件中这样的话除非直接运行该模块if条件下的这些代码是不会执行的因为只有直接执行的模块的名字才是"\_\_main\_\_"
需要说明的是如果我们导入的模块除了定义函数之外还有可以执行代码那么Python解释器在导入这个模块时就会执行这些代码事实上我们可能并不希望如此因此如果我们在模块中编写了执行代码最好是将这些执行代码放入如下所示的条件中这样的话除非直接运行该模块if条件下的这些代码是不会执行的因为只有直接执行的模块的名字才是"\_\_main\_\_"
`module3.py`

View File

@ -2,7 +2,7 @@
### 使用字符串
第二次世界大战促使了现代电子计算机的诞生最初计算机被应用于导弹弹道的计算而在计算机诞生后的很多年时间里计算机处理的信息基本上都是数值型的信息。世界上的第一台电子计算机叫ENIAC电子数值积分计算机诞生于美国的宾夕法尼亚大学每秒钟能够完成约5000次浮点运算。随着时间的推移虽然数值运算仍然是计算机日常工作中最为重要的事情之一但是今天的计算机处理得更多的数据可能都是以文本的方式存在的如果我们希望通过Python程序操作这些文本信息,就必须要先了解字符串类型以及与它相关的知识。
第二次世界大战促使了现代电子计算机的诞生最初计算机被应用于导弹弹道的计算而在计算机诞生后的很多年时间里计算机处理的信息基本上都是数值型的信息。世界上的第一台电子计算机叫ENIAC电子数值积分计算机诞生于美国的宾夕法尼亚大学每秒钟能够完成约5000次浮点运算。随着时间的推移虽然数值运算仍然是计算机日常工作中最为重要的事情之一但是今天的计算机处理得更多的数据可能都是以文本的方式存在的如果我们希望通过Python程序操作这些文本信息就必须要先了解字符串类型以及与它相关的知识。
所谓**字符串**,就是由零个或多个字符组成的有限序列,一般记为![$${\displaystyle s=a_{1}a_{2}\dots a_{n}(0\leq n \leq \infty)}$$](./res/formula_5.png)。在Python程序中如果我们把单个或多个字符用单引号或者双引号包围起来就可以表示一个字符串。

View File

@ -9,7 +9,7 @@ Date: 2018-03-02
"""
import math
for num in range(1, 10000):
for num in range(2, 10000):
result = 0
for factor in range(1, int(math.sqrt(num)) + 1):
if num % factor == 0:

View File

@ -10,24 +10,23 @@ Date: 2018-03-20
# 每个进程都有自己独立的内存空间 所以进程之间共享数据只能通过IPC的方式
from multiprocessing import Process, Queue
from multiprocessing import Process, Queue, current_process
from time import sleep
def sub_task(string, q):
number = q.get()
while number:
print('%d: %s' % (number, string))
sleep(0.001)
number = q.get()
def sub_task(content, counts):
print(f'PID: {current_process().pid}')
counter = 0
while counter < counts:
counter += 1
print(f'{counter}: {content}')
sleep(0.01)
def main():
q = Queue(10)
for number in range(1, 11):
q.put(number)
Process(target=sub_task, args=('Ping', q)).start()
Process(target=sub_task, args=('Pong', q)).start()
number = random.randrange(5, 10)
Process(target=sub_task, args=('Ping', number)).start()
Process(target=sub_task, args=('Pong', number)).start()
if __name__ == '__main__':

View File

@ -85,7 +85,7 @@
常用的工具类:
- `namedtuple`:命令元组,它是一个类工厂,接受类型的名称和属性列表来创建一个类。
- `deque`双端队列是列表的替代实现。Python中的列表底层是基于数组来实现的而deque底层是双向链表因此当你需要在头尾添加和删除元素deque会表现出更好的性能渐近时间复杂度为$O(1)$。
- `deque`双端队列是列表的替代实现。Python中的列表底层是基于数组来实现的而deque底层是双向链表因此当你需要在头尾添加和删除元素deque会表现出更好的性能渐近时间复杂度为$O(1)$。
- `Counter``dict`的子类,键是元素,值是元素的计数,它的`most_common()`方法可以帮助我们获取出现频率最高的元素。`Counter`和`dict`的继承关系我认为是值得商榷的按照CARP原则`Counter`跟`dict`的关系应该设计为关联关系更为合理。
- `OrderedDict``dict`的子类,它记录了键值对插入的顺序,看起来既有字典的行为,也有链表的行为。
- `defaultdict`:类似于字典类型,但是可以通过默认的工厂函数来获得键对应的默认值,相比字典中的`setdefault()`方法,这种做法更加高效。
@ -1110,19 +1110,6 @@ Python中实现并发编程的三种方案多线程、多进程和异步I/O
self.balance = new_balance
class AddMoneyThread(threading.Thread):
"""自定义线程类"""
def __init__(self, account, money):
self.account = account
self.money = money
# 自定义线程的初始化方法中必须调用父类的初始化方法
super().__init__()
def run(self):
# 线程启动之后要执行的操作
self.account.deposit(self.money)
def main():
"""主函数"""
account = Account()
@ -1130,14 +1117,6 @@ Python中实现并发编程的三种方案多线程、多进程和异步I/O
pool = ThreadPoolExecutor(max_workers=10)
futures = []
for _ in range(100):
# 创建线程的第1种方式
# threading.Thread(
# target=account.deposit, args=(1, )
# ).start()
# 创建线程的第2种方式
# AddMoneyThread(account, 1).start()
# 创建线程的第3种方式
# 调用线程池中的线程来执行特定的任务
future = pool.submit(account.deposit, 1)
futures.append(future)
# 关闭线程池

View File

@ -2,6 +2,8 @@
> **说明**:本文使用的部分插图来自*Jon Duckett*先生的*[HTML and CSS: Design and Build Websites](https://www.amazon.cn/dp/1118008189/ref=sr_1_5?__mk_zh_CN=%E4%BA%9A%E9%A9%AC%E9%80%8A%E7%BD%91%E7%AB%99&keywords=html+%26+css&qid=1554609325&s=gateway&sr=8-5)*一书,这是一本非常棒的前端入门书,有兴趣的读者可以在亚马逊或者其他网站上找到该书的购买链接。
HTML 是用来描述网页的一种语言,全称是 Hyper-Text Markup Language即超文本标记语言。我们浏览网页时看到的文字、按钮、图片、视频等元素它们都是通过 HTML 书写并通过浏览器来呈现的。
### HTML简史
1. 1991年10月一个非正式CERN[欧洲核子研究中心](https://zh.wikipedia.org/wiki/%E6%AD%90%E6%B4%B2%E6%A0%B8%E5%AD%90%E7%A0%94%E7%A9%B6%E7%B5%84%E7%B9%94)文件首次公开18个HTML标签这个文件的作者是物理学家[蒂姆·伯纳斯-李](https://zh.wikipedia.org/wiki/%E8%92%82%E5%A7%86%C2%B7%E4%BC%AF%E7%BA%B3%E6%96%AF-%E6%9D%8E),因此他是[万维网](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91)的发明者,也是[万维网联盟](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91%E8%81%94%E7%9B%9F)的主席。
@ -25,6 +27,10 @@
### 使用标签承载内容
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211107163448.png" style="zoom:35%">
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211107163741.png" style="zoom:75%">
#### 结构
- html
@ -93,11 +99,11 @@
- 重要属性 - action / method / enctype
- 表单控件input- type属性
- 文本框 - text / 密码框 - password / 数字框 - number
- 邮箱 - email / 电话 - tel / 日期 - date / 滑条 - range / URL - url / 搜索 - search
- 单选按钮 - radio / 复选按钮 - checkbox
- 文件上传 - file / 隐藏域 - hidden
- 提交按钮 - submit / 图像按钮 - image / 重置按钮 - reset
- 文本框 - `text` / 密码框 - `password` / 数字框 - `number`
- 邮箱 - `email` / 电话 - `tel` / 日期 - `date` / 滑条 - `range` / URL - `url` / 搜索 - `search`
- 单选按钮 - `radio` / 复选按钮 - `checkbox`
- 文件上传 - `file` / 隐藏域 - `hidden`
- 提交按钮 - `submit` / 图像按钮 - `image` / 重置按钮 - `reset`
- 下拉列表 - select / option
- 文本域(多行文本)- textarea
- 组合表单元素 - fieldset / legend
@ -259,7 +265,7 @@
- 赋值运算符
- 算术运算符
- 比较运算符
- 逻辑运算符
- 逻辑运算符`&&`、`||`、`!`
- 分支结构
- `if...else...`
- `switch...cas...default...`

View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>垃圾分类查询助手</title>
<style>
.search, .result {
width: 720px;
margin: 50px auto;
}
.search > input {
width: 520px;
border: none;
outline: none;
text-align: center;
font-size: 36px;
line-height: 36px;
border-bottom: 1px solid gray;
margin: 0 20px;
}
.search button {
background-color: red;
color: white;
font-size: 28px;
border: none;
outline: none;
width: 120px;
}
.result > p, .result > div {
width: 640px;
margin: 0 auto;
}
.result > p, .result span {
text-align: left;
font-size: 28px;
}
.result img {
vertical-align: middle;
}
.explain {
font-size: 12px;
color: darkgray;
}
.result .pre {
font-size: 16px;
}
</style>
</head>
<body>
<div id="app">
<div class="search">
<input type="text" placeholder="请输入垃圾名字" v-model.trim="word" @keydown.enter="search()">
<button @click="search()">查询</button>
</div>
<div class="result">
<p v-if="searched && !results">没有对应的查询结果</p>
<div v-for="result in results">
<p>
<img :src="'images/' + pictures[result.type]" width="56" :alt="types[result.type]">
&nbsp;&nbsp;
<span>{{ result.name }}</span>
&nbsp;&nbsp;
<span class="pre" v-if="result.aipre == 1">(预测结果)</span>
</p>
<p class="explain">说明:{{ result.explain }}</p>
</div>
</div>
</div>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
word: '',
searched: false,
types: ['可回收物', '有害垃圾', '厨余垃圾', '其他垃圾'],
pictures: ['recyclable.png', 'harmful-waste.png', 'kitchen-waste.png', 'other-waste.png'],
results: []
},
methods: {
search() {
if (this.word.trim().length > 0) {
let key = 'e8c5524dd2a365f20908ced735f8e480'
let url = `http://api.tianapi.com/txapi/lajifenlei/?key=${key}&word=${this.word}`
fetch(url)
.then(resp => resp.json())
.then(json => {
this.searched = true
this.results = json.newslist
})
}
}
}
})
</script>
</body>
</html>

View File

@ -748,7 +748,7 @@ Linux系统的命令通常都是如下所示的格式
4. 查看和修改密码有效期 - **chage**
设置hellokitty用户100天后必须修改密码过期前15天通知该用户过期后15天禁用该用户。
设置hellokitty用户100天后必须修改密码过期前15天通知该用户过期后7天禁用该用户。
```Shell
chage -M 100 -W 15 -I 7 hellokitty

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,388 @@
## 关系型数据库和MySQL概述
### 关系型数据库概述
1. 数据持久化 - 将数据保存到能够长久保存数据的存储介质中,在掉电的情况下数据也不会丢失。
2. 数据库发展史 - 网状数据库、层次数据库、关系数据库、NoSQL 数据库、NewSQL 数据库。
> 1970年IBM的研究员E.F.Codd在*Communication of the ACM*上发表了名为*A Relational Model of Data for Large Shared Data Banks*的论文,提出了**关系模型**的概念奠定了关系模型的理论基础。后来Codd又陆续发表多篇文章论述了范式理论和衡量关系系统的12条标准用数学理论奠定了关系数据库的基础。
3. 关系数据库特点。
- 理论基础:**关系代数**(关系运算、集合论、一阶谓词逻辑)。
- 具体表象:用**二维表**(有行和列)组织数据。
- 编程语言:**结构化查询语言**SQL
4. ER模型实体关系模型和概念模型图。
**ER模型**,全称为**实体关系模型**Entity-Relationship Model由美籍华裔计算机科学家陈品山先生提出是概念数据模型的高层描述方式如下图所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210826003119.png" width="75%">
- 实体 - 矩形框
- 属性 - 椭圆框
- 关系 - 菱形框
- 重数 - 1:1一对一 / 1:N一对多 / M:N多对多
实际项目开发中我们可以利用数据库建模工具PowerDesigner来绘制概念数据模型其本质就是 ER 模型),然后再设置好目标数据库系统,将概念模型转换成物理模型,最终生成创建二维表的 SQL很多工具都可以根据我们设计的物理模型图以及设定的目标数据库来导出 SQL 或直接生成数据表)。
![](https://gitee.com/jackfrued/mypic/raw/master/20210826003212.png)
5. 关系数据库产品。
- [Oracle](https://www.oracle.com/index.html) - 目前世界上使用最为广泛的数据库管理系统,作为一个通用的数据库系统,它具有完整的数据管理功能;作为一个关系数据库,它是一个完备关系的产品;作为分布式数据库,它实现了分布式处理的功能。在 Oracle 最新的 12c 版本中,还引入了多承租方架构,使用该架构可轻松部署和管理数据库云。
- [DB2](https://www.ibm.com/analytics/us/en/db2/) - IBM 公司开发的、主要运行于 Unix包括 IBM 自家的 [AIX](https://zh.wikipedia.org/wiki/AIX)、Linux、以及 Windows 服务器版等系统的关系数据库产品。DB2 历史悠久且被认为是最早使用 SQL 的数据库产品,它拥有较为强大的商业智能功能。
- [SQL Server](https://www.microsoft.com/en-us/sql-server/) - 由 Microsoft 开发和推广的关系型数据库产品,最初适用于中小企业的数据管理,但是近年来它的应用范围有所扩展,部分大企业甚至是跨国公司也开始基于它来构建自己的数据管理系统。
- [MySQL](https://www.mysql.com/) - MySQL 是开放源代码的,任何人都可以在 GPLGeneral Public License的许可下下载并根据个性化的需要对其进行修改。MySQL 因为其速度、可靠性和适应性而备受关注。
- [PostgreSQL]() - 在 BSD 许可证下发行的开放源代码的关系数据库产品。
### MySQL 简介
MySQL 最早是由瑞典的 MySQL AB 公司开发的一个开放源码的关系数据库管理系统该公司于2008年被昇阳微系统公司Sun Microsystems收购。在2009年甲骨文公司Oracle收购昇阳微系统公司因此 MySQL 目前也是 Oracle 旗下产品。
MySQL 在过去由于性能高、成本低、可靠性好,已经成为最流行的开源数据库,因此被广泛地应用于中小型网站开发。随着 MySQL 的不断成熟它也逐渐被应用于更多大规模网站和应用比如维基百科、谷歌Google、脸书Facebook、淘宝网等网站都使用了 MySQL 来提供数据持久化服务。
甲骨文公司收购后昇阳微系统公司,大幅调涨 MySQL 商业版的售价,且甲骨文公司不再支持另一个自由软件项目 [OpenSolaris ](https://zh.wikipedia.org/wiki/OpenSolaris) 的发展,因此导致自由软件社区对于 Oracle 是否还会持续支持 MySQL 社区版MySQL 的各个发行版本中唯一免费的版本有所担忧MySQL 的创始人麦克尔·维德纽斯以 MySQL 为基础,创建了 [MariaDB](https://zh.wikipedia.org/wiki/MariaDB)(以他女儿的名字命名的数据库)分支。有许多原来使用 MySQL 数据库的公司(例如:维基百科)已经陆续完成了从 MySQL 数据库到 MariaDB 数据库的迁移。
### 安装 MySQL
#### Windows 环境
1. 通过[官方网站](https://www.mysql.com/)提供的[下载链接](https://dev.mysql.com/downloads/windows/installer/8.0.html)下载“MySQL社区版服务器”安装程序如下图所示建议大家下载离线安装版的MySQL Installer。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105230905.png" style="zoom:50%">
2. 运行 Installer按照下面的步骤进行安装。
- 选择自定义安装。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105231152.jpg" style="zoom:35%">
- 选择需要安装的组件。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105231255.jpg" style="zoom:35%">
- 如果缺少依赖项,需要先安装依赖项。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105231620.png" style="zoom:35%">
- 准备开始安装。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105231719.jpg" style="zoom:35%">
- 安装完成。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232024.jpg" style="zoom:35%">
- 准备执行配置向导。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105231815.jpg" style="zoom:35%">
3. 执行安装后的配置向导。
- 配置服务器类型和网络。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232109.jpg" style="zoom:35%">
- 配置认证方法(保护密码的方式)。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232408.jpg" style="zoom:35%">
- 配置用户和角色。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232521.jpg" style="zoom:35%">
- 配置Windows服务名以及是否开机自启。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232608.jpg" style="zoom:35%">
- 配置日志。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232641.jpg" style="zoom:35%">
- 配置高级选项。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232724.jpg" alt="ACAC15B8633133B65476286A49BFBD7E" style="zoom:35%">
- 应用配置。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232800.jpg" style="zoom:35%">
4. 可以在 Windows 系统的“服务”窗口中启动或停止 MySQL。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105232926.jpg" style="zoom:50%">
5. 配置 PATH 环境变量,以便在命令行提示符窗口使用 MySQL 客户端工具。
- 打开 Windows 的“系统”窗口并点击“高级系统设置”。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105233054.jpg" style="zoom:50%">
- 在“系统属性”的“高级”窗口,点击“环境变量”按钮。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105233312.jpg" style="zoom:50%">
- 修改PATH环境变量将MySQL安装路径下的`bin`文件夹的路径配置到PATH环境变量中。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105233359.jpg" style="zoom:50%">
- 配置完成后,可以尝试在“命令提示符”下使用 MySQL 的命令行工具。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211105233643.jpg" style="zoom:50%">
#### Linux 环境
下面以 CentOS 7.x 环境为例,演示如何安装 MySQL 5.7.x如果需要在其他 Linux 系统下安装其他版本的 MySQL请读者自行在网络上查找对应的安装教程。
1. 安装 MySQL。
可以在 [MySQL 官方网站](<https://www.mysql.com/>)下载安装文件。首先在下载页面中选择平台和版本,然后找到对应的下载链接,直接下载包含所有安装文件的归档文件,解归档之后通过包管理工具进行安装。
```Shell
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.26-1.el7.x86_64.rpm-bundle.tar
tar -xvf mysql-5.7.26-1.el7.x86_64.rpm-bundle.tar
```
如果系统上有 MariaDB 相关的文件,需要先移除 MariaDB 相关的文件。
```Shell
yum list installed | grep mariadb | awk '{print $1}' | xargs yum erase -y
```
更新和安装可能用到的底层依赖库。
```Bash
yum update
yum install -y libaio libaio-devel
```
接下来可以按照如下所示的顺序用 RPMRedhat Package Manager工具安装 MySQL。
```Shell
rpm -ivh mysql-community-common-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-libs-compat-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-devel-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-client-5.7.26-1.el7.x86_64.rpm
rpm -ivh mysql-community-server-5.7.26-1.el7.x86_64.rpm
```
可以使用下面的命令查看已经安装的 MySQL 相关的包。
```Shell
rpm -qa | grep mysql
```
2. 配置 MySQL。
MySQL 的配置文件在`/etc`目录下,名为`my.cnf`,默认的配置文件内容如下所示。
```Shell
cat /etc/my.cnf
```
```INI
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
[mysqld]
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
```
通过配置文件,我们可以修改 MySQL 服务使用的端口、字符集、最大连接数、套接字队列大小、最大数据包大小、日志文件的位置、日志过期时间等配置。当然,我们还可以通过修改配置文件来对 MySQL 服务器进行性能调优和安全管控。
3. 启动 MySQL 服务。
可以使用下面的命令来启动 MySQL。
```Shell
service mysqld start
```
在 CentOS 7 中,更推荐使用下面的命令来启动 MySQL。
```Shell
systemctl start mysqld
```
启动 MySQL 成功后可以通过下面的命令来检查网络端口使用情况MySQL 默认使用`3306`端口。
```Shell
netstat -ntlp | grep mysql
```
也可以使用下面的命令查找是否有名为`mysqld`的进程。
```Shell
pgrep mysqld
```
4. 使用 MySQL 客户端工具连接服务器。
命令行工具:
```Shell
mysql -u root -p
```
> 说明:启动客户端时,`-u`参数用来指定用户名MySQL 默认的超级管理账号为`root``-p`表示要输入密码(用户口令);如果连接的是其他主机而非本机,可以用`-h`来指定连接主机的主机名或IP地址。
如果是首次安装 MySQL可以使用下面的命令来找到默认的初始密码。
```Shell
cat /var/log/mysqld.log | grep password
```
上面的命令会查看 MySQL 的日志带有`password`的行,在显示的结果中`root@localhost:`后面的部分就是默认设置的初始密码。
进入客户端工具后可以通过下面的指令来修改超级管理员root的访问口令为`123456`。
```SQL
set global validate_password_policy=0;
set global validate_password_length=6;
alter user 'root'@'localhost' identified by '123456';
```
> **说明**MySQL 较新的版本默认不允许使用弱口令作为用户口令,所以上面的代码修改了验证用户口令的策略和口令的长度。事实上我们不应该使用弱口令,因为存在用户口令被暴力破解的风险。近年来,**攻击数据库窃取数据和劫持数据库勒索比特币**的事件屡见不鲜,要避免这些潜在的风险,最为重要的一点是**不要让数据库服务器暴露在公网上**(最好的做法是将数据库置于内网,至少要做到不向公网开放数据库服务器的访问端口),另外要保管好`root`账号的口令,应用系统需要访问数据库时,通常不使用`root`账号进行访问,而是**创建其他拥有适当权限的账号来访问**。
再次使用客户端工具连接 MySQL 服务器时,就可以使用新设置的口令了。在实际开发中,为了方便用户操作,可以选择图形化的客户端工具来连接 MySQL 服务器,包括:
- MySQL Workbench官方工具
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211106063939.png" style="zoom:50%">
- Navicat for MySQL界面简单友好
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210521152457.png" style="zoom:50%;">
#### macOS环境
macOS 系统安装 MySQL 是比较简单的,只需要从刚才说到的官方网站下载 DMG 安装文件并运行就可以了,下载的时候需要根据自己使用的是 Intel 的芯片还是苹果的 M1 芯片选择下载链接,如下图所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121215901.png" style="zoom:50%;">
安装成功后可以在“系统偏好设置”中找到“MySQL”在如下所示的画面中可以启动和停止 MySQL 服务器,也可以对 MySQL 核心文件的路径进行配置。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121215153.png" style="zoom:40%;">
### MySQL 基本命令
#### 查看命令
1. 查看所有数据库
```SQL
show databases;
```
2. 查看所有字符集
```SQL
show character set;
```
3. 查看所有的排序规则
```SQL
show collation;
```
4. 查看所有的引擎
```SQL
show engines;
```
5. 查看所有日志文件
```SQL
show binary logs;
```
6. 查看数据库下所有表
```SQL
show tables;
```
#### 获取帮助
在 MySQL 命令行工具中,可以使用`help`命令或`?`来获取帮助,如下所示。
1. 查看`show`命令的帮助。
```MySQL
? show
```
2. 查看有哪些帮助内容。
```MySQL
? contents
```
3. 获取函数的帮助。
```MySQL
? functions
```
4. 获取数据类型的帮助。
```MySQL
? data types
```
#### 其他命令
1. 新建/重建服务器连接 - `connect` / `resetconnection`
2. 清空当前输入 - `\c`。在输入错误时,可以及时使用`\c`清空当前输入并重新开始。
3. 修改终止符(定界符)- `delimiter`。默认的终止符是`;`,可以使用该命令修改成其他的字符,例如修改为`$`符号,可以用`delimiter $`命令。
4. 打开系统默认编辑器 - `edit`。编辑完成保存关闭之后,命令行会自动执行编辑的内容。
5. 查看服务器状态 - `status`
6. 修改默认提示符 - `prompt`
7. 执行系统命令 - `system`。可以将系统命令跟在`system`命令的后面执行,`system`命令也可以缩写为`\!`。
8. 执行 SQL 文件 - `source`。`source`命令后面跟 SQL 文件路径。
9. 重定向输出 - `tee` / `notee`。可以将命令的输出重定向到指定的文件中。
10. 切换数据库 - `use`
11. 显示警告信息 - `warnings`
12. 退出命令行 - `quit`或`exit`。

1468
Day36-40/37.SQL详解.md Normal file

File diff suppressed because it is too large Load Diff

661
Day36-40/38.深入MySQL.md Normal file
View File

@ -0,0 +1,661 @@
## 深入MySQL
### 索引
索引是关系型数据库中用来提升查询性能最为重要的手段。关系型数据库中的索引就像一本书的目录,我们可以想象一下,如果要从一本书中找出某个知识点,但是这本书没有目录,这将是意见多么可怕的事情!我们估计得一篇一篇的翻下去,才能确定这个知识点到底在什么位置。创建索引虽然会带来存储空间上的开销,就像一本书的目录会占用一部分篇幅一样,但是在牺牲空间后换来的查询时间的减少也是非常显著的。
MySQL 数据库中所有数据类型的列都可以被索引。对于MySQL 8.0 版本的 InnoDB 存储引擎来说,它支持三种类型的索引,分别是 B+ 树索引、全文索引和 R 树索引。这里,我们只介绍使用得最为广泛的 B+ 树索引。使用 B+ 树的原因非常简单因为它是目前在基于磁盘进行海量数据存储和排序上最有效率的数据结构。B+ 树是一棵[平衡树](https://zh.wikipedia.org/zh-cn/%E5%B9%B3%E8%A1%A1%E6%A0%91)树的高度通常为3或4但是却可以保存从百万级到十亿级的数据而从这些数据里面查询一条数据只需要3次或4次 I/O 操作。
B+ 树由根节点、中间节点和叶子节点构成其中叶子节点用来保存排序后的数据。由于记录在索引上是排序过的因此在一个叶子节点内查找数据时可以使用二分查找这种查找方式效率非常的高。当数据很少的时候B+ 树只有一个根节点数据也就保存在根节点上。随着记录越来越多B+ 树会发生分裂,根节点不再保存数据,而是提供了访问下一层节点的指针,帮助快速确定数据在哪个叶子节点上。
在创建二维表时,我们通常都会为表指定主键列,主键列上默认会创建索引,而对于 MySQL InnoDB 存储引擎来说,因为它使用的是索引组织表这种数据存储结构,所以主键上的索引就是整张表的数据,而这种索引我们也将其称之为**聚集索引**clustered index。很显然一张表只能有一个聚集索引否则表的数据岂不是要保存多次。我们自己创建的索引都是二级索引secondary index更常见的叫法是**非聚集索引**non-clustered index。通过我们自定义的非聚集索引只能定位记录的主键在获取数据时可能需要再通过主键上的聚集索引进行查询这种现象称为“回表”因此通过非聚集索引检索数据通常比使用聚集索引检索数据要慢。
接下来我们通过一个简单的例子来说明索引的意义,比如我们要根据学生的姓名来查找学生,这个场景在实际开发中应该经常遇到,就跟通过商品名称查找商品是一个道理。我们可以使用 MySQL 的`explain`关键字来查看 SQL 的执行计划(数据库执行 SQL 语句的具体步骤)。
```SQL
explain select * from tb_student where stuname='林震南'\G
```
```
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tb_student
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 11
filtered: 10.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
```
在上面的 SQL 执行计划中,有几项值得我们关注:
1. `select_type`:查询的类型。
- `SIMPLE`:简单 SELECT不需要使用 UNION 操作或子查询。
- `PRIMARY`:如果查询包含子查询,最外层的 SELECT 被标记为 PRIMARY。
- `UNION`UNION 操作中第二个或后面的 SELECT 语句。
- `SUBQUERY`:子查询中的第一个 SELECT。
- `DERIVED`:派生表的 SELECT 子查询。
2. `table`:查询对应的表。
3. `type`MySQL 在表中找到满足条件的行的方式,也称为访问类型,包括:`ALL`(全表扫描)、`index`(索引全扫描,只遍历索引树)、`range`(索引范围扫描)、`ref`(非唯一索引扫描)、`eq_ref`(唯一索引扫描)、`const` / `system`(常量级查询)、`NULL`(不需要访问表或索引)。在所有的访问类型中,很显然 ALL 是性能最差的,它代表的全表扫描是指要扫描表中的每一行才能找到匹配的行。
4. `possible_keys`MySQL 可以选择的索引,但是**有可能不会使用**。
5. `key`MySQL 真正使用的索引,如果为`NULL`就表示没有使用索引。
6. `key_len`:使用的索引的长度,在不影响查询的情况下肯定是长度越短越好。
7. `rows`:执行查询需要扫描的行数,这是一个**预估值**。
8. `extra`:关于查询额外的信息。
- `Using filesort`MySQL 无法利用索引完成排序操作。
- `Using index`:只使用索引的信息而不需要进一步查表来获取更多的信息。
- `Using temporary`MySQL 需要使用临时表来存储结果集,常用于分组和排序。
- `Impossible where``where`子句会导致没有符合条件的行。
- `Distinct`MySQL 发现第一个匹配行后,停止为当前的行组合搜索更多的行。
- `Using where`:查询的列未被索引覆盖,筛选条件并不是索引的前导列。
从上面的执行计划可以看出,当我们通过学生名字查询学生时实际上是进行了全表扫描,不言而喻这个查询性能肯定是非常糟糕的,尤其是在表中的行很多的时候。如果我们需要经常通过学生姓名来查询学生,那么就应该在学生姓名对应的列上创建索引,通过索引来加速查询。
```SQL
create index idx_student_name on tb_student(stuname);
```
再次查看刚才的 SQL 对应的执行计划。
```SQL
explain select * from tb_student where stuname='林震南'\G
```
```
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tb_student
partitions: NULL
type: ref
possible_keys: idx_student_name
key: idx_student_name
key_len: 62
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
```
可以注意到在对学生姓名创建索引后刚才的查询已经不是全表扫描而是基于索引的查询而且扫描的行只有唯一的一行这显然大大的提升了查询的性能。MySQL 中还允许创建前缀索引即对索引字段的前N个字符创建索引这样的话可以减少索引占用的空间但节省了空间很有可能会浪费时间**时间和空间是不可调和的矛盾**),如下所示。
```SQL
create index idx_student_name_1 on tb_student(stuname(1));
```
上面的索引相当于是根据学生姓名的第一个字来创建的索引,我们再看看 SQL 执行计划。
```SQL
explain select * from tb_student where stuname='林震南'\G
```
```
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tb_student
partitions: NULL
type: ref
possible_keys: idx_student_name
key: idx_student_name
key_len: 5
ref: const
rows: 2
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
```
不知道大家是否注意到这一次扫描的行变成了2行因为学生表中有两个姓“林”的学生我们只用姓名的第一个字作为索引的话在查询时通过索引就会找到这两行。
如果要删除索引可以使用下面的SQL。
```SQL
alter table tb_student drop index idx_student_name;
```
或者
```SQL
drop index idx_student_name on tb_student;
```
在创建索引时我们还可以使用复合索引、函数索引MySQL 5.7 开始支持),用好复合索引实现**索引覆盖**可以减少不必要的排序和回表操作,这样就会让查询的性能成倍的提升,有兴趣的读者可以自行研究。
我们简单的为大家总结一下索引的设计原则:
1. **最适合**索引的列是出现在**WHERE子句**和连接子句中的列。
2. 索引列的基数越大(取值多、重复值少),索引的效果就越好。
3. 使用**前缀索引**可以减少索引占用的空间,内存中可以缓存更多的索引。
4. **索引不是越多越好**,虽然索引加速了读操作(查询),但是写操作(增、删、改)都会变得更慢,因为数据的变化会导致索引的更新,就如同书籍章节的增删需要更新目录一样。
5. 使用 InnoDB 存储引擎时,表的普通索引都会保存主键的值,所以**主键要尽可能选择较短的数据类型**,这样可以有效的减少索引占用的空间,提升索引的缓存效果。
最后还有一点需要说明InnoDB 使用的 B-tree 索引,数值类型的列除了等值判断时索引会生效之外,使用`>`、`<`、`>=`、`<=`、`BETWEEN...AND... `、`<>`时,索引仍然生效;对于字符串类型的列,如果使用不以通配符开头的模糊查询,索引也是起作用的,但是其他的情况会导致索引失效,这就意味着很有可能会做全表查询。
### 视图
视图是关系型数据库中将一组查询指令构成的结果集组合成可查询的数据表的对象。简单的说,视图就是虚拟的表,但与数据表不同的是,数据表是一种实体结构,而视图是一种虚拟结构,你也可以将视图理解为保存在数据库中被赋予名字的 SQL 语句。
使用视图可以获得以下好处:
1. 可以将实体数据表隐藏起来,让外部程序无法得知实际的数据结构,让访问者可以使用表的组成部分而不是整个表,降低数据库被攻击的风险。
2. 在大多数的情况下视图是只读的(更新视图的操作通常都有诸多的限制),外部程序无法直接透过视图修改数据。
3. 重用 SQL 语句,将高度复杂的查询包装在视图表中,直接访问该视图即可取出需要的数据;也可以将视图视为数据表进行连接查询。
4. 视图可以返回与实体数据表不同格式的数据,在创建视图的时候可以对数据进行格式化处理。
创建视图。
```SQL
-- 创建视图
create view `vw_avg_score`
as
select `stu_id`, round(avg(`score`), 1) as `avg_score`
from `tb_record` group by `stu_id`;
-- 基于已有的视图创建视图
create view `vw_student_score`
as
select `stu_name`, `avg_score`
from `tb_student` natural join `vw_avg_score`;
```
> **提示**:因为视图不包含数据,所以每次使用视图时,都必须执行查询以获得数据,如果你使用了连接查询、嵌套查询创建了较为复杂的视图,你可能会发现查询性能下降得很厉害。因此,在使用复杂的视图前,应该进行测试以确保其性能能够满足应用的需求。
使用视图。
```SQL
select * from `vw_student_score` order by `avg_score` desc;
```
```
+--------------+----------+
| stuname | avgscore |
+--------------+----------+
| 杨过 | 95.6 |
| 任我行 | 53.5 |
| 王语嫣 | 84.3 |
| 纪嫣然 | 73.8 |
| 岳不群 | 78.0 |
| 东方不败 | 88.0 |
| 项少龙 | 92.0 |
+--------------+----------+
```
既然视图是一张虚拟的表,那么视图的中的数据可以更新吗?视图的可更新性要视具体情况而定,以下类型的视图是不能更新的:
1. 使用了聚合函数(`SUM`、`MIN`、`MAX`、`AVG`、`COUNT`等)、`DISTINCT`、`GROUP BY`、`HAVING`、`UNION`或者`UNION ALL`的视图。
2. `SELECT`中包含了子查询的视图。
3. `FROM`子句中包含了一个不能更新的视图的视图。
4. `WHERE`子句的子查询引用了`FROM`子句中的表的视图。
删除视图。
```SQL
drop view vw_student_score;
```
> **说明**:如果希望更新视图,可以先用上面的命令删除视图,也可以通过`create or replace view`来更新视图。
视图的规则和限制。
1. 视图可以嵌套,可以利用从其他视图中检索的数据来构造一个新的视图。视图也可以和表一起使用。
2. 创建视图时可以使用`order by`子句,但如果从视图中检索数据时也使用了`order by`,那么该视图中原先的`order by`会被覆盖。
3. 视图无法使用索引,也不会激发触发器(实际开发中因为性能等各方面的考虑,通常不建议使用触发器,所以我们也不对这个概念进行介绍)的执行。
### 函数
MySQL 中的函数跟 Python 中的函数太多的差异,因为函数都是用来封装功能上相对独立且会被重复使用的代码的。如果非要找出一些差别来,那么 MySQL 中的函数是可以执行 SQL 语句的。下面的例子,我们通过自定义函数实现了截断超长字符串的功能。
```SQL
delimiter $$
create function truncate_string(
content varchar(10000),
max_length int unsigned
) returns varchar(10000) no sql
begin
declare result varchar(10000) default content;
if char_length(content) > max_length then
set result = left(content, max_length);
set result = concat(result, '……');
end if;
return result;
end $$
delimiter ;
```
> **说明1**:函数声明后面的`no sql`是声明函数体并没有使用 SQL 语句;如果函数体中需要通过 SQL 读取数据,需要声明为`reads sql data`。
>
> **说明2**:定义函数前后的`delimiter`命令是为了修改定界符,因为函数体中的语句都是用`;`表示结束,如果不重新定义定界符,那么遇到的`;`的时候代码就会被截断执行,显然这不是我们想要的效果。
在查询中调用自定义函数。
```SQL
select truncate_string('和我在成都的街头走一走,直到所有的灯都熄灭了也不停留', 10) as short_string;
```
```
+--------------------------------------+
| short_string |
+--------------------------------------+
| 和我在成都的街头走一…… |
+--------------------------------------+
```
### 过程
过程(又称存储过程)是事先编译好存储在数据库中的一组 SQL 的集合,调用过程可以简化应用程序开发人员的工作,减少与数据库服务器之间的通信,对于提升数据操作的性能也是有帮助的。其实迄今为止,我们使用的 SQL 语句都是针对一个或多个表的单条语句,但在实际开发中经常会遇到某个操作需要多条 SQL 语句才能完成的情况。例如,电商网站在受理用户订单时,需要做以下一系列的处理。
1. 通过查询来核对库存中是否有对应的物品以及库存是否充足。
2. 如果库存有物品,需要锁定库存以确保这些物品不再卖给别人, 并且要减少可用的物品数量以反映正确的库存量。
3. 如果库存不足,可能需要进一步与供应商进行交互或者至少产生一条系统提示消息。
4. 不管受理订单是否成功,都需要产生流水记录,而且需要给对应的用户产生一条通知信息。
我们可以通过过程将复杂的操作封装起来,这样不仅有助于保证数据的一致性,而且将来如果业务发生了变动,只需要调整和修改过程即可。对于调用过程的用户来说,过程并没有暴露数据表的细节,而且执行过程比一条条的执行一组 SQL 要快得多。
下面的过程实现了查询某门课程的最高分、最低分和平均分。
```SQL
drop procedure if exists sp_score_stat;
delimiter $$
create procedure sp_score_stat(
courseId int,
out maxScore decimal(4,1),
out minScore decimal(4,1),
out avgScore decimal(4,1)
)
begin
select max(score) into maxScore from tb_record where cou_id=courseId;
select min(score) into minScore from tb_record where cou_id=courseId;
select avg(score) into avgScore from tb_record where cou_id=courseId;
end $$
delimiter ;
```
> **说明**:在定义过程时,因为可能需要书写多条 SQL而分隔这些 SQL 需要使用分号作为分隔符,如果这个时候,仍然用分号表示整段代码结束,那么定义过程的 SQL 就会出现错误,所以上面我们用`delimiter $$`将整段代码结束的标记定义为`$$`,那么代码中的分号将不再表示整段代码的结束,整段代码只会在遇到`end $$`时才会执行。在定义完过程后,通过`delimiter ;`将结束符重新改回成分号(恢复现场)。
上面定义的过程有四个参数,其中第一个参数是输入参数,代表课程的编号,后面的参数都是输出参数,因为过程不能定义返回值,只能通过输出参数将执行结果带出,定义输出参数的关键字是`out`,默认情况下参数都是输入参数。
调用过程。
```SQL
call sp_score_stat(1111, @a, @b, @c);
```
获取输出参数的值。
```SQL
select @a as 最高分, @b as 最低分, @c as 平均分;
```
删除过程。
```SQL
drop procedure sp_score_stat;
```
在过程中,我们可以定义变量、条件,可以使用分支和循环语句,可以通过游标操作查询结果,还可以使用事件调度器,这些内容我们暂时不在此处进行介绍。虽然我们说了很多过程的好处,但是在实际开发中,如果频繁的使用过程并将大量复杂的运算放到过程中,会给据库服务器造成巨大的压力,而数据库往往都是性能瓶颈所在,使用过程无疑是雪上加霜的操作。所以,对于互联网产品开发,我们一般建议让数据库只做好存储,复杂的运算和处理交给应用服务器上的程序去完成,如果应用服务器变得不堪重负了,我们可以比较容易的部署多台应用服务器来分摊这些压力。
如果大家对上面讲到的视图、函数、过程包括我们没有讲到的触发器这些知识有兴趣,建议大家阅读 MySQL 的入门读物[《MySQL必知必会》](https://item.jd.com/12818982.html)进行一般性了解即可,因为这些知识点在大家将来的工作中未必用得上,学了也可能仅仅是为了应付面试而已。
### MySQL 新特性
#### JSON类型
很多开发者在使用关系型数据库做数据持久化的时候,常常感到结构化的存储缺乏灵活性,因为必须事先设计好所有的列以及对应的数据类型。在业务发展和变化的过程中,如果需要修改表结构,这绝对是比较麻烦和难受的事情。从 MySQL 5.7 版本开始MySQL引入了对 JSON 数据类型的支持MySQL 8.0 解决了 JSON 的日志性能瓶颈问题),用好 JSON 类型,其实就是打破了关系型数据库和非关系型数据库之间的界限,为数据持久化操作带来了更多的便捷。
JSON 类型主要分为 JSON 对象和 JSON数组两种如下所示。
1. JSON 对象
```JSON
{"name": "骆昊", "tel": "13122335566", "QQ": "957658"}
```
2. JSON 数组
```JSON
[1, 2, 3]
```
```JSON
[{"name": "骆昊", "tel": "13122335566"}, {"name": "王大锤", "QQ": "123456"}]
```
哪些地方需要用到JSON类型呢举一个简单的例子现在很多产品的用户登录都支持多种方式例如手机号、微信、QQ、新浪微博等但是一般情况下我们又不会要求用户提供所有的这些信息那么用传统的设计方式就需要设计多个列来对应多种登录方式可能还需要允许这些列存在空值这显然不是很好的选择另一方面如果产品又增加了一种登录方式那么就必然要修改之前的表结构这就更让人痛苦了。但是有了 JSON 类型,刚才的问题就迎刃而解了,我们可以做出如下所示的设计。
```SQL
create table `tb_test`
(
`user_id` bigint unsigned,
`login_info` json,
primary key (`user_id`)
) engine=innodb;
insert into `tb_test` values
(1, '{"tel": "13122335566", "QQ": "654321", "wechat": "jackfrued"}'),
(2, '{"tel": "13599876543", "weibo": "wangdachui123"}');
```
如果要查询用户的手机和微信号,可以用如下所示的 SQL 语句。
```SQL
select
`user_id`,
json_unquote(json_extract(`login_info`, '$.tel')) as 手机号,
json_unquote(json_extract(`login_info`, '$.wechat')) as 微信
from `tb_test`;
```
```
+---------+-------------+-----------+
| user_id | 手机号 | 微信 |
+---------+-------------+-----------+
| 1 | 13122335566 | jackfrued |
| 2 | 13599876543 | NULL |
+---------+-------------+-----------+
```
因为支持 JSON 类型MySQL 也提供了配套的处理 JSON 数据的函数,就像上面用到的`json_extract`和`json_unquote`。当然,上面的 SQL 还有更为便捷的写法,如下所示。
```SQL
select
`user_id`,
`login_info` ->> '$.tel' as 手机号,
`login_info` ->> '$.wechat' as 微信
from `tb_test`;
```
再举个例子,如果我们的产品要实现用户画像功能(给用户打标签),然后基于用户画像给用户推荐平台的服务或消费品之类的东西,我们也可以使用 JSON 类型来保存用户画像数据,示意代码如下所示。
创建画像标签表。
```SQL
create table `tb_tags`
(
`tag_id` int unsigned not null comment '标签ID',
`tag_name` varchar(20) not null comment '标签名',
primary key (`tag_id`)
) engine=innodb;
insert into `tb_tags` (`tag_id`, `tag_name`)
values
(1, '70后'),
(2, '80后'),
(3, '90后'),
(4, '00后'),
(5, '爱运动'),
(6, '高学历'),
(7, '小资'),
(8, '有房'),
(9, '有车'),
(10, '爱看电影'),
(11, '爱网购'),
(12, '常点外卖');
```
为用户打标签。
```SQL
create table `tb_users_tags`
(
`user_id` bigint unsigned not null comment '用户ID',
`user_tags` json not null comment '用户标签'
) engine=innodb;
insert into `tb_users_tags` values
(1, '[2, 6, 8, 10]'),
(2, '[3, 10, 12]'),
(3, '[3, 8, 9, 11]');
```
接下来,我们通过一组查询来了解 JSON 类型的巧妙之处。
1. 查询爱看电影(有`10`这个标签的用户ID。
```SQL
select * from `tb_users` where 10 member of (user_tags->'$');
```
2. 查询爱看电影(有`10`这个标签的80后有`2`这个标签用户ID。
```
select * from `tb_users` where json_contains(user_tags->'$', '[2, 10]');
3. 查询爱看电影或80后或90后的用户ID。
```SQL
select `user_id` from `tb_users_tags` where json_overlaps(user_tags->'$', '[2, 3, 10]');
```
> **说明**:上面的查询用到了`member of`谓词和两个 JSON 函数,`json_contains`可以检查 JSON 数组是否包含了指定的元素,而`json_overlaps`可以检查 JSON 数组是否与指定的数组有重叠部分。
#### 窗口函数
MySQL 从8.0开始支持窗口函数,大多数商业数据库和一些开源数据库早已提供了对窗口函数的支持,有的也将其称之为 OLAP联机分析和处理函数听名字就知道跟统计和分析相关。为了帮助大家理解窗口函数我们先说说窗口的概念。
窗口可以理解为记录的集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数。窗口函数和我们上面讲到的聚合函数比较容易混淆,二者的区别主要在于聚合函数是将多条记录聚合为一条记录,窗口函数是每条记录都会执行,执行后记录条数不会变。窗口函数不仅仅是几个函数,它是一套完整的语法,函数只是该语法的一部分,基本语法如下所示:
```SQL
<窗口函数> over (partition by <用于分组的列名> order by <用户排序的列名>)
```
上面语法中,窗口函数的位置可以放以下两种函数:
1. 专用窗口函数,包括:`lead`、`lag`、`first_value`、`last_value`、`rank`、`dense_rank`和`row_number`等。
2. 聚合函数,包括:`sum`、`avg`、`max`、`min`和`count`等。
下面为大家举几个使用窗口函数的简单例子,我们先用如下所示的 SQL 建库建表。
```SQL
-- 创建名为hrs的数据库并指定默认的字符集
create database `hrs` default charset utf8mb4;
-- 切换到hrs数据库
use `hrs`;
-- 创建部门表
create table `tb_dept`
(
`dno` int not null comment '编号',
`dname` varchar(10) not null comment '名称',
`dloc` varchar(20) not null comment '所在地',
primary key (`dno`)
);
-- 插入4个部门
insert into `tb_dept` values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
-- 创建员工表
create table `tb_emp`
(
`eno` int not null comment '员工编号',
`ename` varchar(20) not null comment '员工姓名',
`job` varchar(20) not null comment '员工职位',
`mgr` int comment '主管编号',
`sal` int not null comment '员工月薪',
`comm` int comment '每月补贴',
`dno` int not null comment '所在部门编号',
primary key (`eno`),
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
);
-- 插入14个员工
insert into `tb_emp` values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
(3577, '杨过', '会计', 5566, 2200, null, 10),
(3588, '朱九真', '会计', 5566, 2500, null, 10);
```
例子1查询按月薪从高到低排在第4到第6名的员工的姓名和月薪。
```SQL
select * from (
select
`ename`, `sal`,
row_number() over (order by `sal` desc) as `rank`
from `tb_emp`
) `temp` where `rank` between 4 and 6;
```
> **说明**:上面使用的函数`row_number()`可以为每条记录生成一个行号,在实际工作中可以根据需要将其替换为`rank()`或`dense_rank()`函数,三者的区别可以参考官方文档或阅读[《通俗易懂的学会SQL窗口函数》](https://zhuanlan.zhihu.com/p/92654574)进行了解。在MySQL 8以前的版本我们可以通过下面的方式来完成类似的操作。
>
> ```SQL
> select `rank`, `ename`, `sal` from (
> select @a:=@a+1 as `rank`, `ename`, `sal`
> from `tb_emp`, (select @a:=0) as t1 order by `sal` desc
> ) t2 where `rank` between 4 and 6;
> ```
例子2查询每个部门月薪最高的两名的员工的姓名和部门名称。
```SQL
select `ename`, `sal`, `dname`
from (
select
`ename`, `sal`, `dno`,
rank() over (partition by `dno` order by `sal` desc) as `rank`
from `tb_emp`
) as `temp` natural join `tb_dept` where `rank`<=2;
```
> 说明在MySQL 8以前的版本我们可以通过下面的方式来完成类似的操作。
>
> ```SQL
> select `ename`, `sal`, `dname` from `tb_emp` as `t1`
natural join `tb_dept`
where (
select count(*) from `tb_emp` as `t2`
where `t1`.`dno`=`t2`.`dno` and `t2`.`sal`>`t1`.`sal`
)<2 order by `dno` asc, `sal` desc;
> ```
### 其他内容
#### 范式理论
范式理论是设计关系型数据库中二维表的指导思想。
1. 第一范式:数据表的每个列的值域都是由原子值组成的,不能够再分割。
2. 第二范式:数据表里的所有数据都要和该数据表的键(主键与候选键)有完全依赖关系。
3. 第三范式:所有非键属性都只和候选键有相关性,也就是说非键属性之间应该是独立无关的。
> **说明**:实际工作中,出于效率的考虑,我们在设计表时很有可能做出反范式设计,即故意降低方式级别,增加冗余数据来获得更好的操作性能。
#### 数据完整性
1. 实体完整性 - 每个实体都是独一无二的
- 主键(`primary key` / 唯一约束(`unique`
2. 引用完整性(参照完整性)- 关系中不允许引用不存在的实体
- 外键(`foreign key`
3. 域domain完整性 - 数据是有效的
- 数据类型及长度
- 非空约束(`not null`
- 默认值约束(`default`
- 检查约束(`check`
> **说明**:在 MySQL 8.x 以前,检查约束并不起作用。
#### 数据一致性
1. 事务:一系列对数据库进行读/写的操作,这些操作要么全都成功,要么全都失败。
2. 事务的 ACID 特性
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
- 一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态
- 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
3. MySQL 中的事务操作
- 开启事务环境
```SQL
start transaction
```
- 提交事务
```SQL
commit
```
- 回滚事务
```SQL
rollback
```
4. 查看事务隔离级别
```SQL
show variables like 'transaction_isolation';
```
```
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
```
可以看出MySQL 默认的事务隔离级别是`REPEATABLE-READ`。
5. 修改(当前会话)事务隔离级别
```SQL
set session transaction isolation level read committed;
```
重新查看事务隔离级别,结果如下所示。
```
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
```
关系型数据库的事务是一个很大的话题,因为当存在多个并发事务访问数据时,就有可能出现三类读数据的问题(脏读、不可重复读、幻读)和两类更新数据的问题(第一类丢失更新、第二类丢失更新)。想了解这五类问题的,可以阅读我发布在 CSDN 网站上的[《Java面试题全集》](https://blog.csdn.net/jackfrued/article/details/44921941)一文的第80题。为了避免这些问题关系型数据库底层是有对应的锁机制的按锁定对象不同可以分为表级锁和行级锁按并发事务锁定关系可以分为共享锁和独占锁。然而直接使用锁是非常麻烦的为此数据库为用户提供了自动锁机制只要用户指定适当的事务隔离级别数据库就会通过分析 SQL 语句,然后为事务访问的资源加上合适的锁。此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。想了解 MySQL 事务和锁的细节知识,推荐大家阅读进阶读物[《高性能MySQL》](https://item.jd.com/11220393.html),这也是数据库方面的经典书籍。
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别如下表所示。需要说明的是事务隔离级别和数据访问的并发性是对立的事务隔离级别越高并发性就越差。所以要根据具体的应用来确定到底使用哪种事务隔离级别这个地方没有万能的原则。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121225327.png" style="zoom:50%;">
### 总结
关于 SQL 和 MySQL 的知识肯定远远不止上面列出的这些,比如 SQL 本身的优化、MySQL 性能调优、MySQL 运维相关工具、MySQL 数据的备份和恢复、监控 MySQL 服务、部署高可用架构等,这一系列的问题在这里都没有办法逐一展开来讨论,那就留到有需要的时候再进行讲解吧,各位读者也可以自行探索。

View File

@ -0,0 +1,102 @@
## 数据库相关知识
### 范式理论
范式理论是设计关系型数据库中二维表的指导思想。
1. 第一范式:数据表的每个列的值域都是由原子值组成的,不能够再分割。
2. 第二范式:数据表里的所有数据都要和该数据表的键(主键与候选键)有完全依赖关系。
3. 第三范式:所有非键属性都只和候选键有相关性,也就是说非键属性之间应该是独立无关的。
> **说明**:实际工作中,出于效率的考虑,我们在设计表时很有可能做出反范式设计,即故意降低方式级别,增加冗余数据来获得更好的操作性能。
### 数据完整性
1. 实体完整性 - 每个实体都是独一无二的
- 主键(`primary key` / 唯一约束(`unique`
2. 引用完整性(参照完整性)- 关系中不允许引用不存在的实体
- 外键(`foreign key`
3. 域domain完整性 - 数据是有效的
- 数据类型及长度
- 非空约束(`not null`
- 默认值约束(`default`
- 检查约束(`check`
> **说明**:在 MySQL 8.x 以前,检查约束并不起作用。
### 数据一致性
1. 事务:一系列对数据库进行读/写的操作,这些操作要么全都成功,要么全都失败。
2. 事务的 ACID 特性
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
- 一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态
- 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中
3. MySQL 中的事务操作
- 开启事务环境
```SQL
start transaction
```
- 提交事务
```SQL
commit
```
- 回滚事务
```SQL
rollback
```
4. 查看事务隔离级别
```SQL
show variables like 'transaction_isolation';
```
```
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
```
可以看出MySQL 默认的事务隔离级别是`REPEATABLE-READ`。
5. 修改(当前会话)事务隔离级别
```SQL
set session transaction isolation level read committed;
```
重新查看事务隔离级别,结果如下所示。
```
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
```
关系型数据库的事务是一个很大的话题,因为当存在多个并发事务访问数据时,就有可能出现三类读数据的问题(脏读、不可重复读、幻读)和两类更新数据的问题(第一类丢失更新、第二类丢失更新)。想了解这五类问题的,可以阅读我发布在 CSDN 网站上的[《Java面试题全集》](https://blog.csdn.net/jackfrued/article/details/44921941)一文的第80题。为了避免这些问题关系型数据库底层是有对应的锁机制的按锁定对象不同可以分为表级锁和行级锁按并发事务锁定关系可以分为共享锁和独占锁。然而直接使用锁是非常麻烦的为此数据库为用户提供了自动锁机制只要用户指定适当的事务隔离级别数据库就会通过分析 SQL 语句,然后为事务访问的资源加上合适的锁。此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的。想了解 MySQL 事务和锁的细节知识,推荐大家阅读进阶读物[《高性能MySQL》](https://item.jd.com/11220393.html),这也是数据库方面的经典书籍。
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别如下表所示。需要说明的是事务隔离级别和数据访问的并发性是对立的事务隔离级别越高并发性就越差。所以要根据具体的应用来确定到底使用哪种事务隔离级别这个地方没有万能的原则。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211121225327.png" style="zoom:50%;">
### 总结
关于 SQL 和 MySQL 的知识肯定远远不止上面列出的这些,比如 SQL 本身的优化、MySQL 性能调优、MySQL 运维相关工具、MySQL 数据的备份和恢复、监控 MySQL 服务、部署高可用架构等,这一系列的问题在这里都没有办法逐一展开来讨论,那就留到有需要的时候再进行讲解吧,各位读者也可以自行探索。

View File

@ -0,0 +1,295 @@
## Python程序接入MySQL数据库
在 Python3 中,我们可以使用`mysqlclient`或者`pymysql`三方库来接入 MySQL 数据库并实现数据持久化操作。二者的用法完全相同,只是导入的模块名不一样。我们推荐大家使用纯 Python 的三方库`pymysql`,因为它更容易安装成功。下面我们仍然以之前创建的名为`hrs`的数据库为例,为大家演示如何通过 Python 程序操作 MySQL 数据库实现数据持久化操作。
### 建库建表
```SQL
-- 创建名为hrs的数据库并指定默认的字符集
create database `hrs` default character set utf8mb4;
-- 切换到hrs数据库
use `hrs`;
-- 创建部门表
create table `tb_dept`
(
`dno` int not null comment '编号',
`dname` varchar(10) not null comment '名称',
`dloc` varchar(20) not null comment '所在地',
primary key (`dno`)
);
-- 插入4个部门
insert into `tb_dept` values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
-- 创建员工表
create table `tb_emp`
(
`eno` int not null comment '员工编号',
`ename` varchar(20) not null comment '员工姓名',
`job` varchar(20) not null comment '员工职位',
`mgr` int comment '主管编号',
`sal` int not null comment '员工月薪',
`comm` int comment '每月补贴',
`dno` int not null comment '所在部门编号',
primary key (`eno`),
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
);
-- 插入14个员工
insert into `tb_emp` values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
(3577, '杨过', '会计', 5566, 2200, null, 10),
(3588, '朱九真', '会计', 5566, 2500, null, 10);
```
### 接入MySQL
首先,我们可以在命令行或者 PyCharm 的终端中通过下面的命令安装`pymysql`,如果需要接入 MySQL 8还需要安装一个名为`cryptography`的三方库来支持 MySQL 8 的密码认证方式。
```Shell
pip install pymysql cryptography
```
使用`pymysql`操作 MySQL 的步骤如下所示:
1. 创建连接。MySQL 服务器启动后,提供了基于 TCP (传输控制协议)的网络服务。我们可以通过`pymysql`模块的`connect`函数连接 MySQL 服务器。在调用`connect`函数时,需要指定主机(`host`)、端口(`port`)、用户名(`user`)、口令(`password`)、数据库(`database`)、字符集(`charset`)等参数,该函数会返回一个`Connection`对象。
2. 获取游标。连接 MySQL 服务器成功后,接下来要做的就是向数据库服务器发送 SQL 语句MySQL 会执行接收到的 SQL 并将执行结果通过网络返回。要实现这项操作,需要先通过连接对象的`cursor`方法获取游标(`Cursor`)对象。
3. 发出 SQL。通过游标对象的`execute`方法,我们可以向数据库发出 SQL 语句。
4. 如果执行`insert`、`delete`或`update`操作,需要根据实际情况提交或回滚事务。因为创建连接时,默认开启了事务环境,在操作完成后,需要使用连接对象的`commit`或`rollback`方法,实现事务的提交或回滚,`rollback`方法通常会放在异常捕获代码块`except`中。如果执行`select`操作,需要通过游标对象抓取查询的结果,对应的方法有三个,分别是:`fetchone`、`fetchmany`和`fetchall`。其中`fetchone`方法会抓取到一条记录,并以元组或字典的方式返回;`fetchmany`和`fetchall`方法会抓取到多条记录,以嵌套元组或列表装字典的方式返回。
5. 关闭连接。在完成持久化操作后,请不要忘记关闭连接,释放外部资源。我们通常会在`finally`代码块中使用连接对象的`close`方法来关闭连接。
### 代码实操
下面,我们通过代码实操的方式为大家演示上面说的五个步骤。
#### 插入数据
```Python
import pymysql
no = int(input('部门编号: '))
name = input('部门名称: ')
location = input('部门所在地: ')
# 1. 创建连接Connection
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4')
try:
# 2. 获取游标对象Cursor
with conn.cursor() as cursor:
# 3. 通过游标对象向数据库服务器发出SQL语句
affected_rows = cursor.execute(
'insert into `tb_dept` values (%s, %s, %s)',
(no, name, location)
)
if affected_rows == 1:
print('新增部门成功!!!')
# 4. 提交事务transaction
conn.commit()
except pymysql.MySQLError as err:
# 4. 回滚事务
conn.rollback()
print(type(err), err)
finally:
# 5. 关闭连接释放资源
conn.close()
```
> **说明**:上面的`127.0.0.1`称为回环地址,它代表的是本机。下面的`guest`是我提前创建好的用户,该用户拥有对`hrs`数据库的`insert`、`delete`、`update`和`select`权限。我们不建议大家在项目中直接使用`root`超级管理员账号访问数据库,这样做实在是太危险了。我们可以使用下面的命令创建名为`guest`的用户并为其授权。
>
> ```SQL
> create user 'guest'@'%' identified by 'Guest.618';
> grant insert, delete, update, select on `hrs`.* to 'guest'@'%';
> ```
如果要插入大量数据,建议使用游标对象的`executemany`方法做批处理(一个`insert`操作后面跟上多组数据大家可以尝试向一张表插入10000条记录然后看看不使用批处理一条条的插入和使用批处理有什么差别。游标对象的`executemany`方法第一个参数仍然是 SQL 语句,第二个参数可以是包含多组数据的列表或元组。
#### 删除数据
```Python
import pymysql
no = int(input('部门编号: '))
# 1. 创建连接Connection
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4',
autocommit=True)
try:
# 2. 获取游标对象Cursor
with conn.cursor() as cursor:
# 3. 通过游标对象向数据库服务器发出SQL语句
affected_rows = cursor.execute(
'delete from `tb_dept` where `dno`=%s',
(no, )
)
if affected_rows == 1:
print('删除部门成功!!!')
finally:
# 5. 关闭连接释放资源
conn.close()
```
> **说明**:如果不希望每次 SQL 操作之后手动提交或回滚事务,可以`connect`函数中加一个名为`autocommit`的参数并将它的值设置为`True`,表示每次执行 SQL 成功后自动提交。但是我们建议大家手动提交或回滚,这样可以根据实际业务需要来构造事务环境。如果不愿意捕获异常并进行处理,可以在`try`代码块后直接跟`finally`块,省略`except`意味着发生异常时,代码会直接崩溃并将异常栈显示在终端中。
#### 更新数据
```Python
import pymysql
no = int(input('部门编号: '))
name = input('部门名称: ')
location = input('部门所在地: ')
# 1. 创建连接Connection
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4')
try:
# 2. 获取游标对象Cursor
with conn.cursor() as cursor:
# 3. 通过游标对象向数据库服务器发出SQL语句
affected_rows = cursor.execute(
'update `tb_dept` set `dname`=%s, `dloc`=%s where `dno`=%s',
(name, location, no)
)
if affected_rows == 1:
print('更新部门信息成功!!!')
# 4. 提交事务
conn.commit()
except pymysql.MySQLError as err:
# 4. 回滚事务
conn.rollback()
print(type(err), err)
finally:
# 5. 关闭连接释放资源
conn.close()
```
#### 查询数据
1. 查询部门表的数据。
```Python
import pymysql
# 1. 创建连接Connection
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4')
try:
# 2. 获取游标对象Cursor
with conn.cursor() as cursor:
# 3. 通过游标对象向数据库服务器发出SQL语句
cursor.execute('select `dno`, `dname`, `dloc` from `tb_dept`')
# 4. 通过游标对象抓取数据
row = cursor.fetchone()
while row:
print(row)
row = cursor.fetchone()
except pymysql.MySQLError as err:
print(type(err), err)
finally:
# 5. 关闭连接释放资源
conn.close()
```
>**说明**:上面的代码中,我们通过构造一个`while`循环实现了逐行抓取查询结果的操作。这种方式特别适合查询结果有非常多行的场景。因为如果使用`fetchall`一次性将所有记录抓取到一个嵌套元组中,会造成非常大的内存开销,这在很多场景下并不是一个好主意。如果不愿意使用`while`循环,还可以考虑使用`iter`函数构造一个迭代器来逐行抓取数据,有兴趣的读者可以自行研究。
2. 分页查询员工表的数据。
```Python
import pymysql
page = int(input('页码: '))
size = int(input('大小: '))
# 1. 创建连接Connection
con = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8')
try:
# 2. 获取游标对象Cursor
with con.cursor(pymysql.cursors.DictCursor) as cursor:
# 3. 通过游标对象向数据库服务器发出SQL语句
cursor.execute(
'select `eno`, `ename`, `job`, `sal` from `tb_emp` order by `sal` desc limit %s,%s',
((page - 1) * size, size)
)
# 4. 通过游标对象抓取数据
for emp_dict in cursor.fetchall():
print(emp_dict)
finally:
# 5. 关闭连接释放资源
con.close()
```
### 案例讲解
下面我们为大家讲解一个将数据库表数据导出到 Excel 文件的例子,我们需要先安装`openpyxl`三方库,命令如下所示。
```Bash
pip install openpyxl
```
接下来,我们通过下面的代码实现了将数据库`hrs`中所有员工的编号、姓名、职位、月薪、补贴和部门名称导出到一个 Excel 文件中。
```Python
import openpyxl
import pymysql
# 创建工作簿对象
workbook = openpyxl.Workbook()
# 获得默认的工作表
sheet = workbook.active
# 修改工作表的标题
sheet.title = '员工基本信息'
# 给工作表添加表头
sheet.append(('工号', '姓名', '职位', '月薪', '补贴', '部门'))
# 创建连接Connection
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4')
try:
# 获取游标对象Cursor
with conn.cursor() as cursor:
# 通过游标对象执行SQL语句
cursor.execute(
'select `eno`, `ename`, `job`, `sal`, coalesce(`comm`, 0), `dname` '
'from `tb_emp` natural join `tb_dept`'
)
# 通过游标抓取数据
row = cursor.fetchone()
while row:
# 将数据逐行写入工作表中
sheet.append(row)
row = cursor.fetchone()
# 保存工作簿
workbook.save('hrs.xlsx')
except pymysql.MySQLError as err:
print(err)
finally:
# 关闭连接释放资源
conn.close()
```
大家可以参考上面的例子,试一试把 Excel 文件的数据导入到指定数据库的指定表中,看看是否可以成功。

View File

@ -1,77 +1,75 @@
drop database if exists hrs;
create database hrs default charset utf8mb4 collate utf8_bin;
-- 创建名为hrs的数据库
drop database if exists `hrs`;
create database `hrs` default charset utf8mb4;
use hrs;
-- 切换到hrs数据库
use `hrs`;
drop table if exists tb_emp;
drop table if exists tb_dept;
create table tb_dept
-- 创建部门表
create table `tb_dept`
(
dno int not null comment '编号',
dname varchar(10) not null comment '名称',
dloc varchar(20) not null comment '所在地',
`dno` int not null comment '编号',
`dname` varchar(10) not null comment '名称',
`dloc` varchar(20) not null comment '所在地',
primary key (dno)
);
-- alter table tb_dept add constraint pk_dept_dno primary key(dno);
-- 插入4个部门
insert into `tb_dept` values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
insert into tb_dept values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
create table tb_emp
-- 创建员工表
create table `tb_emp`
(
eno int not null comment '员工编号',
ename varchar(20) not null comment '员工姓名',
job varchar(20) not null comment '员工职位',
mgr int comment '主管编号',
sal int not null comment '员工月薪',
comm int comment '每月补贴',
dno int comment '所在部门编号',
`eno` int not null comment '员工编号',
`ename` varchar(20) not null comment '员工姓名',
`job` varchar(20) not null comment '员工职位',
`mgr` int comment '主管编号',
`sal` int not null comment '员工月薪',
`comm` int comment '每月补贴',
`dno` int comment '所在部门编号',
primary key (eno),
foreign key (dno) references tb_dept(dno),
foreign key (mgr) references tb_emp(eno)
constraint `fk_emp_mgr` foreign key (`mgr`) references tb_emp (`eno`),
constraint `fk_emp_dno` foreign key (`dno`) references tb_dept (`dno`)
);
-- alter table tb_emp add constraint fk_emp_mgr foreign key (mgr) references tb_emp (eno);
-- alter table tb_emp add constraint fk_emp_dno foreign key (dno) references tb_dept (dno);
insert into tb_emp values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
(3577, '杨过', '会计', 5566, 2200, null, 10),
(3588, '朱九真', '会计', 5566, 2500, null, 10);
-- 插入14个员工
insert into `tb_emp` values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
(3577, '杨过', '会计', 5566, 2200, null, 10),
(3588, '朱九真', '会计', 5566, 2500, null, 10);
-- 查询月薪最高的员工姓名和月薪
-- 查询员工的姓名和年薪((月薪+补贴)*13)
-- 查询员工的姓名和年薪(年薪=(sal+comm)*13)
-- 查询有员工的部门的编号和人数
-- 查询所有部门的名称和人数
-- 查询月薪最高的员工(Boss除外)的姓名和月薪
-- 查询月薪超过平均月薪的员工的姓名和月薪
-- 查询超过平均薪的员工的姓名和月薪
-- 查询薪超过其所在部门平均薪的员工的姓名、部门编号和月薪
-- 查询薪水超过其所在部门平均薪水的员工的姓名、部门编号和月薪
-- 查询部门中薪水最高的人姓名、月薪和所在部门名称
-- 查询部门中月薪最高的人姓名、月薪和所在部门名称
-- 查询主管的姓名和职位
-- 查询月薪排名4~6名的员工排名、姓名和月薪
-- 查询每个部门月薪排前2名的员工姓名、月薪和部门编号

View File

@ -1,80 +1,83 @@
-- 如果存在名为school的数据库就删除它
drop database if exists school;
drop database if exists `school`;
-- 创建名为school的数据库并设置默认的字符集和排序方式
create database school default charset utf8mb4;
create database `school` default charset utf8mb4;
-- 切换到school数据库上下文环境
use school;
use `school`;
-- 创建学院表
create table tb_college
create table `tb_college`
(
collid int auto_increment comment '编号',
collname varchar(50) not null comment '名称',
collintro varchar(500) default '' comment '介绍',
primary key (collid)
);
`col_id` int unsigned auto_increment comment '编号',
`col_name` varchar(50) not null comment '名称',
`col_intro` varchar(5000) default '' comment '介绍',
primary key (`col_id`)
) engine=innodb comment '学院表';
-- 创建学生表
create table tb_student
create table `tb_student`
(
stuid int not null comment '学号',
stuname varchar(20) not null comment '姓名',
stusex boolean default 1 comment '性别',
stubirth date not null comment '出生日期',
stuaddr varchar(255) default '' comment '籍贯',
collid int not null comment '所属学院',
primary key (stuid),
foreign key (collid) references tb_college (collid)
);
`stu_id` int unsigned not null comment '学号',
`stu_name` varchar(20) not null comment '姓名',
`stu_sex` boolean default 1 comment '性别',
`stu_birth` date not null comment '出生日期',
`stu_addr` varchar(255) default '' comment '籍贯',
`col_id` int unsigned not null comment '所属学院',
primary key (`stu_id`),
foreign key (`col_id`) references `tb_college` (`col_id`)
) engine=innodb comment '学生表';
-- 创建教师表
create table tb_teacher
create table `tb_teacher`
(
teaid int not null comment '工号',
teaname varchar(20) not null comment '姓名',
teatitle varchar(10) default '讲师' comment '职称',
collid int not null comment '所属学院',
primary key (teaid),
foreign key (collid) references tb_college (collid)
);
`tea_id` int unsigned not null comment '工号',
`tea_name` varchar(20) not null comment '姓名',
`tea_title` varchar(10) default '助教' comment '职称',
`col_id` int unsigned not null comment '所属学院',
primary key (`tea_id`),
foreign key (`col_id`) references `tb_college` (`col_id`)
) engine=innodb comment '老师表';
-- 创建课程表
create table tb_course
create table `tb_course`
(
couid int not null comment '编号',
couname varchar(50) not null comment '名称',
coucredit int not null comment '学分',
teaid int not null comment '授课老师',
primary key (couid),
foreign key (teaid) references tb_teacher (teaid)
);
`cou_id` int unsigned not null comment '编号',
`cou_name` varchar(50) not null comment '名称',
`cou_credit` int unsigned not null comment '学分',
`tea_id` int unsigned not null comment '授课老师',
primary key (`cou_id`),
foreign key (`tea_id`) references `tb_teacher` (`tea_id`)
) engine=innodb comment '课程表';
-- 创建选课记录表
create table tb_record
create table `tb_record`
(
recid int auto_increment comment '选课记录',
sid int not null comment '选课学生',
cid int not null comment '所选课程',
seldate datetime default now() comment '选课时间日期',
score decimal(4,1) comment '考试成绩',
primary key (recid),
foreign key (sid) references tb_student (stuid),
foreign key (cid) references tb_course (couid),
unique (sid, cid)
);
`rec_id` bigint unsigned auto_increment comment '选课记录',
`sid` int unsigned not null comment '学号',
`cid` int unsigned not null comment '课程编号',
`sel_date` date not null comment '选课日期',
`score` decimal(4,1) comment '考试成绩',
primary key (`rec_id`),
foreign key (`sid`) references `tb_student` (`stu_id`),
foreign key (`cid`) references `tb_course` (`cou_id`),
unique (`sid`, `cid`)
) engine=innodb comment '选课记录表';
-- 插入学院数据
insert into tb_college (collname, collintro) values
('计算机学院', '计算机学院1958年设立计算机专业1981年建立计算机科学系1998年设立计算机学院2005年5月为了进一步整合教学和科研资源学校决定计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系计算机科学与技术系、物联网工程系、计算金融系两个研究所图象图形研究所、网络空间安全研究院2015年成立三个教学实验中心计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。'),
('外国语学院', '外国语学院设有7个教学单位6个文理兼收的本科专业拥有1个一级学科博士授予点3个二级学科博士授予点5个一级学科硕士学位授权点5个二级学科硕士学位授权点5个硕士专业授权领域同时还有2个硕士专业学位MTI专业有教职员工210余人其中教授、副教授80余人教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。'),
('经济管理学院', '经济学院前身是创办于1905年的经济科已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光以及当代学者刘诗白等曾先后在此任教或学习。');
insert into `tb_college`
(`col_name`, `col_intro`)
values
('计算机学院', '计算机学院1958年设立计算机专业1981年建立计算机科学系1998年设立计算机学院2005年5月为了进一步整合教学和科研资源学校决定计算机学院和软件学院行政班子合并统一运作、实行教学和学生管理独立运行的模式。 学院下设三个系计算机科学与技术系、物联网工程系、计算金融系两个研究所图象图形研究所、网络空间安全研究院2015年成立三个教学实验中心计算机基础教学实验中心、IBM技术中心和计算机专业实验中心。'),
('外国语学院', '外国语学院设有7个教学单位6个文理兼收的本科专业拥有1个一级学科博士授予点3个二级学科博士授予点5个一级学科硕士学位授权点5个二级学科硕士学位授权点5个硕士专业授权领域同时还有2个硕士专业学位MTI专业有教职员工210余人其中教授、副教授80余人教师中获得中国国内外名校博士学位和正在职攻读博士学位的教师比例占专任教师的60%以上。'),
('经济管理学院', '经济学院前身是创办于1905年的经济科已故经济学家彭迪先、张与九、蒋学模、胡寄窗、陶大镛、胡代光以及当代学者刘诗白等曾先后在此任教或学习。');
-- 插入学生数据
insert into tb_student (stuid, stuname, stusex, stubirth, stuaddr, collid)
insert into `tb_student`
(`stu_id`, `stu_name`, `stu_sex`, `stu_birth`, `stu_addr`, `col_id`)
values
(1001, '', 1, '1990-3-4', '四川成都', 1),
(1001, '', 1, '1990-3-4', '湖南长沙', 1),
(1002, '任我行', 1, '1992-2-2', '湖南长沙', 1),
(1033, '王语嫣', 0, '1989-12-3', '四川成都', 1),
(1572, '岳不群', 1, '1993-7-19', '陕西咸阳', 1),
@ -83,161 +86,51 @@ values
(2035, '东方不败', 1, '1988-6-30', null, 2),
(3011, '林震南', 1, '1985-12-12', '福建莆田', 3),
(3755, '项少龙', 1, '1993-1-25', null, 3),
(3923, '杨不悔', 0, '1985-4-17', '四川成都', 3),
(4040, '炼腰的隔壁老王', 1, '1989-1-1', '四川成都', 2);
-- 删除学生数据
delete from tb_student where stuid=4040;
-- 更新学生数据
update tb_student set stuname='杨过', stuaddr='湖南长沙' where stuid=1001;
(3923, '杨不悔', 0, '1985-4-17', '四川成都', 3);
-- 插入老师数据
insert into tb_teacher (teaid, teaname, teatitle, collid) values
(1122, '张三丰', '教授', 1),
(1133, '宋远桥', '副教授', 1),
(1144, '杨逍', '副教授', 1),
(2255, '范遥', '副教授', 2),
(3366, '韦一笑', default, 3);
insert into `tb_teacher`
(`tea_id`, `tea_name`, `tea_title`, `col_id`)
values
(1122, '张三丰', '教授', 1),
(1133, '宋远桥', '副教授', 1),
(1144, '杨逍', '副教授', 1),
(2255, '范遥', '副教授', 2),
(3366, '韦一笑', default, 3);
-- 插入课程数据
insert into tb_course (couid, couname, coucredit, teaid) values
(1111, 'Python程序设计', 3, 1122),
(2222, 'Web前端开发', 2, 1122),
(3333, '操作系统', 4, 1122),
(4444, '计算机网络', 2, 1133),
(5555, '编译原理', 4, 1144),
(6666, '算法和数据结构', 3, 1144),
(7777, '经贸法语', 3, 2255),
(8888, '成本会计', 2, 3366),
(9999, '审计学', 3, 3366);
insert into `tb_course`
(`cou_id`, `cou_name`, `cou_credit`, `tea_id`)
values
(1111, 'Python程序设计', 3, 1122),
(2222, 'Web前端开发', 2, 1122),
(3333, '操作系统', 4, 1122),
(4444, '计算机网络', 2, 1133),
(5555, '编译原理', 4, 1144),
(6666, '算法和数据结构', 3, 1144),
(7777, '经贸法语', 3, 2255),
(8888, '成本会计', 2, 3366),
(9999, '审计学', 3, 3366);
-- 插入选课数据
insert into tb_record (sid, cid, seldate, score) values
(1001, 1111, '2017-09-01', 95),
(1001, 2222, '2017-09-01', 87.5),
(1001, 3333, '2017-09-01', 100),
(1001, 4444, '2018-09-03', null),
(1001, 6666, '2017-09-02', 100),
(1002, 1111, '2017-09-03', 65),
(1002, 5555, '2017-09-01', 42),
(1033, 1111, '2017-09-03', 92.5),
(1033, 4444, '2017-09-01', 78),
(1033, 5555, '2017-09-01', 82.5),
(1572, 1111, '2017-09-02', 78),
(1378, 1111, '2017-09-05', 82),
(1378, 7777, '2017-09-02', 65.5),
(2035, 7777, '2018-09-03', 88),
(2035, 9999, default, null),
(3755, 1111, default, null),
(3755, 8888, default, null),
(3755, 9999, '2017-09-01', 92);
-- 查询所有学生信息
select * from tb_student;
-- 查询所有课程名称及学分(投影和别名)
select couname, coucredit from tb_course;
select couname as , coucredit as from tb_course;
select stuname as , case stusex when 1 then '' else '' end as from tb_student;
select stuname as , if(stusex, '', '') as from tb_student;
-- 查询所有女学生的姓名和出生日期(筛选)
select stuname, stubirth from tb_student where stusex=0;
-- 查询所有80后学生的姓名、性别和出生日期(筛选)
select stuname, stusex, stubirth from tb_student where stubirth>='1980-1-1' and stubirth<='1989-12-31';
select stuname, stusex, stubirth from tb_student where stubirth between '1980-1-1' and '1989-12-31';
-- 查询姓"杨"的学生姓名和性别(模糊)
select stuname, stusex from tb_student where stuname like '杨%';
-- 查询姓"杨"名字两个字的学生姓名和性别(模糊)
select stuname, stusex from tb_student where stuname like '杨_';
-- 查询姓"杨"名字三个字的学生姓名和性别(模糊)
select stuname, stusex from tb_student where stuname like '杨__';
-- 查询名字中有"不"字或"嫣"字的学生的姓名(模糊)
select stuname, stusex from tb_student where stuname like '%不%' or stuname like '%嫣%';
-- 查询没有录入家庭住址的学生姓名(空值)
select stuname from tb_student where stuaddr is null;
-- 查询录入了家庭住址的学生姓名(空值)
select stuname from tb_student where stuaddr is not null;
-- 查询学生选课的所有日期(去重)
select distinct scdate from tb_score;
-- 查询学生的家庭住址(去重)
select distinct stuaddr from tb_student where stuaddr is not null;
-- 查询男学生的姓名和生日按年龄从大到小排列(排序)
-- asc - ascending - 升序(从小到大)
-- desc - descending - 降序(从大到小)
select stuname as , year(now())-year(stubirth) as from tb_student where stusex=1 order by desc;
-- 聚合函数max / min / count / sum / avg
-- 查询年龄最大的学生的出生日期(聚合函数)
select min(stubirth) from tb_student;
-- 查询年龄最小的学生的出生日期(聚合函数)
select max(stubirth) from tb_student;
-- 查询男女学生的人数(分组和聚合函数)
select count(stuid) from tb_student;
select stusex, count(*) from tb_student group by stusex;
select stusex, min(stubirth) from tb_student group by stusex;
-- 查询课程编号为1111的课程的平均成绩(筛选和聚合函数)
select avg(scmark) from tb_score where couid=1111;
select min(scmark) from tb_score where couid=1111;
select count(scid) from tb_score where couid=1111;
select count(scmark) from tb_score where couid=1111;
-- 查询学号为1001的学生所有课程的平均分(筛选和聚合函数)
select avg(scmark) from tb_score where stuid=1001;
-- 查询每个学生的学号和平均成绩(分组和聚合函数)
select stuid as , avg(scmark) as from tb_score group by stuid;
-- 查询平均成绩大于等于90分的学生的学号和平均成绩
-- 分组以前的筛选使用where子句
-- 分组以后的筛选使用having子句
select stuid as , avg(scmark) as from tb_score group by stuid having >=90;
-- 查询年龄最大的学生的姓名(子查询/嵌套的查询)
select stuname from tb_student where stubirth=(
select min(stubirth) from tb_student
);
-- 查询年龄最大的学生姓名和年龄(子查询+运算)
select stuname as , year(now())-year(stubirth) as from tb_student where stubirth=(
select min(stubirth) from tb_student
);
-- 查询选了两门以上的课程的学生姓名(子查询/分组条件/集合运算)
select stuname from tb_student where stuid in (
select stuid from tb_score group by stuid having count(stuid)>2
)
-- 查询学生姓名、课程名称以及成绩(连接查询)
select stuname, couname, scmark from tb_student t1, tb_course t2, tb_score t3 where t1.stuid=t3.stuid and t2.couid=t3.couid and scmark is not null;
select stuname, couname, scmark from tb_student t1 inner join tb_score t3 on t1.stuid=t3.stuid inner join tb_course t2 on t2.couid=t3.couid where scmark is not null order by scmark desc limit 5 offset 10;
select stuname, couname, scmark from tb_student t1 inner join tb_score t3 on t1.stuid=t3.stuid inner join tb_course t2 on t2.couid=t3.couid where scmark is not null order by scmark desc limit 10, 5;
-- 查询选课学生的姓名和平均成绩(子查询和连接查询)
select stuname, avgmark from tb_student t1, (select stuid, avg(scmark) as avgmark from tb_score group by stuid) t2 where t1.stuid=t2.stuid;
select stuname, avgmark from tb_student t1 inner join
(select stuid, avg(scmark) as avgmark from tb_score group by stuid) t2 on t1.stuid=t2.stuid;
-- 内连接inner join只有满足连接条件的记录才会被查出来
-- 外连接outer join左外连接 / 右外连接 / 全外连接
-- left outer join / right outer join / full outer join
-- 查询每个学生的姓名和选课数量(左外连接和子查询)
select stuname, ifnull(total, 0) from tb_student t1 left outer join (select stuid, count(stuid) as total from tb_score group by stuid) t2 on t1.stuid=t2.stuid;
insert into `tb_record`
(`sid`, `cid`, `sel_date`, `score`)
values
(1001, 1111, '2017-09-01', 95),
(1001, 2222, '2017-09-01', 87.5),
(1001, 3333, '2017-09-01', 100),
(1001, 4444, '2018-09-03', null),
(1001, 6666, '2017-09-02', 100),
(1002, 1111, '2017-09-03', 65),
(1002, 5555, '2017-09-01', 42),
(1033, 1111, '2017-09-03', 92.5),
(1033, 4444, '2017-09-01', 78),
(1033, 5555, '2017-09-01', 82.5),
(1572, 1111, '2017-09-02', 78),
(1378, 1111, '2017-09-05', 82),
(1378, 7777, '2017-09-02', 65.5),
(2035, 7777, '2018-09-03', 88),
(2035, 9999, '2019-09-02', null),
(3755, 1111, '2019-09-02', null),
(3755, 8888, '2019-09-02', null),
(3755, 9999, '2017-09-01', 92);

View File

@ -0,0 +1,144 @@
drop database if exists hrs;
create database hrs default charset utf8mb4;
use hrs;
create table tb_dept
(
dno int not null comment '编号',
dname varchar(10) not null comment '名称',
dloc varchar(20) not null comment '所在地',
primary key (dno)
);
insert into tb_dept values
(10, '会计部', '北京'),
(20, '研发部', '成都'),
(30, '销售部', '重庆'),
(40, '运维部', '深圳');
create table tb_emp
(
eno int not null comment '员工编号',
ename varchar(20) not null comment '员工姓名',
job varchar(20) not null comment '员工职位',
mgr int comment '主管编号',
sal int not null comment '员工月薪',
comm int comment '每月补贴',
dno int comment '所在部门编号',
primary key (eno),
foreign key (dno) references tb_dept (dno)
);
-- alter table tb_emp add constraint pk_emp_eno primary key (eno);
-- alter table tb_emp add constraint uk_emp_ename unique (ename);
-- alter table tb_emp add constraint fk_emp_mgr foreign key (mgr) references tb_emp (eno);
-- alter table tb_emp add constraint fk_emp_dno foreign key (dno) references tb_dept (dno);
insert into tb_emp values
(7800, '张三丰', '总裁', null, 9000, 1200, 20),
(2056, '乔峰', '分析师', 7800, 5000, 1500, 20),
(3088, '李莫愁', '设计师', 2056, 3500, 800, 20),
(3211, '张无忌', '程序员', 2056, 3200, null, 20),
(3233, '丘处机', '程序员', 2056, 3400, null, 20),
(3251, '张翠山', '程序员', 2056, 4000, null, 20),
(5566, '宋远桥', '会计师', 7800, 4000, 1000, 10),
(5234, '郭靖', '出纳', 5566, 2000, null, 10),
(3344, '黄蓉', '销售主管', 7800, 3000, 800, 30),
(1359, '胡一刀', '销售员', 3344, 1800, 200, 30),
(4466, '苗人凤', '销售员', 3344, 2500, null, 30),
(3244, '欧阳锋', '程序员', 3088, 3200, null, 20),
(3577, '杨过', '会计', 5566, 2200, null, 10),
(3588, '朱九真', '会计', 5566, 2500, null, 10);
-- 查询月薪最高的员工姓名和月薪
select ename, sal from tb_emp where sal=(select max(sal) from tb_emp);
select ename, sal from tb_emp where sal>=all(select sal from tb_emp);
-- 查询员工的姓名和年薪((月薪+补贴)*13)
select ename, (sal+ifnull(comm,0))*13 as ann_sal from tb_emp order by ann_sal desc;
-- 查询有员工的部门的编号和人数
select dno, count(*) as total from tb_emp group by dno;
-- 查询所有部门的名称和人数
select dname, ifnull(total,0) as total from tb_dept left join
(select dno, count(*) as total from tb_emp group by dno) tb_temp
on tb_dept.dno=tb_temp.dno;
-- 查询月薪最高的员工(Boss除外)的姓名和月薪
select ename, sal from tb_emp where sal=(
select max(sal) from tb_emp where mgr is not null
);
-- 查询月薪排第2名的员工的姓名和月薪
select ename, sal from tb_emp where sal=(
select distinct sal from tb_emp order by sal desc limit 1,1
);
select ename, sal from tb_emp where sal=(
select max(sal) from tb_emp where sal<(select max(sal) from tb_emp)
);
-- 查询月薪超过平均月薪的员工的姓名和月薪
select ename, sal from tb_emp where sal>(select avg(sal) from tb_emp);
-- 查询月薪超过其所在部门平均月薪的员工的姓名、部门编号和月薪
select ename, t1.dno, sal from tb_emp t1 inner join
(select dno, avg(sal) as avg_sal from tb_emp group by dno) t2
on t1.dno=t2.dno and sal>avg_sal;
-- 查询部门中月薪最高的人姓名、月薪和所在部门名称
select ename, sal, dname
from tb_emp t1, tb_dept t2, (
select dno, max(sal) as max_sal from tb_emp group by dno
) t3 where t1.dno=t2.dno and t1.dno=t3.dno and sal=max_sal;
-- 查询主管的姓名和职位
-- 提示尽量少用in/not in运算尽量少用distinct操作
-- 可以使用存在性判断exists/not exists替代集合运算和去重操作
select ename, job from tb_emp where eno in (
select distinct mgr from tb_emp where mgr is not null
);
select ename, job from tb_emp where eno=any(
select distinct mgr from tb_emp where mgr is not null
);
select ename, job from tb_emp t1 where exists (
select 'x' from tb_emp t2 where t1.eno=t2.mgr
);
-- MySQL8有窗口函数row_number() / rank() / dense_rank()
-- 查询月薪排名4~6名的员工的排名、姓名和月薪
select ename, sal from tb_emp order by sal desc limit 3,3;
select row_num, ename, sal from
(select @a:=@a+1 as row_num, ename, sal
from tb_emp, (select @a:=0) t1 order by sal desc) t2
where row_num between 4 and 6;
-- 窗口函数不适合业务数据库,只适合做离线数据分析
select
ename, sal,
row_number() over (order by sal desc) as row_num,
rank() over (order by sal desc) as ranking,
dense_rank() over (order by sal desc) as dense_ranking
from tb_emp limit 3 offset 3;
select ename, sal, ranking from (
select ename, sal, dense_rank() over (order by sal desc) as ranking from tb_emp
) tb_temp where ranking between 4 and 6;
-- 窗口函数主要用于解决TopN查询问题
-- 查询每个部门月薪排前2名的员工姓名、月薪和部门编号
select ename, sal, dno from (
select ename, sal, dno, rank() over (partition by dno order by sal desc) as ranking
from tb_emp
) tb_temp where ranking<=2;
select ename, sal, dno from tb_emp t1
where (select count(*) from tb_emp t2 where t1.dno=t2.dno and t2.sal>t1.sal)<2
order by dno asc, sal desc;

40
Day36-40/code/order.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
drop database if exists shop;
create database shop default charset utf8;
use shop;
drop table if exists tb_goods;
create table tb_goods
(
gid int not null auto_increment,
gname varchar(50) not null,
gprice decimal(10,2) not null,
gimage varchar(255),
primary key (gid)
);
insert into tb_goods values
(default, '乐事Lays无限薯片', 8.2, 'images/lay.jpg'),
(default, '旺旺 仙贝 加量装 540g', 18.5, 'images/wang.jpg'),
(default, '多儿比Dolbee黄桃水果罐头', 6.8, 'images/dolbee.jpg'),
(default, '王致和 精制料酒 500ml', 7.9, 'images/wine.jpg'),
(default, '陈克明 面条 鸡蛋龙须挂面', 1.0, 'images/noodle.jpg'),
(default, '鲁花 菜籽油 4L', 69.9, 'images/oil.jpg');

22
Day36-40/code/stock.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -65,8 +65,8 @@ from vote import views
urlpatterns = [
path('', views.show_subjects),
path('teachers/', views.show_teachers),
path('praise/', views.prise_or_criticize),
path('criticize/', views.prise_or_criticize),
path('praise/', views.praise_or_criticize),
path('criticize/', views.praise_or_criticize),
path('admin/', admin.site.urls),
]
```

View File

@ -31,7 +31,7 @@
python manage.py migrate polls
```
3. 用下面的SQL语句直接插入两条测试数据通常不能用户的密码直接保存在数据库中因此我们将用户密码处理成对应的MD5摘要。MD5消息摘要算法是一种被广泛使用的密码哈希函数散列函数可以产生出一个128位比特的哈希值散列值用于确保信息传输完整一致。在使用哈希值时通常会将哈希值表示为16进制字符串因此128位的MD5摘要通常表示为32个十六进制符号。
3. 用下面的SQL语句直接插入两条测试数据通常不能用户的密码直接保存在数据库中因此我们将用户密码处理成对应的MD5摘要。MD5消息摘要算法是一种被广泛使用的密码哈希函数散列函数可以产生出一个128位比特的哈希值散列值用于确保信息传输完整一致。在使用哈希值时通常会将哈希值表示为16进制字符串因此128位的MD5摘要通常表示为32个十六进制符号。
```SQL
insert into `tb_user`

View File

@ -212,4 +212,3 @@ queryset = Teacher.objects.values('subject__name').annotate(good=Avg('good_count
```
可见Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。

View File

@ -175,4 +175,3 @@ class SubjectMapper(ModelMapper):
前后端分离的开发需要将前端页面作为静态资源进行部署项目实际上线的时候我们会对整个Web应用进行动静分离静态资源通过Nginx或Apache服务器进行部署生成动态内容的Python程序部署在uWSGI或者Gunicorn服务器上对动态内容的请求由Nginx或Apache路由到uWSGI或Gunicorn服务器上。
在开发阶段我们通常会使用Django自带的测试服务器如果要尝试前后端分离可以先将静态页面放在之前创建的放静态资源的目录下具体的做法可以参考[项目完整代码](https://gitee.com/jackfrued/django19062)。

View File

@ -1,4 +1,4 @@
## 单元测试
Python标准库中提供了名为`unittest` 的模块来支持我们对代码进行单元测试。所谓单元测试是指针对程序中最小的功能单元在Python中指函数或类中的方法进行的测试
请各位读者移步到[《使用Django开发商业项目》](../Day91-100/95.使用Django开发商业项目.md)一文

View File

@ -0,0 +1,200 @@
## 第31课网络数据采集概述
爬虫crawler也经常被称为网络蜘蛛spider是按照一定的规则自动浏览网站并获取所需信息的机器人程序自动化脚本代码被广泛的应用于互联网搜索引擎和数据采集。使用过互联网和浏览器的人都知道网页中除了供用户阅读的文字信息之外还包含一些超链接网络爬虫正是通过网页中的超链接信息不断获得网络上其它页面的地址然后持续的进行数据采集。正因如此网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游所以才被形象的称为爬虫或者网络蜘蛛。
### 爬虫的应用领域
在理想的状态下,所有 ICPInternet Content Provider都应该为自己的网站提供 API 接口来共享它们允许其他程序获取的数据,在这种情况下就根本不需要爬虫程序。国内比较有名的电商平台(如淘宝、京东等)、社交平台(如微博、微信等)等都提供了自己的 API 接口,但是这类 API 接口通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言,及时的获取行业数据和竞对数据是企业生存的重要环节之一,然而对大部分企业来说,数据都是其与生俱来的短板。在这种情况下,合理的利用爬虫来获取数据并从中提取出有商业价值的信息对这些企业来说就显得至关重要的。
爬虫的应用领域其实非常广泛,下面我们列举了其中的一部分,有兴趣的读者可以自行探索相关内容。
1. 搜索引擎
2. 新闻聚合
3. 社交应用
4. 舆情监控
5. 行业数据
### 爬虫合法性探讨
经常听人说起“爬虫写得好,牢饭吃到饱”,那么编程爬虫程序是否违法呢?关于这个问题,我们可以从以下几个角度进行解读。
1. 网络爬虫这个领域目前还属于拓荒阶段,虽然互联网世界已经通过自己的游戏规则建立起了一定的道德规范,即 Robots 协议(全称是“网络爬虫排除标准”),但法律部分还在建立和完善中,也就是说,现在这个领域暂时还是灰色地带。
2. “法不禁止即为许可”,如果爬虫就像浏览器一样获取的是前端显示的数据(网页上的公开信息)而不是网站后台的私密敏感信息,就不太担心法律法规的约束,因为目前大数据产业链的发展速度远远超过了法律的完善程度。
3. 在爬取网站的时候,需要限制自己的爬虫遵守 Robots 协议同时控制网络爬虫程序的抓取数据的速度在使用数据的时候必须要尊重网站的知识产权从Web 2.0时代开始虽然Web上的数据很多都是由用户提供的但是网站平台是投入了运营成本的当用户在注册和发布内容时平台通常就已经获得了对数据的所有权、使用权和分发权。如果违反了这些规定在打官司的时候败诉几率相当高。
4. 适当的隐匿自己的身份在编写爬虫程序时必要的,而且最好不要被对方举证你的爬虫有破坏别人动产(例如服务器)的行为。
5. 不要在公网(如代码托管平台)上去开源或者展示你的爬虫代码,这些行为通常会给自己带来不必要的麻烦。
#### Robots协议
大多数网站都会定义`robots.txt`文件,这是一个君子协议,并不是所有爬虫都必须遵守的游戏规则。下面以淘宝的[`robots.txt`](http://www.taobao.com/robots.txt)文件为例,看看淘宝网对爬虫有哪些限制。
```
User-agent: Baiduspider
Disallow: /
User-agent: baiduspider
Disallow: /
```
通过上面的文件可以看出,淘宝禁止百度爬虫爬取它任何资源,因此当你在百度搜索“淘宝”的时候,搜索结果下方会出现:“由于该网站的`robots.txt`文件存在限制指令(限制搜索引擎抓取),系统无法提供该页面的内容描述”。百度作为一个搜索引擎,至少在表面上遵守了淘宝网的`robots.txt`协议,所以用户不能从百度上搜索到淘宝内部的产品信息。
图1. 百度搜索淘宝的结果
![](https://gitee.com/jackfrued/mypic/raw/master/20210824004320.png)
下面是豆瓣网的[`robots.txt`](https://www.douban.com/robots.txt)文件,大家可以自行解读,看看它做出了什么样的限制。
```
User-agent: *
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Disallow: /group/search
Disallow: /event/search
Disallow: /celebrities/search
Disallow: /location/drama/search
Disallow: /forum/
Disallow: /new_subject
Disallow: /service/iframe
Disallow: /j/
Disallow: /link2/
Disallow: /recommend/
Disallow: /doubanapp/card
Disallow: /update/topic/
Disallow: /share/
Allow: /ads.txt
Sitemap: https://www.douban.com/sitemap_index.xml
Sitemap: https://www.douban.com/sitemap_updated_index.xml
# Crawl-delay: 5
User-agent: Wandoujia Spider
Disallow: /
User-agent: Mediapartners-Google
Disallow: /subject_search
Disallow: /amazon_search
Disallow: /search
Disallow: /group/search
Disallow: /event/search
Disallow: /celebrities/search
Disallow: /location/drama/search
Disallow: /j/
```
### 超文本传输协议HTTP
在开始讲解爬虫之前我们稍微对超文本传输协议HTTP做一些回顾因为我们在网页上看到的内容通常是浏览器执行 HTML (超文本标记语言)得到的结果,而 HTTP 就是传输 HTML 数据的协议。HTTP 和其他很多应用级协议一样是构建在 TCP传输控制协议之上的它利用了 TCP 提供的可靠的传输服务实现了 Web 应用中的数据交换。按照维基百科上的介绍,设计 HTTP 最初的目的是为了提供一种发布和接收 [HTML](https://zh.wikipedia.org/wiki/HTML) 页面的方法,也就是说,这个协议是浏览器和 Web 服务器之间传输的数据的载体。关于 HTTP 的详细信息以及目前的发展状况,大家可以阅读[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)、[《互联网协议入门》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)、[《图解 HTTPS 协议》](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)等文章进行了解。
下图是我在四川省网络通信技术重点实验室工作期间用开源协议分析工具 EtherealWireShark 的前身)截取的访问百度首页时的 HTTP 请求和响应的报文(协议数据),由于 Ethereal 截取的是经过网络适配器的数据,因此可以清晰的看到从物理链路层到应用层的协议数据。
图2. HTTP请求
![http-request](https://gitee.com/jackfrued/mypic/raw/master/20210824003915.png)
HTTP 请求通常是由请求行、请求头、空行、消息体四个部分构成如果没有数据发给服务器消息体就不是必须的部分。请求行中包含了请求方法GET、POST 等,如下表所示)、资源路径和协议版本;请求头由若干键值对构成,包含了浏览器、编码方式、首选语言、缓存策略等信息;请求头的后面是空行和消息体。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210825002720.PNG" width="65%">
图3. HTTP响应
![http-response](https://gitee.com/jackfrued/mypic/raw/master/20210824234158.png)
HTTP 响应通常是由响应行、响应头、空行、消息体四个部分构成,其中消息体是服务响应的数据,可能是 HTML 页面也有可能是JSON或二进制数据等。响应行中包含了协议版本和响应状态码响应状态码有很多种常见的如下表所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210825002802.PNG" width="65%">
#### 相关工具
下面我们先介绍一些开发爬虫程序的辅助工具,这些工具相信能帮助你事半功倍。
1. Chrome Developer Tools谷歌浏览器内置的开发者工具。该工具最常用的几个功能模块是
- 元素ELements用于查看或修改 HTML 元素的属性、CSS 属性、监听事件等。CSS 可以即时修改,即时显示,大大方便了开发者调试页面。
- 控制台Console用于执行一次性代码查看 JavaScript 对象,查看调试日志信息或异常信息。控制台其实就是一个执行 JavaScript 代码的交互式环境。
- 源代码Sources用于查看页面的 HTML 文件源代码、JavaScript 源代码、CSS 源代码,此外最重要的是可以调试 JavaScript 源代码,可以给代码添加断点和单步执行。
- 网络Network用于 HTTP 请求、HTTP 响应以及与网络连接相关的信息。
- 应用Application用于查看浏览器本地存储、后台任务等内容本地存储主要包括Cookie、Local Storage、Session Storage等。
![chrome-developer-tools](https://gitee.com/jackfrued/mypic/raw/master/20210824004034.png)
2. Postman功能强大的网页调试与 RESTful 请求工具。Postman可以帮助我们模拟请求非常方便的定制我们的请求以及查看服务器的响应。
![postman](https://gitee.com/jackfrued/mypic/raw/master/20210824004048.png)
3. HTTPie命令行HTTP客户端。
安装。
```Bash
pip install httpie
```
使用。
```Bash
http --header http --header https://movie.douban.com/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 24 Aug 2021 16:48:00 GMT
Keep-Alive: timeout=30
Server: dae
Set-Cookie: bid=58h4BdKC9lM; Expires=Wed, 24-Aug-22 16:48:00 GMT; Domain=.douban.com; Path=/
Strict-Transport-Security: max-age=15552000
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-DOUBAN-NEWBID: 58h4BdKC9lM
```
4. `builtwith`库:识别网站所用技术的工具。
安装。
```Bash
pip install builtwith
```
使用。
```Python
import ssl
import builtwith
ssl._create_default_https_context = ssl._create_unverified_context
print(builtwith.parse('http://www.bootcss.com/'))
```
5. `python-whois`库:查询网站所有者的工具。
安装。
```Bash
pip3 install python-whois
```
使用。
```Python
import whois
print(whois.whois('https://www.bootcss.com'))
```
### 爬虫的基本工作流程
一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容,当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术,这就需要有调度器(安排线程或进程执行对应的任务)、后台管理程序(监控爬虫的工作状态以及检查数据抓取的结果)等的参与。
![crawler-workflow](https://gitee.com/jackfrued/mypic/raw/master/20210824004107.png)
一般来说,爬虫的工作流程包括以下几个步骤:
1. 设定抓取目标(种子页面/起始页面)并获取网页。
2. 当服务器无法访问时,按照指定的重试次数尝试重新下载页面。
3. 在需要的时候设置用户代理或隐藏真实IP否则可能无法访问页面。
4. 对获取的页面进行必要的解码操作然后抓取出需要的信息。
5. 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息。
6. 对链接进行进一步的处理(获取页面并重复上面的动作)。
7. 将有用的信息进行持久化以备后续的处理。

View File

@ -1,319 +0,0 @@
## 网络爬虫和相关工具
### 网络爬虫的概念
网络爬虫web crawler以前经常称之为网络蜘蛛spider是按照一定的规则自动浏览万维网并获取信息的机器人程序或脚本曾经被广泛的应用于互联网搜索引擎。使用过互联网和浏览器的人都知道网页中除了供用户阅读的文字信息之外还包含一些超链接。网络爬虫系统正是通过网页中的超链接信息不断获得网络上的其它页面。正因如此网络数据采集的过程就像一个爬虫或者蜘蛛在网络上漫游所以才被形象的称为网络爬虫或者网络蜘蛛。
#### 爬虫的应用领域
在理想的状态下所有ICPInternet Content Provider都应该为自己的网站提供API接口来共享它们允许其他程序获取的数据在这种情况下爬虫就不是必需品国内比较有名的电商平台如淘宝、京东等、社交平台如腾讯微博等等网站都提供了自己的Open API但是这类Open API通常会对可以抓取的数据以及抓取数据的频率进行限制。对于大多数的公司而言及时的获取行业相关数据是企业生存的重要环节之一然而大部分企业在行业数据方面的匮乏是其与生俱来的短板合理的利用爬虫来获取数据并从中提取出有商业价值的信息是至关重要的。当然爬虫还有很多重要的应用领域下面列举了其中的一部分
1. 搜索引擎
2. 新闻聚合
3. 社交应用
4. 舆情监控
5. 行业数据
### 合法性和背景调研
#### 爬虫合法性探讨
1. 网络爬虫领域目前还属于拓荒阶段,虽然互联网世界已经通过自己的游戏规则建立起一定的道德规范(Robots协议全称是“网络爬虫排除标准”),但法律部分还在建立和完善中,也就是说,现在这个领域暂时还是灰色地带。
2. “法不禁止即为许可”,如果爬虫就像浏览器一样获取的是前端显示的数据(网页上的公开信息)而不是网站后台的私密敏感信息,就不太担心法律法规的约束,因为目前大数据产业链的发展速度远远超过了法律的完善程度。
3. 在爬取网站的时候需要限制自己的爬虫遵守Robots协议同时控制网络爬虫程序的抓取数据的速度在使用数据的时候必须要尊重网站的知识产权从Web 2.0时代开始虽然Web上的数据很多都是由用户提供的但是网站平台是投入了运营成本的当用户在注册和发布内容时平台通常就已经获得了对数据的所有权、使用权和分发权。如果违反了这些规定在打官司的时候败诉几率相当高。
#### Robots.txt文件
大多数网站都会定义robots.txt文件下面以淘宝的[robots.txt](http://www.taobao.com/robots.txt)文件为例,看看该网站对爬虫有哪些限制。
```
User-agent: Baiduspider
Allow: /article
Allow: /oshtml
Disallow: /product/
Disallow: /
User-Agent: Googlebot
Allow: /article
Allow: /oshtml
Allow: /product
Allow: /spu
Allow: /dianpu
Allow: /oversea
Allow: /list
Disallow: /
User-agent: Bingbot
Allow: /article
Allow: /oshtml
Allow: /product
Allow: /spu
Allow: /dianpu
Allow: /oversea
Allow: /list
Disallow: /
User-Agent: 360Spider
Allow: /article
Allow: /oshtml
Disallow: /
User-Agent: Yisouspider
Allow: /article
Allow: /oshtml
Disallow: /
User-Agent: Sogouspider
Allow: /article
Allow: /oshtml
Allow: /product
Disallow: /
User-Agent: Yahoo! Slurp
Allow: /product
Allow: /spu
Allow: /dianpu
Allow: /oversea
Allow: /list
Disallow: /
User-Agent: *
Disallow: /
```
注意上面robots.txt第一段的最后一行通过设置“Disallow: /”禁止百度爬虫访问除了“Allow”规定页面外的其他所有页面。因此当你在百度搜索“淘宝”的时候搜索结果下方会出现“由于该网站的robots.txt文件存在限制指令限制搜索引擎抓取系统无法提供该页面的内容描述”。百度作为一个搜索引擎至少在表面上遵守了淘宝网的robots.txt协议所以用户不能从百度上搜索到淘宝内部的产品信息。
![](./res/baidu-search-taobao.png)
### 相关工具介绍
#### HTTP协议
在开始讲解爬虫之前我们稍微对HTTP超文本传输协议做一些回顾因为我们在网页上看到的内容通常是浏览器执行HTML语言得到的结果而HTTP就是传输HTML数据的协议。HTTP和其他很多应用级协议一样是构建在TCP传输控制协议之上的它利用了TCP提供的可靠的传输服务实现了Web应用中的数据交换。按照维基百科上的介绍设计HTTP最初的目的是为了提供一种发布和接收[HTML](https://zh.wikipedia.org/wiki/HTML)页面的方法也就是说这个协议是浏览器和Web服务器之间传输的数据的载体。关于这个协议的详细信息以及目前的发展状况大家可以阅读阮一峰老师的[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)、[《互联网协议入门》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)系列以及[《图解HTTPS协议》](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)进行了解下图是我在四川省网络通信技术重点实验室工作期间用开源协议分析工具Ethereal抓包工具WireShark的前身截取的访问百度首页时的HTTP请求和响应的报文协议数据由于Ethereal截取的是经过网络适配器的数据因此可以清晰的看到从物理链路层到应用层的协议数据。
HTTP请求请求行+请求头+空行+[消息体]
![](./res/http-request.png)
HTTP响应响应行+响应头+空行+消息体):
![](./res/http-response.png)
> 说明但愿这两张如同泛黄照片般的截图帮助你大概的了解到HTTP是一个怎样的协议。
#### 相关工具
1. Chrome Developer Tools谷歌浏览器内置的开发者工具。
![](./res/chrome-developer-tools.png)
2. Postman功能强大的网页调试与RESTful请求工具。
![](./res/postman.png)
3. HTTPie命令行HTTP客户端。
```Bash
pip3 install httpie
```
```Bash
http --header http://www.scu.edu.cn
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, max-age=600
Connection: Keep-Alive
Content-Encoding: gzip
Content-Language: zh-CN
Content-Length: 14403
Content-Type: text/html
Date: Sun, 27 May 2018 15:38:25 GMT
ETag: "e6ec-56d3032d70a32-gzip"
Expires: Sun, 27 May 2018 15:48:25 GMT
Keep-Alive: timeout=5, max=100
Last-Modified: Sun, 27 May 2018 13:44:22 GMT
Server: VWebServer
Vary: User-Agent,Accept-Encoding
X-Frame-Options: SAMEORIGIN
```
4. `builtwith`库:识别网站所用技术的工具。
```Bash
pip3 install builtwith
```
```Python
>>> import builtwith
>>> builtwith.parse('http://www.bootcss.com/')
{'web-servers': ['Nginx'], 'font-scripts': ['Font Awesome'], 'javascript-frameworks': ['Lo-dash', 'Underscore.js', 'Vue.js', 'Zepto', 'jQuery'], 'web-frameworks': ['Twitter Bootstrap']}
>>>
>>> import ssl
>>> ssl._create_default_https_context = ssl._create_unverified_context
>>> builtwith.parse('https://www.jianshu.com/')
{'web-servers': ['Tengine'], 'web-frameworks': ['Twitter Bootstrap', 'Ruby on Rails'], 'programming-languages': ['Ruby']}
```
5. `python-whois`库:查询网站所有者的工具。
```Bash
pip3 install python-whois
```
```Python
>>> import whois
>>> whois.whois('baidu.com')
{'domain_name': ['BAIDU.COM', 'baidu.com'], 'registrar': 'MarkMonitor, Inc.', 'whois_server': 'whois.markmonitor.com', 'referral_url': None, 'updated_date': [datetime.datetime(2017, 7, 28, 2, 36, 28), datetime.datetime(2017, 7, 27, 19, 36, 28)], 'creation_date': [datetime.datetime(1999, 10, 11, 11, 5, 17), datetime.datetime(1999, 10, 11, 4, 5, 17)], 'expiration_date': [datetime.datetime(2026, 10, 11, 11, 5, 17), datetime.datetime(2026, 10, 11, 0, 0)], 'name_servers': ['DNS.BAIDU.COM', 'NS2.BAIDU.COM', 'NS3.BAIDU.COM', 'NS4.BAIDU.COM', 'NS7.BAIDU.COM', 'dns.baidu.com', 'ns4.baidu.com', 'ns3.baidu.com', 'ns7.baidu.com', 'ns2.baidu.com'], 'status': ['clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited', 'clientTransferProhibited https://icann.org/epp#clientTransferProhibited', 'clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited', 'serverDeleteProhibited https://icann.org/epp#serverDeleteProhibited', 'serverTransferProhibited https://icann.org/epp#serverTransferProhibited', 'serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited', 'clientUpdateProhibited (https://www.icann.org/epp#clientUpdateProhibited)', 'clientTransferProhibited (https://www.icann.org/epp#clientTransferProhibited)', 'clientDeleteProhibited (https://www.icann.org/epp#clientDeleteProhibited)', 'serverUpdateProhibited (https://www.icann.org/epp#serverUpdateProhibited)', 'serverTransferProhibited (https://www.icann.org/epp#serverTransferProhibited)', 'serverDeleteProhibited (https://www.icann.org/epp#serverDeleteProhibited)'], 'emails': ['abusecomplaints@markmonitor.com', 'whoisrelay@markmonitor.com'], 'dnssec': 'unsigned', 'name': None, 'org': 'Beijing Baidu Netcom Science Technology Co., Ltd.', 'address': None, 'city': None, 'state': 'Beijing', 'zipcode': None, 'country': 'CN'}
```
6. `robotparser`模块:解析`robots.txt`的工具。
```Python
>>> from urllib import robotparser
>>> parser = robotparser.RobotFileParser()
>>> parser.set_url('https://www.taobao.com/robots.txt')
>>> parser.read()
>>> parser.can_fetch('Baiduspider', 'http://www.taobao.com/article')
True
>>> parser.can_fetch('Baiduspider', 'http://www.taobao.com/product')
False
```
### 一个简单的爬虫
一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容,当然更为高级的爬虫在数据采集和处理时会使用并发编程或分布式技术,这就需要有调度器(安排线程或进程执行对应的任务)、后台管理程序(监控爬虫的工作状态以及检查数据抓取的结果)等的参与。
![](./res/crawler-workflow.png)
一般来说,爬虫的工作流程包括以下几个步骤:
1. 设定抓取目标(种子页面/起始页面)并获取网页。
2. 当服务器无法访问时,按照指定的重试次数尝试重新下载页面。
3. 在需要的时候设置用户代理或隐藏真实IP否则可能无法访问页面。
4. 对获取的页面进行必要的解码操作然后抓取出需要的信息。
5. 在获取的页面中通过某种方式(如正则表达式)抽取出页面中的链接信息。
6. 对链接进行进一步的处理(获取页面并重复上面的动作)。
7. 将有用的信息进行持久化以备后续的处理。
下面的例子给出了一个从“搜狐体育”上获取NBA新闻标题和链接的爬虫。
```Python
import re
from collections import deque
from urllib.parse import urljoin
import requests
LI_A_PATTERN = re.compile(r'<li class="item">.*?</li>')
A_TEXT_PATTERN = re.compile(r'<a\s+[^>]*?>(.*?)</a>')
A_HREF_PATTERN = re.compile(r'<a\s+[^>]*?href="(.*?)"\s*[^>]*?>')
def decode_page(page_bytes, charsets):
"""通过指定的字符集对页面进行解码"""
for charset in charsets:
try:
return page_bytes.decode(charset)
except UnicodeDecodeError:
pass
def get_matched_parts(content_string, pattern):
"""从字符串中提取所有跟正则表达式匹配的内容"""
return pattern.findall(content_string, re.I) \
if content_string else []
def get_matched_part(content_string, pattern, group_no=1):
"""从字符串中提取跟正则表达式匹配的内容"""
match = pattern.search(content_string)
if match:
return match.group(group_no)
def get_page_html(seed_url, *, charsets=('utf-8', )):
"""获取页面的HTML代码"""
resp = requests.get(seed_url)
if resp.status_code == 200:
return decode_page(resp.content, charsets)
def repair_incorrect_href(current_url, href):
"""修正获取的href属性"""
if href.startswith('//'):
href = urljoin('http://', href)
elif href.startswith('/'):
href = urljoin(current_url, href)
return href if href.startswith('http') else ''
def start_crawl(seed_url, pattern, *, max_depth=-1):
"""开始爬取数据"""
new_urls, visited_urls = deque(), set()
new_urls.append((seed_url, 0))
while new_urls:
current_url, depth = new_urls.popleft()
if depth != max_depth:
page_html = get_page_html(current_url, charsets=('utf-8', 'gbk'))
contents = get_matched_parts(page_html, pattern)
for content in contents:
text = get_matched_part(content, A_TEXT_PATTERN)
href = get_matched_part(content, A_HREF_PATTERN)
if href:
href = repair_incorrect_href(href)
print(text, href)
if href and href not in visited_urls:
new_urls.append((href, depth + 1))
def main():
"""主函数"""
start_crawl(
seed_url='http://sports.sohu.com/nba_a.shtml',
pattern=LI_A_PATTERN,
max_depth=2
)
if __name__ == '__main__':
main()
```
### 爬虫注意事项
通过上面的例子,我们对爬虫已经有了一个感性的认识,在编写爬虫时有以下一些注意事项:
1. 上面的代码使用了`requests`三方库来获取网络资源,这是一个非常优质的三方库,关于它的用法可以参考它的[官方文档](https://requests.readthedocs.io/zh_CN/latest/)。
2. 上面的代码中使用了双端队列(`deque`来保存待爬取的URL。双端队列相当于是使用链式存储结构的`list`在双端队列的头尾添加和删除元素性能都比较好刚好可以用来构造一个FIFO先进先出的队列结构。
3. 处理相对路径。有的时候我们从页面中获取的链接不是一个完整的绝对链接而是一个相对链接这种情况下需要将其与URL前缀进行拼接`urllib.parse`中的`urljoin()`函数可以完成此项操作)。
4. 设置代理服务。有些网站会限制访问的区域例如美国的Netflix屏蔽了很多国家的访问有些爬虫需要隐藏自己的身份在这种情况下可以设置使用代理服务器代理服务器有免费的服务器和付费的商业服务器但后者稳定性和可用性都更好强烈建议在商业项目中使用付费的商业代理服务器。如果使用`requests`三方库,可以在请求方法中添加`proxies`参数来指定代理服务器;如果使用标准库,可以通过修改`urllib.request`中的`ProxyHandler`来为请求设置代理服务器。
5. 限制下载速度。如果我们的爬虫获取网页的速度过快,可能就会面临被封禁或者产生“损害动产”的风险(这个可能会导致吃官司且败诉),可以在两次获取页面数据之间添加延时从而对爬虫进行限速。
6. 避免爬虫陷阱。有些网站会动态生成页面内容,这会导致产生无限多的页面(例如在线万年历通常会有无穷无尽的链接)。可以通过记录到达当前页面经过了多少个链接(链接深度)来解决该问题,当达到事先设定的最大深度时,爬虫就不再像队列中添加该网页中的链接了。
7. 避开蜜罐链接。网站上的有些链接是浏览器中不可见的这种链接通常是故意诱使爬虫去访问的蜜罐一旦访问了这些链接服务器就会判定请求是来自于爬虫的这样可能会导致被服务器封禁IP地址。如何避开这些蜜罐链接我们在后面为大家进行讲解。
8. SSL相关问题。如果使用标准库的`urlopen`打开一个HTTPS链接时会验证一次SSL证书如果不做出处理会产生错误提示“SSL: CERTIFICATE_VERIFY_FAILED”可以通过以下两种方式加以解决
- 使用未经验证的上下文
```Python
import ssl
request = urllib.request.Request(url='...', headers={...})
context = ssl._create_unverified_context()
web_page = urllib.request.urlopen(request, context=context)
```
- 设置全局性取消证书验证
```Python
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
```

View File

@ -0,0 +1,133 @@
## 第32课用Python获取网络数据
网络数据采集是 Python 语言非常擅长的领域上节课我们讲到实现网络数据采集的程序通常称之为网络爬虫或蜘蛛程序。即便是在大数据时代数据对于中小企业来说仍然是硬伤和短板有些数据需要通过开放或付费的数据接口来获得其他的行业数据和竞对数据则必须要通过网络数据采集的方式来获得。不管使用哪种方式获取网络数据资源Python 语言都是非常好的选择,因为 Python 的标准库和三方库都对网络数据采集提供了良好的支持。
### requests库
要使用 Python 获取网络数据,我们推荐大家使用名为`requests` 的三方库,这个库我们在之前的课程中其实已经使用过了。按照官方网站的解释,`requests`是基于 Python 标准库进行了封装,简化了通过 HTTP 或 HTTPS 访问网络资源的操作。上课我们提到过HTTP 是一个请求响应式的协议,当我们在浏览器中输入正确的 [URL](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL)(通常也称为网址)并按下 Enter 键时,我们就向网络上的 [Web 服务器](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_web_server)发送了一个 HTTP 请求,服务器在收到请求后会给我们一个 HTTP 响应。在 Chrome 浏览器中的菜单中打开“开发者工具”切换到“Network”选项卡就能够查看 HTTP 请求和响应到底是什么样子的,如下图所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20210822093434.png)
通过`requests`库,我们可以让 Python 程序向浏览器一样向 Web 服务器发起请求,并接收服务器返回的响应,从响应中我们就可以提取出想要的数据。浏览器呈现给我们的网页是用 [HTML](https://developer.mozilla.org/zh-CN/docs/Web/HTML) 编写的,浏览器相当于是 HTML 的解释器环境,我们看到的网页中的内容都包含在 HTML 的标签中。在获取到 HTML 代码后,就可以从标签的属性或标签体中提取内容。下面例子演示了如何获取网页 HTML 代码,我们通过`requests`库的`get`函数,获取了搜狐首页的代码。
```Python
import requests
resp = requests.get('https://www.sohu.com/')
if resp.status_code == 200:
print(resp.text)
```
> **说明**:上面代码中的变量`resp`是一个`Response`对象(`requests`库封装的类型),通过该对象的`status_code`属性可以获取响应状态码,而该对象的`text`属性可以帮我们获取到页面的 HTML 代码。
由于`Response`对象的`text`是一个字符串,所以我们可以利用之前讲过的正则表达式的知识,从页面的 HTML 代码中提取新闻的标题和链接,代码如下所示。
```Python
import re
import requests
pattern = re.compile(r'<a.*?href="(.*?)".*?title="(.*?)".*?>')
resp = requests.get('https://www.sohu.com/')
if resp.status_code == 200:
all_matches = pattern.findall(resp.text)
for href, title in all_matches:
print(href)
print(title)
```
除了文本内容,我们也可以使用`requests`库通过 URL 获取二进制资源。下面的例子演示了如何获取百度 Logo 并保存到名为`baidu.png`的本地文件中。可以在百度的首页上右键点击百度Logo并通过“复制图片地址”菜单项获取图片的 URL。
```Python
import requests
resp = requests.get('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png')
with open('baidu.png', 'wb') as file:
file.write(resp.content)
```
> **说明**`Response`对象的`content`属性可以获得服务器响应的二进制数据。
`requests`库非常好用而且功能上也比较强大和完整,具体的内容我们在使用的过程中为大家一点点剖析。想解锁关于`requests`库更多的知识,可以阅读它的[官方文档](https://docs.python-requests.org/zh_CN/latest/)。
### 编写爬虫代码
接下来,我们以“豆瓣电影”为例,为大家讲解如何编写爬虫代码。按照上面提供的方法,我们先使用`requests`获取到网页的HTML代码然后将整个代码看成一个长字符串这样我们就可以使用正则表达式的捕获组从字符串提取我们需要的内容。下面的代码演示了如何从[豆瓣电影](https://movie.douban.com/)获取排前250名的电影的名称。[豆瓣电影Top250](https://movie.douban.com/top250)的页面结构和对应代码如下图所示可以看出每页共展示了25部电影如果要获取到 Top250 数据我们共需要访问10个页面对应的地址是<https://movie.douban.com/top250?start=xxx>,这里的`xxx`如果为`0`就是第一页,如果`xxx`的值是`100`,那么我们可以访问到第五页。为了代码简单易读,我们只获取电影的标题和评分。
![](https://gitee.com/jackfrued/mypic/raw/master/20210822093447.png)
```Python
import random
import re
import time
import requests
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
# 如果不设置HTTP请求头中的User-Agent豆瓣会检测出不是浏览器而阻止我们的请求。
# 通过get函数的headers参数设置User-Agent的值具体的值可以在浏览器的开发者工具查看到。
# 用爬虫访问大部分网站时,将爬虫伪装成来自浏览器的请求都是非常重要的一步。
headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'}
)
# 通过正则表达式获取class属性为title且标签体不以&开头的span标签并用捕获组提取标签内容
pattern1 = re.compile(r'<span class="title">([^&]*?)</span>')
titles = pattern1.findall(resp.text)
# 通过正则表达式获取class属性为rating_num的span标签并用捕获组提取标签内容
pattern2 = re.compile(r'<span class="rating_num".*?>(.*?)</span>')
ranks = pattern2.findall(resp.text)
# 使用zip压缩两个列表循环遍历所有的电影标题和评分
for title, rank in zip(titles, ranks):
print(title, rank)
# 随机休眠1-5秒避免爬取页面过于频繁
time.sleep(random.random() * 4 + 1)
```
> **说明**通过分析豆瓣网的robots协议我们发现豆瓣网并不拒绝百度爬虫获取它的数据因此我们也可以将爬虫伪装成百度的爬虫将`get`函数的`headers`参数修改为:`headers={'User-Agent': 'BaiduSpider'}`。
### 使用 IP 代理
让爬虫程序隐匿自己的身份对编写爬虫程序来说是比较重要的,很多网站对爬虫都比较反感的,因为爬虫会耗费掉它们很多的网络带宽并制造很多无效的流量。要隐匿身份通常需要使用**商业 IP 代理**(如蘑菇代理、芝麻代理、快代理等),让被爬取的网站无法获取爬虫程序来源的真实 IP 地址,也就无法简单的通过 IP 地址对爬虫程序进行封禁。
下面以[蘑菇代理](http://www.moguproxy.com/)为例,为大家讲解商业 IP 代理的使用方法。首先需要在该网站注册一个账号,注册账号后就可以[购买](http://www.moguproxy.com/buy)相应的套餐来获得商业 IP 代理。作为商业用途,建议大家购买不限量套餐,这样可以根据实际需要获取足够多的代理 IP 地址;作为学习用途,可以购买包时套餐或根据自己的需求来决定。蘑菇代理提供了两种接入代理的方式,分别是 API 私密代理和 HTTP 隧道代理,前者是通过请求蘑菇代理的 API 接口获取代理服务器地址,后者是直接使用统一的入口(蘑菇代理提供的域名)进行接入。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210829080647.png" width="75%">
下面我们以HTTP隧道代理为例为大家讲解接入 IP 代理的方式,大家也可以直接参考蘑菇代理官网提供的代码来为爬虫设置代理。
```Python
import requests
APP_KEY = 'Wnp******************************XFx'
PROXY_HOST = 'secondtransfer.moguproxy.com:9001'
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
# 需要在HTTP请求头设置代理的身份认证方式
headers={
'Proxy-Authorization': f'Basic {APP_KEY}',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4'
},
# 设置代理服务器
proxies={
'http': f'http://{PROXY_HOST}',
'https': f'https://{PROXY_HOST}'
},
verify=False
)
pattern1 = re.compile(r'<span class="title">([^&]*?)</span>')
titles = pattern1.findall(resp.text)
pattern2 = re.compile(r'<span class="rating_num".*?>(.*?)</span>')
ranks = pattern2.findall(resp.text)
for title, rank in zip(titles, ranks):
print(title, rank)
```
> **说明**:上面的代码需要修改`APP_KEY`为自己创建的订单对应的`Appkey`值,这个值可以在用户中心用户订单中查看到。蘑菇代理提供了免费的 API 代理和 HTTP 隧道代理试用,但是试用的代理接通率不能保证,建议大家还是直接购买一个在自己支付能力范围内的代理服务来体验。
### 简单的总结
Python 语言能做的事情真的很多就网络数据采集这一项而言Python 几乎是一枝独秀的,大量的企业和个人都在使用 Python 从网络上获取自己需要的数据,这可能也是你将来日常工作的一部分。另外,用编写正则表达式的方式从网页中提取内容虽然可行,但是写出一个能够满足需求的正则表达式本身也不是件容易的事情,这一点对于新手来说尤为明显。在下一节课中,我们将会为大家介绍另外两种从页面中提取数据的方法,虽然从性能上来讲,它们可能不如正则表达式,但是却降低了编码的复杂性,相信大家会喜欢上它们的。

View File

@ -0,0 +1,149 @@
## 第33课用Python解析HTML页面
在前面的课程中,我们讲到了使用`request`三方库获取网络资源,还介绍了一些前端的基础知识。接下来,我们继续探索如何解析 HTML 代码,从页面中提取出有用的信息。之前,我们尝试过用正则表达式的捕获组操作提取页面内容,但是写出一个正确的正则表达式也是一件让人头疼的事情。为了解决这个问题,我们得先深入的了解一下 HTML 页面的结构,并在此基础上研究另外的解析页面的方法。
### HTML 页面的结构
我们在浏览器中打开任意一个网站,然后通过鼠标右键菜单,选择“显示网页源代码”菜单项,就可以看到网页对应的 HTML 代码。
![image-20210822094218269](https://gitee.com/jackfrued/mypic/raw/master/20210822094218.png)
代码的第`1`行是文档类型声明,第`2`行的`<html>`标签是整个页面根标签的开始标签,最后一行是根标签的结束标签`</html>`。`<html>`标签下面有两个子标签`<head>`和`<body>`,放在`<body>`标签下的内容会显示在浏览器窗口中,这部分内容是网页的主体;放在`<head>`标签下的内容不会显示在浏览器窗口中但是却包含了页面重要的元信息通常称之为网页的头部。HTML 页面大致的代码结构如下所示。
```HTML
<!doctype html>
<html>
<head>
<!-- 页面的元信息,如字符编码、标题、关键字、媒体查询等 -->
</head>
<body>
<!-- 页面的主体,显示在浏览器窗口中的内容 -->
</body>
</html>
```
标签、层叠样式表CSS、JavaScript 是构成 HTML 页面的三要素其中标签用来承载页面要显示的内容CSS 负责对页面的渲染,而 JavaScript 用来控制页面的交互式行为。要实现 HTML 页面的解析,可以使用 XPath 的语法,它原本是 XML 的一种查询语法,可以根据 HTML 标签的层次结构提取标签中的内容或标签属性;此外,也可以使用 CSS 选择器来定位页面元素,就跟用 CSS 渲染页面元素是同样的道理。
### XPath 解析
XPath 是在 XMLeXtensible Markup Language文档中查找信息的一种语法XML 跟 HTML 类似也是一种用标签承载数据的标签语言,不同之处在于 XML 的标签是可扩展的,可以自定义的,而且 XML 对语法有更严格的要求。XPath 使用路径表达式来选取 XML 文档中的节点或者节点集,这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。下面我们通过一个例子来说明如何使用 XPath 对页面进行解析。
```XML
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="zh">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
```
对于上面的 XML 文件,我们可以用如下所示的 XPath 语法获取文档中的节点。
| 路径表达式 | 结果 |
| --------------- | ------------------------------------------------------------ |
| `/bookstore` | 选取根元素 bookstore。**注意**:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
| `//book` | 选取所有 book 子元素,而不管它们在文档中的位置。 |
| `//@lang` | 选取名为 lang 的所有属性。 |
| `/bookstore/book[1]` | 选取属于 bookstore 子元素的第一个 book 元素。 |
| `/bookstore/book[last()]` | 选取属于 bookstore 子元素的最后一个 book 元素。 |
| `/bookstore/book[last()-1]` | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
| `/bookstore/book[position()<3]` | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
| `//title[@lang]` | 选取所有拥有名为 lang 的属性的 title 元素。 |
| `//title[@lang='eng']` | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
| `/bookstore/book[price>35.00]` | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
| `/bookstore/book[price>35.00]/title` | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
XPath还支持通配符用法如下所示。
| 路径表达式 | 结果 |
| -------------- | --------------------------------- |
| `/bookstore/*` | 选取 bookstore 元素的所有子元素。 |
| `//*` | 选取文档中的所有元素。 |
| `//title[@*]` | 选取所有带有属性的 title 元素。 |
如果要选取多个节点,可以使用如下所示的方法。
| 路径表达式 | 结果 |
| ---------------------------------- | ------------------------------------------------------------ |
| `//book/title \| //book/price` | 选取 book 元素的所有 title 和 price 元素。 |
| `//title \| //price` | 选取文档中的所有 title 和 price 元素。 |
| `/bookstore/book/title \| //price` | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
> **说明**:上面的例子来自于“菜鸟教程”网站上的 [XPath 教程](<https://www.runoob.com/xpath/xpath-tutorial.html>),有兴趣的读者可以自行阅读原文。
当然,如果不理解或不熟悉 XPath 语法,可以在浏览器的开发者工具中按照如下所示的方法查看元素的 XPath 语法,下图是在 Chrome 浏览器的开发者工具中查看豆瓣网电影详情信息中影片标题的 XPath 语法。
![](https://gitee.com/jackfrued/mypic/raw/master/20210822093707.png)
实现 XPath 解析需要三方库`lxml` 的支持,可以使用下面的命令安装`lxml`。
```Bash
pip install lxml
```
下面我们用 XPath 解析方式改写之前获取豆瓣电影 Top250的代码如下所示。
```Python
from lxml import etree
import requests
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
headers={'User-Agent': 'BaiduSpider'}
)
tree = etree.HTML(resp.text)
# 通过XPath语法从页面中提取电影标题
title_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]')
# 通过XPath语法从页面中提取电影评分
rank_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]')
for title_span, rank_span in zip(title_spans, rank_spans):
print(title_span.text, rank_span.text)
```
### CSS 选择器解析
对于熟悉 CSS 选择器和 JavaScript 的开发者来说,通过 CSS 选择器获取页面元素可能是更为简单的选择,因为浏览器中运行的 JavaScript 本身就可以`document`对象的`querySelector()`和`querySelectorAll()`方法基于 CSS 选择器获取页面元素。在 Python 中,我们可以利用三方库`beautifulsoup4`或`pyquery`来做同样的事情。Beautiful Soup 可以用来解析 HTML 和 XML 文档,修复含有未闭合标签等错误的文档,通过为待解析的页面在内存中创建一棵树结构,实现对从页面中提取数据操作的封装。可以用下面的命令来安装 Beautiful Soup。
```Python
pip install beautifulsoup4
```
下面是使用`bs4`改写的获取豆瓣电影Top250电影名称的代码。
```Python
import bs4
import requests
for page in range(1, 11):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={(page - 1) * 25}',
headers={'User-Agent': 'BaiduSpider'}
)
# 创建BeautifulSoup对象
soup = bs4.BeautifulSoup(resp.text, 'lxml')
# 通过CSS选择器从页面中提取包含电影标题的span标签
title_spans = soup.select('div.info > div.hd > a > span:nth-child(1)')
# 通过CSS选择器从页面中提取包含电影评分的span标签
rank_spans = soup.select('div.info > div.bd > div > span.rating_num')
for title_span, rank_span in zip(title_spans, rank_spans):
print(title_span.text, rank_span.text)
```
关于 BeautifulSoup 更多的知识,可以参考它的[官方文档](https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/)。
### 简单的总结
下面我们对三种解析方式做一个简单比较。
| 解析方式 | 对应的模块 | 速度 | 使用难度 |
| -------------- | ---------------- | ------ | -------- |
| 正则表达式解析 | `re` | 快 | 困难 |
| XPath 解析 | `lxml` | 快 | 一般 |
| CSS 选择器解析 | `bs4`或`pyquery` | 不确定 | 简单 |

View File

@ -1,382 +0,0 @@
## 数据采集和解析
通过上一个章节的讲解,我们已经了解到了开发一个爬虫需要做的工作以及一些常见的问题,下面我们给出一个爬虫开发相关技术的清单以及这些技术涉及到的标准库和第三方库,稍后我们会一一介绍这些内容。
1. 下载数据 - **urllib** / **requests** / **aiohttp** / **httpx**
2. 解析数据 - **re** / **lxml** / **beautifulsoup4** / **pyquery**
3. 缓存和持久化 - **mysqlclient** / **sqlalchemy** / **peewee** / **redis** / **pymongo**
4. 生成数字签名 - **hashlib**
5. 序列化和压缩 - **pickle** / **json** / **zlib**
6. 调度器 - **multiprocessing** / **threading** / **concurrent.futures**
### HTML页面
```HTML
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<style type="text/css">
/* 此处省略层叠样式表代码 */
</style>
</head>
<body>
<div class="wrapper">
<header>
<h1>Yoko's Kitchen</h1>
<nav>
<ul>
<li><a href="" class="current">Home</a></li>
<li><a href="">Classes</a></li>
<li><a href="">Catering</a></li>
<li><a href="">About</a></li>
<li><a href="">Contact</a></li>
</ul>
</nav>
</header>
<section class="courses">
<article>
<figure>
<img src="images/bok-choi.jpg" alt="Bok Choi" />
<figcaption>Bok Choi</figcaption>
</figure>
<hgroup>
<h2>Japanese Vegetarian</h2>
<h3>Five week course in London</h3>
</hgroup>
<p>A five week introduction to traditional Japanese vegetarian meals, teaching you a selection of rice and noodle dishes.</p>
</article>
<article>
<figure>
<img src="images/teriyaki.jpg" alt="Teriyaki sauce" />
<figcaption>Teriyaki Sauce</figcaption>
</figure>
<hgroup>
<h2>Sauces Masterclass</h2>
<h3>One day workshop</h3>
</hgroup>
<p>An intensive one-day course looking at how to create the most delicious sauces for use in a range of Japanese cookery.</p>
</article>
</section>
<aside>
<section class="popular-recipes">
<h2>Popular Recipes</h2>
<a href="">Yakitori (grilled chicken)</a>
<a href="">Tsukune (minced chicken patties)</a>
<a href="">Okonomiyaki (savory pancakes)</a>
<a href="">Mizutaki (chicken stew)</a>
</section>
<section class="contact-details">
<h2>Contact</h2>
<p>Yoko's Kitchen<br>
27 Redchurch Street<br>
Shoreditch<br>
London E2 7DP</p>
</section>
</aside>
<footer>
&copy; 2011 Yoko's Kitchen
</footer>
</div>
<script>
/* 此处省略JavaScript代码 */
</script>
</body>
</html>
```
如上所示的HTML页面通常由三部分构成分别是用来承载内容的Tag标签、负责渲染页面的CSS层叠样式表以及控制交互式行为的JavaScript。通常我们可以在浏览器的右键菜单中通过“查看网页源代码”的方式获取网页的代码并了解页面的结构当然我们也可以通过浏览器提供的开发人员工具来了解更多的信息。
#### 使用requests获取页面
在上一节课的代码中我们使用了三方库`requests`来获取页面,下面我们对`requests`库的用法做进一步说明。
1. GET请求和POST请求。
```Python
import requests
resp = requests.get('http://www.baidu.com/index.html')
print(resp.status_code)
print(resp.headers)
print(resp.cookies)
print(resp.content.decode('utf-8'))
resp = requests.post('http://httpbin.org/post', data={'name': 'Hao', 'age': 40})
print(resp.text)
data = resp.json()
print(type(data))
```
2. URL参数和请求头。
```Python
resp = requests.get(
url='https://movie.douban.com/top250',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/83.0.4103.97 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;'
'q=0.9,image/webp,image/apng,*/*;'
'q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
)
print(resp.status_code)
```
3. 复杂的POST请求文件上传
```Python
resp = requests.post(
url='http://httpbin.org/post',
files={'file': open('data.xlsx', 'rb')}
)
print(resp.text)
```
4. 操作Cookie。
```Python
cookies = {'key1': 'value1', 'key2': 'value2'}
resp = requests.get('http://httpbin.org/cookies', cookies=cookies)
print(resp.text)
jar = requests.cookies.RequestsCookieJar()
jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies')
jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere')
resp = requests.get('http://httpbin.org/cookies', cookies=jar)
print(resp.text)
```
5. 设置代理服务器。
```Python
requests.get('https://www.taobao.com', proxies={
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
})
```
> **说明**:关于`requests`库的相关知识,还是强烈建议大家自行阅读它的[官方文档](https://requests.readthedocs.io/zh_CN/latest/)。
6. 设置请求超时。
```Python
requests.get('https://github.com', timeout=10)
```
### 页面解析
#### 几种解析方式的比较
| 解析方式 | 对应的模块 | 速度 | 使用难度 | 备注 |
| -------------- | ------------- | ------ | -------- | ------------------------------------------- |
| 正则表达式解析 | re | 快 | 困难 | 常用正则表达式<br/>在线正则表达式测试 |
| XPath解析 | lxml | 快 | 一般 | 需要安装C语言依赖库<br/>唯一支持XML的解析器 |
| CSS选择器解析 | bs4 / pyquery | 不确定 | 简单 | |
> **说明**`BeautifulSoup`可选的解析器包括Python标准库中的`html.parser`、`lxml`的HTML解析器、`lxml`的XML解析器和`html5lib`。
#### 使用正则表达式解析页面
如果你对正则表达式没有任何的概念,那么推荐先阅读[《正则表达式30分钟入门教程》](https://deerchao.cn/tutorials/regex/regex.htm)然后再阅读我们之前讲解在Python中如何使用正则表达式一文。
下面的例子演示了如何用正则表达式解析“豆瓣电影Top250”中的中文电影名称。
```Python
import random
import re
import time
import requests
PATTERN = re.compile(r'<a[^>]*?>\s*<span class="title">(.*?)</span>')
for page in range(10):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={page * 25}',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/83.0.4103.97 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;'
'q=0.9,image/webp,image/apng,*/*;'
'q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
},
)
items = PATTERN.findall(resp.text)
for item in items:
print(item)
time.sleep(random.randint(1, 5))
```
#### XPath解析和lxml
XPath是在XML文档中查找信息的一种语法它使用路径表达式来选取XML文档中的节点或者节点集。这里所说的XPath节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。
```XML
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="zh">三国演义</title>
<price>39.95</price>
</book>
</bookstore>
```
对于上面的XML文件我们可以用如下所示的XPath语法获取文档中的节点。
| 路径表达式 | 结果 |
| --------------- | ------------------------------------------------------------ |
| bookstore | 选取 bookstore 元素的所有子节点。 |
| /bookstore | 选取根元素 bookstore。注释假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
| bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
| //book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
| bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
| //@lang | 选取名为 lang 的所有属性。 |
在使用XPath语法时还可以使用XPath中的谓词。
| 路径表达式 | 结果 |
| ---------------------------------- | ------------------------------------------------------------ |
| /bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
| /bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
| /bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
| /bookstore/book[position()<3] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素 |
| //title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
| //title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
| /bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
| /bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
XPath还支持通配符用法如下所示。
| 路径表达式 | 结果 |
| ------------ | --------------------------------- |
| /bookstore/* | 选取 bookstore 元素的所有子元素。 |
| //* | 选取文档中的所有元素。 |
| //title[@*] | 选取所有带有属性的 title 元素。 |
如果要选取多个节点,可以使用如下所示的方法。
| 路径表达式 | 结果 |
| -------------------------------- | ------------------------------------------------------------ |
| //book/title \| //book/price | 选取 book 元素的所有 title 和 price 元素。 |
| //title \| //price | 选取文档中的所有 title 和 price 元素。 |
| /bookstore/book/title \| //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
> **说明**:上面的例子来自于菜鸟教程网站上[XPath教程](<https://www.runoob.com/xpath/xpath-tutorial.html>),有兴趣的读者可以自行阅读原文。
当然如果不理解或者不太熟悉XPath语法可以在Chrome浏览器中按照如下所示的方法查看元素的XPath语法。
![](./res/douban-xpath.png)
下面的例子演示了如何用XPath解析“豆瓣电影Top250”中的中文电影名称。
```Python
from lxml import etree
import requests
for page in range(10):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={page * 25}',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/83.0.4103.97 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;'
'q=0.9,image/webp,image/apng,*/*;'
'q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
)
html = etree.HTML(resp.text)
spans = html.xpath('/html/body/div[3]/div[1]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]')
for span in spans:
print(span.text)
```
### BeautifulSoup的使用
BeautifulSoup是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航、查找、修改文档的方式。
1. 遍历文档树
- 获取标签
- 获取标签属性
- 获取标签内容
- 获取子(孙)节点
- 获取父节点/祖先节点
- 获取兄弟节点
2. 搜索树节点
- find / find_all
- select_one / select
> **说明**更多内容可以参考BeautifulSoup的[官方文档](https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/)。
下面的例子演示了如何用CSS选择器解析“豆瓣电影Top250”中的中文电影名称。
```Python
import random
import time
import bs4
import requests
for page in range(10):
resp = requests.get(
url=f'https://movie.douban.com/top250?start={page * 25}',
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/83.0.4103.97 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;'
'q=0.9,image/webp,image/apng,*/*;'
'q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
},
)
soup = bs4.BeautifulSoup(resp.text, 'lxml')
elements = soup.select('.info>div>a')
for element in elements:
span = element.select_one('.title')
print(span.text)
time.sleep(random.random() * 5)
```
### 例子 - 获取知乎发现上的问题链接
```Python
import re
from urllib.parse import urljoin
import bs4
import requests
def main():
headers = {'user-agent': 'Baiduspider'}
base_url = 'https://www.zhihu.com/'
resp = requests.get(urljoin(base_url, 'explore'), headers=headers)
soup = bs4.BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
links_set = set()
for a_tag in soup.find_all('a', {'href': href_regex}):
if 'href' in a_tag.attrs:
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
links_set.add(full_url)
print('Total %d question pages found.' % len(links_set))
print(links_set)
if __name__ == '__main__':
main()
```

View File

@ -0,0 +1,382 @@
## 第34课Python中的并发编程-1
现如今,我们使用的计算机早已是多 CPU 或多核的计算机,而我们使用的操作系统基本都支持“多任务”,这使得我们可以同时运行多个程序,也可以将一个程序分解为若干个相对独立的子任务,让多个子任务“并行”或“并发”的执行,从而缩短程序的执行时间,同时也让用户获得更好的体验。因此当下,不管用什么编程语言进行开发,实现“并行”或“并发”编程已经成为了程序员的标配技能。为了讲述如何在 Python 程序中实现“并行”或“并发”,我们需要先了解两个重要的概念:进程和线程。
### 线程和进程
我们通过操作系统运行一个程序会创建出一个或多个进程,进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。简单的说,进程是操作系统分配存储空间的基本单位,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据;操作系统管理所有进程的执行,为它们合理的分配资源。一个进程可以通过 fork 或 spawn 的方式创建新的进程来执行其他的任务,不过新的进程也有自己独立的内存空间,因此两个进程如果要共享数据,必须通过进程间通信机制来实现,具体的方式包括管道、信号、套接字等。
一个进程还可以拥有多个执行线索,简单的说就是拥有多个可以获得 CPU 调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当然在单核 CPU 系统中,多个线程不可能同时执行,因为在某个时刻只有一个线程能够获得 CPU多个线程通过共享 CPU 执行时间的方式来达到并发的效果。
在程序中使用多线程技术通常都会带来不言而喻的好处,最主要的体现在提升程序的性能和改善用户体验,今天我们使用的软件几乎都用到了多线程技术,这一点可以利用系统自带的进程监控工具(如 macOS 中的“活动监视器”、Windows 中的“任务管理器”)来证实,如下图所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210822094243.png" width="80%">
这里,我们还需要跟大家再次强调两个概念:**并发**concurrency和**并行**parallel。**并发**通常是指同一时刻只能有一条指令执行,但是多个线程对应的指令被快速轮换地执行。比如一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。由于处理器执行指令的速度和切换的速度极快,人们完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行,但微观上其实只有一个线程在执行。**并行**是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器,不论是从宏观上还是微观上,多个线程可以在同一时刻一起执行的。很多时候,我们并不用严格区分并发和并行两个词,所以我们有时候也把 Python 中的多线程、多进程以及异步 I/O 都视为实现并发编程的手段但实际上前面两者也可以实现并行编程当然这里还有一个全局解释器锁GIL的问题我们稍后讨论。
### 多线程编程
Python 标准库中`threading`模块的`Thread`类可以帮助我们非常轻松的实现多线程编程。我们用一个联网下载文件的例子来对比使用多线程和不使用多线程到底有什么区别,代码如下所示。
不使用多线程的下载。
```Python
import random
import time
def download(*, filename):
start = time.time()
print(f'开始下载 {filename}.')
time.sleep(random.randint(3, 6))
print(f'{filename} 下载完成.')
end = time.time()
print(f'下载耗时: {end - start:.3f}秒.')
def main():
start = time.time()
download(filename='Python从入门到住院.pdf')
download(filename='MySQL从删库到跑路.avi')
download(filename='Linux从精通到放弃.mp4')
end = time.time()
print(f'总耗时: {end - start:.3f}秒.')
if __name__ == '__main__':
main()
```
> **说明**:上面的代码并没有真正实现联网下载的功能,而是通过`time.sleep()`休眠一段时间来模拟下载文件需要一些时间上的开销,跟实际下载的状况比较类似。
运行上面的代码,可以得到如下所示的运行结果。可以看出,当我们的程序只有一个工作线程时,每个下载任务都需要等待上一个下载任务执行结束才能开始,所以程序执行的总耗时是三个下载任务各自执行时间的总和。
```
开始下载Python从入门到住院.pdf.
Python从入门到住院.pdf下载完成.
下载耗时: 3.005秒.
开始下载MySQL从删库到跑路.avi.
MySQL从删库到跑路.avi下载完成.
下载耗时: 5.006秒.
开始下载Linux从精通到放弃.mp4.
Linux从精通到放弃.mp3下载完成.
下载耗时: 6.007秒.
总耗时: 14.018秒.
```
事实上,上面的三个下载任务之间并没有逻辑上的因果关系,三者是可以“并发”的,下一个下载任务没有必要等待上一个下载任务结束,为此,我们可以使用多线程编程来改写上面的代码。
```Python
import random
import time
from threading import Thread
def download(*, filename):
start = time.time()
print(f'开始下载 {filename}.')
time.sleep(random.randint(3, 6))
print(f'{filename} 下载完成.')
end = time.time()
print(f'下载耗时: {end - start:.3f}秒.')
def main():
threads = [
Thread(target=download, kwargs={'filename': 'Python从入门到住院.pdf'}),
Thread(target=download, kwargs={'filename': 'MySQL从删库到跑路.avi'}),
Thread(target=download, kwargs={'filename': 'Linux从精通到放弃.mp4'})
]
start = time.time()
# 启动三个线程
for thread in threads:
thread.start()
# 等待线程结束
for thread in threads:
thread.join()
end = time.time()
print(f'总耗时: {end - start:.3f}秒.')
if __name__ == '__main__':
main()
```
某次的运行结果如下所示。
```
开始下载 Python从入门到住院.pdf.
开始下载 MySQL从删库到跑路.avi.
开始下载 Linux从精通到放弃.mp4.
MySQL从删库到跑路.avi 下载完成.
下载耗时: 3.005秒.
Python从入门到住院.pdf 下载完成.
下载耗时: 5.006秒.
Linux从精通到放弃.mp4 下载完成.
下载耗时: 6.003秒.
总耗时: 6.004秒.
```
通过上面的运行结果可以发现,整个程序的执行时间几乎等于耗时最长的一个下载任务的执行时间,这也就意味着,三个下载任务是并发执行的,不存在一个等待另一个的情况,这样做很显然提高了程序的执行效率。简单的说,如果程序中有非常耗时的执行单元,而这些耗时的执行单元之间又没有逻辑上的因果关系,即 B 单元的执行不依赖于 A 单元的执行结果,那么 A 和 B 两个单元就可以放到两个不同的线程中,让他们并发的执行。这样做的好处除了减少程序执行的等待时间,还可以带来更好的用户体验,因为一个单元的阻塞不会造成程序的“假死”,因为程序中还有其他的单元是可以运转的。
#### 使用 Thread 类创建线程对象
通过上面的代码可以看出,直接使用`Thread`类的构造器就可以创建线程对象,而线程对象的`start()`方法可以启动一个线程。线程启动后会执行`target`参数指定的函数,当然前提是获得 CPU 的调度;如果`target`指定的线程要执行的目标函数有参数,需要通过`args`参数为其进行指定,对于关键字参数,可以通过`kwargs`参数进行传入。`Thread`类的构造器还有很多其他的参数,我们遇到的时候再为大家进行讲解,目前需要大家掌握的,就是`target`、`args`和`kwargs`。
#### 继承 Thread 类自定义线程
除了上面的代码展示的创建线程的方式外,还可以通过继承`Thread`类并重写`run()`方法的方式来自定义线程,具体的代码如下所示。
```Python
import random
import time
from threading import Thread
class DownloadThread(Thread):
def __init__(self, filename):
self.filename = filename
super().__init__()
def run(self):
start = time.time()
print(f'开始下载 {self.filename}.')
time.sleep(random.randint(3, 6))
print(f'{self.filename} 下载完成.')
end = time.time()
print(f'下载耗时: {end - start:.3f}秒.')
def main():
threads = [
DownloadThread('Python从入门到住院.pdf'),
DownloadThread('MySQL从删库到跑路.avi'),
DownloadThread('Linux从精通到放弃.mp4')
]
start = time.time()
# 启动三个线程
for thread in threads:
thread.start()
# 等待线程结束
for thread in threads:
thread.join()
end = time.time()
print(f'总耗时: {end - start:.3f}秒.')
if __name__ == '__main__':
main()
```
#### 使用线程池
我们还可以通过线程池的方式将任务放到多个线程中去执行通过线程池来使用线程应该是多线程编程最理想的选择。事实上线程的创建和释放都会带来较大的开销频繁的创建和释放线程通常都不是很好的选择。利用线程池可以提前准备好若干个线程在使用的过程中不需要再通过自定义的代码创建和释放线程而是直接复用线程池中的线程。Python 内置的`concurrent.futures`模块提供了对线程池的支持,代码如下所示。
```Python
import random
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Thread
def download(*, filename):
start = time.time()
print(f'开始下载 {filename}.')
time.sleep(random.randint(3, 6))
print(f'{filename} 下载完成.')
end = time.time()
print(f'下载耗时: {end - start:.3f}秒.')
def main():
with ThreadPoolExecutor(max_workers=4) as pool:
filenames = ['Python从入门到住院.pdf', 'MySQL从删库到跑路.avi', 'Linux从精通到放弃.mp4']
start = time.time()
for filename in filenames:
pool.submit(download, filename=filename)
end = time.time()
print(f'总耗时: {end - start:.3f}秒.')
if __name__ == '__main__':
main()
```
### 守护线程
所谓“守护线程”就是在主线程结束的时候,不值得再保留的执行线程。这里的不值得保留指的是守护线程会在其他非守护线程全部运行结束之后被销毁,它守护的是当前进程内所有的非守护线程。简单的说,守护线程会跟随主线程一起挂掉,而主线程的生命周期就是一个进程的生命周期。如果不理解,我们可以看一段简单的代码。
```Python
import time
from threading import Thread
def display(content):
while True:
print(content, end='', flush=True)
time.sleep(0.1)
def main():
Thread(target=display, args=('Ping', )).start()
Thread(target=display, args=('Pong', )).start()
if __name__ == '__main__':
main()
```
> **说明**:上面的代码中,我们将`print`函数的参数`flush`设置为`True`,这是因为`flush`参数的值如果为`False`,而`print`又没有做换行处理,就会导致每次`print`输出的内容被放到操作系统的输出缓冲区,直到缓冲区被输出的内容塞满,才会清空缓冲区产生一次输出。上述现象是操作系统为了减少 I/O 中断,提升 CPU 利用率做出的设定,为了让代码产生直观交互,我们才将`flush`参数设置为`True`,强制每次输出都清空输出缓冲区。
上面的代码运行起来之后是不会停止的,因为两个子线程中都有死循环,除非你手动中断代码的执行。但是,如果在创建线程对象时,将名为`daemon`的参数设置为`True`,这两个线程就会变成守护线程,那么在其他线程结束时,即便有死循环,两个守护线程也会挂掉,不会再继续执行下去,代码如下所示。
```Python
import time
from threading import Thread
def display(content):
while True:
print(content, end='', flush=True)
time.sleep(0.1)
def main():
Thread(target=display, args=('Ping', ), daemon=True).start()
Thread(target=display, args=('Pong', ), daemon=True).start()
time.sleep(5)
if __name__ == '__main__':
main()
```
上面的代码,我们在主线程中添加了一行`time.sleep(5)`让主线程休眠5秒在这个过程中输出`Ping`和`Pong`的守护线程会持续运转直到主线程在5秒后结束这两个守护线程也被销毁不再继续运行。
> **思考**如果将上面代码第12行的`daemon=True`去掉,代码会怎样执行?有兴趣的读者可以尝试一下,并看看实际执行的结果跟你想象的是否一致。
### 资源竞争
在编写多线程代码时,不可避免的会遇到多个线程竞争同一个资源(对象)的情况。在这种情况下,如果没有合理的机制来保护被竞争的资源,那么就有可能出现非预期的状况。下面的代码创建了`100`个线程向同一个银行账户(初始余额为`0`元)转账,每个线程转账金额为`1`元。在正常的情况下,我们的银行账户最终的余额应该是`100`元,但是运行下面的代码我们并不能得到`100`元这个结果。
```Python
import time
from concurrent.futures import ThreadPoolExecutor
class Account(object):
"""银行账户"""
def __init__(self):
self.balance = 0.0
def deposit(self, money):
"""存钱"""
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
def main():
"""主函数"""
account = Account()
with ThreadPoolExecutor(max_workers=16) as pool:
for _ in range(100):
pool.submit(account.deposit, 1)
print(account.balance)
if __name__ == '__main__':
main()
```
上面代码中的`Account`类代表了银行账户,它的`deposit`方法代表存款行为,参数`money`代表存入的金额,该方法通过`time.sleep`函数模拟受理存款需要一段时间。我们通过线程池的方式启动了`100`个线程向一个账户转账,但是上面的代码并不能运行出`100`这个我们期望的结果,这就是在多个线程竞争一个资源的时候,可能会遇到的数据不一致的问题。注意上面代码的第`14`行,当多个线程都执行到这行代码时,它们会在相同的余额上执行加上存入金额的操作,这就会造成“丢失更新”现象,即之前修改数据的成果被后续的修改给覆盖掉了,所以才得不到正确的结果。
要解决上面的问题可以使用锁机制通过锁对操作数据的关键代码加以保护。Python 标准库的`threading`模块提供了`Lock`和`RLock`类来支持锁机制,这里我们不去深究二者的区别,建议大家直接使用`RLock`。接下来,我们给银行账户添加一个锁对象,通过锁对象来解决刚才存款时发生“丢失更新”的问题,代码如下所示。
```Python
import time
from concurrent.futures import ThreadPoolExecutor
from threading import RLock
class Account(object):
"""银行账户"""
def __init__(self):
self.balance = 0.0
self.lock = RLock()
def deposit(self, money):
# 获得锁
self.lock.acquire()
try:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
finally:
# 释放锁
self.lock.release()
def main():
"""主函数"""
account = Account()
with ThreadPoolExecutor(max_workers=16) as pool:
for _ in range(100):
pool.submit(account.deposit, 1)
print(account.balance)
if __name__ == '__main__':
main()
```
上面代码中,获得锁和释放锁的操作也可以通过上下文语法来实现,使用上下文语法会让代码更加简单优雅,这也是我们推荐大家使用的方式。
```Python
import time
from concurrent.futures import ThreadPoolExecutor
from threading import RLock
class Account(object):
"""银行账户"""
def __init__(self):
self.balance = 0.0
self.lock = RLock()
def deposit(self, money):
# 通过上下文语法获得锁和释放锁
with self.lock:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
def main():
"""主函数"""
account = Account()
with ThreadPoolExecutor(max_workers=16) as pool:
for _ in range(100):
pool.submit(account.deposit, 1)
print(account.balance)
if __name__ == '__main__':
main()
```
> **思考**将上面的代码修改为5个线程向银行账户存钱5个线程从银行账户取钱取钱的线程在银行账户余额不足时需要停下来等待存钱的线程将钱存入后再尝试取钱。这里需要用到线程调度的知识大家可以自行研究下`threading`模块中的`Condition`类,看看是否能够完成这个任务。
### GIL问题
如果使用官方的 Python 解释器(通常称之为 CPython运行 Python 程序,我们并不能通过使用多线程的方式将 CPU 的利用率提升到逼近400%对于4核 CPU或逼近800%对于8核 CPU这样的水平因为 CPython 在执行代码时,会受到 GIL全局解释器锁的限制。具体的说CPython 在执行任何代码时,都需要对应的线程先获得 GIL然后每执行100条字节码指令CPython 就会让获得 GIL 的线程主动释放 GIL这样别的线程才有机会执行。因为 GIL 的存在,无论你的 CPU 有多少个核,我们编写的 Python 代码也没有机会真正并行的执行。
GIL 是官方 Python 解释器在设计上的历史遗留问题,要解决这个问题,让多线程能够发挥 CPU 的多核优势,需要重新实现一个不带 GIL 的 Python 解释器。这个问题按照官方的说法,在 Python 发布4.0版本时会得到解决,就让我们拭目以待吧。当下,对于 CPython 而言,如果希望充分发挥 CPU 的多核优势,可以考虑使用多进程,因为每个进程都对应一个 Python 解释器,因此每个进程都有自己独立的 GIL这样就可以突破 GIL 的限制。在下一个章节中,我们会为大家介绍关于多进程的相关知识,并对多线程和多进程的代码及其执行效果进行比较。

View File

@ -0,0 +1,254 @@
## 第35课Python中的并发编程-2
在上一课中我们说过,由于 GIL 的存在CPython 中的多线程并不能发挥 CPU 的多核优势,如果希望突破 GIL 的限制,可以考虑使用多进程。对于多进程的程序,每个进程都有一个属于自己的 GIL所以多进程不会受到 GIL 的影响。那么,我们应该如何在 Python 程序中创建和使用多进程呢?
###创建进程
在 Python 中可以基于`Process`类来创建进程,虽然进程和线程有着本质的差别,但是`Process`类和`Thread`类的用法却非常类似。在使用`Process`类的构造器创建对象时,也是通过`target`参数传入一个函数来指定进程要执行的代码,而`args`和`kwargs`参数可以指定该函数使用的参数值。
```Python
from multiprocessing import Process, current_process
from time import sleep
def sub_task(content, nums):
# 通过current_process函数获取当前进程对象
# 通过进程对象的pid和name属性获取进程的ID号和名字
print(f'PID: {current_process().pid}')
print(f'Name: {current_process().name}')
# 通过下面的输出不难发现每个进程都有自己的nums列表进程之间本就不共享内存
# 在创建子进程时复制了父进程的数据结构三个进程从列表中pop(0)得到的值都是20
counter, total = 0, nums.pop(0)
print(f'Loop count: {total}')
sleep(0.5)
while counter < total:
counter += 1
print(f'{counter}: {content}')
sleep(0.01)
def main():
nums = [20, 30, 40]
# 创建并启动进程来执行指定的函数
Process(target=sub_task, args=('Ping', nums)).start()
Process(target=sub_task, args=('Pong', nums)).start()
# 在主进程中执行sub_task函数
sub_task('Good', nums)
if __name__ == '__main__':
main()
```
> **说明**:上面的代码通过`current_process`函数获取当前进程对象,再通过进程对象的`pid`属性获取进程ID。在 Python 中,使用`os`模块的`getpid`函数也可以达到同样的效果。
如果愿意,也可以使用`os`模块的`fork`函数来创建进程,调用该函数时,操作系统自动把当前进程(父进程)复制一份(子进程),父进程的`fork`函数会返回子进程的ID而子进程中的`fork`函数会返回`0`也就是说这个函数调用一次会在父进程和子进程中得到两个不同的返回值。需要注意的是Windows 系统并不支持`fork`函数,如果你使用的是 Linux 或 macOS 系统,可以试试下面的代码。
```Python
import os
print(f'PID: {os.getpid()}')
pid = os.fork()
if pid == 0:
print(f'子进程 - PID: {os.getpid()}')
print('Todo: 在子进程中执行的代码')
else:
print(f'父进程 - PID: {os.getpid()}')
print('Todo: 在父进程中执行的代码')
```
简而言之,我们还是推荐大家通过直接使用`Process`类、继承`Process`类和使用进程池(`ProcessPoolExecutor`)这三种方式来创建和使用多进程,这三种方式不同于上面的`fork`函数,能够保证代码的兼容性和可移植性。具体的做法跟之前讲过的创建和使用多线程的方式比较接近,此处不再进行赘述。
### 多进程和多线程的比较
对于爬虫这类 I/O 密集型任务来说,使用多进程并没有什么优势;但是对于计算密集型任务来说,多进程相比多线程,在效率上会有显著的提升,我们可以通过下面的代码来加以证明。下面的代码会通过多线程和多进程两种方式来判断一组大整数是不是质数,很显然这是一个计算密集型任务,我们将任务分别放到多个线程和多个进程中来加速代码的执行,让我们看看多线程和多进程的代码具体表现有何不同。
我们先实现一个多线程的版本,代码如下所示。
```Python
import concurrent.futures
PRIMES = [
1116281,
1297337,
104395303,
472882027,
533000389,
817504243,
982451653,
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419
] * 5
def is_prime(n):
"""判断素数"""
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return n != 1
def main():
"""主函数"""
with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
```
假设上面的代码保存在名为`example.py`的文件中,在 Linux 或 macOS 系统上,可以使用`time python example.py`命令执行程序并获得操作系统关于执行时间的统计,在我的 macOS 上,某次的运行结果的最后一行输出如下所示。
```
python example09.py 38.69s user 1.01s system 101% cpu 39.213 total
```
从运行结果可以看出,多线程的代码只能让 CPU 利用率达到100%,这其实已经证明了多线程的代码无法利用 CPU 多核特性来加速代码的执行,我们再看看多进程的版本,我们将上面代码中的线程池(`ThreadPoolExecutor`)更换为进程池(`ProcessPoolExecutor`)。
多进程的版本。
```Python
import concurrent.futures
PRIMES = [
1116281,
1297337,
104395303,
472882027,
533000389,
817504243,
982451653,
112272535095293,
112582705942171,
112272535095293,
115280095190773,
115797848077099,
1099726899285419
] * 5
def is_prime(n):
"""判断素数"""
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return n != 1
def main():
"""主函数"""
with concurrent.futures.ProcessPoolExecutor(max_workers=16) as executor:
for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
print('%d is prime: %s' % (number, prime))
if __name__ == '__main__':
main()
```
> **提示**:运行上面的代码时,可以通过操作系统的任务管理器(资源监视器)来查看是否启动了多个 Python 解释器进程。
我们仍然通过`time python example.py`的方式来执行上述代码,运行结果的最后一行如下所示。
```
python example09.py 106.63s user 0.57s system 389% cpu 27.497 total
```
可以看出,多进程的版本在我使用的这台电脑上,让 CPU 的利用率达到了将近400%,而运行代码时用户态耗费的 CPU 的时间106.63秒几乎是代码运行总时间27.497秒的4倍从这两点都可以看出我的电脑使用了一款4核的 CPU。当然要知道自己的电脑有几个 CPU 或几个核,可以直接使用下面的代码。
```Python
import os
print(os.cpu_count())
```
综上所述,多进程可以突破 GIL 的限制,充分利用 CPU 多核特性,对于计算密集型任务,这一点是相当重要的。常见的计算密集型任务包括科学计算、图像处理、音视频编解码等,如果这些计算密集型任务本身是可以并行的,那么使用多进程应该是更好的选择。
### 进程间通信
在讲解进程间通信之前先给大家一个任务启动两个进程一个输出“Ping”一个输出“Pong”两个进程输出的“Ping”和“Pong”加起来一共有50个时就结束程序。听起来是不是非常简单但是实际编写代码时由于多个进程之间不能够像多个线程之间直接通过共享内存的方式交换数据所以下面的代码是达不到我们想要的结果的。
```Python
from multiprocessing import Process
from time import sleep
counter = 0
def sub_task(string):
global counter
while counter < 50:
print(string, end='', flush=True)
counter += 1
sleep(0.01)
def main():
Process(target=sub_task, args=('Ping', )).start()
Process(target=sub_task, args=('Pong', )).start()
if __name__ == '__main__':
main()
```
上面的代码看起来没毛病但是最后的结果是“Ping”和“Pong”各输出了50个。再次提醒大家当我们在程序中创建进程的时候子进程会复制父进程及其所有的数据结构每个子进程有自己独立的内存空间这也就意味着两个子进程中各有一个`counter`变量,它们都会从`0`加到`50`,所以结果就可想而知了。要解决这个问题比较简单的办法是使用`multiprocessing`模块中的`Queue`类它是可以被多个进程共享的队列底层是通过操作系统底层的管道和信号量semaphore机制来实现的代码如下所示。
```Python
import time
from multiprocessing import Process, Queue
def sub_task(content, queue):
counter = queue.get()
while counter < 50:
print(content, end='', flush=True)
counter += 1
queue.put(counter)
time.sleep(0.01)
counter = queue.get()
def main():
queue = Queue()
queue.put(0)
p1 = Process(target=sub_task, args=('Ping', queue))
p1.start()
p2 = Process(target=sub_task, args=('Pong', queue))
p2.start()
while p1.is_alive() and p2.is_alive():
pass
queue.put(50)
if __name__ == '__main__':
main()
```
> **提示**`multiprocessing.Queue`对象的`get`方法默认在队列为空时是会阻塞的,直到获取到数据才会返回。如果不希望该方法阻塞以及需要指定阻塞的超时时间,可以通过指定`block`和`timeout`参数进行设定。
上面的代码通过`Queue`类的`get`和`put`方法让三个进程(`p1`、`p2`和主进程)实现了数据的共享,这就是所谓的进程间的通信,通过这种方式,当`Queue`中取出的值已经大于等于`50`时,`p1`和`p2`就会跳出`while`循环从而终止进程的执行。代码第22行的循环是为了等待`p1`和`p2`两个进程中的一个结束,这时候主进程还需要向`Queue`中放置一个大于等于`50`的值,这样另一个尚未结束的进程也会因为读到这个大于等于`50`的值而终止。
进程间通信的方式还有很多,比如使用套接字也可以实现两个进程的通信,甚至于这两个进程并不在同一台主机上,有兴趣的读者可以自行了解。
### 简单的总结
在 Python 中,我们还可以通过`subprocess`模块的`call`函数执行其他的命令来创建子进程,相当于就是在我们的程序中调用其他程序,这里我们暂不探讨这些知识,有兴趣的读者可以自行研究。
对于Python开发者来说以下情况需要考虑使用多线程
1. 程序需要维护许多共享的状态尤其是可变状态Python 中的列表、字典、集合都是线程安全的(多个线程同时操作同一个列表、字典或集合,不会引发错误和数据问题),所以使用线程而不是进程维护共享状态的代价相对较小。
2. 程序会花费大量时间在 I/O 操作上,没有太多并行计算的需求且不需占用太多的内存。
那么在遇到下列情况时,应该考虑使用多进程:
1. 程序执行计算密集型任务(如:音视频编解码、数据压缩、科学计算等)。
2. 程序的输入可以并行的分成块,并且可以将运算结果合并。
3. 程序在内存使用方面没有任何限制且不强依赖于 I/O 操作(如读写文件、套接字等)。

View File

@ -0,0 +1,215 @@
## 第36课Python中的并发编程-3
爬虫是典型的 I/O 密集型任务I/O 密集型任务的特点就是程序会经常性的因为 I/O 操作而进入阻塞状态,比如我们之前使用`requests`获取页面代码或二进制内容,发出一个请求之后,程序必须要等待网站返回响应之后才能继续运行,如果目标网站不是很给力或者网络状况不是很理想,那么等待响应的时间可能会很久,而在这个过程中整个程序是一直阻塞在那里,没有做任何的事情。通过前面的课程,我们已经知道了可以通过多线程的方式为爬虫提速,使用多线程的本质就是,当一个线程阻塞的时候,程序还有其他的线程可以继续运转,因此整个程序就不会在阻塞和等待中浪费了大量的时间。
事实上,还有一种非常适合 I/O 密集型任务的并发编程方式,我们称之为异步编程,你也可以将它称为异步 I/O。这种方式并不需要启动多个线程或多个进程来实现并发它是通过多个子程序相互协作的方式来提升 CPU 的利用率,解决了 I/O 密集型任务 CPU 利用率很低的问题,我一般将这种方式称为“协作式并发”。这里,我不打算探讨操作系统的各种 I/O 模式,因为这对很多读者来说都太过抽象;但是我们得先抛出两组概念给大家,一组叫做“阻塞”和“非阻塞”,一组叫做“同步”和“异步”。
### 基本概念
#### 阻塞
阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续处理其他的事情,则称该程序在该操作上是阻塞的。阻塞随时都可能发生,最典型的就是 I/O 中断(包括网络 I/O 、磁盘 I/O 、用户输入等)、休眠操作、等待某个线程执行结束,甚至包括在 CPU 切换上下文时,程序都无法真正的执行,这就是所谓的阻塞。
#### 非阻塞
程序在等待某操作过程中,自身不被阻塞,可以继续处理其他的事情,则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。显然,某个操作的阻塞可能会导程序耗时以及效率低下,所以我们会希望把它变成非阻塞的。
#### 同步
不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,我们称这些程序单元是同步执行的。例如前面讲过的给银行账户存钱的操作,我们在代码中使用了“锁”作为通信信号,让多个存钱操作强制排队顺序执行,这就是所谓的同步。
#### 异步
不同程序单元在执行过程中无需通信协调,也能够完成一个任务,这种方式我们就称之为异步。例如,使用爬虫下载页面时,调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是不相关的,也无需相互通知协调。很显然,异步操作的完成时刻和先后顺序并不能确定。
很多人都不太能准确的把握这几个概念,这里我们简单的总结一下,同步与异步的关注点是**消息通信机制**,最终表现出来的是“有序”和“无序”的区别;阻塞和非阻塞的关注点是**程序在等待消息时状态**,最终表现出来的是程序在等待时能不能做点别的。如果想深入理解这些内容,推荐大家阅读经典著作[《UNIX网络编程》](https://item.jd.com/11880047.html),这本书非常的赞。
### 生成器和协程
前面我们说过,异步编程是一种“协作式并发”,即通过多个子程序相互协作的方式提升 CPU 的利用率,从而减少程序在阻塞和等待中浪费的时间,最终达到并发的效果。我们可以将多个相互协作的子程序称为“协程”,它是实现异步编程的关键。在介绍协程之前,我们先通过下面的代码,看看什么是生成器。
```Python
def fib(max_count):
a, b = 0, 1
for _ in range(max_count):
a, b = b, a + b
yield a
```
上面我们编写了一个生成斐波那契数列的生成器,调用上面的`fib`函数并不是执行该函数获得返回值,因为`fib`函数中有一个特殊的关键字`yield`。这个关键字使得`fib`函数跟普通的函数有些区别,调用该函数会得到一个生成器对象,我们可以通过下面的代码来验证这一点。
```Python
gen_obj = fib(20)
print(gen_obj)
```
输出:
```
<generator object fib at 0x106daee40>
```
我们可以使用内置函数`next`从生成器对象中获取斐波那契数列的值,也可以通过`for-in`循环对生成器能够提供的值进行遍历,代码如下所示。
```Python
for value in gen_obj:
print(value)
```
生成器经过预激活,就是一个协程,它可以跟其他子程序协作。
```Python
def calc_average():
total, counter = 0, 0
avg_value = None
while True:
curr_value = yield avg_value
total += curr_value
counter += 1
avg_value = total / counter
def main():
obj = calc_average()
# 生成器预激活
obj.send(None)
for _ in range(5):
print(obj.send(float(input())))
if __name__ == '__main__':
main()
```
上面的`main`函数首先通过生成器对象的`send`方法发送一个`None`值来将其激活为协程,也可以通过`next(obj)`达到同样的效果。接下来,协程对象会接收`main`函数发送的数据并产出(`yield`)数据的平均值。通过上面的例子,不知道大家是否看出两段子程序是怎么“协作”的。
### 异步函数
Python 3.5版本中,引入了两个非常有意思的元素,一个叫`async`,一个叫`await`它们在Python 3.7版本中成为了正式的关键字。通过这两个关键字,可以简化协程代码的编写,可以用更为简单的方式让多个子程序很好的协作起来。我们通过一个例子来加以说明,请大家先看看下面的代码。
```Python
import time
def display(num):
time.sleep(1)
print(num)
def main():
start = time.time()
for i in range(1, 10):
display(i)
end = time.time()
print(f'{end - start:.3f}秒')
if __name__ == '__main__':
main()
```
上面的代码每次执行都会依次输出`1`到`9`的数字,每个间隔`1`秒钟,整个代码需要执行大概需要`9`秒多的时间,这一点我相信大家都能看懂。不知道大家是否意识到,这段代码就是以同步和阻塞的方式执行的,同步可以从代码的输出看出来,而阻塞是指在调用`display`函数发生休眠时,整个代码的其他部分都不能继续执行,必须等待休眠结束。
接下来,我们尝试用异步的方式改写上面的代码,让`display`函数以异步的方式运转。
```Python
import asyncio
import time
async def display(num):
await asyncio.sleep(1)
print(num)
def main():
start = time.time()
objs = [display(i) for i in range(1, 10)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(objs))
loop.close()
end = time.time()
print(f'{end - start:.3f}秒')
if __name__ == '__main__':
main()
```
Python 中的`asyncio`模块提供了对异步 I/O 的支持。上面的代码中,我们首先在`display`函数前面加上了`async`关键字使其变成一个异步函数,调用异步函数不会执行函数体而是获得一个协程对象。我们将`display`函数中的`time.sleep(1)`修改为`await asyncio.sleep(1)`,二者的区别在于,后者不会让整个代码陷入阻塞,因为`await`操作会让其他协作的子程序有获得 CPU 资源而得以运转的机会。为了让这些子程序可以协作起来,我们需要将他们放到一个事件循环(实现消息分派传递的系统)上,因为**当协程遭遇 I/O 操作阻塞时,就会到事件循环中监听 I/O 操作是否完成,并注册自身的上下文以及自身的唤醒函数(以便恢复执行),之后该协程就变为阻塞状态**。上面的第12行代码创建了`9`个协程对象并放到一个列表中第13行代码通过`asyncio`模块的`get_event_loop`函数获得了系统的事件循环第14行通过`asyncio`模块的`run_until_complete`函数将协程对象挂载到事件循环上。执行上面的代码会发现,`9`个分别会阻塞`1`秒钟的协程总共只阻塞了约`1`秒种的时间,因为**阻塞的协程对象会放弃对 CPU 的占有而不是让 CPU 处于闲置状态,这种方式大大的提升了 CPU 的利用率**。而且我们还会注意到,数字并不是按照从`1`到`9`的顺序打印输出的,这正是我们想要的结果,说明它们是**异步执行**的。对于爬虫这样的 I/O 密集型任务来说,这种协作式并发在很多场景下是比使用多线程更好的选择,因为这种做法减少了管理和维护多个线程以及多个线程切换所带来的开销。
### aiohttp库
我们之前使用的`requests`三方库并不支持异步 I/O如果希望使用异步 I/O 的方式来加速爬虫代码的执行,我们可以安装和使用名为`aiohttp`的三方库。
安装`aiohttp`。
```Bash
pip install aiohttp
```
下面的代码使用`aiohttp`抓取了`10`个网站的首页并解析出它们的标题。
```Python
import asyncio
import re
import aiohttp
from aiohttp import ClientSession
TITLE_PATTERN = re.compile(r'<title.*?>(.*?)</title>', re.DOTALL)
async def fetch_page_title(url):
async with aiohttp.ClientSession(headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36',
}) as session: # type: ClientSession
async with session.get(url, ssl=False) as resp:
if resp.status == 200:
html_code = await resp.text()
matcher = TITLE_PATTERN.search(html_code)
title = matcher.group(1).strip()
print(title)
def main():
urls = [
'https://www.python.org/',
'https://www.jd.com/',
'https://www.baidu.com/',
'https://www.taobao.com/',
'https://git-scm.com/',
'https://www.sohu.com/',
'https://gitee.com/',
'https://www.amazon.com/',
'https://www.usa.gov/',
'https://www.nasa.gov/'
]
objs = [fetch_page_title(url) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(objs))
loop.close()
if __name__ == '__main__':
main()
```
输出:
```
京东(JD.COM)-正品低价、品质保障、配送及时、轻松购物!
搜狐
淘宝网 - 淘!我喜欢
百度一下,你就知道
Gitee - 基于 Git 的代码托管和研发协作平台
Git
NASA
Official Guide to Government Information and Services &#124; USAGov
Amazon.com. Spend less. Smile more.
Welcome to Python.org
```
从上面的输出可以看出,网站首页标题的输出顺序跟它们的 URL 在列表中的顺序没有关系。代码的第11行到第13行创建了`ClientSession`对象,通过它的`get`方法可以向指定的 URL 发起请求如第14行所示跟`requests`中的`Session`对象并没有本质区别唯一的区别是这里使用了异步上下文。代码第16行的`await`会让因为 I/O 操作阻塞的子程序放弃对 CPU 的占用这使得其他的子程序可以运转起来去抓取页面。代码的第17行和第18行使用了正则表达式捕获组操作解析网页标题。`fetch_page_title`是一个被`async`关键字修饰的异步函数调用该函数会获得协程对象如代码第35行所示。后面的代码跟之前的例子没有什么区别相信大家能够理解。
大家可以尝试将`aiohttp`换回到`requests`,看看不使用异步 I/O 也不使用多线程,到底和上面的代码有什么区别,相信通过这样的对比,大家能够更深刻的理解我们之前强调的几个概念:同步和异步,阻塞和非阻塞。

View File

@ -0,0 +1,185 @@
## 第37课并发编程在爬虫中的应用
之前的课程,我们已经为大家介绍了 Python 中的多线程、多进程和异步编程,通过这三种手段,我们可以实现并发或并行编程,这一方面可以加速代码的执行,另一方面也可以带来更好的用户体验。爬虫程序是典型的 I/O 密集型任务,对于 I/O 密集型任务来说,多线程和异步 I/O 都是很好的选择,因为当程序的某个部分因 I/O 操作阻塞时,程序的其他部分仍然可以运转,这样我们不用在等待和阻塞中浪费大量的时间。下面我们以爬取“[360图片](https://image.so.com/)”网站的图片并保存到本地为例,为大家分别展示使用单线程、多线程和异步 I/O 编程的爬虫程序有什么区别,同时也对它们的执行效率进行简单的对比。
“360图片”网站的页面使用了 [Ajax](https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX) 技术,这是很多网站都会使用的一种异步加载数据和局部刷新页面的技术。简单的说,页面上的图片都是通过 JavaScript 代码异步获取 JSON 数据并动态渲染生成的,而且整个页面还使用了瀑布式加载(一边向下滚动,一边加载更多的图片)。我们在浏览器的“开发者工具”中可以找到提供动态内容的数据接口,如下图所示,我们需要的图片信息就在服务器返回的 JSON 数据中。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211205221352.png" style="zoom:50%;">
例如要获取“美女”频道的图片我们可以请求如下所示的URL其中参数`ch`表示请求的频道,`=`后面的参数值`beauty`就代表了“美女”频道,参数`sn`相当于是页码,`0`表示第一页(共`30`张图片),`30`表示第二页,`60`表示第三页,以此类推。
```
https://image.so.com/zjl?ch=beauty&sn=0
```
### 单线程版本
通过上面的 URL 下载“美女”频道共`90`张图片。
```Python
"""
example04.py - 单线程版本爬虫
"""
import os
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
download_picture(pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
```
在 macOS 或 Linux 系统上,我们可以使用`time`命令来了解上面代码的执行时间以及 CPU 的利用率,如下所示。
```Bash
time python3 example04.py
```
下面是单线程爬虫代码在我的电脑上执行的结果。
```
python3 example04.py 2.36s user 0.39s system 12% cpu 21.578 total
```
这里我们只需要关注代码的总耗时为`21.578`秒CPU 利用率为`12%`。
### 多线程版本
我们使用之前讲到过的线程池技术,将上面的代码修改为多线程版本。
```Python
"""
example05.py - 多线程版本爬虫
"""
import os
from concurrent.futures import ThreadPoolExecutor
import requests
def download_picture(url):
filename = url[url.rfind('/') + 1:]
resp = requests.get(url)
if resp.status_code == 200:
with open(f'images/beauty/{filename}', 'wb') as file:
file.write(resp.content)
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
with ThreadPoolExecutor(max_workers=16) as pool:
for page in range(3):
resp = requests.get(f'https://image.so.com/zjl?ch=beauty&sn={page * 30}')
if resp.status_code == 200:
pic_dict_list = resp.json()['list']
for pic_dict in pic_dict_list:
pool.submit(download_picture, pic_dict['qhimg_url'])
if __name__ == '__main__':
main()
```
执行如下所示的命令。
```Bash
time python3 example05.py
```
代码的执行结果如下所示:
```
python3 example05.py 2.65s user 0.40s system 95% cpu 3.193 total
```
### 异步I/O版本
我们使用`aiohttp`将上面的代码修改为异步 I/O 的版本。为了以异步 I/O 的方式实现网络资源的获取和写文件操作,我们首先得安装三方库`aiohttp`和`aiofile`,命令如下所示。
```Bash
pip install aiohttp aiofile
```
`aiohttp` 的用法在之前的课程中已经做过简要介绍,`aiofile`模块中的`async_open`函数跟 Python 内置函数`open`的用法大致相同,只不过它支持异步操作。下面是异步 I/O 版本的爬虫代码。
```Python
"""
example06.py - 异步I/O版本爬虫
"""
import asyncio
import json
import os
import aiofile
import aiohttp
async def download_picture(session, url):
filename = url[url.rfind('/') + 1:]
async with session.get(url, ssl=False) as resp:
if resp.status == 200:
data = await resp.read()
async with aiofile.async_open(f'images/beauty/{filename}', 'wb') as file:
await file.write(data)
async def fetch_json():
async with aiohttp.ClientSession() as session:
for page in range(3):
async with session.get(
url=f'https://image.so.com/zjl?ch=beauty&sn={page * 30}',
ssl=False
) as resp:
if resp.status == 200:
json_str = await resp.text()
result = json.loads(json_str)
for pic_dict in result['list']:
await download_picture(session, pic_dict['qhimg_url'])
def main():
if not os.path.exists('images/beauty'):
os.makedirs('images/beauty')
loop = asyncio.get_event_loop()
loop.run_until_complete(fetch_json())
loop.close()
if __name__ == '__main__':
main()
```
执行如下所示的命令。
```Bash
time python3 example06.py
```
代码的执行结果如下所示:
```
python3 example06.py 0.82s user 0.21s system 27% cpu 3.782 total
```
### 总结
通过上面三段代码执行结果的比较,我们可以得出一个结论,使用多线程和异步 I/O 都可以改善爬虫程序的性能,因为我们不用将时间浪费在因 I/O 操作造成的等待和阻塞上,而`time`命令的执行结果也告诉我们,单线程的代码 CPU 利用率仅仅只有`12%`,而多线程版本的 CPU 利用率则高达`95%`;单线程版本的爬虫执行时间约`21`秒,而多线程和异步 I/O 的版本仅执行了`3`秒钟。另外,在运行时间差别不大的情况下,多线程的代码比异步 I/O 的代码耗费了更多的 CPU 资源,这是因为多线程的调度和切换也需要花费 CPU 时间。至此,三种方式在 I/O 密集型任务上的优劣已经一目了然,当然这只是在我的电脑上跑出来的结果。如果网络状况不是很理想或者目标网站响应很慢,那么使用多线程和异步 I/O 的优势将更为明显,有兴趣的读者可以自行试验。

View File

@ -1,118 +0,0 @@
## 存储数据
### 存储海量数据
数据持久化的首选方案应该是关系型数据库关系型数据库的产品很多包括Oracle、MySQL、SQLServer、PostgreSQL等。如果要存储海量的低价值数据文档数据库也是不错的选择MongoDB是文档数据库中的佼佼者有兴趣的读者可以自行研究。
下面的代码演示了如何使用MySQL来保存从知乎发现上爬取到的链接和页面。
```SQL
create database zhihu default charset utf8;
create user 'hellokitty'@'%' identified by 'Hellokitty.618';
grant all privileges on zhihu.* to 'hellokitty'@'%';
flush privileges;
use zhihu;
create table `tb_explore`
(
`id` integer auto_increment,
`url` varchar(1024) not null,
`page` longblob not null,
`digest` char(48) unique not null,
`idate` datetime default now(),
primary key (`id`)
);
```
```Python
import hashlib
import pickle
import re
import zlib
from urllib.parse import urljoin
import MySQLdb
import bs4
import requests
conn = MySQLdb.connect(host='1.2.3.4', port=3306,
user='hellokitty', password='Hellokitty.618',
database='zhihu', charset='utf8',
autocommit=True)
def write_to_db(url, page, digest):
try:
with conn.cursor() as cursor:
cursor.execute(
'insert into tb_explore (url, page, digest) values (%s, %s, %s) ',
(url, page, digest)
)
except MySQLdb.MySQLError as err:
print(err)
def main():
base_url = 'https://www.zhihu.com/'
seed_url = urljoin(base_url, 'explore')
headers = {'user-agent': 'Baiduspider'}
try:
resp = requests.get(seed_url, headers=headers)
soup = bs4.BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
for a_tag in soup.find_all('a', {'href': href_regex}):
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
digest = hashlib.sha1(full_url.encode()).hexdigest()
html_page = requests.get(full_url, headers=headers).text
zipped_page = zlib.compress(pickle.dumps(html_page))
write_to_db(full_url, zipped_page, digest)
finally:
conn.close()
if __name__ == '__main__':
main()
```
### 数据缓存
通过[《网络数据采集和解析》](./67.数据采集和解析.md)一文我们已经知道了如何从指定的页面中抓取数据以及如何保存抓取的结果但是我们没有考虑过这么一种情况就是我们可能需要从已经抓取过的页面中提取出更多的数据重新去下载这些页面对于规模不大的网站倒是问题也不大但是如果能够把这些页面缓存起来对应用的性能会有明显的改善。下面的例子演示了如何使用Redis来缓存知乎发现上的页面。
```Python
import hashlib
import pickle
import re
import zlib
from urllib.parse import urljoin
import bs4
import redis
import requests
def main():
base_url = 'https://www.zhihu.com/'
seed_url = urljoin(base_url, 'explore')
client = redis.Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
headers = {'user-agent': 'Baiduspider'}
resp = requests.get(seed_url, headers=headers)
soup = bs4.BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
for a_tag in soup.find_all('a', {'href': href_regex}):
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
field_key = hashlib.sha1(full_url.encode()).hexdigest()
if not client.hexists('spider:zhihu:explore', field_key):
html_page = requests.get(full_url, headers=headers).text
zipped_page = zlib.compress(pickle.dumps(html_page))
client.hset('spider:zhihu:explore', field_key, zipped_page)
print('Total %d question pages found.' % client.hlen('spider:zhihu:explore'))
if __name__ == '__main__':
main()
```

View File

@ -1,236 +0,0 @@
## 并发下载
### 多线程和多进程补充知识点
#### threading.local类
使用线程时最不愿意遇到的情况就是多个线程竞争资源,在这种情况下为了保证资源状态的正确性,我们可能需要对资源进行加锁保护的处理,这一方面会导致程序失去并发性,另外如果多个线程竞争多个资源时,还有可能因为加锁方式的不当导致[死锁](https://zh.wikipedia.org/wiki/%E6%AD%BB%E9%94%81)。要解决多个线程竞争资源的问题,其中一个方案就是让每个线程都持有资源的副本(拷贝),这样每个线程可以操作自己所持有的资源,从而规避对资源的竞争。
要实现将资源和持有资源的线程进行绑定的操作,最简单的做法就是使用`threading`模块的`local`类,在网络爬虫开发中,就可以使用`local`类为每个线程绑定一个MySQL数据库连接或Redis客户端对象这样通过线程可以直接获得这些资源既解决了资源竞争的问题又避免了在函数和方法调用时传递这些资源。具体的请参考本章多线程爬取“手机搜狐网”Redis版的实例代码。
#### concurrent.futures模块
Python3.2带来了`concurrent.futures` 模块,这个模块包含了线程池和进程池、管理并行编程任务、处理非确定性的执行流程、进程/线程同步等功能。关于这部分的内容推荐大家阅读[《Python并行编程》](http://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/index.html)。
#### 分布式进程
使用多进程的时候可以将进程部署在多个主机节点上Python的`multiprocessing`模块不但支持多进程,其中`managers`子模块还支持把多进程部署到多个节点上。当然,要部署分布式进程,首先需要一个服务进程作为调度者,进程之间通过网络进行通信来实现对进程的控制和调度,由于`managers`模块已经对这些做出了很好的封装,因此在无需了解网络通信细节的前提下,就可以编写分布式多进程应用。具体的请参照本章分布式多进程爬取“手机搜狐网”的实例代码。
### 协程和异步I/O
#### 协程的概念
协程coroutine通常又称之为微线程或纤程它是相互协作的一组子程序函数。所谓相互协作指的是在执行函数A时可以随时中断去执行函数B然后又中断继续执行函数A。注意这一过程并不是函数调用因为没有调用语句整个过程看似像多线程然而协程只有一个线程执行。协程通过`yield`关键字和 `send()`操作来转移执行权,协程之间不是调用者与被调用者的关系。
协程的优势在于以下两点:
1. 执行效率极高,因为子程序(函数)切换不是线程切换,由程序自身控制,没有切换线程的开销。
2. 不需要多线程的锁机制,因为只有一个线程,也不存在竞争资源的问题,当然也就不需要对资源加锁保护,因此执行效率高很多。
> **说明**协程适合处理的是I/O密集型任务处理CPU密集型任务并不是它擅长的如果要提升CPU的利用率可以考虑“多进程+多线程”或者“多进程+协程”的工作模式。
#### 历史回顾
1. Python 2.2第一次提出了生成器最初称之为迭代器的概念PEP 255
2. Python 2.5:引入了将对象发送回暂停了的生成器这一特性即生成器的`send()`方法PEP 342
3. Python 3.3:添加了`yield from`特性,允许从迭代器中返回任何值(注意生成器本身也是迭代器),这样我们就可以串联生成器并且重构出更好的生成器。
4. Python 3.4:引入`asyncio.coroutine`装饰器用来标记作为协程的函数,协程函数和`asyncio`及其事件循环一起使用来实现异步I/O操作。
5. Python 3.5:引入了`async`和`await`,可以使用`async def`来定义一个协程函数,这个函数中不能包含任何形式的`yield`语句,但是可以使用`return`或`await`从协程中返回值。
协程实现了协作式并发通过提高CPU的利用率来达到改善性能的目的。著名的三方库[`aiohttp`](https://github.com/aio-libs/aiohttp)就是通过协程的方式实现了HTTP客户端和HTTP服务器的功能较之`requests`有更好的获取数据的性能,有兴趣可以阅读它的[官方文档](https://aiohttp.readthedocs.io/en/stable/)。
```Python
import asyncio
import aiohttp
async def download(url):
print('Fetch:', url)
async with aiohttp.ClientSession() as session:
async with session.get(url, ssl=False) as resp:
print(url, '--->', resp.status)
print(url, '--->', resp.headers)
print('\n\n', await resp.text())
def main():
loop = asyncio.get_event_loop()
urls = [
'https://www.baidu.com',
'http://www.sohu.com/',
'http://www.sina.com.cn/',
'https://www.taobao.com/',
'http://jd.com/'
]
tasks = [download(url) for url in urls]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
if __name__ == '__main__':
main()
```
### 实例 - 多线程爬取“手机搜狐网”所有页面
下面我们把之间讲的所有知识结合起来,用面向对象的方式实现一个爬取“手机搜狐网”的多线程爬虫。
```Python
import pickle
import zlib
from enum import Enum, unique
from hashlib import sha1
from random import random
from threading import Thread, current_thread, local
from time import sleep
from urllib.parse import urlparse
import pymongo
import redis
import requests
from bs4 import BeautifulSoup
from bson import Binary
@unique
class SpiderStatus(Enum):
IDLE = 0
WORKING = 1
def decode_page(page_bytes, charsets=('utf-8',)):
page_html = None
for charset in charsets:
try:
page_html = page_bytes.decode(charset)
break
except UnicodeDecodeError:
pass
return page_html
class Retry(object):
def __init__(self, *, retry_times=3,
wait_secs=5, errors=(Exception, )):
self.retry_times = retry_times
self.wait_secs = wait_secs
self.errors = errors
def __call__(self, fn):
def wrapper(*args, **kwargs):
for _ in range(self.retry_times):
try:
return fn(*args, **kwargs)
except self.errors as e:
print(e)
sleep((random() + 1) * self.wait_secs)
return None
return wrapper
class Spider(object):
def __init__(self):
self.status = SpiderStatus.IDLE
@Retry()
def fetch(self, current_url, *, charsets=('utf-8', ),
user_agent=None, proxies=None):
thread_name = current_thread().name
print(f'[{thread_name}]: {current_url}')
headers = {'user-agent': user_agent} if user_agent else {}
resp = requests.get(current_url,
headers=headers, proxies=proxies)
return decode_page(resp.content, charsets) \
if resp.status_code == 200 else None
def parse(self, html_page, *, domain='m.sohu.com'):
soup = BeautifulSoup(html_page, 'lxml')
for a_tag in soup.body.select('a[href]'):
parser = urlparse(a_tag.attrs['href'])
scheme = parser.scheme or 'http'
netloc = parser.netloc or domain
if scheme != 'javascript' and netloc == domain:
path = parser.path
query = '?' + parser.query if parser.query else ''
full_url = f'{scheme}://{netloc}{path}{query}'
redis_client = thread_local.redis_client
if not redis_client.sismember('visited_urls', full_url):
redis_client.rpush('m_sohu_task', full_url)
def extract(self, html_page):
pass
def store(self, data_dict):
# redis_client = thread_local.redis_client
# mongo_db = thread_local.mongo_db
pass
class SpiderThread(Thread):
def __init__(self, name, spider):
super().__init__(name=name, daemon=True)
self.spider = spider
def run(self):
redis_client = redis.Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
mongo_client = pymongo.MongoClient(host='1.2.3.4', port=27017)
thread_local.redis_client = redis_client
thread_local.mongo_db = mongo_client.msohu
while True:
current_url = redis_client.lpop('m_sohu_task')
while not current_url:
current_url = redis_client.lpop('m_sohu_task')
self.spider.status = SpiderStatus.WORKING
current_url = current_url.decode('utf-8')
if not redis_client.sismember('visited_urls', current_url):
redis_client.sadd('visited_urls', current_url)
html_page = self.spider.fetch(current_url)
if html_page not in [None, '']:
hasher = hasher_proto.copy()
hasher.update(current_url.encode('utf-8'))
doc_id = hasher.hexdigest()
sohu_data_coll = mongo_client.msohu.webpages
if not sohu_data_coll.find_one({'_id': doc_id}):
sohu_data_coll.insert_one({
'_id': doc_id,
'url': current_url,
'page': Binary(zlib.compress(pickle.dumps(html_page)))
})
self.spider.parse(html_page)
self.spider.status = SpiderStatus.IDLE
def is_any_alive(spider_threads):
return any([spider_thread.spider.status == SpiderStatus.WORKING
for spider_thread in spider_threads])
thread_local = local()
hasher_proto = sha1()
def main():
redis_client = redis.Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
if not redis_client.exists('m_sohu_task'):
redis_client.rpush('m_sohu_task', 'http://m.sohu.com/')
spider_threads = [SpiderThread('thread-%d' % i, Spider())
for i in range(10)]
for spider_thread in spider_threads:
spider_thread.start()
while redis_client.exists('m_sohu_task') or is_any_alive(spider_threads):
sleep(5)
print('Over!')
if __name__ == '__main__':
main()
```

View File

@ -1,158 +0,0 @@
## 解析动态内容
根据权威机构发布的全球互联网可访问性审计报告全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案一是JavaScript逆向工程另一种是渲染JavaScript获得渲染后的内容。
### JavaScript逆向工程
下面我们以“360图片”网站为例说明什么是JavaScript逆向工程。其实所谓的JavaScript逆向工程就是找到通过Ajax技术动态获取数据的接口。在浏览器中输入<http://image.so.com/z?ch=beauty>就可以打开“360图片”的“美女”版块如下图所示。
![](./res/image360-website.png)
但是当我们在浏览器中通过右键菜单“显示网页源代码”的时候居然惊奇的发现页面的HTML代码中连一个`<img>`标签都没有那么我们看到的图片是怎么显示出来的呢原来所有的图片都是通过JavaScript动态加载的而在浏览器的“开发人员工具”的“网络”中可以找到获取这些图片数据的网络API接口如下图所示。
![](./res/api-image360.png)
那么结论就很简单了只要我们找到了这些网络API接口那么就能通过这些接口获取到数据当然实际开发的时候可能还要对这些接口的参数以及接口返回的数据进行分析了解每个参数的意义以及返回的JSON数据的格式这样才能在我们的爬虫中使用这些数据。
### 使用Selenium
尽管很多网站对自己的网络API接口进行了保护增加了获取数据的难度但是只要经过足够的努力绝大多数还是可以被逆向工程的但是在实际开发中我们可以通过浏览器渲染引擎来避免这些繁琐的工作WebKit就是一个利用的渲染引擎。
WebKit的代码始于1998年的KHTML项目当时它是Konqueror浏览器的渲染引擎。2001年苹果公司从这个项目的代码中衍生出了WebKit并应用于Safari浏览器早期的Chrome浏览器也使用了该内核。在Python中我们可以通过Qt框架获得WebKit引擎并使用它来渲染页面获得动态内容关于这个内容请大家自行阅读[《爬虫技术:动态页面抓取超级指南》](http://python.jobbole.com/84600/)一文。
如果没有打算用上面所说的方式来渲染页面并获得动态内容其实还有一种替代方案就是使用自动化测试工具Selenium它提供了浏览器自动化的API接口这样就可以通过操控浏览器来获取动态内容。首先可以使用pip来安装Selenium。
```Shell
pip3 install selenium
```
下面以“阿里V任务”的“直播服务”为例来演示如何使用Selenium获取到动态内容并抓取主播图片。
```Python
import requests
from bs4 import BeautifulSoup
def main():
resp = requests.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
soup = BeautifulSoup(resp.text, 'lxml')
for img_tag in soup.select('img[src]'):
print(img_tag.attrs['src'])
if __name__ == '__main__':
main()
```
运行上面的程序会发现没有任何的输出因为页面的HTML代码上根本找不到`<img>`标签。接下来我们使用Selenium来获取到页面上的动态内容再提取主播图片。
```Python
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
def main():
driver = webdriver.Chrome()
driver.get('https://v.taobao.com/v/content/live?catetype=704&from=taonvlang')
soup = BeautifulSoup(driver.page_source, 'lxml')
for img_tag in soup.body.select('img[src]'):
print(img_tag.attrs['src'])
if __name__ == '__main__':
main()
```
在上面的程序中我们通过Selenium实现对Chrome浏览器的操控如果要操控其他的浏览器可以创对应的浏览器对象例如Firefox、IE等。运行上面的程序如果看到如下所示的错误提示那是说明我们还没有将Chrome浏览器的驱动添加到PATH环境变量中也没有在程序中指定Chrome浏览器驱动所在的位置。
```Shell
selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH. Please see https://sites.google.com/a/chromium.org/chromedriver/home
```
为了解决上面的问题可以到Selenium的[官方网站](https://www.seleniumhq.org)找到浏览器驱动的下载链接并下载需要的驱动在Linux或macOS系统下可以通过下面的命令来设置PATH环境变量Windows下配置环境变量也非常简单不清楚的可以自行了解。
```Shell
export PATH=$PATH:/Users/Hao/Downloads/Tools/chromedriver/
```
其中`/Users/Hao/Downloads/Tools/chromedriver/ `就是chromedriver所在的路径。当然更为简单的办法是把chromedriver直接放在虚拟环境中跟Python解释器位于同一个路径下就可以了。
### WebDriver用法详解
表1. 定位页面元素的方法
表2. WebDriver的常用属性
| 属性 | 描述 |
| --------------------- | ----------------------------- |
| current_url | 当前页面的URL |
| current_window_handle | 当前窗口的句柄(引用) |
| name | WebDriver实例底层浏览器的名称 |
| orientation | 当前设备的方向(横屏、竖屏) |
| page_source | 当前页面的源代码(动态内容) |
| title | 当前页面的标题 |
| window_handles | WebDriver打开的所有窗口的句柄 |
表3. WebDriver的常用方法
| 方法 | 描述 |
| ----------------------------------- | -------------------------------------- |
| back() / forward() | 在浏览历史记录中后退/前进 |
| close() / quit() | 关闭当前浏览器窗口 / 退出WebDriver实例 |
| get(url) | 加载指定URL的页面到浏览器中 |
| maximize_window() | 将浏览器窗口最大化 |
| refresh() | 刷新当前页面 |
| switch_to_active_element() | 获得页面上得到焦点的元素 |
| switch_to_alert() | 把焦点切换至弹出的警告框 |
| set_page_load_timeout(time_to_wait) | 设置页面加载超时时间 |
| set_script_timeout(time_to_wait) | 设置JavaScript执行超时时间 |
| implicit_wait(time_to_wait) | 设置等待元素被找到或目标指令完成 |
### WebElement用法
表1. WebElement常用属性
| | |
| ---- | ---- |
| | |
| | |
| | |
表2. WebElement常用方法
| | |
| ---- | ---- |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
### Select用法
### Alert用法
### 元素等待机制
#### 隐式等待
#### 显示等待
### 高级特性
#### 鼠标和键盘事件
#### 调用JavaScript
#### 屏幕截图和录制
#### 操作Cookie

View File

@ -111,9 +111,6 @@ class TaobaoDownloaderMiddleWare(object):
def __init__(self, timeout=None):
self.timeout = timeout
# options = webdriver.ChromeOptions()
# options.add_argument('--headless')
# self.browser = webdriver.Chrome(options=options)
options = webdriver.ChromeOptions()
options.add_argument('--headless')
self.browser = webdriver.Chrome(options)
@ -128,7 +125,6 @@ class TaobaoDownloaderMiddleWare(object):
def process_request(self, request, spider):
try:
self.browser.get(request.url)
# Chrome对象的page_source代表了页面的HTML代码带动态内容
return HtmlResponse(url=request.url, body=self.browser.page_source,
request=request, encoding='utf-8', status=200)
except TimeoutException:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from io import StringIO
from urllib.parse import urlencode
import re

View File

@ -1,328 +0,0 @@
## 数据分析概述
当今世界对信息技术的依赖程度在不断加深,每天都会有大量的数据产生,我们经常会感到数据越来越多,但是要从中发现有价值的信息却越来越难。这里所说的信息,可以理解为对数据集处理之后的结果,是从数据集中提炼出的可用于其他场合的结论性的东西,而**从原始数据中抽取出有价值的信息**的这个过程我们就称之为**数据分析**,它是数据科学工作的一部分。
### 数据分析师的职责和技能栈
我们通常将从事数据分析、数据科学和数据相关产品的岗位都统称为数据分析岗位,但是根据工作性质的不同,又可以分为**数据分析方向**、**数据挖掘方向**、**数据产品方向**和**数据工程方向**。我们通常所说的数据分析师主要是指**业务数据分析师**,很多数据分析师的职业生涯都是从这个岗位开始的,而且这个岗位也是招聘数量最大的岗位。业务数据分析师在公司通常不属于研发部门而**属于运营部门**,所以这个岗位也称为**数据运营**或**商业分析**,通常招聘信息对这个岗位的描述是:
1. 负责各部门相关的报表。
2. 建立和优化指标体系。
3. 监控数据波动和异常,找出问题。
4. 优化和驱动业务,推动数字化运营。
5. 找出潜在的市场和产品的上升空间。
根据上面的描述,作为业务数据分析师,我们的工作不是给领导一个简单浅显的结论,而是结合公司的业务,完成**揪出异常**、**找到原因**、**探索趋势**的工作。所以作为数据分析师不管是用Python语言、Excel、SPSS或其他的商业智能工具工具只是达成目标的手段**数据思维是核心技能**,而从实际业务问题出发到最终发现数据中的商业价值是终极目标。数据分析师在很多公司只是一个基础岗位,精于业务的数据分析师可以向**数据分析经理**或**数据运营总监**等管理岗位发展;对于熟悉机器学习算法的数据分析师来说,可以向**数据挖掘工程师**或**算法专家**方向发展而这些岗位除了需要相应的数学和统计学知识在编程能力方面也比数据分析师有更高的要求可能还需要有大数据存储和处理的相关经验作为数据产品经理除了传统产品经理的技能栈之外也需要较强的技术能力例如要了解常用的推荐算法、机器学习模型能够为算法的改进提供依据能够制定相关埋点的规范和口径虽然不需要精通各种算法但是要站在产品的角度去考虑数据模型、指标、算法等的落地数据工程师是一个偏技术的岗位基本上的成长道路都是从SQL开始逐步向Hadoop生态圈迁移然后每天跟Flume和Kafka亲密接触的一个过程。
以下是我总结的数据分析师的技能栈,仅供参考。
1. 计算机科学(数据分析工具、编程语言、数据库、……)
2. 数学和统计学(数据思维、统计思维)
3. 人工智能(机器学习算法)
4. 业务理解能力(沟通、表达、经验)
5. 总结和表述能力商业PPT、文字总结
### 数据分析的流程
我们提到数分析这个词很多时候可能指的都是**狭义的数据分析**,这类数据分析主要目标就是生成可视化报表并通过这些报表来洞察业务中的问题。**广义的数据分析**还包含了数据挖掘的部分,不仅要通过数据实现对业务的监控和分析,还要利用机器学习算法,找出隐藏在数据背后的知识,并利用这些知识为将来的决策提供支撑。简单的说,**一个完整的数据分析应该包括基本的数据分析和深入的数据挖掘两个部分**。
基本的数据分析工作一般包含以下几个方面的内容,当然因为行业和工作内容的不同会略有差异。
1. 确定目标(输入):理解业务,确定指标口径
2. 获取数据:数据库、电子表格、三方接口、网络爬虫、开放数据集、……
3. 清洗数据:缺失值处理、异常值处理、格式化处理、数据变换、归一化、离散化、……
4. 探索数据:运算、统计、分组、聚合、可视化(趋势、变化、分布等)、……
5. 数据报告(输出):数据发布,工作成果总结汇报
6. 分析洞察(后续):数据监控、发现趋势、洞察异常、……
深入的数据挖掘工作应该包含一下几个方面的内容,当然因为行业和工作内容的不同会略有差异。
1. 确定目标(输入):理解业务,明确挖掘目标
2. 数据准备:数据采集、数据描述、数据探索、质量判定、……
3. 数据加工:提取数据、清洗数据、数据变换、归一化、离散化、特殊编码、降维、特征选择、……
4. 数据建模:模型比较、模型选择、算法应用、……
5. 模型评估:交叉检验、参数调优、结果评价、……
6. 模型部署(输出):模型落地,业务改进,运营监控、报告撰写
### 数据分析相关库
使用Python从事数据科学相关的工作是一个非常棒的选择因为Python整个生态圈中有大量的成熟的用于数据科学的软件包工具库。而且不同于其他的用于数据科学的编程语言Julia、RPython除了可以用于数据科学能做的事情还很多可以说Python语言几乎是无所不能的。
#### 三大神器
1. [NumPy](https://numpy.org/):支持常见的数组和矩阵操作,通过`ndarray`类实现了对多维数组的封装提供了操作这些数组的方法和函数集。由于NumPy内置了并行运算功能当使用多核CPU时Numpy会自动做并行计算。
2. [Pandas](https://pandas.pydata.org/)pandas的核心是其特有的数据结构`DataFrame`和`Series`这使得pandas可以处理包含不同类型的数据的负责表格和时间序列这一点是NumPy的`ndarray`做不到的。使用pandas可以轻松顺利的加载各种形式的数据然后对数据进行切片、切块、处理缺失值、聚合、重塑和可视化等操作。
3. [Matplotlib](https://matplotlib.org/)matplotlib是一个包含各种绘图模块的库能够根据我们提供的数据创建高质量的图形。此外matplotlib还提供了pylab模块这个模块包含了很多像[MATLAB](https://www.mathworks.com/products/matlab.html)一样的绘图组件。
#### 其他相关库
1. [SciPy](https://scipy.org/)完善了NumPy的功能封装了大量科学计算的算法包括线性代数、稀疏矩阵、信号和图像处理、最优化问题、快速傅里叶变换等。
2. [Seaborn](https://seaborn.pydata.org/)Seaborn是基于matplotlib的图形可视化工具直接使用matplotlib虽然可以定制出漂亮的统计图表但是总体来说还不够简单方便Seaborn相当于是对matplotlib做了封装让用户能够以更简洁有效的方式做出各种有吸引力的统计图表。
3. [Scikit-learn](https://scikit-learn.org/)Scikit-learn最初是SciPy的一部分它是Python数据科学运算的核心提供了大量机器学习可能用到的工具包括数据预处理、监督学习分类、回归、无监督学习聚类、模式选择、交叉检验等。
4. [Statsmodels](https://www.statsmodels.org/stable/index.html):包含了经典统计学和经济计量学算法的库。
### 安装和使用Anaconda
如果希望快速开始使用Python处理数据科学相关的工作建议大家直接安装Anaconda它是工具包最为齐全的Python科学计算发行版本。对于新手来说先安装官方的Python解释器再逐个安装工作中会使用到的库文件会比较麻烦尤其是在Windows环境下经常会因为构建工具或DLL文件的缺失导致安装失败而一般新手也很难根据错误提示信息采取正确的解决措施容易产生严重的挫败感。
对于个人用户来说可以从Anaconda的[官方网站](https://www.anaconda.com/)下载它的“个人版Individual Edition”安装程序安装完成后你的计算机上不仅拥有了Python环境和Spyder类似于PyCharm的集成开发工具还拥有了与数据科学工作相关的近200个工具包包括我们上面提到的那些库。除此之外Anaconda还提供了一个名为conda的包管理工具通过这个工具不仅可以管理Python的工具包还可以用于创建运行Python程序的虚拟环境。
可以通过Anaconda官网提供的下载链接选择适合自己操作系统的安装程序建议大家选择图形化的安装程序下载完成后双击安装程序开始安装如下所示。
![](res/download-anaconda.png)
![](res/install-anaconda.png)
完成安装后macOS用户可以在“应用程序”或“Launchpad”中找到名为“Anaconda-Navigator”的应用程序运行该程序可以看到如下所示的界面我们可以在这里选择需要执行的操作。
![](res/run-anaconda-navigator.png)
对于Windows用户建议按照安装向导的提示和推荐的选项来安装Anaconda除了安装路径基本也没有什么需要选择的安装完成后可以在“开始菜单”中找到“Anaconda3”。
#### conda命令
如果希望使用conda工具来管理依赖项或者创建项目的虚拟环境可以在终端或命令行提示符中使用conda命令。Windows用户可以在“开始菜单”中找到“Anaconda3”然后点击“Anaconda Prompt”来启动支持conda的命令行提示符。macOS用户建议直接使用“Anaconda-Navigator”中的“Environments”通过可视化的方式对虚拟环境和依赖项进行管理。
1. 版本和帮助信息。
- 查看版本:`conda -V`或`conda --version`
- 获取帮助:`conda -h`或`conda --help`
- 相关信息:`conda list`
2. 虚拟环境相关。
- 显示所有虚拟环境:`conda env list`
- 创建虚拟环境:`conda create --name venv`
- 指定Python版本创建虚拟环境`conda create --name venv python=3.7`
- 指定Python版本创建虚拟环境并安装指定依赖项`conda create --name venv python=3.7 numpy pandas`
- 通过克隆现有虚拟环境的方式创建虚拟环境:`conda create --name venv2 --clone venv`
- 分享虚拟环境并重定向到指定的文件中:`conda env export > environment.yml`
- 通过分享的虚拟环境文件创建虚拟环境:`conda env create -f environment.yml`
- 激活虚拟环境:`conda activate venv`
- 退出虚拟环境:`conda deactivate`
- 删除虚拟环境:`conda remove --name venv --all`
> **说明**:上面的命令中,`venv`和`venv2`是虚拟环境文件夹的名字,可以将其替换为自己喜欢的名字,但是**强烈建议**使用英文且不要出现空格或其他特殊字符。
3. 包(三方库或工具)管理。
- 查看已经安装的包:`conda list`
- 搜索指定的包:`conda search matplotlib`
- 安装指定的包:`conda install matplotlib`
- 更新指定的包:`conda update matplotlib`
- 移除指定的包:`conda remove matplotlib`
> **说明**:在搜索、安装和更新软件包时,默认会连接到官方网站进行操作,如果觉得速度不给力,可以将默认的官方网站替换为国内的镜像网站,推荐使用清华大学的开源镜像网站。将默认源更换为国内镜像的命令是:`conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ `。如果需要换回默认源,可以使用命令`conda config --remove-key channels`。
### 使用Notebook
#### 安装和启动Notebook
如果已经安装了AnacondamacOS用户可以按照上面所说的方式在“Anaconda-Navigator”中直接启动“Jupyter Notebook”以下统一简称为Notebook。Windows用户可以在“开始菜单”中找到Anaconda文件夹接下来选择运行文件夹中的“Jupyter Notebook”就可以开始数据科学的探索之旅。
对于安装了Python环境但是没有安装Anaconda的用户可以用Python的包管理工具pip来安装`jupyter`然后在终端Windows系统称之为命令行提示符中运行`jupyter notebook`命令来启动Notebook如下所示。
安装Notebook
```Bash
pip install jupyter
```
安装三大神器:
```Bash
pip install numpy pandas matplotlib
```
运行Notebook
```Bash
jupyter notebook
```
Notebook是基于网页的用于交互计算的应用程序可以用于代码开发、文档撰写、代码运行和结果展示。简单的说你可以在网页中直接**编写代码**和**运行代码**代码的运行结果也会直接在代码块下方进行展示。如在编写代码的过程中需要编写说明文档可在同一个页面中使用Markdonw格式进行编写而且可以直接看到渲染后的效果。此外Notebook的设计初衷是提供一个能够支持多种编程语言的工作环境目前它能够支持超过40种编程语言包括Python、R、Julia、Scala等。
首先我们可以创建一个用于书写Python代码的Notebook如下图所示。
![](res/jupyter-create-notebook.png)
接下来,我们就可以编写代码、撰写文档和运行程序啦,如下图所示。
![](res/use-jupyter-notebook.png)
#### Notebook使用技巧
如果使用Python做工程化的项目开发PyCharm肯定是最好的选择它提供了一个集成开发环境应该具有的所有功能尤其是智能提示、代码补全、自动纠错这类功能会让开发人员感到非常舒服。如果使用Python做数据科学相关的工作Notebook并不比PyCharm逊色在数据和图表展示方面Notebook更加优秀。这个工具的使用非常简单大家可以看看Notebook菜单栏相信理解起来不会有太多困难在知乎上有一篇名为[《最详尽使用指南超快上手Jupyter Notebook》](https://zhuanlan.zhihu.com/p/32320214)的文章也可以帮助大家快速认识Notebook。
下面我为大家介绍一些Notebook的使用技巧希望能够帮助大家提升工作效率。
1. 自动补全。在使用Notebook编写代码时按`Tab`键会获得代码提示。
2. 获得帮助。在使用Notebook时如果希望了解一个对象如变量、类、函数等的相关信息或使用方式可以在对象后面使用`?`并运行代码, 窗口下方会显示出对应的信息,帮助我们了解该对象,如下所示。
![](res/notebook-get-help.png)
3. 搜索命名。如果只记得一个类或一个函数名字的一部分,可以使用通配符`*`并配合`?`进行搜索,如下所示。
![](res/notebook-search-namespace.png)
4. 调用命令。可以在Notebook中使用`!`后面跟系统命令的方式来执行系统命令。
5. 魔法指令。Notebook中有很多非常有趣且有用的魔法指令例如可以使用`%timeit`测试语句的执行时间,可以使用`%pwd`查看当前工作目录等。如果想查看所有的魔法指令,可以使用`%lsmagic`,如果了解魔法指令的用法,可以使用`%magic`来查看,如下图所示。
![](res/notebook-magic-command.png)
常用的魔法指令有:
| 魔法指令 | 功能说明 |
| ------------------------------------------- | ------------------------------------------ |
| `%pwd` | 查看当前工作目录 |
| `%ls` | 列出当前或指定文件夹下的内容 |
| `%cat` | 查看指定文件的内容 |
| `%hist` | 查看输入历史 |
| `%matplotlib inline` | 设置在页面中嵌入matplotlib输出的统计图表 |
| `%config Inlinebackend.figure_format='svg'` | 设置统计图表使用SVG格式矢量图 |
| `%run` | 运行指定的程序 |
| `%load` | 加载指定的文件到单元格中 |
| `%quickref` | 显示IPython的快速参考 |
| `%timeit` | 多次运行代码并统计代码执行时间 |
| `%prun` | 用`cProfile.run`运行代码并显示分析器的输出 |
| `%who` / `%whos` | 显示命名空间中的变量 |
| `%xdel` | 删除一个对象并清理所有对它的引用 |
6. 快捷键。Notebook中的很多操作可以通过快捷键来实现使用快捷键可以提升我们的工作效率。Notebook的快捷键又可以分为命令模式下的快捷键和编辑模式下的快捷键所谓编辑模式就是处于输入代码或撰写文档状态的模式在编辑模式下按`Esc`可以回到命令模式,在命令模式下按`Enter`可以进入编辑模式。
命令模式下的快捷键:
| 快捷键 | 功能说明 |
| ------------------------------- | -------------------------------------------- |
| Alt + EnterOption + Enter | 运行当前单元格并在下面插入新的单元格 |
| Shift + Enter | 运行当前单元格并选中下方的单元格 |
| Ctrl + EnterCommand + Enter | 运行当前单元格 |
| j / k、Shift + j / Shift + k | 选中下方/上方单元格、连续选中下方/上方单元格 |
| a / b | 在下方/上方插入新的单元格 |
| c / x | 复制单元格 / 剪切单元格 |
| v / Shift + v | 在下方/上方粘贴单元格 |
| dd / z | 删除单元格 / 恢复删除的单元格 |
| l / Shift + l | 显示或隐藏当前/所有单元格行号 |
| ii / 00 | 中断/重启Notebook内核 |
| Space / Shift + Space | 向下/向上滚动页面 |
编辑模式下的快捷键:
| 快捷键 | 功能说明 |
| ------------------------------------------------ | -------------------------------------- |
| Shift + Tab | 获得提示信息 |
| Ctrl + ]Command + ]/ Ctrl + [Command + [ | 增加/减少缩进 |
| Alt + EnterOption + Enter | 运行当前单元格并在下面插入新的单元格 |
| Shift + Enter | 运行当前单元格并选中下方的单元格 |
| Ctrl + EnterCommand + Enter | 运行当前单元格 |
| Ctrl + Left / RightCommand + Left / Right | 光标移到行首/行尾 |
| Ctrl + Up / DownCommand + Up / Down | 光标移动代码开头/结尾处 |
| Up / Down | 光标上移/下移一行或移到上/下一个单元格 |
> **温馨提示**:如果记不住这些快捷键也没有关系,在命令模式下按`h`键可以打开Notebook的帮助系统马上就可以看到快捷键的设置而且可以根据实际的需要重新编辑快捷键如下图所示。
>
> ![](res/notebook-shortcut.png)
### 补充知识
> **温馨提示**GitHub默认不支持对Markdown文档中数学公式的渲染为了不影响浏览文档你可以为浏览器安装支持GitHub渲染LaTex数学公式的插件如Chrome浏览器的MathJax Plugin for GitHub插件、Firefox浏览器的LatexMathifyGitHub插件等。
#### 描述型统计
1. 集中趋势
- **众数**mode数据集合中出现频次最多的数据。数据的趋势越集中众数的代表性就越好。众数不受极值的影响但是无法保证唯一性。
- **均值**mean均值代表某个数据集的整体水平它的缺点是容易受极值的影响可以使用加权平均值来消除极值的影响但是可能事先并不清楚数据的权重所以对于正数可以用几何平均值来替代算术平均值二者的计算公式如下所示。
算术平均值:$\bar{x}=\frac{\sum_{i=1}^{n}x_{i}}{n}=\frac{x_{1}+x_{2}+\cdots +x_{n}}{n}$
几何平均值:$\left(\prod_{i=1}^{n}x_{i}\right)^{\frac{1}{n}}={\sqrt[{n}]{x_{1}x_{2} \cdots x_{n}}}$
- **分位数**将一个随机变量的概率分布范围分为几个具有相同概率的连续区间比如最常见的中位数二分位数median就是将数据集划分为数量相等的上下两个部分。除此之外常见的分位数还有四分位数quartile、百分位数percentile等。
- 中位数:当数据量$n$是奇数时,${Q}=x_{\frac{n+1}{2}}$,当数据量$n$是偶数时,$Q=(x_{\frac{n}{2}} + x_{{\frac{n}{2}}+1}) / 2$。
- 四分位数:
**第一四分位数**$Q_1$),又称**较小四分位数**或**下四分位数**等于该样本中所有数值由小到大排列后第25%的数字。
**第二四分位数**$Q_2$),即**中位数**等于该样本中所有数值由小到大排列后第50%的数字。
**第三四分位数**$Q_3$),又称**较大四分位数**或**上四分位数**等于该样本中所有数值由小到大排列后第75%的数字。
**四分位距离**$IQR$Inter-Quartile Range即$Q_3-Q_1$的值。
在实际工作中,我们经常通过四分位数再配合[箱线图](https://zhuanlan.zhihu.com/p/110580568)来发现异常值。例如,小于$Q_1 - 1.5 \times IQR$的值或大于$Q3 + 1.5 \times IQR$的值可以视为普通异常值,而小于$Q_1 - 3 \times IQR$的值或大于$Q3 + 3 \times IQR$的值通常视为极度异常值。这种检测异常值的方法跟[“3西格玛法则”](https://zh.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7%E5%8E%9F%E5%89%87)的道理是一致的,如下图所示。
![](res/quartile_and_3sigma.png)
2. 离散趋势
- **极值**就是最大值maximum、最小值minimum代表着数据集合中的上限和下限。
- **极差**range又称“全距”是一组数据中的最大观测值和最小观测值之差记作$R$。一般情况下,极差越大,离散程度越大,数据受极值的影响越严重。
- **方差**variance将每个值与均值的偏差进行平方最后除以总数据量的值。简单来说就是表示数据与期望值的偏离程度。方差越大就意味着每个值与平均值的差值平方和越大、越不稳定、波动越剧烈因此代表着数据整体比较分散呈现出离散的趋势而方差越小代表着每个值与平均值的差值平方和越小、越稳定、波动越平滑因此代表着数据整体很集中。
- **标准差**standard deviation将方差进行平方根与方差一样都是表示数据与期望值的偏离程度。
- **分位差**:分位数的差值,如上面提到的四分位距离。
3. 分布
- **峰态**:峰态就是概率分布曲线的峰值高低,是尖峰、平顶峰,还是正态峰。
- **偏度**:偏度就是峰值与平均值的偏离程度,是左偏还是右偏。
#### 推理性统计
1. 基本概念
- 随机试验:在相同条件下对某种随机现象进行观测的试验。随机试验满足三个特点:
- 可以在相同条件下重复的进行。
- 每次试验的结果不止一个,事先可以明确指出全部可能的结果。
- 重复试验的结果以随机的方式出现(事先不确定会出现哪个结果)。
- 随机变量:如果$X$指定给概率空间$S$中每一个事件$e$有一个实数$X(e)$,同时针对每一个实数$r$都有一个事件集合$A_r$与其相对应,其中$A_r=\{e: X(e) \le r\}$,那么$X$被称作随机变量。从这个定义看出,$X$的本质是一个实值函数,以给定事件为自变量的实值函数,因为函数在给定自变量时会产生因变量,所以将$X$称为随机变量。
- 概率质量函数/概率密度函数:概率质量函数是描述离散型随机变量为特定取值的概率的函数,通常缩写为**PMF**。概率密度函数是描述连续型随机变量在某个确定的取值点可能性的函数,通常缩写为**PDF**。二者的区别在于,概率密度函数本身不是概率,只有对概率密度函数在某区间内进行积分后才是概率。
2. 概率分布
- 离散型分布:如果随机发生的事件之间是毫无联系的,每一次随机事件发生都是独立的、不连续的、不受其他事件影响的,那么这些事件的概率分布就属于离散型分布。
- 二项分布binomial distribution$n$个独立的是/非试验中成功的次数的离散概率分布,其中每次试验的成功概率为$p$。一般地,如果随机变量$X$服从参数为$n$和$p$的二项分布,记为$X\sim B(n,p)$。$n$次试验中正好得到$k$次成功的概率由概率质量函数给出,$\displaystyle f(k,n,p)=\Pr(X=k)={n \choose k}p^{k}(1-p)^{n-k}$,对于$k= 0, 1, 2, ..., n$,其中${n \choose k}={\frac {n!}{k!(n-k)!}}$。
- 泊松分布poisson distribution适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内受到的服务请求的次数、汽车站台的候客人数、机器出现的故障数、自然灾害发生的次数、DNA序列的变异数、放射性原子核的衰变数等等。泊松分布的概率质量函数为$P(X=k)=\frac{e^{-\lambda}\lambda^k}{k!}$,泊松分布的参数$\lambda$是单位时间(或单位面积)内随机事件的平均发生率。
- 连续型分布:
- 均匀分布uniform distribution如果连续型随机变量$X$具有概率密度函数$f(x)=\begin{cases}{\frac{1}{b-a}} \quad &{a \leq x \leq b} \\ {0} \quad &{\mbox{other}}\end{cases}$,则称$X$服从$[a,b]$上的均匀分布,记作$X\sim U[a,b]$。
- 指数分布exponential distribution如果连续型随机变量$X$具有概率密度函数$f(x)=\begin{cases} \lambda e^{- \lambda x} \quad &{x \ge 0} \\ {0} \quad &{x \lt 0} \end{cases}$,则称$X$服从参数为$\lambda$的指数分布,记为$X \sim Exp(\lambda)$。指数分布可以用来表示独立随机事件发生的时间间隔,比如旅客进入机场的时间间隔、客服中心接入电话的时间间隔、知乎上出现新问题的时间间隔等等。指数分布的一个重要特征是无记忆性(无后效性),这表示如果一个随机变量呈指数分布,它的条件概率遵循:$P(T \gt s+t\ |\ T \gt t)=P(T \gt s), \forall s,t \ge 0$。
- 正态分布normal distribution又名**高斯分布**Gaussian distribution是一个非常常见的连续概率分布经常用自然科学和社会科学中来代表一个不明的随机变量。若随机变量$X$服从一个位置参数为$\mu$、尺度参数为$\sigma$的正态分布,记为$X \sim N(\mu,\sigma^2)$,其概率密度函数为:$\displaystyle f(x)={\frac {1}{\sigma {\sqrt {2\pi }}}}e^{-{\frac {\left(x-\mu \right)^{2}}{2\sigma ^{2}}}}$。
- 伽马分布gamma distribution假设$X_1, X_2, ... X_n$为连续发生事件的等候时间,且这$n$次等候时间为独立的,那么这$n$次等候时间之和$Y$$Y=X_1+X_2+...+X_n$)服从伽玛分布,即$Y \sim \Gamma(\alpha,\beta)$,其中$\alpha=n, \beta=\lambda$,这里的$\lambda$是连续发生事件的平均发生频率。
- 卡方分布chi-square distribution若$k$个随机变量$Z_1,Z_2,...,Z_k$是相互独立且符合标准正态分布数学期望为0方差为1的随机变量则随机变量$Z$的平方和$X=\sum_{i=1}^{k}Z_i^2$被称为服从自由度为$k$的卡方分布,记为$X \sim \chi^2(k)$。
3. 大数定律:样本数量越多,则其算术平均值就有越高的概率接近期望值。
- 弱大数定律(辛钦定理):样本均值依概率收敛于期望值,即对于任意正数$\epsilon$,有:$\lim_{n \to \infty}P(|\bar{X_n}-\mu|>\epsilon)=0$。
- 强大数定律样本均值以概率1收敛于期望值$P(\lim_{n \to \infty}\bar{X_n}=\mu)=1$。
4. 中心极限定理:如果统计对象是大量独立的随机变量,那么这些变量的平均值分布就会趋向于正态分布,不管原来它们的概率分布是什么类型,即:$X_1, X_2, ..., X_n$是一组独立同分布的随机变量,且有$E(x_i)=\mu, D(X_i)=\sigma ^2$,当$n$足够大时,均值$\bar{X}=\frac{\sum_i^nX_i}{n}$的分布接近于$N(\mu,\sigma ^2/n)$正态分布,如果对$\bar{X}$进行标准化处理,可以得到$X'=\frac{\bar{X} - \mu}{\sigma / \sqrt n}$标准正态分布。
5. 假设检验
假设检验就是通过抽取样本数据,并且通过**小概率反证法**去验证整体情况的方法。假设检验的核心思想是小概率反证法首先假设想推翻的命题是成立的然后试图找出矛盾找出不合理的地方来证明命题为假命题即在原假设零假设null hypothesis的前提下估算某事件发生的可能性如果该事件是小概率事件在一次研究中本来是不可能发生的现在却发生了这时候就可以推翻原假设接受备择假设alternative hypothesis。如果该事件不是小概率事件我们就找不到理由来推翻之前的假设实际中可引申为接受所做的无效假设。
假设检验会存在两种错误情况,一种称为“拒真”,一种称为“取伪”。如果原假设是对的,但你拒绝了原假设,这种错误就叫作“拒真”,这个错误的概率也叫作显著性水平$\alpha$,或称为容忍度;如果原假设是错的,但你承认了原假设,这种错误就叫作“取伪”,这个错误的概率我们记为$\beta$。
6. 条件概率和贝叶斯定理
**条件概率**是指事件A在事件B发生的条件下发生的概率通常记为$P(A|B)$。设A与B为样本空间$\Omega$中的两个事件,其中$P(B) \gt 0$。那么在事件B发生的条件下事件A发生的条件概率为$P(A|B)=\frac{P(A \cap B)}{P(B)}$,其中$P(A \cap B)$是联合概率即A和B两个事件共同发生的概率。
事件A在事件B已发生的条件下发生的概率与事件B在事件A已发生的条件下发生的概率是不一样的。然而这两者是有确定的关系的**贝叶斯定理**就是对这种关系的陈述,即:$P(A|B)=\frac{P(A)P(B|A)}{P(B)}$,其中:
- $P(A|B)$是已知B发生后A的条件概率也称为A的后验概率。
- $P(A)$是A的先验概率也称为边缘概率是不考虑B时A发生的概率。
- $P(B|A)$是已知A发生后B的条件概率称为B的似然性。
- $P(B)$是B的先验概率。
按照上面的描述,贝叶斯定理可以表述为:`后验概率 = (似然性 * 先验概率) / 标准化常量`​,简单的说就是后验概率与先验概率和相似度的乘积成正比。
描述性统计通常用于研究表象,将现象用数据的方式描述出来;推理性统计通常用于推测本质,也就是你看到的表象的东西有多大概率符合你对隐藏在表象后的本质的猜测。

View File

@ -1,398 +0,0 @@
## Pandas的应用
Pandas是Wes McKinney在2008年开发的一个强大的**分析结构化数据**的工具集。Pandas以NumPy为基础数据表示和运算提供了用于数据处理的函数和方法对数据分析和数据挖掘提供了很好的支持同时Pandas还可以跟数据可视化工具Matplotlib很好的整合在一起非常轻松愉快的实现数据的可视化展示。
Pandas核心的数据类型是`Series`(数据系列)、`DataFrame`(数据表/数据框),分别用于处理一维和二维的数据,除此之外还有一个名为`Index`的类型及其子类型,它为`Series`和`DataFrame`提供了索引功能。日常工作中以`DataFrame`使用最为广泛因为二维的数据本质就是一个有行有列的表格想一想Excel电子表格和关系型数据库中的二维表。上述这些类型都提供了大量的处理数据的方法数据分析师可以以此为基础实现对数据的各种常规处理。
### Series的应用
Pandas库中的`Series`对象可以用来表示一维数据结构,跟数组非常类似,但是多了一些额外的功能。`Series`的内部结构包含了两个数组,其中一个用来保存数据,另一个用来保存数据的索引,如下图所示。
![](res/pandas-series.png)
#### 创建Series对象
> **提示**在执行下面的代码之前请先导入pandas以及相关的库文件具体的做法可以参考上一章。
- 方法1通过列表或数组创建Series对象。
代码:
```Python
# data参数表示数据index参数表示索引标签
# 如果没有指定index属性默认使用数字索引
ser1 = pd.Series(data=[320, 180, 300, 405], index=['一季度', '二季度', '三季度', '四季度'])
ser1
```
输出:
```
一季度 320
二季度 180
三季度 300
四季度 405
dtype: int64
```
- 方法2通过字典创建Series对象。
代码:
```Python
# 字典中的键就是索引标签,字典中的值就是数据
ser2 = pd.Series({'一季度': 320, '二季度': 180, '三季度': 300, '四季度': 405})
ser2
```
输出:
```
一季度 320
二季度 180
三季度 300
四季度 405
dtype: int64
```
#### 索引和切片
跟数组一样Series对象也可以进行索引和切片操作不同的是Series对象因为内部维护了一个保存索引的数组所以除了可以使用整数索引通过位置检索数据外还可以通过自己设置的索引标签获取对应的数据。
- 使用正负向整数索引
代码:
```Python
print(ser2[0], ser2[2], ser2[-1])
ser2[0], ser2[-1] = 350, 360
print(ser2)
```
输出:
```
320 300 405
一季度 350
二季度 180
三季度 300
四季度 360
dtype: int64
```
- 使用自己设置的标签索引
代码:
```Python
print(ser2['一季度'], ser2['三季度'])
ser2['一季度'] = 380
print(ser2)
```
输出:
```
350 300
一季度 380
二季度 180
三季度 300
四季度 360
dtype: int64
```
- 切片操作
代码:
```Python
print(ser2[1:3])
print(ser2['二季度':'四季度'])
```
输出:
```
二季度 180
三季度 300
dtype: int64
二季度 500
三季度 500
四季度 520
dtype: int64
```
代码:
```Python
ser2[1:3] = 400, 500
ser2
```
输出:
```
一季度 380
二季度 400
三季度 500
四季度 360
dtype: int64
```
- 花式索引
代码:
```Python
print(ser2[['二季度', '四季度']])
ser2[['二季度', '四季度']] = 500, 520
print(ser2)
```
输出:
```
二季度 400
四季度 360
dtype: int64
一季度 380
二季度 500
三季度 500
四季度 520
dtype: int64
```
- 布尔索引
代码:
```Python
ser2[ser2 >= 500]
```
输出:
```
二季度 500
三季度 500
四季度 520
dtype: int64
```
####属性和方法
Series对象的常用属性如下表所示。
| 属性 | 说明 |
| ------------------------- | --------------------------------------- |
| `dtype` / `dtypes` | 返回`Series`对象的数据类型 |
| `hasnans` | 判断`Series`对象中有没有空值 |
| `at` / `iat` | 通过索引访问`Series`对象中的单个值 |
| `loc` / `iloc` | 通过一组索引访问`Series`对象中的一组值 |
| `index` | 返回`Series`对象的索引 |
| `is_monotonic` | 判断`Series`对象中的数据是否单调 |
| `is_monotonic_increasing` | 判断`Series`对象中的数据是否单调递增 |
| `is_monotonic_decreasing` | 判断`Series`对象中的数据是否单调递减 |
| `is_unique` | 判断`Series`对象中的数据是否独一无二 |
| `size` | 返回`Series`对象中元素的个数 |
| `values` | 以`ndarray`的方式返回`Series`对象中的值 |
`Series`对象的方法很多,我们通过下面的代码为大家介绍一些常用的方法。
- 统计相关方法
代码:
```Python
# 求和
print(ser2.sum())
# 求均值
print(ser2.mean())
# 求最大
print(ser2.max())
# 求最小
print(ser2.min())
# 计数
print(ser2.count())
# 求标准差
print(ser2.std())
# 求方差
print(ser2.var())
# 求中位数
print(ser2.median())
```
输出:
```
1900
475.0
520
380
4
64.03124237432849
4100.0
500.0
```
`Series`对象还有一个名为`describe()`的方法,可以获得上述所有的描述性统计信息,如下所示。
代码:
```Python
ser2.describe()
```
输出:
```
count 4.000000
mean 475.000000
std 64.031242
min 380.000000
25% 470.000000
50% 500.000000
75% 505.000000
max 520.000000
dtype: float64
```
> **提示**:因为`describe()`返回的也是一个`Series`对象,所以也可以用`ser2.describe()['mean']`来获取平均值。
如果`Series`对象的数据中有重复元素,我们可以使用`unique()`方法获得去重之后的`Series`对象,如果想要统计重复元素重复的次数,可以使用`value_counts()`方法,这个方法会返回一个`Series`对象,它的索引就是原来的`Series`对象中的元素,而每个元素出现的次数就是返回的`Series`对象中的数据,在默认情况下会按照元素出现次数做降序排列。
代码:
```Python
ser3 = pd.Series(data=['apple', 'banana', 'apple', 'pitaya', 'apple', 'pitaya', 'durian'])
ser3.value_counts()
```
输出:
```
apple 3
pitaya 2
durian 1
banana 1
dtype: int64
```
- 数据处理方法
`Series`对象的`dropna`和`fillna`方法分别用来删除空值和填充空值,具体的用法如下所示。
```Python
ser4 = pd.Series(data=[10, 20, np.NaN, 30, np.NaN])
ser4.dropna()
```
输出:
```
0 10.0
1 20.0
3 30.0
dtype: float64
```
代码:
```Python
# 将空值填充为40
ser4.fillna(value=40)
```
输出:
```
0 10.0
1 20.0
2 40.0
3 30.0
4 40.0
dtype: float64
```
代码:
```Python
# backfill或bfill表示用后一个元素的值填充空值
# ffill或pad表示用前一个元素的值填充空值
ser4.fillna(method='ffill')
```
输出:
```
0 10.0
1 20.0
2 20.0
3 30.0
4 30.0
dtype: float64
```
需要提醒大家注意的是,`dropna`和`fillna`方法都有一个名为`inplace`的参数,它的默认值是`False`,表示删除空值或填充空值不会修改原来的`Series`对象,而是返回一个新的`Series`对象来表示删除或填充空值后的数据系列,如果将`inplace`参数的值修改为`True`,那么删除或填充空值会就地操作,直接修改原来的`Series`对象,那么方法的返回值是`None`。后面我们会接触到的很多方法,包括`DataFrame`对象的很多方法都会有这个参数,它们的意义跟这里是一样的。
`Series`对象的`apply`和`map`方法可以用于对数据进行处理,代码如下所示。
`Series`对象的`mask`和`where`方法可以将满足或不满足条件的值进行替换。
`Series`对象的`sort_index`和`sort_values`方法可以用于对索引和数据的排序,具体的用法请参考下面的例子。
`Series`对象的`value_counts`方法可以统计每个值出现的次数并且以从大到小的顺序进行排列,`Series`对象的`unique`方法可以获取到不重复的值。
#### 绘制图表
Series对象有一个名为`plot`的方法可以用来生成图表如果选择生成折线图、散点图、柱状图等默认会使用Series对象的索引作为横坐标使用Series对象的数据作为纵坐标。
利用Series对象的数据也可以生成表示占比的饼图和显示数据分布的直方图如下面的代码所示。
### DataFrame的应用
#### 创建DataFrame对象
#### 基本属性和方法
#### 索引和切片
#### 相关运算
#### 缺失值处理
#### 数据离散化
#### 数据的合并
#### 交叉表和透视表
1. 交叉表:根据一个数据系列计算另一个数据系列的统计结果得到的`DataFrame`对象。
2. 透视表:将`DataFrame`的列分别作为行索引和列索引,然后对指定的列应用聚合函数得到的结果。
#### 分组和聚合
#### 绘制图表

View File

@ -1,70 +0,0 @@
## 数据可视化
数据可视化简单的说就是将数据呈现为漂亮的统计图表然后进一步发现数据中包含的规律以及隐藏的信息。之前的课程我们已经为大家展示了Python在数据处理方面的优势为大家介绍了NumPy和Pandas的应用以此为基础我们可以进一步使用[Matplotlib](https://matplotlib.org/)和[Seaborn](https://seaborn.pydata.org/)来实现数据的可视化,将数据处理的结果展示为直观的可视化图表。
### Matplotlib的应用
#### 安装和导入
对于使用Anaconda的用户在安装Anaconda时已经携带了数据分析和可视化的库无需再单独安装Matplotlib。如果没有安装Anaconda但是有Python环境可以使用Python的包管理工具pip来安装命令如下所示。
```Shell
pip install matplotlib
```
接下来我们在Jupyter Notebook中用下面的方式导入Matplotlib。
```Python
from matplotlib import pyplot as plt
```
通过下面的魔法指令,可以让创建的图表直接内嵌在浏览器窗口中显示。
```Python
%matplotlib inline
```
通过下面的魔法指令可以生成矢量图SVG
```Python
%config InlineBackend.figure_format='svg'
```
#### 绘图的流程
1. 创建画布
2. 绘制图像
3. 显示(保存)图像
#### 绘制的例子
```Python
```
运行程序,效果如下图所示。
#### 解决中文显示问题
#### 定制图表效果
#### 图形的种类和意义
1. 绘制散点图
2. 绘制柱状图
3. 绘制直方图
4. 绘制饼图
#### 显示多个坐标系
### Seaborn的应用

View File

@ -1,14 +0,0 @@
## 数据分析项目实战
### 2020年北京积分落户分析
### 某招聘网站招聘数据分析
### 某电商网站订单数据分析

View File

@ -1,424 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"%matplotlib inline\n",
"%config InlineBackend.figure_format='svg'"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"plt.rcParams['font.sans-serif'] = 'FZJKai-Z03S'\n",
"plt.rcParams['axes.unicode_minus'] = False"
]
},
{
"cell_type": "code",
"execution_count": 61,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"一季度 320\n",
"二季度 180\n",
"三季度 300\n",
"四季度 405\n",
"dtype: int64"
]
},
"execution_count": 61,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser1 = pd.Series(data=[320, 180, 300, 405], index=['一季度', '二季度', '三季度', '四季度'])\n",
"ser1"
]
},
{
"cell_type": "code",
"execution_count": 62,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"一季度 320\n",
"二季度 180\n",
"三季度 300\n",
"四季度 405\n",
"dtype: int64"
]
},
"execution_count": 62,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser2 = pd.Series({'一季度': 320, '二季度': 180, '三季度': 300, '四季度': 405})\n",
"ser2"
]
},
{
"cell_type": "code",
"execution_count": 63,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"320 300 405\n",
"一季度 350\n",
"二季度 180\n",
"三季度 300\n",
"四季度 360\n",
"dtype: int64\n"
]
}
],
"source": [
"print(ser2[0], ser2[2], ser2[-1])\n",
"ser2[0], ser2[-1] = 350, 360 \n",
"print(ser2)"
]
},
{
"cell_type": "code",
"execution_count": 64,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"350 300\n",
"一季度 380\n",
"二季度 180\n",
"三季度 300\n",
"四季度 360\n",
"dtype: int64\n"
]
}
],
"source": [
"print(ser2['一季度'], ser2['三季度'])\n",
"ser2['一季度'] = 380\n",
"print(ser2)"
]
},
{
"cell_type": "code",
"execution_count": 65,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"二季度 180\n",
"三季度 300\n",
"dtype: int64\n",
"二季度 180\n",
"三季度 300\n",
"四季度 360\n",
"dtype: int64\n",
"一季度 380\n",
"二季度 400\n",
"三季度 500\n",
"四季度 360\n",
"dtype: int64\n"
]
}
],
"source": [
"print(ser2[1:3])\n",
"print(ser2['二季度': '四季度'])\n",
"ser2[1:3] = 400, 500\n",
"print(ser2)"
]
},
{
"cell_type": "code",
"execution_count": 66,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"二季度 400\n",
"四季度 360\n",
"dtype: int64\n",
"一季度 380\n",
"二季度 500\n",
"三季度 500\n",
"四季度 520\n",
"dtype: int64\n"
]
}
],
"source": [
"print(ser2[['二季度', '四季度']])\n",
"ser2[['二季度', '四季度']] = 500, 520\n",
"print(ser2)"
]
},
{
"cell_type": "code",
"execution_count": 68,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"二季度 500\n",
"三季度 500\n",
"四季度 520\n",
"dtype: int64\n"
]
}
],
"source": [
"print(ser2[ser2 >= 500])"
]
},
{
"cell_type": "code",
"execution_count": 70,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1900\n",
"475.0\n",
"520\n",
"380\n",
"4\n",
"64.03124237432849\n",
"4100.0\n",
"500.0\n"
]
}
],
"source": [
"# 求和\n",
"print(ser2.sum())\n",
"# 求均值\n",
"print(ser2.mean())\n",
"# 求最大\n",
"print(ser2.max())\n",
"# 求最小\n",
"print(ser2.min())\n",
"# 计数\n",
"print(ser2.count())\n",
"# 求标准差\n",
"print(ser2.std())\n",
"# 求方差\n",
"print(ser2.var())\n",
"# 求中位数\n",
"print(ser2.median())"
]
},
{
"cell_type": "code",
"execution_count": 78,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"count 4.000000\n",
"mean 475.000000\n",
"std 64.031242\n",
"min 380.000000\n",
"25% 470.000000\n",
"50% 500.000000\n",
"75% 505.000000\n",
"max 520.000000\n",
"dtype: float64"
]
},
"execution_count": 78,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser2.describe()"
]
},
{
"cell_type": "code",
"execution_count": 99,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"apple 3\n",
"pitaya 2\n",
"durian 1\n",
"banana 1\n",
"dtype: int64"
]
},
"execution_count": 99,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser3 = pd.Series(data=['apple', 'banana', 'apple', 'pitaya', 'apple', 'pitaya', 'durian'])\n",
"ser3.value_counts()"
]
},
{
"cell_type": "code",
"execution_count": 80,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0 10.0\n",
"1 20.0\n",
"3 30.0\n",
"dtype: float64"
]
},
"execution_count": 80,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser4 = pd.Series(data=[10, 20, np.NaN, 30, np.NaN])\n",
"ser4.dropna()"
]
},
{
"cell_type": "code",
"execution_count": 82,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0 10.0\n",
"1 20.0\n",
"2 40.0\n",
"3 30.0\n",
"4 40.0\n",
"dtype: float64"
]
},
"execution_count": 82,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser4.fillna(value=40)"
]
},
{
"cell_type": "code",
"execution_count": 98,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0 10.0\n",
"1 20.0\n",
"2 20.0\n",
"3 30.0\n",
"4 30.0\n",
"dtype: float64"
]
},
"execution_count": 98,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ser4.fillna(method='ffill')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -1,45 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -1,45 +0,0 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -0,0 +1,433 @@
## Pandas的应用-5
### DataFrame的应用
#### 窗口计算
`DataFrame`对象的`rolling`方法允许我们将数据置于窗口中然后就可以使用函数对窗口中的数据进行运算和处理。例如我们获取了某只股票近期的数据想制作5日均线和10日均线那么就需要先设置窗口再进行运算。我们可以使用三方库`pandas-datareader`来获取指定的股票在某个时间段内的数据,具体的操作如下所示。
安装`pandas-datareader`三方库。
```Bash
pip install pandas-datareader
```
通过`pandas-datareader` 提供的`get_data_stooq`从 Stooq 网站获取百度股票代码BIDU近期股票数据。
```Python
import pandas_datareader as pdr
baidu_df = pdr.get_data_stooq('BIDU', start='2021-11-22', end='2021-12-7')
baidu_df.sort_index(inplace=True)
baidu_df
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208205710.png" style="zoom:38%;">
上面的`DataFrame`有`Open`、`High`、`Low`、`Close`、`Volume`五个列,分别代表股票的开盘价、最高价、最低价、收盘价和成交量,接下来我们对百度的股票数据进行窗口计算。
```Python
baidu_df.rolling(5).mean()
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208205932.png" style="zoom:38%;">
上面的`Close` 列的数据就是我们需要的5日均线当然我们也可以用下面的方法直接在`Close`列对应的`Series`对象上计算5日均线。
```Python
baidu_df.Close.rolling(5).mean()
```
输出:
```
Date
2021-11-22 NaN
2021-11-23 NaN
2021-11-24 NaN
2021-11-26 NaN
2021-11-29 150.608
2021-11-30 151.014
2021-12-01 150.682
2021-12-02 150.196
2021-12-03 147.062
2021-12-06 146.534
2021-12-07 146.544
Name: Close, dtype: float64
```
#### 相关性判定
在统计学中我们通常使用协方差covariance来衡量两个随机变量的联合变化程度。如果变量 $X$ 的较大值主要与另一个变量 $Y$ 的较大值相对应,而两者较小值也相对应,那么两个变量倾向于表现出相似的行为,协方差为正。如果一个变量的较大值主要对应于另一个变量的较小值,则两个变量倾向于表现出相反的行为,协方差为负。简单的说,协方差的正负号显示着两个变量的相关性。方差是协方差的一种特殊情况,即变量与自身的协方差。
$$
cov(X,Y) = E((X - \mu)(Y - \upsilon)) = E(X \cdot Y) - \mu\upsilon
$$
如果 $X$ 和 $Y$ 是统计独立的那么二者的协方差为0这是因为在 $X$ 和 $Y$ 独立的情况下:
$$
E(X \cdot Y) = E(X) \cdot E(Y) = \mu\upsilon
$$
协方差的数值大小取决于变量的大小,通常是不容易解释的,但是正态形式的协方差可以显示两变量线性关系的强弱。在统计学中,皮尔逊积矩相关系数就是正态形式的协方差,它用于度量两个变量 $X$ 和 $Y$ 之间的相关程度(线性相关),其值介于`-1`到`1`之间。
$$
\rho{X,Y} = \frac {cov(X, Y)} {\sigma_{X}\sigma_{Y}}
$$
估算样本的协方差和标准差,可以得到样本皮尔逊系数,通常用希腊字母 $\rho$ 表示。
$$
\rho = \frac {\sum_{i=1}^{n}(X_i - \bar{X})(Y_i - \bar{Y})} {\sqrt{\sum_{i=1}^{n}(X_i - \bar{X})^2} \sqrt{\sum_{i=1}^{n}(Y_i - \bar{Y})^2}}
$$
我们用 $\rho$ 值判断指标的相关性时遵循以下两个步骤。
1. 判断指标间是正相关、负相关,还是不相关。
- 当 $ \rho \gt 0 $,认为变量之间是正相关,也就是两者的趋势一致。
- 当 $ \rho \lt 0 $,认为变量之间是负相关,也就是两者的趋势相反。
- 当 $ \rho = 0 $,认为变量之间是不相关的,但并不代表两个指标是统计独立的。
2. 判断指标间的相关程度。
- 当 $ \rho $ 的绝对值在 $ [0.6,1] $ 之间,认为变量之间是强相关的。
- 当 $ \rho $ 的绝对值在 $ [0.1,0.6) $ 之间,认为变量之间是弱相关的。
- 当 $ \rho $ 的绝对值在 $ [0,0.1) $ 之间,认为变量之间没有相关性。
皮尔逊相关系数适用于:
1. 两个变量之间是线性关系,都是连续数据。
2. 两个变量的总体是正态分布,或接近正态的单峰分布。
3. 两个变量的观测值是成对的,每对观测值之间相互独立。
`DataFrame`对象的`cov`方法和`corr`方法分别用于计算协方差和相关系数,`corr`方法的第一个参数`method`的默认值是`pearson`,表示计算皮尔逊相关系数;除此之外,还可以指定`kendall`或`spearman`来获得肯德尔系数或斯皮尔曼等级相关系数。
接下来,我们从名为`boston_house_price.csv`的文件中获取著名的[波士顿房价数据集](https://www.heywhale.com/mw/dataset/590bd595812ede32b73f55f2)来创建一个`DataFrame`,我们通过`corr`方法计算可能影响房价的`13`个因素中,哪些跟房价是正相关或负相关的,代码如下所示。
```Python
boston_df = pd.read_csv('data/csv/boston_house_price.csv')
boston_df.corr()
```
> **说明**:如果需要上面例子中的 CSV 文件,可以通过下面的百度云盘地址进行获取,数据在《从零开始学数据分析》目录中。链接:<https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g>提取码e7b4。
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208213325.png">
斯皮尔曼相关系数对数据条件的要求没有皮尔逊相关系数严格,只要两个变量的观测值是成对的等级评定资料,或者是由连续变量观测资料转化得到的等级资料,不论两个变量的总体分布形态、样本容量的大小如何,都可以用斯皮尔曼等级相关系数来进行研究。我们通过下面的方式来计算斯皮尔曼相关系数。
```Python
boston_df.corr('spearman')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208213518.png">
在 Notebook 或 JupyterLab 中,我们可以为`PRICE`列添加渐变色,用颜色直观的展示出跟房价负相关、正相关、不相关的列,`DataFrame`对象`style`属性的`background_gradient`方法可以完成这个操作,代码如下所示。
```Python
boston_df.corr('spearman').style.background_gradient('RdYlBu', subset=['PRICE'])
```
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208215228.png">
上面代码中的`RdYlBu`代表的颜色如下所示,相关系数的数据值越接近`1`,颜色越接近红色;数据值越接近`1`,颜色越接近蓝色;数据值在`0`附件则是黄色。
```Python
plt.get_cmap('RdYlBu')
```
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208215057.png">
### Index的应用
我们再来看看`Index`类型,它为`Series`和`DataFrame`对象提供了索引服务,常用的`Index`有以下几种,我们直接上代码。
1. 范围索引(`RangeIndex`
代码:
```Python
sales_data = np.random.randint(400, 1000, 12)
month_index = pd.RangeIndex(1, 13, name='月份')
ser = pd.Series(data=sales_data, index=month_index)
ser
```
输出:
```
月份
1 703
2 705
3 557
4 943
5 961
6 615
7 788
8 985
9 921
10 951
11 874
12 609
dtype: int64
```
2. 分类索引(`CategoricalIndex`
代码:
```Python
cate_index = pd.CategoricalIndex(
['苹果', '香蕉', '苹果', '苹果', '桃子', '香蕉'],
ordered=True,
categories=['苹果', '香蕉', '桃子']
)
ser = pd.Series(data=amount, index=cate_index)
ser
```
输出:
```
苹果 6
香蕉 6
苹果 7
苹果 6
桃子 8
香蕉 6
dtype: int64
```
基于索引分组数据,然后使用`sum`进行求和。
```Python
ser.groupby(level=0).sum()
```
输出:
```
苹果 19
香蕉 12
桃子 8
dtype: int64
```
3. 多级索引(`MultiIndex`
代码:
```Python
ids = np.arange(1001, 1006)
sms = ['期中', '期末']
index = pd.MultiIndex.from_product((ids, sms), names=['学号', '学期'])
courses = ['语文', '数学', '英语']
scores = np.random.randint(60, 101, (10, 3))
df = pd.DataFrame(data=scores, columns=courses, index=index)
df
```
> **说明**:上面的代码使用了`MultiIndex`的类方法`from_product`,该方法通过`ids`和`sms`两组数据的笛卡尔积构造了多级索引。
输出:
```
语文 数学 英语
学号 学期
1001 期中 93 77 60
期末 93 98 84
1002 期中 64 78 71
期末 70 71 97
1003 期中 72 88 97
期末 99 100 63
1004 期中 80 71 61
期末 91 62 72
1005 期中 82 95 67
期末 84 78 86
```
根据第一级索引分组数据,按照期中成绩占`25%`,期末成绩占`75%` 的方式计算每个学生每门课的成绩。
```Python
# 计算每个学生的成绩期中占25%期末占75%
df.groupby(level=0).agg(lambda x: x.values[0] * 0.25 + x.values[1] * 0.75)
```
输出:
```
语文 数学 英语
学号
1001 93.00 92.75 78.00
1002 68.50 72.75 90.50
1003 92.25 97.00 71.50
1004 88.25 64.25 69.25
1005 83.50 82.25 81.25
```
4. 日期时间索引(`DatetimeIndex`
通过`date_range()`函数,我们可以创建日期时间索引,代码如下所示。
代码:
```Python
pd.date_range('2021-1-1', '2021-6-1', periods=10)
```
输出:
```
DatetimeIndex(['2021-01-01 00:00:00', '2021-01-17 18:40:00',
'2021-02-03 13:20:00', '2021-02-20 08:00:00',
'2021-03-09 02:40:00', '2021-03-25 21:20:00',
'2021-04-11 16:00:00', '2021-04-28 10:40:00',
'2021-05-15 05:20:00', '2021-06-01 00:00:00'],
dtype='datetime64[ns]', freq=None)
```
代码:
```Python
pd.date_range('2021-1-1', '2021-6-1', freq='W')
```
输出:
```
DatetimeIndex(['2021-01-03', '2021-01-10', '2021-01-17', '2021-01-24',
'2021-01-31', '2021-02-07', '2021-02-14', '2021-02-21',
'2021-02-28', '2021-03-07', '2021-03-14', '2021-03-21',
'2021-03-28', '2021-04-04', '2021-04-11', '2021-04-18',
'2021-04-25', '2021-05-02', '2021-05-09', '2021-05-16',
'2021-05-23', '2021-05-30'],
dtype='datetime64[ns]', freq='W-SUN')
```
通过`DateOffset`类型,我们可以设置时间差并和`DatetimeIndex`进行运算,具体的操作如下所示。
代码:
```Python
index = pd.date_range('2021-1-1', '2021-6-1', freq='W')
index - pd.DateOffset(days=2)
```
输出:
```
DatetimeIndex(['2021-01-01', '2021-01-08', '2021-01-15', '2021-01-22',
'2021-01-29', '2021-02-05', '2021-02-12', '2021-02-19',
'2021-02-26', '2021-03-05', '2021-03-12', '2021-03-19',
'2021-03-26', '2021-04-02', '2021-04-09', '2021-04-16',
'2021-04-23', '2021-04-30', '2021-05-07', '2021-05-14',
'2021-05-21', '2021-05-28'],
dtype='datetime64[ns]', freq=None)
```
代码:
```Python
index + pd.DateOffset(days=2)
```
输出:
```
DatetimeIndex(['2021-01-05', '2021-01-12', '2021-01-19', '2021-01-26',
'2021-02-02', '2021-02-09', '2021-02-16', '2021-02-23',
'2021-03-02', '2021-03-09', '2021-03-16', '2021-03-23',
'2021-03-30', '2021-04-06', '2021-04-13', '2021-04-20',
'2021-04-27', '2021-05-04', '2021-05-11', '2021-05-18',
'2021-05-25', '2021-06-01'],
dtype='datetime64[ns]', freq=None)
```
可以使用`DatatimeIndex`类型的相关方法来处理数据,例如`shift()`方法可以通过时间前移或后移数据。我们仍然以上面百度股票数据为例,来演示`shift()`方法的使用,代码如下所示。
代码:
```Python
baidu_df.shift(3, fill_value=0)
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208220551.png" style="zoom:150%;">
代码:
```Python
baidu_df.shift(-1, fill_value=0)
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208220713.png" style="zoom:150%;">
通过`asfreq()`方法,我们可以指定一个时间频率抽取对应的数据,代码如下所示。
代码:
```Python
baidu_df.asfreq('5D')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221202.png">
代码:
```Python
baidu_df.asfreq('5D', method='ffill')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221249.png" style="zoom:150%;">
通过`resample()`方法,我们可以基于时间对数据进行重采样,相当于根据时间周期对数据进行了分组操作,代码如下所示。
代码:
```Python
baidu_df.resample('1M').mean()
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221429.png">
> **说明**:上面的代码中,`W`表示一周,`5D`表示`5`天,`1M`表示`1`个月。
如果要实现日期时间的时区转换,我们首先用`tz_localize()`方法将日期时间本地化,代码如下所示。
代码:
```Python
baidu_df = baidu_df.tz_localize('Asia/Chongqing')
baidu_df
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221947.png">
在对时间本地化以后,我们使用`tz_convert()`方法就可以实现转换时区,代码如下所示。
代码:
```Python
baidu_df.tz_convert('America/New_York')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208222404.png">

View File

@ -0,0 +1,62 @@
## 数据分析概述
当今世界对信息技术的依赖程度在不断加深,每天都会有大量的数据产生,我们经常会感到数据越来越多,但是要从中发现有价值的信息却越来越难。这里所说的信息,可以理解为对数据集处理之后的结果,是从数据集中提炼出的可用于其他场合的结论性的东西,而**从原始数据中抽取出有价值的信息**的这个过程我们就称之为**数据分析**,它是数据科学工作的一部分。
### 数据分析师的职责和技能栈
我们通常将从事数据分析、数据挖掘和数据产品的岗位都统称为数据分析岗位,但是根据工作性质的不同,又可以分为偏业务的**数据分析方向**、偏算法的**数据挖掘方向**、偏产品的**数据产品方向**和偏开发的**数据工程方向**。我们通常所说的数据分析师主要是指**业务数据分析师**,很多数据分析师的职业生涯都是从这个岗位开始的,而且这个岗位也是招聘数量最多的岗位。业务数据分析师在公司通常不属于研发部门而**属于运营部门**,所以这个岗位也称为**数据运营**或**商业分析**通常招聘信息对这个岗位的描述JD
1. 负责各部门相关的报表。
2. 建立和优化指标体系。
3. 监控数据波动和异常,找出问题。
4. 优化和驱动业务,推动数字化运营。
5. 找出潜在的市场和产品的上升空间。
根据上面的描述,作为业务数据分析师,我们的工作不是给领导一个简单浅显的结论,而是结合公司的业务,完成**监控数据**、**揪出异常**、**找到原因**、**探索趋势**的工作。所以作为数据分析师,不管是用 Python 语言、Excel、SPSS或其他的商业智能工具工具只是达成目标的手段**数据思维是核心技能**,而从实际业务问题出发到最终**发现数据中的商业价值**是终极目标。数据分析师在很多公司只是一个基础岗位,精于业务的数据分析师可以向**数据分析经理**或**数据运营总监**等管理岗位发展;对于熟悉机器学习算法的数据分析师来说,可以向**数据挖掘工程师**或**算法专家**方向发展,而这些岗位除了需要相应的数学和统计学知识,在编程能力方面也比数据分析师有更高的要求,可能还需要有大数据存储和处理的相关经验;作为数据产品经理,除了传统产品经理的技能栈之外,也需要较强的技术能力,例如要了解常用的推荐算法、机器学习模型,能够为算法的改进提供依据,能够制定相关埋点的规范和口径,虽然不需要精通各种算法,但是要站在产品的角度去考虑数据模型、指标、算法等的落地;数据工程师是一个偏技术的岗位,基本上的成长道路都是从 SQL 开始,逐步向 Hadoop 生态圈迁移,需要有 Java 语言的编程经验。
以下是我总结的数据分析师的技能栈,仅供参考。
1. 计算机科学(数据分析工具、编程语言、数据库)
2. 数学和统计学(数据思维、统计思维)
3. 人工智能(机器学习算法)
4. 业务理解能力(沟通、表达、经验)
5. 总结和表述能力商业PPT、文字总结
### 数据分析的流程
我们提到数分析这个词很多时候可能指的都是**狭义的数据分析**,这类数据分析主要目标就是生成可视化报表并通过这些报表来洞察业务中的问题。**广义的数据分析**还包含了数据挖掘的部分,不仅要通过数据实现对业务的监控和分析,还要利用机器学习算法,找出隐藏在数据背后的知识,并利用这些知识为将来的决策提供支撑。简单的说,**一个完整的数据分析应该包括基本的数据分析和深入的数据挖掘两个部分**。
基本的数据分析工作一般包含以下几个方面的内容,当然因为行业和工作内容的不同会略有差异。
1. 确定目标(输入):理解业务,确定指标口径
2. 获取数据:数据仓库、电子表格、三方接口、网络爬虫、开放数据集等
3. 清洗数据:缺失值/重复值/异常值处理、数据变换(格式化、规范化)、数据归约、离散化等
4. 探索数据:运算、统计、分组、聚合、可视化
5. 数据报告(输出):数据发布,工作成果总结汇报
6. 分析洞察(后续):解释数据的变化,提出对应的方案
深入的数据挖掘工作通常包含以下几个方面的内容,当然因为行业和工作内容的不同会略有差异。
1. 确定目标(输入):理解业务,明确挖掘目标
2. 数据准备:数据采集、数据描述、数据探索、质量判定等
3. 数据加工:提取数据、清洗数据、数据变换、特殊编码、降维、特征选择等
4. 数据建模:模型比较、模型选择、算法应用
5. 模型评估:交叉检验、参数调优、结果评价
6. 模型部署(输出):模型落地、业务改进、运营监控、报告撰写
### 数据分析相关库
使用 Python 从事数据科学相关的工作是一个非常棒的选择,因为 Python 整个生态圈中有大量的成熟的用于数据科学的软件包工具库。而且不同于其他的用于数据科学的编程语言Julia、RPython 除了可以用于数据科学,能做的事情还很多,可以说 Python 语言几乎是无所不能的。
#### 三大神器
1. [NumPy](https://numpy.org/):支持常见的数组和矩阵操作,通过`ndarray`类实现了对多维数组的封装,提供了操作这些数组的方法和函数集。由于 NumPy 内置了并行运算功能,当使用多核 CPU 时Numpy会自动做并行计算。
2. [Pandas](https://pandas.pydata.org/)pandas的核心是其特有的数据结构`DataFrame`和`Series`,这使得 pandas 可以处理包含不同类型的数据的负责表格和时间序列这一点是NumPy的`ndarray`做不到的。使用 pandas可以轻松顺利的加载各种形式的数据然后对数据进行切片、切块、处理缺失值、聚合、重塑和可视化等操作。
3. [Matplotlib](https://matplotlib.org/)matplotlib 是一个包含各种绘图模块的库能够根据我们提供的数据创建高质量的图形。此外matplotlib 还提供了 pylab 模块,这个模块包含了很多像 [MATLAB](https://www.mathworks.com/products/matlab.html) 一样的绘图组件。
#### 其他相关库
1. [SciPy](https://scipy.org/):完善了 NumPy 的功能,封装了大量科学计算的算法,包括线性代数、稀疏矩阵、信号和图像处理、最优化问题、快速傅里叶变换等。
2. [Seaborn](https://seaborn.pydata.org/)seaborn 是基于 matplotlib 的图形可视化工具,直接使用 matplotlib 虽然可以定制出漂亮的统计图表但是总体来说还不够简单方便seaborn 相当于是对 matplotlib 做了封装,让用户能够以更简洁有效的方式做出各种有吸引力的统计图表。
3. [Scikit-learn](https://scikit-learn.org/)scikit-learn 最初是 SciPy 的一部分,它是 Python 数据科学运算的核心,提供了大量机器学习可能用到的工具,包括:数据预处理、监督学习(分类、回归)、无监督学习(聚类)、模式选择、交叉检验等。
4. [Statsmodels](https://www.statsmodels.org/stable/index.html):包含了经典统计学和经济计量学算法的库。

164
Day66-80/67.环境准备.md Normal file
View File

@ -0,0 +1,164 @@
## 环境准备
如果希望快速开始使用 Python 处理数据科学相关的工作,建议大家直接安装 Anaconda然后使用 Anaconda 中集成的 Notebook 或 JupyterLab 工具来编写代码。因为对于新手来说,先安装官方的 Python 解释器,再逐个安装工作中会使用到的三方库文件会比较麻烦,尤其是在 Windows 环境下,经常会因为构建工具或 DLL 文件的缺失导致安装失败,而一般新手也很难根据错误提示信息采取正确的解决措施,容易产生严重的挫败感。
### 安装和使用 Anaconda
对于个人用户来说,可以从 Anaconda 的[官方网站](https://www.anaconda.com/)下载它的“个人版Individual Edition”安装程序安装完成后你的计算机上不仅拥有了 Python 环境和 Spyder类似于PyCharm的集成开发工具还拥有了与数据科学工作相关的近200个工具包包括我们上面提到 Python 数据分析三大神器。除此之外Anaconda 还提供了一个名为 conda 的包管理工具,通过这个工具不仅可以管理 Python 的工具包,还可以用于创建运行 Python 程序的虚拟环境。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211005111417.png" width="100%">
如上图所示,可以通过 Anaconda 官网提供的下载链接选择适合自己操作系统的安装程序建议大家选择图形化的安装程序下载完成后双击安装程序开始安装。安装过程基本使用默认设置即可完成安装后macOS 用户可以在“应用程序”或“Launchpad”中找到名为“Anaconda-Navigator”的应用程序运行该程序可以看到如下所示的界面我们可以在这里选择需要执行的操作。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211005111729.png" width="85%">
对于 Windows 用户,建议按照安装向导的提示和推荐的选项来安装 Anaconda除了安装路径基本也没有什么需要选择的安装完成后可以在“开始菜单”中找到“Anaconda3”。
#### conda命令
如果希望使用 conda 工具来管理依赖项或者创建项目的虚拟环境,可以在终端或命令行提示符中使用 conda 命令。Windows 用户可以在“开始菜单”中找到“Anaconda3”然后点击“Anaconda Prompt”来启动支持 conda 的命令行提示符。macOS 用户建议直接使用“Anaconda-Navigator”中的“Environments”通过可视化的方式对虚拟环境和依赖项进行管理。
1. 版本和帮助信息。
- 查看版本:`conda -V`或`conda --version`
- 获取帮助:`conda -h`或`conda --help`
- 相关信息:`conda list`
2. 虚拟环境相关。
- 显示所有虚拟环境:`conda env list`
- 创建虚拟环境:`conda create --name venv`
- 指定 Python 版本创建虚拟环境:`conda create --name venv python=3.7`
- 指定 Python 版本创建虚拟环境并安装指定依赖项:`conda create --name venv python=3.7 numpy pandas`
- 通过克隆现有虚拟环境的方式创建虚拟环境:`conda create --name venv2 --clone venv`
- 分享虚拟环境并重定向到指定的文件中:`conda env export > environment.yml`
- 通过分享的虚拟环境文件创建虚拟环境:`conda env create -f environment.yml`
- 激活虚拟环境:`conda activate venv`
- 退出虚拟环境:`conda deactivate`
- 删除虚拟环境:`conda remove --name venv --all`
> **说明**:上面的命令中,`venv`和`venv2`是虚拟环境文件夹的名字,可以将其替换为自己喜欢的名字,但是**强烈建议**使用英文且不要出现空格或其他特殊字符。
3. 包(三方库或工具)管理。
- 查看已经安装的包:`conda list`
- 搜索指定的包:`conda search matplotlib`
- 安装指定的包:`conda install matplotlib`
- 更新指定的包:`conda update matplotlib`
- 移除指定的包:`conda remove matplotlib`
> **说明**:在搜索、安装和更新软件包时,默认会连接到官方网站进行操作,如果觉得速度不给力,可以将默认的官方网站替换为国内的镜像网站,推荐使用清华大学的开源镜像网站。将默认源更换为国内镜像的命令是:`conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ `。如果需要换回默认源,可以使用命令`conda config --remove-key channels`。
### 使用Notebook
#### 安装和启动Notebook
如果已经安装了 AnacondamacOS 用户可以按照上面所说的方式在“Anaconda-Navigator”中直接启动“Jupyter Notebook”以下统一简称为 Notebook。Windows 用户可以在“开始菜单”中找到 Anaconda 文件夹接下来选择运行文件夹中的“Jupyter Notebook”就可以开始数据科学的探索之旅。
对于安装了 Python 环境但是没有安装 Anaconda 的用户,可以用 Python 的包管理工具`pip`来安装`jupyter`然后在终端Windows 系统为命令行提示符)中运行`jupyter notebook`命令来启动 Notebook如下所示。
安装 Notebook
```Bash
pip install jupyter
```
安装三大神器:
```Bash
pip install numpy pandas matplotlib
```
运行 Notebook
```Bash
jupyter notebook
```
Notebook 是基于网页的用于交互计算的应用程序,可以用于代码开发、文档撰写、代码运行和结果展示。简单的说,你可以在网页中直接**编写代码**和**运行代码**,代码的运行结果也会直接在代码块下方进行展示。如在编写代码的过程中需要编写说明文档,可在同一个页面中使用 Markdown 格式进行编写而且可以直接看到渲染后的效果。此外Notebook 的设计初衷是提供一个能够支持多种编程语言的工作环境目前它能够支持超过40种编程语言包括 Python、R、Julia、Scala 等。
首先,我们可以创建一个用于书写 Python 代码的 Notebook如下图所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005113911.png)
接下来,我们就可以编写代码、撰写文档和运行程序啦,如下图所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005113900.png)
#### Notebook使用技巧
如果使用 Python 做工程化的项目开发PyCharm 肯定是最好的选择,它提供了一个集成开发环境应该具有的所有功能,尤其是智能提示、代码补全、自动纠错这类功能会让开发人员感到非常舒服。如果使用 Python 做数据科学相关的工作Notebook 并不比 PyCharm 逊色,在数据和图表展示方面 Notebook 更加优秀。这个工具的使用非常简单,大家可以看看 Notebook 菜单栏,相信理解起来不会有太多困难,在知乎上有一篇名为[《最详尽使用指南超快上手Jupyter Notebook》](https://zhuanlan.zhihu.com/p/32320214)的文章,也可以帮助大家快速认识 Notebook。
> **说明**[Jupyter 官网](https://jupyter.org/)上还有一个名为 JupyterLab 的工具被称之为“Next-Generation Notebook”用户界面较之 Notebook 更加友好,有兴趣的读者可以使用`pip install jupyterlab`命令来安装这个工具,然后通过`jupyter lab`来启动它。
下面我为大家介绍一些 Notebook 的使用技巧,希望能够帮助大家提升工作效率。
1. 自动补全。在使用 Notebook 编写代码时,按`Tab`键会获得代码提示。
2. 获得帮助。在使用 Notebook 时,如果希望了解一个对象(如变量、类、函数等)的相关信息或使用方式,可以在对象后面使用`?`并运行代码, 窗口下方会显示出对应的信息,帮助我们了解该对象,如下所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005113848.png)
3. 搜索命名。如果只记得一个类或一个函数名字的一部分,可以使用通配符`*`并配合`?`进行搜索,如下所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005113836.png)
4. 调用命令。可以在 Notebook 中使用`!`后面跟系统命令的方式来执行系统命令。
5. 魔法指令。Notebook 中有很多非常有趣且有用的魔法指令,例如可以使用`%timeit`测试语句的执行时间,可以使用`%pwd`查看当前工作目录等。如果想查看所有的魔法指令,可以使用`%lsmagic`,如果了解魔法指令的用法,可以使用`%magic`来查看,如下图所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005113825.png)
常用的魔法指令有:
| 魔法指令 | 功能说明 |
| ------------------------------------------- | ------------------------------------------ |
| `%pwd` | 查看当前工作目录 |
| `%ls` | 列出当前或指定文件夹下的内容 |
| `%cat` | 查看指定文件的内容 |
| `%hist` | 查看输入历史 |
| `%matplotlib inline` | 设置在页面中嵌入matplotlib输出的统计图表 |
| `%config Inlinebackend.figure_format='svg'` | 设置统计图表使用SVG格式矢量图 |
| `%run` | 运行指定的程序 |
| `%load` | 加载指定的文件到单元格中 |
| `%quickref` | 显示IPython的快速参考 |
| `%timeit` | 多次运行代码并统计代码执行时间 |
| `%prun` | 用`cProfile.run`运行代码并显示分析器的输出 |
| `%who` / `%whos` | 显示命名空间中的变量 |
| `%xdel` | 删除一个对象并清理所有对它的引用 |
6. 快捷键。Notebook 中的很多操作可以通过快捷键来实现使用快捷键可以提升工作效率。Notebook 的快捷键又可以分为命令模式下的快捷键和编辑模式下的快捷键,所谓编辑模式就是处于输入代码或撰写文档状态的模式,在编辑模式下按`Esc`可以回到命令模式,在命令模式下按`Enter`可以进入编辑模式。
命令模式下的快捷键:
| 快捷键 | 功能说明 |
| ------------------------------- | -------------------------------------------- |
| Alt + EnterOption + Enter | 运行当前单元格并在下面插入新的单元格 |
| Shift + Enter | 运行当前单元格并选中下方的单元格 |
| Ctrl + EnterCommand + Enter | 运行当前单元格 |
| j / k、Shift + j / Shift + k | 选中下方/上方单元格、连续选中下方/上方单元格 |
| a / b | 在下方/上方插入新的单元格 |
| c / x | 复制单元格 / 剪切单元格 |
| v / Shift + v | 在下方/上方粘贴单元格 |
| dd / z | 删除单元格 / 恢复删除的单元格 |
| l / Shift + l | 显示或隐藏当前/所有单元格行号 |
| ii / 00 | 中断/重启Notebook内核 |
| Space / Shift + Space | 向下/向上滚动页面 |
编辑模式下的快捷键:
| 快捷键 | 功能说明 |
| ------------------------------------------------ | -------------------------------------- |
| Shift + Tab | 获得提示信息 |
| Ctrl + ]Command + ]/ Ctrl + [Command + [ | 增加/减少缩进 |
| Alt + EnterOption + Enter | 运行当前单元格并在下面插入新的单元格 |
| Shift + Enter | 运行当前单元格并选中下方的单元格 |
| Ctrl + EnterCommand + Enter | 运行当前单元格 |
| Ctrl + Left / RightCommand + Left / Right | 光标移到行首/行尾 |
| Ctrl + Up / DownCommand + Up / Down | 光标移动代码开头/结尾处 |
| Up / Down | 光标上移/下移一行或移到上/下一个单元格 |
> **温馨提示**:如果记不住这些快捷键也没有关系,在命令模式下按`h`键可以打开 Notebook 的帮助系统,马上就可以看到快捷键的设置,而且可以根据实际的需要重新编辑快捷键,如下图所示。
>
> ![](https://gitee.com/jackfrued/mypic/raw/master/20211005113812.png)

View File

@ -1,8 +1,8 @@
## NumPy的应用
## NumPy的应用-1
Numpy是一个开源的Python科学计算库**用于快速处理任意维度的数组**。Numpy**支持常见的数组和矩阵操作**对于同样的数值计算任务使用NumPy不仅代码要简洁的多而且NumPy的性能远远优于原生Python基本是一个到两个数量级的差距而且数据量越大NumPy的优势就越明显。
Numpy 是一个开源的 Python 科学计算库,**用于快速处理任意维度的数组**。Numpy **支持常见的数组和矩阵操作**,对于同样的数值计算任务,使用 NumPy 不仅代码要简洁的多,而且 NumPy 的性能远远优于原生 Python基本是一个到两个数量级的差距而且数据量越大NumPy 的优势就越明显。
Numpy最为核心的数据类型是`ndarray`,使用`ndarray`可以处理一维、二维和多维数组该对象相当于是一个快速而灵活的大数据容器。NumPy底层代码使用C语言编写解决了GIL的限制`ndarray`在存储数据的时候数据与数据的地址都是连续的这样就给使得批量操作速度很快远远优于Python中的`list`;另一方面`ndarray`对象提供了更多的方法来处理数据尤其是和统计相关的方法这些方法也是Python原生的`list`没有的。
Numpy 最为核心的数据类型是`ndarray`,使用`ndarray`可以处理一维、二维和多维数组该对象相当于是一个快速而灵活的大数据容器。NumPy 底层代码使用 C 语言编写,解决了 GIL 的限制,`ndarray`在存储数据的时候,数据与数据的地址都是连续的,这样就给使得批量操作速度很快,远远优于 Python 中的`list`;另一方面`ndarray`对象提供了更多的方法来处理数据,尤其是和统计相关的方法,这些方法也是 Python 原生的`list`没有的。
### 准备工作
@ -22,7 +22,7 @@ Numpy最为核心的数据类型是`ndarray`,使用`ndarray`可以处理一维
import matplotlib.pyplot as plt
```
> **说明**:如果已经启动了Notebook但尚未安装相关依赖库例如NumPy可以在Notebook的单元格中输入`!pip install numpy`并运行该单元格来安装NumPy其他库如法炮制。安装成功后选择“Kernel”服务菜单的“Restart”重启选项来重启Notebook内核前面有讲到重启的快捷键来使新安装的库生效。上面我们不仅导入了NumPy还将pandas和matplotlib库一并导入了。
> **说明**:如果已经启动了 Notebook 但尚未安装相关依赖库,例如尚未安装`numpy`,可以在 Notebook 的单元格中输入`!pip install numpy`并运行该单元格来安装 NumPy也可以一次性安装多个三方库需要在单元格中输入!pip install numpy pandas matplotlib`。注意上面的代码我们不仅导入了NumPy还将 pandas 和 matplotlib 库一并导入了。
### 创建数组对象
@ -348,7 +348,7 @@ Numpy最为核心的数据类型是`ndarray`,使用`ndarray`可以处理一维
```Python
array18 = plt.imread('guido.jpg')
array18
array18
```
输出:
@ -405,7 +405,7 @@ array18
[ 79, 67, 53]]], dtype=uint8)
```
> **说明**:上面的代码读取了当前路径下名为`guido.jpg` 的图片文件计算机系统中的图片通常由若干行若干列的像素点构成而每个像素点又是由红绿蓝三原色构成的所以能够用三维数组来表示。读取图片用到了matplotlib库的`imread`函数。
> **说明**:上面的代码读取了当前路径下名为`guido.jpg` 的图片文件,计算机系统中的图片通常由若干行若干列的像素点构成,而每个像素点又是由红绿蓝三原色构成的,所以能够用三维数组来表示。读取图片用到了`matplotlib`库的`imread`函数。
### 数组对象的属性
@ -455,7 +455,7 @@ array18
`ndarray`对象元素的数据类型可以参考如下所示的表格。
![](res/ndarray-dtype.png)
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211005114813.png" width="85%">
4. `ndim`属性:数组的维度
@ -533,11 +533,11 @@ array18
True False
```
> **说明**上面的代码用到了数组的切片操作它类似于Python中`list`类型的切片,但在细节上又不完全相同,下面会专门讲解这个知识点。通过上面的代码可以发现,`ndarray`切片后得到的新的数组对象跟原来的数组对象共享了内存中的数据,因此`array22`的`base`属性就是`array19`对应的数组对象。
> **说明**:上面的代码用到了数组的切片操作,它类似于 Python 中`list`类型的切片,但在细节上又不完全相同,下面会专门讲解这个知识点。通过上面的代码可以发现,`ndarray`切片后得到的新的数组对象跟原来的数组对象共享了内存中的数据,因此`array22`的`base`属性就是`array19`对应的数组对象。
### 数组的索引和切片
和Python中的列表类似NumPy的`ndarray`对象可以进行索引和切片操作,通过索引可以获取或修改数组中的元素,通过切片可以取出数组的一部分。
Python 中的列表类似NumPy 的`ndarray`对象可以进行索引和切片操作,通过索引可以获取或修改数组中的元素,通过切片可以取出数组的一部分。
1. 索引运算(普通索引)
@ -598,7 +598,7 @@ array18
2. 切片运算(切片索引)
切片是形如`[开始索引:结束索引:步长]`的语法,通过指定**开始索引**(默认值无穷小)、**结束索引**(默认值无穷大)和**步长**默认值1从数组中取出指定部分的元素并构成新的数组。因为开始索引、结束索引和步长都有默认值所以它们都可以省略如果不指定步长第二个冒号也可以省略。一维数组的切片运算跟Python中的`list`类型的切片非常类似,此处不再赘述,二维数组的切片可以参考下面的代码,相信非常容易理解。
切片是形如`[开始索引:结束索引:步长]`的语法,通过指定**开始索引**(默认值无穷小)、**结束索引**(默认值无穷大)和**步长**默认值1从数组中取出指定部分的元素并构成新的数组。因为开始索引、结束索引和步长都有默认值所以它们都可以省略如果不指定步长第二个冒号也可以省略。一维数组的切片运算跟 Python 中的`list`类型的切片非常类似,此处不再赘述,二维数组的切片可以参考下面的代码,相信非常容易理解。
代码:
@ -693,15 +693,15 @@ array18
[3 1]]
```
关于数组的索引和切片运算,大家可以通过下面的两张图来增强印象,这两张图来自[《利用Python进行数据分析》](https://item.jd.com/12398725.html)一书它是pandas的作者Wes McKinney撰写的Python数据分析领域的经典教科书有兴趣的读者可以购买和阅读原书。
关于数组的索引和切片运算,大家可以通过下面的两张图来增强印象,这两张图来自[《利用Python进行数据分析》](https://item.jd.com/12398725.html)一书,它是`pandas`库的作者 Wes McKinney 撰写的 Python 数据分析领域的经典教科书,有兴趣的读者可以购买和阅读原书。
![](res/ndarray-index.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115005.png)
![](res/ndarray-slice.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115041.png)
3. 花式索引fancy index
花式索引Fancy indexing是指利用整数数组进行索引这里所说的整数数组可以是NumPy的`ndarray`也可以是Python中`list`、`tuple`等可迭代类型,可以使用正向或负向索引。
花式索引Fancy indexing是指利用整数数组进行索引这里所说的整数数组可以是 NumPy 的`ndarray`,也可以是 Python 中`list`、`tuple`等可迭代类型,可以使用正向或负向索引。
一维数组的花式索引,代码:
@ -811,7 +811,7 @@ array18
array([5, 6, 7, 8, 9])
```
> **提示**:切片操作虽然创建了新的数组对象,但是新数组和原数组共享了数组中的数据,简单的说,如果通过新数组对象或原数组对象修改数组中的数据,其实修改的是同一块数据。花式索引和布尔索引也会创建新的数组对象,而且新数组复制了原数组的元素,新数组和原数组并不是共享数据的关系,这一点通过前面讲的数组的`base`属性也可以了解到,大家一定要注意。
> **提示**:切片操作虽然创建了新的数组对象,但是新数组和原数组共享了数组中的数据,简单的说,如果通过新数组对象或原数组对象修改数组中的数据,其实修改的是同一块数据。花式索引和布尔索引也会创建新的数组对象,而且新数组复制了原数组的元素,新数组和原数组并不是共享数据的关系,这一点通过前面讲的数组的`base`属性也可以了解到,在使用的时候要引起注意。
#### 案例:通过数组切片处理图像
@ -830,7 +830,7 @@ plt.imshow(guido_image)
plt.imshow(guido_image[::-1])
```
![](res/image-flip-1.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115228.png)
对数组的1轴进行反向切片实现图像的水平翻转。
@ -838,21 +838,21 @@ plt.imshow(guido_image[::-1])
plt.imshow(guido_image[:,::-1])
```
![](res/image-flip-2.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115242.png)
将Guido的头切出来。
Guido 的头切出来。
```Python
plt.imshow(guido_image[30:350, 90:300])
```
![](res/image-flip-3.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115305.png)
### 数组对象的方法
#### 统计方法
`ndarray`对象的统计方法主要包括:`sum`、`mean`、`std`、`var`、`min`、`max`、`argmin`、`argmax`、`cumsum`等,分别用于对数组中的元素求和、求平均、求标准差、求方差、找最大、找最小、求累积和等,请参考下面的代码。
统计方法主要包括:`sum()`、`mean()`、`std()`、`var()`、`min()`、`max()`、`argmin()`、`argmax()`、`cumsum()`等,分别用于对数组中的元素求和、求平均、求标准差、求方差、找最大、找最小、求累积和等,请参考下面的代码。
```Python
array28 = np.array([1, 2, 3, 4, 5, 5, 4, 3, 2, 1])
@ -887,7 +887,7 @@ print(array28.cumsum())
在数学上,**点积**dot product又称**数量积**或**标量积**,是一种接受两个等长的数字序列,返回单个数字的代数运算。从代数角度看,先对两个数字序列中的每组对应元素求积,再对所有积求和,结果即为点积,即:$\boldsymbol{A} \cdot \boldsymbol{B} = \sum_{i=1}^{n}a_ib_i$。从几何角度看,点积则是两个向量的长度与它们夹角余弦的积,即:$\boldsymbol{A} \cdot \boldsymbol{B}=|\boldsymbol{A}||\boldsymbol{B}|\cos{\theta}$。
在欧几里得几何中,两个笛卡尔坐标向量的点积也称为**内积**inner productNumPy中也提供了实现内积的函数但是内积的含义要高于点积点积相当于是内积在欧几里得空间$\mathbb{R}^n$的特例,而内积可以推广到**赋范向量空间**(不理解没有关系,当我没说就行了)
在欧几里得几何中,两个笛卡尔坐标向量的点积也称为**内积**inner productNumPy 中也提供了实现内积的函数,但是内积的含义要高于点积,点积相当于是内积在欧几里得空间$\mathbb{R}^n$的特例,而内积可以推广到赋范向量空间。
一维数组的点积运算,代码:
@ -920,7 +920,7 @@ print(array28.cumsum())
> **说明**:可以看出,二维数组的点积就是矩阵乘法运算。
4. `dump()`方法保存数组到文件中可以通过NumPy中的`load`函数从保存的文件中加载数据创建数组。
4. `dump()`方法:保存数组到文件中,可以通过 NumPy 中的`load()`函数从保存的文件中加载数据创建数组。
代码:
@ -1021,410 +1021,3 @@ print(array28.cumsum())
12. `tolist()`方法将数组转成Python中的`list`。
### 数组的运算
使用NumPy最为方便的是当需要对数组元素进行运算时不用编写循环代码遍历每个元素所有的运算都会自动的**矢量化**使用高效的提前编译的底层语言代码来对数据序列进行数学操作。简单的说就是NumPy中的数学运算和数学函数会自动作用于数组中的每个成员。
#### 数组跟标量的运算
代码:
```Python
array35 = np.arange(1, 10)
print(array35 + 10)
print(array35 * 10)
```
输出:
```
[11 12 13 14 15 16 17 18 19]
[10 20 30 40 50 60 70 80 90]
```
#### 数组跟数组的运算
代码:
```Python
array36 = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])
print(array35 + array36)
print(array35 * array36)
print(array35 ** array36)
```
输出:
```
[ 2 3 4 6 7 8 10 11 12]
[ 1 2 3 8 10 12 21 24 27]
[ 1 2 3 16 25 36 343 512 729]
```
#### 通用一元函数
通用函数是对`ndarray`中的数据执行元素级运算的函数。你可以将其看做普通函数(接收一个标量值作为参数,返回一个标量值)的矢量化包装器,如下所示。
代码:
```Python
print(np.sqrt(array35))
print(np.log2(array35))
```
输出:
```
[1. 1.41421356 1.73205081 2. 2.23606798 2.44948974
2.64575131 2.82842712 3. ]
[0. 1. 1.5849625 2. 2.32192809 2.5849625
2.80735492 3. 3.169925 ]
```
**表1通用一元函数**
| 函数 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `abs` / `fabs` | 求绝对值的函数 |
| `sqrt` | 求平方根的函数,相当于`array ** 0.5 ` |
| `square` | 求平方的函数,相当于`array ** 2` |
| `exp` | 计算$e^x$的函数 |
| `log` / `log10` / `log2` | 对数函数(`e`为底 / `10`为底 / `2`为底) |
| `sign` | 符号函数(`1` - 正数;`0` - 零;`-1` - 负数) |
| `ceil` / `floor` | 上取整 / 下取整 |
| `isnan` | 返回布尔数组NaN对应`True`非NaN对应`False` |
| `isfinite` / `isinf` | 判断数值是否为无穷大的函数 |
| `cos` / `cosh` / `sin` | 三角函数 |
| `sinh` / `tan` / `tanh` | 三角函数 |
| `arccos` / `arccosh` / `arcsin` | 反三角函数 |
| `arcsinh` / `arctan` / `arctanh` | 反三角函数 |
| `rint` / `around` | 四舍五入函数 |
#### 通用二元函数
代码:
```Python
array37 = np.array([[4, 5, 6], [7, 8, 9]])
array38 = np.array([[1, 2, 3], [3, 2, 1]])
print(array37 * array38)
print(np.power(array37, array38))
```
输出:
```
[[ 4 10 18]
[21 16 9]]
[[ 4 25 216]
[343 64 9]]
```
**表2通用二元函数**
| 函数 | 说明 |
| --------------------------------- | ---- |
| `add(x, y)` / `substract(x, y)` | 加法函数 / 减法函数 |
|`multiply(x, y)` / `divide(x, y)`|乘法函数 / 除法函数|
| `floor_divide(x, y)` / `mod(x, y)` | 整除函数 / 求模函数 |
|`allclose(x, y)`|检查数组`x`和`y`元素是否几乎相等|
| `power(x, y)` | 数组$x$的元素$x_i$和数组$y$的元素$y_i$,计算$x_i^{y_i}$ |
| `maximum(x, y)` / `fmax(x, y)` | 两两比较元素获取最大值 / 获取最大值忽略NaN |
| `minimum(x, y)` / `fmin(x, y)` | 两两比较元素获取最小值 / 获取最小值忽略NaN |
| `inner(x, y)` | 内积运算 |
| `cross(x, y) `/ `outer(x, y)` | 叉积运算 / 外积运算 |
| `intersect1d(x, y)` | 计算`x`和`y`的交集,返回这些元素构成的有序数组 |
| `union1d(x, y)` | 计算`x`和`y`的并集,返回这些元素构成的有序数组 |
| `in1d(x, y)` | 返回由判断`x` 的元素是否在`y`中得到的布尔值构成的数组 |
| `setdiff1d(x, y)` | 计算`x`和`y`的差集,返回这些元素构成的数组 |
| `setxor1d(x, y)` | 计算`x`和`y`的对称差,返回这些元素构成的数组 |
>**补充说明**:在二维空间内,两个向量$\boldsymbol{A}=\begin{bmatrix} a_1 \\ a_2 \end{bmatrix}$和$\boldsymbol{B}=\begin{bmatrix} b_1 \\ b_2 \end{bmatrix}$的叉积是这样定义的:$\boldsymbol{A}\times \boldsymbol{B}=\begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix}=a_1b_2 - a_2b_1$,其中$\begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix}$称为行列式。但是一定要注意,叉积并不等同于行列式,行列式的运算结果是一个标量,而叉积运算的结果是一个向量。如果不明白,我们可以看看三维空间两个向量,$\boldsymbol{A}=\begin{bmatrix} a_1 \\ a_2 \\ a_3 \end{bmatrix}$和$\boldsymbol{B}=\begin{bmatrix} b_1 \\ b_2 \\ b_3 \end{bmatrix}$的叉积是$\left< \hat{i} \begin{vmatrix} a_2 \quad a_3 \\ b_2 \quad b_3 \end{vmatrix}, -\hat{j} \begin{vmatrix} a_1 \quad a_3 \\ b_1 \quad b_3 \end{vmatrix}, \hat{k} \begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix} \right>$,其中$\hat{i}, \hat{j}, \hat{k}$代表每个维度的单位向量。
#### 广播机制
上面的例子中,两个二元运算的数组形状是完全相同的,我们再来研究一下,两个形状不同的数组是否可以直接做二元运算或使用二元函数进行运算,请看下面的例子。
代码:
```Python
array39 = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]])
array40 = np.array([1, 2, 3])
array39 + array40
```
输出:
```
array([[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]])
```
代码:
```Python
array41 = np.array([[1], [2], [3], [4]])
array39 + array41
```
输出:
```
array([[1, 1, 1],
[3, 3, 3],
[5, 5, 5],
[7, 7, 7]])
```
通过上面的例子我们发现形状不同的数组仍然有机会进行二元运算但也绝对不是任意的数组都可以进行二元运算。简单的说只有两个数组后缘维度相同或者其中一个数组后缘维度为1时两个数组才能进行二元运算。所谓后缘维度指的是数组`shape`属性对应的元组中最后一个元素的值从后往前数最后一个维度的值例如我们之前打开的图像对应的数组后缘维度为33行4列的二维数组后缘维度为4而有5个元素的一维数组后缘维度为5。后缘维度相同或者其中一个数组为1就可以应用广播机制对元素进行扩散从而满足两个数组对应元素做运算的需求如下图所示。
![](res/broadcast-1.png)
![](res/broadcast-2.png)
![](res/broadcast-3.png)
### 其他常用函数
除了上面讲到的函数外NumPy中还提供了很多用于处理数组的函数`ndarray`对象的很多方法也可以通过直接调用函数来实现,下表给出了一些常用的函数。
**表3NumPy其他常用函数**
| 函数 | 说明 |
| ------------------- | ------------------------------------------------ |
| `unique` | 去除数组重复元素,返回唯一元素构成的有序数组 |
| `copy` | 返回拷贝数组得到的数组 |
| `sort` | 返回数组元素排序后的拷贝 |
| `split` / `hsplit` / `vsplit` | 将数组拆成若干个子数组 |
| `stack` / `hstack` / `vstack` | 将多个数组堆叠成新数组 |
| `concatenate` | 沿着指定的轴连接多个数组构成新数组 |
| `append` / `insert` | 向数组末尾追加元素 / 在数组指定位置插入元素 |
| `argwhere` | 找出数组中非0元素的位置 |
| `extract` / `select` / `where` | 按照指定的条件从数组中抽取或处理数组元素 |
| `flip` | 沿指定的轴翻转数组中的元素 |
| `fromiter` | 通过迭代器创建数组对象 |
| `fromregex` | 通过读取文件和正则表达式解析获取数据创建数组对象 |
| `repeat` / `tile` | 通过对元素的重复来创建新数组 |
| `roll` | 沿指定轴对数组元素进行移位 |
| `resize` | 重新调整数组的大小 |
| `place` / `put` | 将数组中满足条件的元素/指定的元素替换为指定的值 |
| `ptp` | 沿指定的轴计算极差(最大值与最小值的差) |
| `median` | 沿指定轴计算中位数 |
| `partition` | 用选定的元素对数组进行一次划分并返回划分后的数组 |
> **提示**:上面的`resize`函数和`ndarray`对象的`resize`方法是有区别的,`resize`函数在调整数组大小时会重复数组中的元素作为填补多出来的元素的值,而`ndarry`对象的`resize`方法是用0来填补多出来的元素。这些小细节不清楚暂时也不要紧但是如果用到对应的功能了就要引起注意。
代码:
```Python
array42 = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]])
array43 = np.array([[4, 4, 4], [5, 5, 5], [6, 6, 6]])
np.hstack((array42, array43))
```
输出:
```
array([[1, 1, 1, 4, 4, 4],
[2, 2, 2, 5, 5, 5],
[3, 3, 3, 6, 6, 6]])
```
代码:
```Python
np.vstack((array42, array43))
```
输出:
```
array([[1, 1, 1],
[2, 2, 2],
[3, 3, 3],
[4, 4, 4],
[5, 5, 5],
[6, 6, 6]])
```
代码:
```Python
np.concatenate((array42, array43))
```
输出:
```
array([[1, 1, 1],
[2, 2, 2],
[3, 3, 3],
[4, 4, 4],
[5, 5, 5],
[6, 6, 6]])
```
代码:
```Python
np.concatenate((array42, array43), axis=1)
```
输出:
```
array([[1, 1, 1, 4, 4, 4],
[2, 2, 2, 5, 5, 5],
[3, 3, 3, 6, 6, 6]])
```
### 矩阵运算
NumPy中提供了专门用于线性代数linear algebra的模块和表示矩阵的类型`matrix`,当然我们通过二维数组也可以表示一个矩阵,官方并不推荐使用`matrix`类而是建议使用二维数组,而且有可能在将来的版本中会移除`matrix`类。无论如何,利用这些已经封装好的类和函数,我们可以轻松愉快的实现线性代数中很多的操作。
#### 线性代数快速回顾
1. **向量**也叫**矢量**,是一个同时具有大小和方向,且满足平行四边形法则的几何对象。与向量相对的概念叫**标量**或**数量**,标量只有大小、绝大多数情况下没有方向。
2. 向量可以进行**加**、**减**、**数乘**、**点积**、**叉积**等运算。
3. **行列式**由向量组成,它的性质可以由向量解释。
4. 行列式可以使用**行列式公式**计算:$det(\boldsymbol{A})=\sum_{n!} \pm {a_{1\alpha}a_{2\beta} \cdots a_{n\omega}}$。
5. 高阶行列式可以用**代数余子式**展开成多个低阶行列式,如:$det(\boldsymbol{A})=a_{11}C_{11}+a_{12}C_{12}+ \cdots +a_{1n}C_{1n}$。
6. **矩阵**是由一系列元素排成的矩形阵列,矩阵里的元素可以是数字、符号或数学公式。
7. 矩阵可以进行**加法**、**减法**、**数乘**、**乘法**、**转置**等运算。
8. **逆矩阵**用$\boldsymbol{A^{-1}}$表示,$\boldsymbol{A}\boldsymbol{A^{-1}}=\boldsymbol{A^{-1}}\boldsymbol{A}=\boldsymbol{I}$;没有逆矩阵的方阵是**奇异矩阵**
9. 如果一个方阵是**满秩矩阵**,该方阵对应的线性方程有唯一解。
#### NumPy中矩阵相关函数
1. 创建矩阵对象。
代码:
```Python
# matrix构造函数可以传入类数组对象也可以传入字符串
m1 = np.matrix('1 2 3; 4 5 6')
m1
```
输出:
```
matrix([[1, 2, 3],
[4, 5, 6]])
```
代码:
```Python
# asmatrix函数也可以写成mat函数它们其实是同一个函数
m2 = np.asmatrix(np.array([[1, 1], [2, 2], [3, 3]]))
m2
```
输出:
```
matrix([[1, 1],
[2, 2],
[3, 3]])
```
代码:
```Python
# 矩阵乘法运算等同于m1.dot(m2)
m1 * m2
```
输出:
```
matrix([[14, 14],
[32, 32]])
```
2. 矩阵对象的属性。
| 属性 | 说明 |
| ------- | ----------------------------------------- |
| `A` | 获取矩阵对象对应的`ndarray`对象 |
| `A1` | 获取矩阵对象对应的扁平化后的`ndarray`对象 |
| `I` | 可逆矩阵的逆矩阵 |
| `T` | 矩阵的转置 |
| `H` | 矩阵的共轭转置 |
| `shape` | 矩阵的形状 |
| `size` | 矩阵元素的个数 |
3. 矩阵对象的方法。
矩阵对象的方法跟之前讲过的`ndarray`数组对象的方法基本差不多,此处不再进行赘述。
#### NumPy的线性代数模块
NumPy的`linalg`模块中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的函数它们跟MATLAB和R等语言所使用的是相同的行业标准线性代数库下面的表格列出了`numpy`以及`linalg`模块中常用的跟线性代数相关的函数。
| 函数 | 说明 |
| --------------- | ------------------------------------------------------------ |
| `diag` | 以一维数组的形式返回方阵的对角线元素或将一维数组转换为方阵非对角元素元素为0 |
| `vdot` | 向量的点积 |
| `dot` | 数组的点积(矩阵乘法) |
| `inner` | 数组的内积 |
| `outer` | 数组的叉积 |
| `trace` | 计算对角线元素的和 |
| `norm` | 求模运算 |
| `det` | 计算行列式的值(在方阵上计算得到的标量) |
| `matrix_rank` | 计算矩阵的秩 |
| `eig` | 计算矩阵的特征值eigenvalue和特征向量eigenvector |
| `inv` | 计算非奇异矩阵($n$阶方阵)的逆矩阵 |
| `pinv` | 计算矩阵的摩尔-彭若斯Moore-Penrose广义逆 |
| `qr` | QR分解把矩阵分解成一个正交矩阵与一个上三角矩阵的积 |
| `svd` | 计算奇异值分解singular value decomposition |
| `solve` | 解线性方程组$\boldsymbol{A}\boldsymbol{x}=\boldsymbol{b}$,其中$\boldsymbol{A}$是一个方阵 |
| `lstsq` | 计算$\boldsymbol{A}\boldsymbol{x}=\boldsymbol{b}$的最小二乘解 |
大家如果有兴趣可以用下面的代码验证上面的函数。
代码:
```Python
m3 = np.array([[1., 2.], [3., 4.]])
np.linalg.inv(m3)
```
输出:
```
array([[-2. , 1. ],
[ 1.5, -0.5]])
```
代码:
```Python
m4 = np.array([[1, 3, 5], [2, 4, 6], [4, 7, 9]])
np.linalg.det(m4)
```
输出:
```
2
```
代码:
```Python
# 解线性方程组ax=b
# 3x + y = 9x + 2y = 8
a = np.array([[3,1], [1,2]])
b = np.array([9, 8])
np.linalg.solve(a, b)
```
输出:
```
array([2., 3.])
```

View File

@ -0,0 +1,413 @@
## NumPy的应用-2
### 数组的运算
使用 NumPy 最为方便的是当需要对数组元素进行运算时,不用编写循环代码遍历每个元素,所有的运算都会自动的**矢量化**使用高效的提前编译的底层语言代码来对数据序列进行数学操作。简单的说就是NumPy 中的数学运算和数学函数会自动作用于数组中的每个成员。
#### 数组跟标量的运算
代码:
```Python
array35 = np.arange(1, 10)
print(array35 + 10)
print(array35 * 10)
```
输出:
```
[11 12 13 14 15 16 17 18 19]
[10 20 30 40 50 60 70 80 90]
```
#### 数组跟数组的运算
代码:
```Python
array36 = np.array([1, 1, 1, 2, 2, 2, 3, 3, 3])
print(array35 + array36)
print(array35 * array36)
print(array35 ** array36)
```
输出:
```
[ 2 3 4 6 7 8 10 11 12]
[ 1 2 3 8 10 12 21 24 27]
[ 1 2 3 16 25 36 343 512 729]
```
#### 通用一元函数
通用函数是对`ndarray`中的数据执行元素级运算的函数。你可以将其看做普通函数(接收一个标量值作为参数,返回一个标量值)的矢量化包装器,如下所示。
代码:
```Python
print(np.sqrt(array35))
print(np.log2(array35))
```
输出:
```
[1. 1.41421356 1.73205081 2. 2.23606798 2.44948974
2.64575131 2.82842712 3. ]
[0. 1. 1.5849625 2. 2.32192809 2.5849625
2.80735492 3. 3.169925 ]
```
**表1通用一元函数**
| 函数 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `abs` / `fabs` | 求绝对值的函数 |
| `sqrt` | 求平方根的函数,相当于`array ** 0.5 ` |
| `square` | 求平方的函数,相当于`array ** 2` |
| `exp` | 计算$e^x$的函数 |
| `log` / `log10` / `log2` | 对数函数(`e`为底 / `10`为底 / `2`为底) |
| `sign` | 符号函数(`1` - 正数;`0` - 零;`-1` - 负数) |
| `ceil` / `floor` | 上取整 / 下取整 |
| `isnan` | 返回布尔数组NaN对应`True`非NaN对应`False` |
| `isfinite` / `isinf` | 判断数值是否为无穷大的函数 |
| `cos` / `cosh` / `sin` | 三角函数 |
| `sinh` / `tan` / `tanh` | 三角函数 |
| `arccos` / `arccosh` / `arcsin` | 反三角函数 |
| `arcsinh` / `arctan` / `arctanh` | 反三角函数 |
| `rint` / `round` | 四舍五入函数 |
#### 通用二元函数
代码:
```Python
array37 = np.array([[4, 5, 6], [7, 8, 9]])
array38 = np.array([[1, 2, 3], [3, 2, 1]])
print(array37 ** array38)
print(np.power(array37, array38))
```
输出:
```
[[ 4 25 216]
[343 64 9]]
[[ 4 25 216]
[343 64 9]]
```
**表2通用二元函数**
| 函数 | 说明 |
| --------------------------------- | ---- |
| `add(x, y)` / `substract(x, y)` | 加法函数 / 减法函数 |
|`multiply(x, y)` / `divide(x, y)`|乘法函数 / 除法函数|
| `floor_divide(x, y)` / `mod(x, y)` | 整除函数 / 求模函数 |
|`allclose(x, y)`|检查数组`x`和`y`元素是否几乎相等|
| `power(x, y)` | 数组$x$的元素$x_i$和数组$y$的元素$y_i$,计算$x_i^{y_i}$ |
| `maximum(x, y)` / `fmax(x, y)` | 两两比较元素获取最大值 / 获取最大值忽略NaN |
| `minimum(x, y)` / `fmin(x, y)` | 两两比较元素获取最小值 / 获取最小值忽略NaN |
| `inner(x, y)` | 内积运算 |
| `cross(x, y) `/ `outer(x, y)` | 叉积运算 / 外积运算 |
| `intersect1d(x, y)` | 计算`x`和`y`的交集,返回这些元素构成的有序数组 |
| `union1d(x, y)` | 计算`x`和`y`的并集,返回这些元素构成的有序数组 |
| `in1d(x, y)` | 返回由判断`x` 的元素是否在`y`中得到的布尔值构成的数组 |
| `setdiff1d(x, y)` | 计算`x`和`y`的差集,返回这些元素构成的数组 |
| `setxor1d(x, y)` | 计算`x`和`y`的对称差,返回这些元素构成的数组 |
>**补充说明**:在二维空间内,两个向量$\boldsymbol{A}=\begin{bmatrix} a_1 \\ a_2 \end{bmatrix}$和$\boldsymbol{B}=\begin{bmatrix} b_1 \\ b_2 \end{bmatrix}$的叉积是这样定义的:$\boldsymbol{A}\times \boldsymbol{B}=\begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix}=a_1b_2 - a_2b_1$,其中$\begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix}$称为行列式。但是一定要注意,叉积并不等同于行列式,行列式的运算结果是一个标量,而叉积运算的结果是一个向量。如果不明白,我们可以看看三维空间两个向量,$\boldsymbol{A}=\begin{bmatrix} a_1 \\ a_2 \\ a_3 \end{bmatrix}$和$\boldsymbol{B}=\begin{bmatrix} b_1 \\ b_2 \\ b_3 \end{bmatrix}$的叉积是$\left< \hat{i} \begin{vmatrix} a_2 \quad a_3 \\ b_2 \quad b_3 \end{vmatrix}, -\hat{j} \begin{vmatrix} a_1 \quad a_3 \\ b_1 \quad b_3 \end{vmatrix}, \hat{k} \begin{vmatrix} a_1 \quad a_2 \\ b_1 \quad b_2 \end{vmatrix} \right>$,其中$\hat{i}, \hat{j}, \hat{k}$代表每个维度的单位向量。
#### 广播机制
上面的例子中,两个二元运算的数组形状是完全相同的,我们再来研究一下,两个形状不同的数组是否可以直接做二元运算或使用二元函数进行运算,请看下面的例子。
代码:
```Python
array39 = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [3, 3, 3]])
array40 = np.array([1, 2, 3])
array39 + array40
```
输出:
```
array([[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]])
```
代码:
```Python
array41 = np.array([[1], [2], [3], [4]])
array39 + array41
```
输出:
```
array([[1, 1, 1],
[3, 3, 3],
[5, 5, 5],
[7, 7, 7]])
```
通过上面的例子我们发现形状不同的数组仍然有机会进行二元运算但也绝对不是任意的数组都可以进行二元运算。简单的说只有两个数组后缘维度相同或者其中一个数组后缘维度为1时广播机制会被触发而通过广播机制如果能够使两个数组的形状一致才能进行二元运算。所谓后缘维度指的是数组`shape`属性对应的元组中最后一个元素的值从后往前数最后一个维度的值例如我们之前打开的图像对应的数组后缘维度为33行4列的二维数组后缘维度为4而有5个元素的一维数组后缘维度为5。简单的说就是后缘维度相同或者其中一个数组的后缘维度为1就可以应用广播机制而广播机制如果能够使得数组的形状一致就满足了两个数组对应元素做运算的需求如下图所示。
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115640.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115658.png)
![](https://gitee.com/jackfrued/mypic/raw/master/20211005115800.png)
### 其他常用函数
除了上面讲到的函数外NumPy 中还提供了很多用于处理数组的函数,`ndarray`对象的很多方法也可以通过直接调用函数来实现,下表给出了一些常用的函数。
**表3NumPy其他常用函数**
| 函数 | 说明 |
| ------------------- | ------------------------------------------------ |
| `unique` | 去除数组重复元素,返回唯一元素构成的有序数组 |
| `copy` | 返回拷贝数组得到的数组 |
| `sort` | 返回数组元素排序后的拷贝 |
| `split` / `hsplit` / `vsplit` | 将数组拆成若干个子数组 |
| `stack` / `hstack` / `vstack` | 将多个数组堆叠成新数组 |
| `concatenate` | 沿着指定的轴连接多个数组构成新数组 |
| `append` / `insert` | 向数组末尾追加元素 / 在数组指定位置插入元素 |
| `argwhere` | 找出数组中非0元素的位置 |
| `extract` / `select` / `where` | 按照指定的条件从数组中抽取或处理数组元素 |
| `flip` | 沿指定的轴翻转数组中的元素 |
| `fromiter` | 通过迭代器创建数组对象 |
| `fromregex` | 通过读取文件和正则表达式解析获取数据创建数组对象 |
| `repeat` / `tile` | 通过对元素的重复来创建新数组 |
| `roll` | 沿指定轴对数组元素进行移位 |
| `resize` | 重新调整数组的大小 |
| `place` / `put` | 将数组中满足条件的元素/指定的元素替换为指定的值 |
| `ptp` | 沿指定的轴计算极差(最大值与最小值的差) |
| `median` | 沿指定轴计算中位数 |
| `partition` | 用选定的元素对数组进行一次划分并返回划分后的数组 |
> **提示**:上面的`resize`函数和`ndarray`对象的`resize`方法是有区别的,`resize`函数在调整数组大小时会重复数组中的元素作为填补多出来的元素的值,而`ndarry`对象的`resize`方法是用0来填补多出来的元素。这些小细节不清楚暂时也不要紧但是如果用到对应的功能了就要引起注意。
代码:
```Python
array42 = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3]])
array43 = np.array([[4, 4, 4], [5, 5, 5], [6, 6, 6]])
np.hstack((array42, array43))
```
输出:
```
array([[1, 1, 1, 4, 4, 4],
[2, 2, 2, 5, 5, 5],
[3, 3, 3, 6, 6, 6]])
```
代码:
```Python
np.vstack((array42, array43))
```
输出:
```
array([[1, 1, 1],
[2, 2, 2],
[3, 3, 3],
[4, 4, 4],
[5, 5, 5],
[6, 6, 6]])
```
代码:
```Python
np.concatenate((array42, array43))
```
输出:
```
array([[1, 1, 1],
[2, 2, 2],
[3, 3, 3],
[4, 4, 4],
[5, 5, 5],
[6, 6, 6]])
```
代码:
```Python
np.concatenate((array42, array43), axis=1)
```
输出:
```
array([[1, 1, 1, 4, 4, 4],
[2, 2, 2, 5, 5, 5],
[3, 3, 3, 6, 6, 6]])
```
### 矩阵运算
NumPy 中提供了专门用于线性代数linear algebra的模块和表示矩阵的类型`matrix`,当然我们通过二维数组也可以表示一个矩阵,官方并不推荐使用`matrix`类而是建议使用二维数组,而且有可能在将来的版本中会移除`matrix`类。无论如何,利用这些已经封装好的类和函数,我们可以轻松愉快的实现线性代数中很多的操作。
#### 线性代数快速回顾
1. **向量**也叫**矢量**,是一个同时具有大小和方向,且满足平行四边形法则的几何对象。与向量相对的概念叫**标量**或**数量**,标量只有大小、绝大多数情况下没有方向。
2. 向量可以进行**加**、**减**、**数乘**、**点积**、**叉积**等运算。
3. **行列式**由向量组成,它的性质可以由向量解释。
4. 行列式可以使用**行列式公式**计算:$det(\boldsymbol{A})=\sum_{n!} \pm {a_{1\alpha}a_{2\beta} \cdots a_{n\omega}}$。
5. 高阶行列式可以用**代数余子式**展开成多个低阶行列式,如:$det(\boldsymbol{A})=a_{11}C_{11}+a_{12}C_{12}+ \cdots +a_{1n}C_{1n}$。
6. **矩阵**是由一系列元素排成的矩形阵列,矩阵里的元素可以是数字、符号或数学公式。
7. 矩阵可以进行**加法**、**减法**、**数乘**、**乘法**、**转置**等运算。
8. **逆矩阵**用$\boldsymbol{A^{-1}}$表示,$\boldsymbol{A}\boldsymbol{A^{-1}}=\boldsymbol{A^{-1}}\boldsymbol{A}=\boldsymbol{I}$;没有逆矩阵的方阵是**奇异矩阵**
9. 如果一个方阵是**满秩矩阵**(矩阵的秩等于矩阵的阶数),该方阵对应的线性方程有唯一解。
> **说明****矩阵的秩**是指矩阵中线性无关的行/列向量的最大个数,同时也是矩阵对应的线性变换的像空间的维度。
#### NumPy中矩阵相关函数
1. 创建矩阵对象。
代码:
```Python
# matrix构造函数可以传入类数组对象也可以传入字符串
m1 = np.matrix('1 2 3; 4 5 6')
m1
```
输出:
```
matrix([[1, 2, 3],
[4, 5, 6]])
```
代码:
```Python
# asmatrix函数也可以写成mat函数它们其实是同一个函数
m2 = np.asmatrix(np.array([[1, 1], [2, 2], [3, 3]]))
m2
```
输出:
```
matrix([[1, 1],
[2, 2],
[3, 3]])
```
代码:
```Python
m1 * m2
```
输出:
```
matrix([[14, 14],
[32, 32]])
```
> **说明**:注意`matrix`对象和`ndarray`对象乘法运算的差别,如果两个二维数组要做矩阵乘法运算,应该使用`@`运算符或`matmul`函数,而不是`*`运算符。
2. 矩阵对象的属性。
| 属性 | 说明 |
| ------- | ----------------------------------------- |
| `A` | 获取矩阵对象对应的`ndarray`对象 |
| `A1` | 获取矩阵对象对应的扁平化后的`ndarray`对象 |
| `I` | 可逆矩阵的逆矩阵 |
| `T` | 矩阵的转置 |
| `H` | 矩阵的共轭转置 |
| `shape` | 矩阵的形状 |
| `size` | 矩阵元素的个数 |
3. 矩阵对象的方法。
矩阵对象的方法跟之前讲过的`ndarray`数组对象的方法基本差不多,此处不再进行赘述。
#### NumPy的线性代数模块
NumPy 的`linalg`模块中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的函数,它们跟 MATLAB 和 R 等语言所使用的是相同的行业标准线性代数库,下面的表格列出了`numpy`以及`linalg`模块中常用的跟线性代数相关的函数。
| 函数 | 说明 |
| --------------- | ------------------------------------------------------------ |
| `diag` | 以一维数组的形式返回方阵的对角线元素或将一维数组转换为方阵非对角元素元素为0 |
| `vdot` | 向量的点积 |
| `dot` | 数组的点积 |
| `inner` | 数组的内积 |
| `outer` | 数组的叉积 |
| `trace` | 计算对角线元素的和 |
| `norm` | 求模(范数)运算 |
| `det` | 计算行列式的值(在方阵上计算会得到一个标量) |
| `matrix_rank` | 计算矩阵的秩 |
| `eig` | 计算矩阵的特征值eigenvalue和特征向量eigenvector |
| `inv` | 计算非奇异矩阵($n$阶方阵)的逆矩阵 |
| `pinv` | 计算矩阵的摩尔-彭若斯Moore-Penrose广义逆 |
| `qr` | QR分解把矩阵分解成一个正交矩阵与一个上三角矩阵的积 |
| `svd` | 计算奇异值分解singular value decomposition |
| `solve` | 解线性方程组$\boldsymbol{A}\boldsymbol{x}=\boldsymbol{b}$,其中$\boldsymbol{A}$是一个方阵 |
| `lstsq` | 计算$\boldsymbol{A}\boldsymbol{x}=\boldsymbol{b}$的最小二乘解 |
大家如果有兴趣可以用下面的代码验证上面的函数。
代码:
```Python
m3 = np.array([[1., 2.], [3., 4.]])
np.linalg.inv(m3)
```
输出:
```
array([[-2. , 1. ],
[ 1.5, -0.5]])
```
代码:
```Python
m4 = np.array([[1, 3, 5], [2, 4, 6], [4, 7, 9]])
np.linalg.det(m4)
```
输出:
```
2
```
代码:
```Python
# 解线性方程组ax=b
# 3x + y = 9x + 2y = 8
a = np.array([[3,1], [1,2]])
b = np.array([9, 8])
np.linalg.solve(a, b)
```
输出:
```
array([2., 3.])
```

View File

@ -0,0 +1,686 @@
## Pandas的应用-1
Pandas是Wes McKinney在2008年开发的一个强大的**分析结构化数据**的工具集。Pandas以NumPy为基础数据表示和运算提供了用于数据处理的函数和方法对数据分析和数据挖掘提供了很好的支持同时Pandas还可以跟数据可视化工具Matplotlib很好的整合在一起非常轻松愉快的实现数据的可视化展示。
Pandas核心的数据类型是`Series`(数据系列)、`DataFrame`(数据表/数据框),分别用于处理一维和二维的数据,除此之外还有一个名为`Index`的类型及其子类型,它为`Series`和`DataFrame`提供了索引功能。日常工作中以`DataFrame`使用最为广泛因为二维的数据本质就是一个有行有列的表格想一想Excel电子表格和关系型数据库中的二维表。上述这些类型都提供了大量的处理数据的方法数据分析师可以以此为基础实现对数据的各种常规处理。
### Series的应用
Pandas库中的`Series`对象可以用来表示一维数据结构,跟数组非常类似,但是多了一些额外的功能。`Series`的内部结构包含了两个数组,其中一个用来保存数据,另一个用来保存数据的索引。
#### 创建Series对象
> **提示**:在执行下面的代码之前,请先导入`pandas`以及相关的库文件,具体的做法可以参考上一章。
##### 方法1通过列表或数组创建Series对象
代码:
```Python
# data参数表示数据index参数表示数据的索引标签
# 如果没有指定index属性默认使用数字索引
ser1 = pd.Series(data=[320, 180, 300, 405], index=['一季度', '二季度', '三季度', '四季度'])
ser1
```
输出:
```
一季度 320
二季度 180
三季度 300
四季度 405
dtype: int64
```
##### 方法2通过字典创建Series对象。
代码:
```Python
# 字典中的键就是数据的索引(标签),字典中的值就是数据
ser2 = pd.Series({'一季度': 320, '二季度': 180, '三季度': 300, '四季度': 405})
ser2
```
输出:
```
一季度 320
二季度 180
三季度 300
四季度 405
dtype: int64
```
#### 索引和切片
跟数组一样Series对象也可以进行索引和切片操作不同的是Series对象因为内部维护了一个保存索引的数组所以除了可以使用整数索引通过位置检索数据外还可以通过自己设置的索引标签获取对应的数据。
##### 使用整数索引
代码:
```Python
print(ser2[0], ser[1], ser[2], ser[3])
ser2[0], ser2[3] = 350, 360
print(ser2)
```
输出:
```
320 180 300 405
一季度 350
二季度 180
三季度 300
四季度 360
dtype: int64
```
> **提示**:如果要使用负向索引,必须在创建`Series`对象时通过`index`属性指定非数值类型的标签。
##### 使用自定义的标签索引
代码:
```Python
print(ser2['一季度'], ser2['三季度'])
ser2['一季度'] = 380
print(ser2)
```
输出:
```
350 300
一季度 380
二季度 180
三季度 300
四季度 360
dtype: int64
```
##### 切片操作
代码:
```Python
print(ser2[1:3])
print(ser2['二季度':'四季度'])
```
输出:
```
二季度 180
三季度 300
dtype: int64
二季度 500
三季度 500
四季度 520
dtype: int64
```
代码:
```Python
ser2[1:3] = 400, 500
ser2
```
输出:
```
一季度 380
二季度 400
三季度 500
四季度 360
dtype: int64
```
##### 花式索引
代码:
```Python
print(ser2[['二季度', '四季度']])
ser2[['二季度', '四季度']] = 500, 520
print(ser2)
```
输出:
```
二季度 400
四季度 360
dtype: int64
一季度 380
二季度 500
三季度 500
四季度 520
dtype: int64
```
##### 布尔索引
代码:
```Python
ser2[ser2 >= 500]
```
输出:
```
二季度 500
三季度 500
四季度 520
dtype: int64
```
####属性和方法
Series对象的常用属性如下表所示。
| 属性 | 说明 |
| ------------------------- | --------------------------------------- |
| `dtype` / `dtypes` | 返回`Series`对象的数据类型 |
| `hasnans` | 判断`Series`对象中有没有空值 |
| `at` / `iat` | 通过索引访问`Series`对象中的单个值 |
| `loc` / `iloc` | 通过一组索引访问`Series`对象中的一组值 |
| `index` | 返回`Series`对象的索引 |
| `is_monotonic` | 判断`Series`对象中的数据是否单调 |
| `is_monotonic_increasing` | 判断`Series`对象中的数据是否单调递增 |
| `is_monotonic_decreasing` | 判断`Series`对象中的数据是否单调递减 |
| `is_unique` | 判断`Series`对象中的数据是否独一无二 |
| `size` | 返回`Series`对象中元素的个数 |
| `values` | 以`ndarray`的方式返回`Series`对象中的值 |
`Series`对象的方法很多,我们通过下面的代码为大家介绍一些常用的方法。
##### 统计相关的方法
`Series`对象支持各种获取描述性统计信息的方法。
代码:
```Python
# 求和
print(ser2.sum())
# 求均值
print(ser2.mean())
# 求最大
print(ser2.max())
# 求最小
print(ser2.min())
# 计数
print(ser2.count())
# 求标准差
print(ser2.std())
# 求方差
print(ser2.var())
# 求中位数
print(ser2.median())
```
`Series`对象还有一个名为`describe()`的方法,可以获得上述所有的描述性统计信息,如下所示。
代码:
```Python
ser2.describe()
```
输出:
```
count 4.000000
mean 475.000000
std 64.031242
min 380.000000
25% 470.000000
50% 500.000000
75% 505.000000
max 520.000000
dtype: float64
```
> **提示**:因为`describe()`返回的也是一个`Series`对象,所以也可以用`ser2.describe()['mean']`来获取平均值。
如果`Series`对象有重复的值,我们可以使用`unique()`方法获得去重之后的`Series`对象;可以使用`nunique()`方法统计不重复值的数量;如果想要统计每个值重复的次数,可以使用`value_counts()`方法,这个方法会返回一个`Series`对象,它的索引就是原来的`Series`对象中的值,而每个值出现的次数就是返回的`Series`对象中的数据,在默认情况下会按照出现次数做降序排列。
代码:
```Python
ser3 = pd.Series(data=['apple', 'banana', 'apple', 'pitaya', 'apple', 'pitaya', 'durian'])
ser3.value_counts()
```
输出:
```
apple 3
pitaya 2
durian 1
banana 1
dtype: int64
```
代码:
```Python
ser3.nunique()
```
输出:
```
4
```
##### 数据处理的方法
`Series`对象的`isnull()`和`notnull()`方法可以用于空值的判断,代码如下所示。
代码:
```Python
ser4 = pd.Series(data=[10, 20, np.NaN, 30, np.NaN])
ser4.isnull()
```
输出:
```
0 False
1 False
2 True
3 False
4 True
dtype: bool
```
代码:
```Python
ser4.notnull()
```
输出:
```
0 True
1 True
2 False
3 True
4 False
dtype: bool
```
`Series`对象的`dropna()`和`fillna()`方法分别用来删除空值和填充空值,具体的用法如下所示。
代码:
```Python
ser4.dropna()
```
输出:
```
0 10.0
1 20.0
3 30.0
dtype: float64
```
代码:
```Python
# 将空值填充为40
ser4.fillna(value=40)
```
输出:
```
0 10.0
1 20.0
2 40.0
3 30.0
4 40.0
dtype: float64
```
代码:
```Python
# backfill或bfill表示用后一个元素的值填充空值
# ffill或pad表示用前一个元素的值填充空值
ser4.fillna(method='ffill')
```
输出:
```
0 10.0
1 20.0
2 20.0
3 30.0
4 30.0
dtype: float64
```
需要提醒大家注意的是,`dropna()`和`fillna()`方法都有一个名为`inplace`的参数,它的默认值是`False`,表示删除空值或填充空值不会修改原来的`Series`对象,而是返回一个新的`Series`对象来表示删除或填充空值后的数据系列,如果将`inplace`参数的值修改为`True`,那么删除或填充空值会就地操作,直接修改原来的`Series`对象,那么方法的返回值是`None`。后面我们会接触到的很多方法,包括`DataFrame`对象的很多方法都会有这个参数,它们的意义跟这里是一样的。
`Series`对象的`mask()`和`where()`方法可以将满足或不满足条件的值进行替换,如下所示。
代码:
```Python
ser5 = pd.Series(range(5))
ser5.where(ser5 > 0)
```
输出:
```
0 NaN
1 1.0
2 2.0
3 3.0
4 4.0
dtype: float64
```
代码:
```Python
ser5.where(ser5 > 1, 10)
```
输出:
```
0 10
1 10
2 2
3 3
4 4
dtype: int64
```
代码:
```Python
ser5.mask(ser5 > 1, 10)
```
输出:
```
0 0
1 1
2 10
3 10
4 10
dtype: int64
```
`Series`对象的`duplicated()`方法可以帮助我们找出重复的数据,而`drop_duplicates()`方法可以帮我们删除重复数据。
代码:
```Python
ser3.duplicated()
```
输出:
```
0 False
1 False
2 True
3 False
4 True
5 True
6 False
dtype: bool
```
代码:
```Python
ser3.drop_duplicates()
```
输出:
```
0 apple
1 banana
3 pitaya
6 durian
dtype: object
```
`Series`对象的`apply()`和`map()`方法非常重要,它们可以用于数据处理,把数据映射或转换成我们期望的样子,这个操作在数据分析的数据准备阶段非常重要。
代码:
```Python
ser6 = pd.Series(['cat', 'dog', np.nan, 'rabbit'])
ser6
```
输出:
```
0 cat
1 dog
2 NaN
3 rabbit
dtype: object
```
代码:
```Python
ser6.map({'cat': 'kitten', 'dog': 'puppy'})
```
输出:
```
0 kitten
1 puppy
2 NaN
3 NaN
dtype: object
```
代码:
```Python
ser6.map('I am a {}'.format, na_action='ignore')
```
输出:
```
0 I am a cat
1 I am a dog
2 NaN
3 I am a rabbit
dtype: object
```
代码:
```Python
ser7 = pd.Series([20, 21, 12], index=['London', 'New York', 'Helsinki'])
ser7
```
输出:
```
London 20
New York 21
Helsinki 12
dtype: int64
```
代码:
```Python
ser7.apply(np.square)
```
输出:
```
London 400
New York 441
Helsinki 144
dtype: int64
```
代码:
```Python
ser7.apply(lambda x, value: x - value, args=(5, ))
```
输出:
```
London 15
New York 16
Helsinki 7
dtype: int64
```
##### 排序和取头部值的方法
`Series`对象的`sort_index()`和`sort_values()`方法可以用于对索引和数据的排序,排序方法有一个名为`ascending`的布尔类型参数,该参数用于控制排序的结果是升序还是降序;而名为`kind`的参数则用来控制排序使用的算法,默认使用了`quicksort`,也可以选择`mergesort`或`heapsort`;如果存在空值,那么可以用`na_position`参数空值放在最前还是最后,默认是`last`,代码如下所示。
代码:
```Python
ser8 = pd.Series(
data=[35, 96, 12, 57, 25, 89],
index=['grape', 'banana', 'pitaya', 'apple', 'peach', 'orange']
)
# 按值从小到大排序
ser8.sort_values()
```
输出:
```
pitaya 12
peach 25
grape 35
apple 57
orange 89
banana 96
dtype: int64
```
代码:
```Python
# 按索引从大到小排序
ser8.sort_index(ascending=False)
```
输出:
```
pitaya 12
peach 25
orange 89
grape 35
banana 96
apple 57
dtype: int64
```
如果要从`Series`对象中找出元素中最大或最小的“Top-N”实际上是不需要对所有的值进行排序的可以使用`nlargest()`和`nsmallest()`方法来完成,如下所示。
代码:
```Python
# 值最大的3个
ser8.nlargest(3)
```
输出:
```
banana 96
orange 89
apple 57
dtype: int64
```
代码:
```Python
# 值最小的2个
ser8.nsmallest(2)
```
输出:
```
pitaya 12
peach 25
dtype: int64
```
#### 绘制图表
Series对象有一个名为`plot`的方法可以用来生成图表如果选择生成折线图、饼图、柱状图等默认会使用Series对象的索引作为横坐标使用Series对象的数据作为纵坐标。
首先导入`matplotlib`中`pyplot`模块并进行必要的配置。
```Python
import matplotlib.pyplot as plt
# 配置支持中文的非衬线字体(默认的字体无法显示中文)
plt.rcParams['font.sans-serif'] = ['SimHei', ]
# 使用指定的中文字体时需要下面的配置来避免负号无法显示
plt.rcParams['axes.unicode_minus'] = False
```
创建`Series`对象并绘制对应的柱状图。
```Python
ser9 = pd.Series({'一季度': 400, '二季度': 520, '三季度': 180, '四季度': 380})
# 通过Series对象的plot方法绘图kind='bar'表示绘制柱状图)
ser9.plot(kind='bar', color=['r', 'g', 'b', 'y'])
# x轴的坐标旋转到0度中文水平显示
plt.xticks(rotation=0)
# 在柱状图的柱子上绘制数字
for i in range(4):
plt.text(i, ser9[i] + 5, ser9[i], ha='center')
# 显示图像
plt.show()
```
![](res/series-bar-graph.png)
绘制反映每个季度占比的饼图。
```Python
# autopct参数可以配置在饼图上显示每块饼的占比
ser9.plot(kind='pie', autopct='%.1f%%')
# 设置y轴的标签显示在饼图左侧的文字
plt.ylabel('各季度占比')
plt.show()
```
![](res/series-pie-graph.png)

View File

@ -0,0 +1,509 @@
## Pandas的应用-2
### DataFrame的应用
#### 创建DataFrame对象
##### 通过二维数组创建`DataFrame`对象
代码:
```Python
scores = np.random.randint(60, 101, (5, 3))
courses = ['语文', '数学', '英语']
ids = [1001, 1002, 1003, 1004, 1005]
df1 = pd.DataFrame(data=scores, columns=courses, index=ids)
df1
```
输出:
```
语文 数学 英语
1001 69 80 79
1002 71 60 100
1003 94 81 93
1004 88 88 67
1005 82 66 60
```
##### 通过字典创建`DataFrame`对象
代码:
```Python
scores = {
'语文': [62, 72, 93, 88, 93],
'数学': [95, 65, 86, 66, 87],
'英语': [66, 75, 82, 69, 82],
}
ids = [1001, 1002, 1003, 1004, 1005]
df2 = pd.DataFrame(data=scores, index=ids)
df2
```
输出:
```
语文 数学 英语
1001 69 80 79
1002 71 60 100
1003 94 81 93
1004 88 88 67
1005 82 66 60
```
##### 读取 CSV 文件创建`DataFrame`对象
可以通过`pandas` 模块的`read_csv`函数来读取 CSV 文件,`read_csv`函数的参数非常多,下面接受几个比较重要的参数。
- `sep` / `delimiter`:分隔符,默认是`,`。
- `header`:表头(列索引)的位置,默认值是`infer`,用第一行的内容作为表头(列索引)。
- `index_col`:用作行索引(标签)的列。
- `usecols`:需要加载的列,可以使用序号或者列名。
- `true_values` / `false_values`:哪些值被视为布尔值`True` / `False`
- `skiprows`:通过行号、索引或函数指定需要跳过的行。
- `skipfooter`:要跳过的末尾行数。
- `nrows`:需要读取的行数。
- `na_values`:哪些值被视为空值。
代码:
```Python
df3 = pd.read_csv('2018年北京积分落户数据.csv', index_col='id')
df3
```
输出:
```
name birthday company score
id
1 杨x 1972-12 北京利德xxxx 122.59
2 纪x 1974-12 北京航天xxxx 121.25
3 王x 1974-05 品牌联盟xxxx 118.96
4 杨x 1975-07 中科专利xxxx 118.21
5 张x 1974-11 北京阿里xxxx 117.79
... ... ... ... ...
6015 孙x 1978-08 华为海洋xxxx 90.75
6016 刘x 1976-11 福斯流体xxxx 90.75
6017 周x 1977-10 赢创德固xxxx 90.75
6018 赵x 1979-07 澳科利耳xxxx 90.75
6019 贺x 1981-06 北京宝洁xxxx 90.75
6019 rows × 4 columns
```
> **说明**:如果需要上面例子中的 CSV 文件可以通过下面的百度云盘地址进行获取数据在《从零开始学数据分析》目录中。链接https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g提取码e7b4。
##### 读取Excel文件创建`DataFrame`对象
可以通过`pandas` 模块的`read_excel`函数来读取 Exce l文件该函数与上面的`read_csv`非常相近,多了一个`sheet_name`参数来指定数据表的名称,但是不同于 CSV 文件,没有`sep`或`delimiter`这样的参数。下面的代码中,`read_excel`函数的`skiprows`参数是一个 Lambda 函数,通过该 Lambda 函数指定只读取 Excel 文件的表头和其中10%的数据,跳过其他的数据。
代码:
```Python
import random
df4 = pd.read_excel(
io='小宝剑大药房2018年销售数据.xlsx',
usecols=['购药时间', '社保卡号', '商品名称', '销售数量', '应收金额', '实收金额'],
skiprows=lambda x: x > 0 and random.random() > 0.1
)
df4
```
> **说明**:如果需要上面例子中的 Excel 文件可以通过下面的百度云盘地址进行获取数据在《从零开始学数据分析》目录中。链接https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g提取码e7b4。
输出:
```
购药时间 社保卡号 商品名称 销售数量 应收金额 实收金额
0 2018-03-23 星期三 10012157328 强力xx片 1 13.8 13.80
1 2018-07-12 星期二 108207828 强力xx片 1 13.8 13.80
2 2018-01-17 星期日 13358228 清热xx液 1 28.0 28.00
3 2018-07-11 星期一 10031402228 三九xx灵 5 149.0 130.00
4 2018-01-20 星期三 10013340328 三九xx灵 3 84.0 73.92
... ... ... ... ... ... ...
618 2018-03-05 星期六 10066059228 开博xx通 2 56.0 49.28
619 2018-03-22 星期二 10035514928 开博xx通 1 28.0 25.00
620 2018-04-15 星期五 1006668328 开博xx通 2 56.0 50.00
621 2018-04-24 星期日 10073294128 高特xx灵 1 5.6 5.60
622 2018-04-24 星期日 10073294128 高特xx灵 10 56.0 56.0
623 rows × 6 columns
```
##### 通过SQL从数据库读取数据创建`DataFrame`对象
`pandas`模块的`read_sql`函数可以通过 SQL 语句从数据库中读取数据创建`DataFrame`对象,该函数的第二个参数代表了需要连接的数据库。对于 MySQL 数据库,我们可以通过`pymysql`或`mysqlclient`来创建数据库连接,得到一个`Connection` 对象,而这个对象就是`read_sql`函数需要的第二个参数,代码如下所示。
代码:
```Python
import pymysql
# 创建一个MySQL数据库的连接对象
conn = pymysql.connect(
host='47.104.31.138', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4'
)
# 通过SQL从数据库读取数据创建DataFrame
df5 = pd.read_sql('select * from tb_emp', conn, index_col='eno')
df5
```
> **提示**:执行上面的代码需要先安装`pymysql`库,如果尚未安装,可以先在 Notebook 的单元格中先执行`!pip install pymysql`,然后再运行上面的代码。上面的代码连接的是我部署在阿里云上的 MySQL 数据库,公网 IP 地址:`47.104.31.138`,用户名:`guest`,密码:`Guest.618`,数据库:`hrs`,表名:`tb_emp`,字符集:`utf8mb4`,大家可以使用这个数据库,但是不要进行恶意的访问。
输出:
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344.0 1800 200.0 30
2056 乔峰 分析师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3211 张无忌 程序员 2056.0 3200 NaN 20
3233 丘处机 程序员 2056.0 3400 NaN 20
3244 欧阳锋 程序员 3088.0 3200 NaN 20
3251 张翠山 程序员 2056.0 4000 NaN 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
3577 杨过 会计 5566.0 2200 NaN 10
3588 朱九真 会计 5566.0 2500 NaN 10
4466 苗人凤 销售员 3344.0 2500 NaN 30
5234 郭靖 出纳 5566.0 2000 NaN 10
5566 宋远桥 会计师 7800.0 4000 1000.0 10
7800 张三丰 总裁 NaN 9000 1200.0 20
```
#### 基本属性和方法
在开始讲解`DataFrame`的属性和方法前,我们先从之前提到的`hrs`数据库中读取三张表的数据,创建出三个`DataFrame`对象,代码如下所示。
```Python
import pymysql
conn = pymysql.connect(
host='47.104.31.138', port=3306,
user='guest', password='Guest.618',
database='hrs', charset='utf8mb4'
)
dept_df = pd.read_sql('select * from tb_dept', conn, index_col='dno')
emp_df = pd.read_sql('select * from tb_emp', conn, index_col='eno')
emp2_df = pd.read_sql('select * from tb_emp2', conn, index_col='eno')
```
得到的三个`DataFrame`对象如下所示。
部门表(`dept_df`),其中`dno`是部门的编号,`dname`和`dloc`分别是部门的名称和所在地。
```
dname dloc
dno
10 会计部 北京
20 研发部 成都
30 销售部 重庆
40 运维部 天津
```
员工表(`emp_df`),其中`eno`是员工编号,`ename`、`job`、`mgr`、`sal`、`comm`和`dno`分别代表员工的姓名、职位、主管编号、月薪、补贴和部门编号。
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344.0 1800 200.0 30
2056 乔峰 分析师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3211 张无忌 程序员 2056.0 3200 NaN 20
3233 丘处机 程序员 2056.0 3400 NaN 20
3244 欧阳锋 程序员 3088.0 3200 NaN 20
3251 张翠山 程序员 2056.0 4000 NaN 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
3577 杨过 会计 5566.0 2200 NaN 10
3588 朱九真 会计 5566.0 2500 NaN 10
4466 苗人凤 销售员 3344.0 2500 NaN 30
5234 郭靖 出纳 5566.0 2000 NaN 10
5566 宋远桥 会计师 7800.0 4000 1000.0 10
7800 张三丰 总裁 NaN 9000 1200.0 20
```
> **说明**:在数据库中`mgr`和`comm`两个列的数据类型是`int`,但是因为有缺失值(空值),读取到`DataFrame`之后,列的数据类型变成了`float`,因为我们通常会用`float`类型的`NaN`来表示空值。
员工表(`emp2_df`),跟上面的员工表结构相同,但是保存了不同的员工数据。
```
ename job mgr sal comm dno
eno
9800 骆昊 架构师 7800 30000 5000 20
9900 王小刀 程序员 9800 10000 1200 20
9700 王大锤 程序员 9800 8000 600 20
```
`DataFrame`对象的属性如下表所示。
| 属性名 | 说明 |
| -------------- | ----------------------------------- |
| `at` / `iat` | 通过标签获取`DataFrame`中的单个值。 |
| `columns` | `DataFrame`对象列的索引 |
| `dtypes` | `DataFrame`对象每一列的数据类型 |
| `empty` | `DataFrame`对象是否为空 |
| `loc` / `iloc` | 通过标签获取`DataFrame`中的一组值。 |
| `ndim` | `DataFrame`对象的维度 |
| `shape` | `DataFrame`对象的形状(行数和列数) |
| `size` | `DataFrame`对象中元素的个数 |
| `values` | `DataFrame`对象的数据对应的二维数组 |
关于`DataFrame`的方法,首先需要了解的是`info()`方法,它可以帮助我们了解`DataFrame`的相关信息,如下所示。
代码:
```Python
emp_df.info()
```
输出:
```
<class 'pandas.core.frame.DataFrame'>
Int64Index: 14 entries, 1359 to 7800
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ename 14 non-null object
1 job 14 non-null object
2 mgr 13 non-null float64
3 sal 14 non-null int64
4 comm 6 non-null float64
5 dno 14 non-null int64
dtypes: float64(2), int64(2), object(2)
memory usage: 1.3+ KB
```
如果需要查看`DataFrame`的头部或尾部的数据,可以使用`head()`或`tail()`方法,这两个方法的默认参数是`5`,表示获取`DataFrame`最前面5行或最后面5行的数据如下所示。
```Python
emp_df.head()
```
输出:
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344 1800 200 30
2056 乔峰 分析师 7800 5000 1500 20
3088 李莫愁 设计师 2056 3500 800 20
3211 张无忌 程序员 2056 3200 NaN 20
3233 丘处机 程序员 2056 3400 NaN 20
```
#### 获取数据
##### 索引和切片
如果要获取`DataFrame`的某一列,例如取出上面`emp_df`的`ename`列,可以使用下面的两种方式。
```Python
emp_df.ename
```
或者
```Python
emp_df['ename']
```
执行上面的代码可以发现,我们获得的是一个`Series`对象。事实上,`DataFrame`对象就是将多个`Series`对象组合到一起的结果。
如果要获取`DataFrame`的某一行,可以使用整数索引或我们设置的索引,例如取出员工编号为`2056`的员工数据,代码如下所示。
```Python
emp_df.iloc[1]
```
或者
```Python
emp_df.loc[2056]
```
通过执行上面的代码我们发现,单独取`DataFrame` 的某一行或某一列得到的都是`Series`对象。我们当然也可以通过花式索引来获取多个行或多个列的数据,花式索引的结果仍然是一个`DataFrame`对象。
获取多个列:
```Python
emp_df[['ename', 'job']]
```
获取多个行:
```Python
emp_df.loc[[2056, 7800, 3344]]
```
如果要获取或修改`DataFrame` 对象某个单元格的数据,需要同时指定行和列的索引,例如要获取员工编号为`2056`的员工的职位信息,代码如下所示。
```Python
emp_df['job'][2056]
```
或者
```Python
emp_df.loc[2056]['job']
```
或者
```Python
emp_df.loc[2056, 'job']
```
我们推荐大家使用第三种做法,因为它只做了一次索引运算。如果要将该员工的职位修改为“架构师”,可以使用下面的代码。
```Python
emp_df.loc[2056, 'job'] = '架构师'
```
当然,我们也可以通过切片操作来获取多行多列,相信大家一定已经想到了这一点。
```Python
emp_df.loc[2056:3344]
```
输出:
```
ename job mgr sal comm dno
eno
2056 乔峰 分析师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3211 张无忌 程序员 2056.0 3200 NaN 20
3233 丘处机 程序员 2056.0 3400 NaN 20
3244 欧阳锋 程序员 3088.0 3200 NaN 20
3251 张翠山 程序员 2056.0 4000 NaN 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
```
##### 数据筛选
上面我们提到了花式索引,相信大家已经联想到了布尔索引。跟`ndarray`和`Series`一样,我们可以通过布尔索引对`DataFrame`对象进行数据筛选,例如我们要从`emp_df`中筛选出月薪超过`3500`的员工,代码如下所示。
```Python
emp_df[emp_df.sal > 3500]
```
输出:
```
ename job mgr sal comm dno
eno
2056 乔峰 分析师 7800.0 5000 1500.0 20
3251 张翠山 程序员 2056.0 4000 NaN 20
5566 宋远桥 会计师 7800.0 4000 1000.0 10
7800 张三丰 总裁 NaN 9000 1200.0 20
```
当然,我们也可以组合多个条件来进行数据筛选,例如从`emp_df`中筛选出月薪超过`3500`且部门编号为`20`的员工,代码如下所示。
```Python
emp_df[(emp_df.sal > 3500) & (emp_df.dno == 20)]
```
输出:
```
ename job mgr sal comm dno
eno
2056 乔峰 分析师 7800.0 5000 1500.0 20
3251 张翠山 程序员 2056.0 4000 NaN 20
7800 张三丰 总裁 NaN 9000 1200.0 20
```
除了使用布尔索引,`DataFrame`对象的`query`方法也可以实现数据筛选,`query`方法的参数是一个字符串,它代表了筛选数据使用的表达式,而且更符合 Python 程序员的使用习惯。下面我们使用`query`方法将上面的效果重新实现一遍,代码如下所示。
```Python
emp_df.query('sal > 3500 and dno == 20')
```
#### 重塑数据
有的时候,我们做数据分析需要的原始数据可能并不是来自一个地方,就像上面的例子中,我们从关系型数据库中读取了三张表,得到了三个`DataFrame`对象,但实际工作可能需要我们把他们的数据整合到一起。例如:`emp_df`和`emp2_df`其实都是员工的数据,而且数据结构完全一致,我们可以使用`pandas`提供的`concat`函数实现两个或多个`DataFrame`的数据拼接,代码如下所示。
```Python
all_emp_df = pd.concat([emp_df, emp2_df])
```
输出:
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344.0 1800 200.0 30
2056 乔峰 分析师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3211 张无忌 程序员 2056.0 3200 NaN 20
3233 丘处机 程序员 2056.0 3400 NaN 20
3244 欧阳锋 程序员 3088.0 3200 NaN 20
3251 张翠山 程序员 2056.0 4000 NaN 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
3577 杨过 会计 5566.0 2200 NaN 10
3588 朱九真 会计 5566.0 2500 NaN 10
4466 苗人凤 销售员 3344.0 2500 NaN 30
5234 郭靖 出纳 5566.0 2000 NaN 10
5566 宋远桥 会计师 7800.0 4000 1000.0 10
7800 张三丰 总裁 NaN 9000 1200.0 20
9800 骆昊 架构师 7800.0 30000 5000.0 20
9900 王小刀 程序员 9800.0 10000 1200.0 20
9700 王大锤 程序员 9800.0 8000 600.0 20
```
上面的代码将两个代表员工数据的`DataFrame`拼接到了一起,接下来我们使用`merge`函数将员工表和部门表的数据合并到一张表中,代码如下所示。
先使用`reset_index`方法重新设置`all_emp_df`的索引,这样`eno` 不再是索引而是一个普通列,`reset_index`方法的`inplace`参数设置为`True`表示,重置索引的操作直接在`all_emp_df`上执行,而不是返回修改后的新对象。
```Python
all_emp_df.reset_index(inplace=True)
```
通过`merge`函数合并数据,当然,也可以调用`DataFrame`对象的`merge`方法来达到同样的效果。
```Python
pd.merge(dept_df, all_emp_df, how='inner', on='dno')
```
输出:
```
dno dname dloc eno ename job mgr sal comm
0 10 会计部 北京 3577 杨过 会计 5566.0 2200 NaN
1 10 会计部 北京 3588 朱九真 会计 5566.0 2500 NaN
2 10 会计部 北京 5234 郭靖 出纳 5566.0 2000 NaN
3 10 会计部 北京 5566 宋远桥 会计师 7800.0 4000 1000.0
4 20 研发部 成都 2056 乔峰 架构师 7800.0 5000 1500.0
5 20 研发部 成都 3088 李莫愁 设计师 2056.0 3500 800.0
6 20 研发部 成都 3211 张无忌 程序员 2056.0 3200 NaN
7 20 研发部 成都 3233 丘处机 程序员 2056.0 3400 NaN
8 20 研发部 成都 3244 欧阳锋 程序员 3088.0 3200 NaN
9 20 研发部 成都 3251 张翠山 程序员 2056.0 4000 NaN
10 20 研发部 成都 7800 张三丰 总裁 NaN 9000 1200.0
11 20 研发部 成都 9800 骆昊 架构师 7800.0 30000 5000.0
12 20 研发部 成都 9900 王小刀 程序员 9800.0 10000 1200.0
13 20 研发部 成都 9700 王大锤 程序员 9800.0 8000 600.0
14 30 销售部 重庆 1359 胡一刀 销售员 3344.0 1800 200.0
15 30 销售部 重庆 3344 黄蓉 销售主管 7800.0 3000 800.0
16 30 销售部 重庆 4466 苗人凤 销售员 3344.0 2500 NaN
```
`merge`函数的一个参数代表合并的左表、第二个参数代表合并的右表有SQL编程经验的同学对这两个词是不是感觉到非常亲切。正如大家猜想的那样`DataFrame`对象的合并跟数据库中的表连接非常类似,所以上面代码中的`how`代表了合并两张表的方式,有`left`、`right`、`inner`、`outer`四个选项;而`on`则代表了基于哪个列实现表的合并,相当于 SQL 表连接中的连表条件,如果左右两表对应的列列名不同,可以用`left_on`和`right_on`参数取代`on`参数分别进行指定。
如果对上面的代码稍作修改,将`how`参数修改为`left`,大家可以思考一下代码执行的结果。
```Python
pd.merge(dept_df, all_emp_df, how='left', on='dno')
```
运行结果比之前的输出多出了如下所示的一行,这是因为`left`代表左外连接,也就意味着左表`dept_df`中的数据会被完整的查出来,但是在`all_emp_df`中又没有编号为`40` 部门的员工,所以对应的位置都被填入了空值。
```
17 40 运维部 天津 NaN NaN NaN NaN NaN NaN
```

View File

@ -0,0 +1,553 @@
## Pandas的应用-3
### DataFrame的应用
#### 数据清洗
通常,我们从 Excel、CSV 或数据库中获取到的数据并不是非常完美的,里面可能因为系统或人为的原因混入了重复值或异常值,也可能在某些字段上存在缺失值;再者,`DataFrame`中的数据也可能存在格式不统一、量纲不统一等各种问题。因此,在开始数据分析之前,对数据进行清洗就显得特别重要。
##### 缺失值
可以使用`DataFrame`对象的`isnull`或`isna`方法来找出数据表中的缺失值,如下所示。
```Python
emp_df.isnull()
```
或者
```Python
emp_df.isna()
```
输出:
```
ename job mgr sal comm dno
eno
1359 False False False False False False
2056 False False False False False False
3088 False False False False False False
3211 False False False False True False
3233 False False False False True False
3244 False False False False True False
3251 False False False False True False
3344 False False False False False False
3577 False False False False True False
3588 False False False False True False
4466 False False False False True False
5234 False False False False True False
5566 False False False False False False
7800 False False True False False False
```
相对应的,`notnull`和`notna`方法可以将非空的值标记为`True`。如果想删除这些缺失值,可以使用`DataFrame`对象的`dropna`方法,该方法的`axis`参数可以指定沿着0轴还是1轴删除也就是说当遇到空值时是删除整行还是删除整列默认是沿0轴进行删除的代码如下所示。
```Python
emp_df.dropna()
```
输出:
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344.0 1800 200.0 30
2056 乔峰 架构师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
5566 宋远桥 会计师 7800.0 4000 1000.0 10
```
如果要沿着1轴进行删除可以使用下面的代码。
```Python
emp_df.dropna(axis=1)
```
输出:
```
ename job sal dno
eno
1359 胡一刀 销售员 1800 30
2056 乔峰 架构师 5000 20
3088 李莫愁 设计师 3500 20
3211 张无忌 程序员 3200 20
3233 丘处机 程序员 3400 20
3244 欧阳锋 程序员 3200 20
3251 张翠山 程序员 4000 20
3344 黄蓉 销售主管 3000 30
3577 杨过 会计 2200 10
3588 朱九真 会计 2500 10
4466 苗人凤 销售员 2500 30
5234 郭靖 出纳 2000 10
5566 宋远桥 会计师 4000 10
7800 张三丰 总裁 9000 20
```
> **注意**`DataFrame`对象的很多方法都有一个名为`inplace`的参数,该参数的默认值为`False`,表示我们的操作不会修改原来的`DataFrame`对象,而是将处理后的结果通过一个新的`DataFrame`对象返回。如果将该参数的值设置为`True`,那么我们的操作就会在原来的`DataFrame`上面直接修改,方法的返回值为`None`。简单的说,上面的操作并没有修改`emp_df`,而是返回了一个新的`DataFrame`对象。
在某些特定的场景下,我们可以对空值进行填充,对应的方法是`fillna`,填充空值时可以使用指定的值(通过`value`参数进行指定),也可以用表格中前一个单元格(通过设置参数`method=ffill`)或后一个单元格(通过设置参数`method=bfill`)的值进行填充,当代码如下所示。
```Python
emp_df.fillna(value=0)
```
> **注意**:填充的值如何选择也是一个值得探讨的话题,实际工作中,可能会使用某种统计量(如:均值、众数等)进行填充,或者使用某种插值法(如:随机插值法、拉格朗日插值法等)进行填充,甚至有可能通过回归模型、贝叶斯模型等对缺失数据进行填充。
输出:
```
ename job mgr sal comm dno
eno
1359 胡一刀 销售员 3344.0 1800 200.0 30
2056 乔峰 分析师 7800.0 5000 1500.0 20
3088 李莫愁 设计师 2056.0 3500 800.0 20
3211 张无忌 程序员 2056.0 3200 0.0 20
3233 丘处机 程序员 2056.0 3400 0.0 20
3244 欧阳锋 程序员 3088.0 3200 0.0 20
3251 张翠山 程序员 2056.0 4000 0.0 20
3344 黄蓉 销售主管 7800.0 3000 800.0 30
3577 杨过 会计 5566.0 2200 0.0 10
3588 朱九真 会计 5566.0 2500 0.0 10
4466 苗人凤 销售员 3344.0 2500 0.0 30
5234 郭靖 出纳 5566.0 2000 0.0 10
5566 宋远桥 会计师 7800.0 4000 1000.0 10
7800 张三丰 总裁 0.0 9000 1200.0 20
```
##### 重复值
接下来,我们先给之前的部门表添加两行数据,让部门表中名为“研发部”和“销售部”的部门各有两个。
```Python
dept_df.loc[50] = {'dname': '研发部', 'dloc': '上海'}
dept_df.loc[60] = {'dname': '销售部', 'dloc': '长沙'}
dept_df
```
输出:
```
dname dloc
dno
10 会计部 北京
20 研发部 成都
30 销售部 重庆
40 运维部 天津
50 研发部 上海
60 销售部 长沙
```
现在,我们的数据表中有重复数据了,我们可以通过`DataFrame`对象的`duplicated`方法判断是否存在重复值,该方法在不指定参数时默认判断行索引是否重复,我们也可以指定根据部门名称`dname`判断部门是否重复,代码如下所示。
```Python
dept_df.duplicated('dname')
```
输出:
```
dno
10 False
20 False
30 False
40 False
50 True
60 True
dtype: bool
```
从上面的输出可以看到,`50`和`60`两个部门从部门名称上来看是重复的,如果要删除重复值,可以使用`drop_duplicates`方法,该方法的`keep`参数可以控制在遇到重复值时,保留第一项还是保留最后一项,或者多个重复项一个都不用保留,全部删除掉。
```Python
dept_df.drop_duplicates('dname')
```
输出:
```
dname dloc
dno
10 会计部 北京
20 研发部 成都
30 销售部 重庆
40 运维部 天津
```
将`keep`参数的值修改为`last`。
```Python
dept_df.drop_duplicates('dname', keep='last')
```
输出:
```
dname dloc
dno
10 会计部 北京
40 运维部 天津
50 研发部 上海
60 销售部 长沙
```
##### 异常值
异常值在统计学上的全称是疑似异常值也称作离群点outlier异常值的分析也称作离群点分析。异常值是指样本中出现的“极端值”数据值看起来异常大或异常小其分布明显偏离其余的观测值。实际工作中有些异常值可能是由系统或人为原因造成的但有些异常值却不是它们能够重复且稳定的出现属于正常的极端值例如很多游戏产品中头部玩家的数据往往都是离群的极端值。所以我们既不能忽视异常值的存在也不能简单地把异常值从数据分析中剔除。重视异常值的出现分析其产生的原因常常成为发现问题进而改进决策的契机。
异常值的检测有Z-score 方法、IQR 方法、DBScan 聚类、孤立森林等,这里我们对前两种方法做一个简单的介绍。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211004192858.png" style="zoom:50%;">
如果数据服从正态分布依据3σ法则异常值被定义与平均值的偏差超过三倍标准差的值。在正态分布下距离平均值3σ之外的值出现的概率为$ P(|x-\mu|>3\sigma)<0.003 $属于小概率事件如果数据不服从正态分布那么可以用远离平均值的多少倍的标准差来描述这里的倍数就是Z-scoreZ-score以标准差为单位去度量某一原始分数偏离平均值的距离公式如下所示
$$
z = \frac {X - \mu} {\sigma}
$$
Z-score需要根据经验和实际情况来决定通常把远离标准差`3`倍距离以上的数据点视为离群点下面的代给出了如何通过Z-score方法检测异常值。
```Python
import numpy as np
def detect_outliers_zscore(data, threshold=3):
avg_value = np.mean(data)
std_value = np.std(data)
z_score = np.abs((data - avg_value) / std_value)
return data[z_score > threshold]
```
IQR 方法中的IQRInter-Quartile Range代表四分位距离即上四分位数Q3和下四分位数Q1的差值。通常情况下可以认为小于 $ Q1 - 1.5 \times IQR $ 或大于 $ Q3 + 1.5 \times IQR $ 的就是异常值,而这种检测异常值的方法也是箱线图(后面会讲到)默认使用的方法。下面的代给出了如何通过 IQR 方法检测异常值。
```Python
import numpy as np
def detect_outliers_iqr(data, whis=1.5):
q1, q3 = np.quantile(data, [0.25, 0.75])
iqr = q3 - q1
lower, upper = q1 - whis * iqr, q3 + whis * iqr
return data[(data < lower) | (data > upper)]
```
如果要删除异常值,可以使用`DataFrame`对象的`drop`方法,该方法可以根据行索引或列索引删除指定的行或列。例如我们认为月薪低于`2000`或高于`8000`的是员工表中的异常值,可以用下面的代码删除对应的记录。
```Python
emp_df.drop(emp_df[(emp_df.sal > 8000) | (emp_df.sal < 2000)].index)
```
如果要替换掉异常值,可以通过给单元格赋值的方式来实现,也可以使用`replace`方法将指定的值替换掉。例如我们要将月薪为`1800`和`9000`的替换为月薪的平均值,补贴为`800`的替换为`1000`,代码如下所示。
```Python
avg_sal = np.mean(emp_df.sal).astype(int)
emp_df.replace({'sal': [1800, 9000], 'comm': 800}, {'sal': avg_sal, 'comm': 1000})
```
##### 预处理
对数据进行预处理也是一个很大的话题,它包含了对数据的拆解、变换、归约、离散化等操作。我们先来看看数据的拆解。如果数据表中的数据是一个时间日期,我们通常都需要从年、季度、月、日、星期、小时、分钟等维度对其进行拆解,如果时间日期是用字符串表示的,可以先通过`pandas`的`to_datetime`函数将其处理成时间日期。
在下面的例子中,我们先读取 Excel 文件,获取到一组销售数据,其中第一列就是销售日期,我们将其拆解为“月份”、“季度”和“星期”,代码如下所示。
```Python
sales_df = pd.read_excel(
'2020年销售数据.xlsx',
usecols=['销售日期', '销售区域', '销售渠道', '品牌', '销售额']
)
sales_df.info()
```
> **说明**:如果需要上面例子中的 Excel 文件可以通过下面的百度云盘地址进行获取数据在《从零开始学数据分析》目录中。链接https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g提取码e7b4。
输出:
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1945 entries, 0 to 1944
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 销售日期 1945 non-null datetime64[ns]
1 销售区域 1945 non-null object
2 销售渠道 1945 non-null object
3 品牌 1945 non-null object
4 销售额 1945 non-null int64
dtypes: datetime64[ns](1), int64(1), object(3)
memory usage: 76.1+ KB
```
```Python
sales_df['月份'] = sales_df['销售日期'].dt.month
sales_df['季度'] = sales_df['销售日期'].dt.quarter
sales_df['星期'] = sales_df['销售日期'].dt.weekday
sales_df
```
输出:
```
销售日期 销售区域 销售渠道 品牌 销售额 月份 季度 星期
0 2020-01-01 上海 拼多多 八匹马 8217 1 1 2
1 2020-01-01 上海 抖音 八匹马 6351 1 1 2
2 2020-01-01 上海 天猫 八匹马 14365 1 1 2
3 2020-01-01 上海 天猫 八匹马 2366 1 1 2
4 2020-01-01 上海 天猫 皮皮虾 15189 1 1 2
... ... ... ... ... ... ... ... ...
1940 2020-12-30 北京 京东 花花姑娘 6994 12 4 2
1941 2020-12-30 福建 实体 八匹马 7663 12 4 2
1942 2020-12-31 福建 实体 花花姑娘 14795 12 4 3
1943 2020-12-31 福建 抖音 八匹马 3481 12 4 3
1944 2020-12-31 福建 天猫 八匹马 2673 12 4 3
```
在上面的代码中,通过日期时间类型的`Series`对象的`dt` 属性,获得一个访问日期时间的对象,通过该对象的`year`、`month`、`quarter`、`hour`等属性,就可以获取到年、月、季度、小时等时间信息,获取到的仍然是一个`Series`对象,它包含了一组时间信息,所以我们通常也将这个`dt`属性称为“日期时间向量”。
我们再来说一说字符串类型的数据的处理,我们先从指定的 Excel 文件中读取某招聘网站的招聘数据。
```Python
jobs_df = pd.read_csv(
'某招聘网站招聘数据.csv',
usecols=['city', 'companyFullName', 'positionName', 'salary']
)
jobs_df.info()
```
> **说明**:如果需要上面例子中的 Excel 文件可以通过下面的百度云盘地址进行获取数据在《从零开始学数据分析》目录中。链接https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g提取码e7b4。
输出:
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3140 entries, 0 to 3139
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 city 3140 non-null object
1 companyFullName 3140 non-null object
2 positionName 3140 non-null object
3 salary 3140 non-null object
dtypes: object(4)
memory usage: 98.2+ KB
```
查看前`5`条数据。
```Python
jobs_df.head()
```
输出:
```
city companyFullName positionName salary
0 北京 达疆网络科技(上海)有限公司 数据分析岗 15k-30k
1 北京 北京音娱时光科技有限公司 数据分析 10k-18k
2 北京 北京千喜鹤餐饮管理有限公司 数据分析 20k-30k
3 北京 吉林省海生电子商务有限公司 数据分析 33k-50k
4 北京 韦博网讯科技(北京)有限公司 数据分析 10k-15k
```
上面的数据表一共有`3140`条数据,但并非所有的职位都是“数据分析”的岗位,如果要筛选出数据分析的岗位,可以通过检查`positionName`字段是否包含“数据分析”这个关键词,这里需要模糊匹配,应该如何实现呢?我们可以先获取`positionName`列,因为这个`Series`对象的`dtype`是字符串,所以可以通过`str`属性获取对应的字符串向量,然后就可以利用我们熟悉的字符串的方法来对其进行操作,代码如下所示。
```Python
jobs_df = jobs_df[jobs_df.positionName.str.contains('数据分析')]
jobs_df.shape
```
输出:
```
(1515, 4)
```
可以看出,筛选后的数据还有`1515`条。接下来,我们还需要对`salary`字段进行处理,如果我们希望统计所有岗位的平均工资或每个城市的平均工资,首先需要将用范围表示的工资处理成其中间值,代码如下所示。
```Python
jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?')
```
> **说明**上面的代码通过正则表达式捕获组从字符串中抽取出两组数字分别对应工资的下限和上限对正则表达式不熟悉的读者可以阅读我的知乎专栏“从零开始学Python”中的[《正则表达式的应用》](https://zhuanlan.zhihu.com/p/158929767)一文。
输出:
```
0 1
0 15 30
1 10 18
2 20 30
3 33 50
4 10 15
... ... ...
3065 8 10
3069 6 10
3070 2 4
3071 6 12
3088 8 12
```
需要提醒大家的是,抽取出来的两列数据都是字符串类型的值,我们需要将其转换成`int`类型,才能计算平均值,对应的方法是`DataFrame`对象的`applymap`方法,该方法的参数是一个函数,而该函数会作用于`DataFrame`中的每个元素。完成这一步之后,我们就可以使用`apply`方法将上面的`DataFrame`处理成中间值,`apply`方法的参数也是一个函数,可以通过指定`axis`参数使其作用于`DataFrame` 对象的行或列,代码如下所示。
```Python
temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
temp_df.apply(np.mean, axis=1)
```
输出:
```
0 22.5
1 14.0
2 25.0
3 41.5
4 12.5
...
3065 9.0
3069 8.0
3070 3.0
3071 9.0
3088 10.0
Length: 1515, dtype: float64
```
接下来,我们可以用上面的结果替换掉原来的`salary`列或者增加一个新的列来表示职位对应的工资,完整的代码如下所示。
```Python
temp_df = jobs_df.salary.str.extract(r'(\d+)[kK]?-(\d+)[kK]?').applymap(int)
jobs_df['salary'] = temp_df.apply(np.mean, axis=1)
jobs_df.head()
```
输出:
```
city companyFullName positionName salary
0 北京 达疆网络科技(上海)有限公司 数据分析岗 22.5
1 北京 北京音娱时光科技有限公司 数据分析 14.0
2 北京 北京千喜鹤餐饮管理有限公司 数据分析 25.0
3 北京 吉林省海生电子商务有限公司 数据分析 41.5
4 北京 韦博网讯科技(北京)有限公司 数据分析 12.5
```
`applymap`和`apply`两个方法在数据预处理的时候经常用到,`Series`对象也有`apply`方法,也是用于数据的预处理,但是`DataFrame`对象还有一个名为`transform` 的方法,也是通过传入的函数对数据进行变换,类似`Series`对象的`map`方法。需要强调的是,`apply`方法具有归约效果的,简单的说就是能将较多的数据处理成较少的数据或一条数据;而`transform`方法没有归约效果,只能对数据进行变换,原来有多少条数据,处理后还是有多少条数据。
如果要对数据进行深度的分析和挖掘,字符串、日期时间这样的非数值类型都需要处理成数值,因为非数值类型没有办法计算相关性,也没有办法进行$\chi^2$检验等操作。对于字符串类型,通常可以其分为以下三类,再进行对应的处理。
1. 有序变量Ordinal Variable字符串表示的数据有顺序关系那么可以对字符串进行序号化处理。
2. 分类变量Categorical Variable/ 名义变量Nominal Variable字符串表示的数据没有大小关系和等级之分那么就可以使用独热编码的方式处理成哑变量虚拟变量矩阵。
3. 定距变量Scale Variable字符串本质上对应到一个有大小高低之分的数据而且可以进行加减运算那么只需要将字符串处理成对应的数值即可。
对于第1类和第3类我们可以用上面提到的`apply`或`transform`方法来处理,也可以利用`scikit-learn`中的`OrdinalEncoder`处理第1类字符串这个我们在后续的课程中会讲到。对于第2类字符串可以使用`pandas`的`get_dummies()`函数来生成哑变量(虚拟变量)矩阵,代码如下所示。
```Python
persons_df = pd.DataFrame(
data={
'姓名': ['关羽', '张飞', '赵云', '马超', '黄忠'],
'职业': ['医生', '医生', '程序员', '画家', '教师'],
'学历': ['研究生', '大专', '研究生', '高中', '本科']
}
)
persons_df
```
输出:
```
姓名 职业 学历
0 关羽 医生 研究生
1 张飞 医生 大专
2 赵云 程序员 研究生
3 马超 画家 高中
4 黄忠 教师 本科
```
将职业处理成哑变量矩阵。
```Python
pd.get_dummies(persons_df['职业'])
```
输出:
```
医生 教师 画家 程序员
0 1 0 0 0
1 1 0 0 0
2 0 0 0 1
3 0 0 1 0
4 0 1 0 0
```
将学历处理成大小不同的值。
```Python
def handle_education(x):
edu_dict = {'高中': 1, '大专': 3, '本科': 5, '研究生': 10}
return edu_dict.get(x, 0)
persons_df['学历'].apply(handle_education)
```
输出:
```
0 10
1 3
2 10
3 1
4 5
Name: 学历, dtype: int64
```
我们再来说说数据离散化。离散化也叫分箱如果变量的取值是连续值那么它的取值有无数种可能在进行数据分组的时候就会非常的不方便这个时候将连续变量离散化就显得非常重要。之所以把离散化叫做分箱是因为我们可以预先设置一些箱子每个箱子代表了数据取值的范围这样就可以将连续的值分配到不同的箱子中从而实现离散化。下面的例子读取了2018年北京积分落户数据我们可以根据落户积分对数据进行分组具体的做法如下所示。
```Python
luohu_df = pd.read_csv('data/2018年北京积分落户数据.csv', index_col='id')
luohu_df.score.describe()
```
输出:
```
count 6019.000000
mean 95.654552
std 4.354445
min 90.750000
25% 92.330000
50% 94.460000
75% 97.750000
max 122.590000
Name: score, dtype: float64
```
可以看出,落户积分的最大值是`122.59`,最小值是`90.75`,那么我们可以构造一个从`90`分到`125`分,每`5`分一组的`7`个箱子,`pandas`的`cut`函数可以帮助我们首先数据分箱,代码如下所示。
```Python
bins = np.arange(90, 126, 5)
pd.cut(luohu_df.score, bins, right=False)
```
> **说明**`cut`函数的`right`参数默认值为`True`,表示箱子左开右闭;修改为`False`可以让箱子的右边界为开区间,左边界为闭区间,大家看看下面的输出就明白了。
输出:
```
id
1 [120, 125)
2 [120, 125)
3 [115, 120)
4 [115, 120)
5 [115, 120)
...
6015 [90, 95)
6016 [90, 95)
6017 [90, 95)
6018 [90, 95)
6019 [90, 95)
Name: score, Length: 6019, dtype: category
Categories (7, interval[int64, left]): [[90, 95) < [95, 100) < [100, 105) < [105, 110) < [110, 115) < [115, 120) < [120, 125)]
```
我们可以根据分箱的结果对数据进行分组,然后使用聚合函数对每个组进行统计,这是数据分析中经常用到的操作,下一个章节会为大家介绍。除此之外,`pandas`还提供了一个名为`qcut`的函数,可以指定分位数对数据进行分箱,有兴趣的读者可以自行研究。

View File

@ -0,0 +1,484 @@
## Pandas的应用-4
### DataFrame的应用
#### 数据分析
经过前面的学习,我们已经将数据准备就绪而且变成了我们想要的样子,接下来就是最为重要的数据分析阶段了。当我们拿到一大堆数据的时候,如何从数据中迅速的解读出有价值的信息,这就是数据分析要解决的问题。首先,我们可以获取数据的描述性统计信息,通过描述性统计信息,我们可以了解数据的集中趋势和离散趋势。
例如,我们有如下所示的学生成绩表。
```Python
import numpy as np
import pandas as pd
scores = np.random.randint(50, 101, (5, 3))
names = ('关羽', '张飞', '赵云', '马超', '黄忠')
courses = ('语文', '数学', '英语')
df = pd.DataFrame(data=scores, columns=courses, index=names)
df
```
输出:
```
语文 数学 英语
关羽 96 72 73
张飞 72 70 97
赵云 74 51 79
马超 100 54 54
黄忠 89 100 88
```
我们可以通过`DataFrame`对象的方法`mean`、`max`、`min`、`std`、`var`等方法分别获取每个学生或每门课程的平均分、最高分、最低分、标准差、方差等信息,也可以直接通过`describe`方法直接获取描述性统计信息,代码如下所示。
计算每门课程成绩的平均分。
```Python
df.mean()
```
输出:
```
语文 86.2
数学 69.4
英语 78.2
dtype: float64
```
计算每个学生成绩的平均分。
```Python
df.mean(axis=1)
```
输出:
```
关羽 80.333333
张飞 79.666667
赵云 68.000000
马超 69.333333
黄忠 92.333333
dtype: float64
```
计算每门课程成绩的方差。
```Python
df.var()
```
输出:
```
语文 161.2
数学 379.8
英语 265.7
dtype: float64
```
> **说明**:通过方差可以看出,数学成绩波动最大,最不稳定。
获取每门课程的描述性统计信息。
```Python
df.describe()
```
输出:
```
语文 数学 英语
count 5.000000 5.000000 5.000000
mean 86.200000 69.400000 78.200000
std 12.696456 19.488458 16.300307
min 72.000000 51.000000 54.000000
25% 74.000000 54.000000 73.000000
50% 89.000000 70.000000 79.000000
75% 96.000000 72.000000 88.000000
max 100.000000 100.000000 97.000000
```
##### 排序和Top-N
如果需要对数据进行排序,可以使用`DataFrame`对象的`sort_values`方法,该方法的`by`参数可以指定根据哪个列或哪些列进行排序,而`ascending`参数可以指定升序或是降序。例如,下面的代码展示了如何将学生表按语文成绩排降序。
```Python
df.sort_values(by='语文', ascending=False)
```
输出:
```
语文 数学 英语
马超 100 54 54
关羽 96 72 73
黄忠 89 100 88
赵云 74 51 79
张飞 72 70 97
```
如果`DataFrame`数据量很大排序将是一个非常耗费时间的操作。有的时候我们只需要获得排前N名或后N名的数据这个时候其实没有必要对整个数据进行排序而是直接利用堆结构找出Top-N的数据。`DataFrame`的`nlargest`和`nsmallest`方法就提供对Top-N操作的支持代码如下所示。
找出语文成绩前3名的学生信息。
```Python
df.nlargest(3, '语文')
```
输出:
```
语文 数学 英语
马超 100 54 54
关羽 96 72 73
黄忠 89 100 88
```
找出数学成绩最低的3名学生的信息。
```Python
df.nsmallest(3, '数学')
```
输出:
```
语文 数学 英语
赵云 74 51 79
马超 100 54 54
张飞 72 70 97
```
##### 分组聚合操作
我们先从 Excel 文件中读取一组销售数据,然后再为大家演示如何进行分组聚合操作。
```Python
df = pd.read_excel('2020年销售数据.xlsx')
df.head()
```
> **说明**:如果需要上面例子中的 Excel 文件可以通过下面的阿里云盘地址进行获取该文件在“我的分享”下面的“数据集”目录中。地址https://www.aliyundrive.com/s/oPi7DRAVKRm。
输出:
```
销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量
0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83
1 2020-01-01 上海 抖音 205635-402 八匹马 219 29
2 2020-01-01 上海 天猫 205654-021 八匹马 169 85
3 2020-01-01 上海 天猫 205654-519 八匹马 169 14
4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61
```
如果我们要统计每个销售区域的销售总额,可以先通过“售价”和“销售数量”计算出销售额,为`DataFrame`添加一个列,代码如下所示。
```Python
df['销售额'] = df['售价'] * df['销售数量']
df.head()
```
输出:
```
销售日期 销售区域 销售渠道 销售订单 品牌 售价 销售数量 销售额
0 2020-01-01 上海 拼多多 182894-455 八匹马 99 83 8217
1 2020-01-01 上海 抖音 205635-402 八匹马 219 29 6351
2 2020-01-01 上海 天猫 205654-021 八匹马 169 85 14365
3 2020-01-01 上海 天猫 205654-519 八匹马 169 14 2366
4 2020-01-01 上海 天猫 377781-010 皮皮虾 249 61 15189
```
然后再根据“销售区域”列对数据进行分组,这里我们使用的是`DataFrame`对象的`groupby`方法。分组之后,我们取“销售额”这个列在分组内进行求和处理,代码和结果如下所示。
```Python
df.groupby('销售区域').销售额.sum()
```
输出:
```
销售区域
上海 11610489
北京 12477717
南京 1767301
安徽 895463
广东 1617949
江苏 537079
浙江 687862
福建 10178227
Name: 销售额, dtype: int64
```
如果我们要统计每个月的销售总额我们可以将“销售日期”作为groupby`方法的参数,当然这里需要先将“销售日期”处理成月,代码和结果如下所示。
```Python
df.groupby(df['销售日期'].dt.month).销售额.sum()
```
输出:
```
销售日期
1 5409855
2 4608455
3 4164972
4 3996770
5 3239005
6 2817936
7 3501304
8 2948189
9 2632960
10 2375385
11 2385283
12 1691973
Name: 销售额, dtype: int64
```
接下来我们将难度升级,统计每个销售区域每个月的销售总额,这又该如何处理呢?事实上,`groupby`方法的第一个参数可以是一个列表,列表中可以指定多个分组的依据,大家看看下面的代码和输出结果就明白了。
```Python
df.groupby(['销售区域', df['销售日期'].dt.month]).销售额.sum()
```
输出:
```
销售区域 销售日期
上海 1 1679125
2 1689527
3 1061193
4 1082187
5 841199
6 785404
7 863906
8 734937
9 1107693
10 412108
11 825169
12 528041
北京 1 1878234
2 1807787
3 1360666
4 1205989
5 807300
6 1216432
7 1219083
8 645727
9 390077
10 671608
11 678668
12 596146
南京 7 841032
10 710962
12 215307
安徽 4 341308
5 554155
广东 3 388180
8 469390
9 365191
11 395188
江苏 4 537079
浙江 3 248354
8 439508
福建 1 1852496
2 1111141
3 1106579
4 830207
5 1036351
6 816100
7 577283
8 658627
9 769999
10 580707
11 486258
12 352479
Name: 销售额, dtype: int64
```
如果希望统计出每个区域的销售总额以及每个区域单笔金额的最高和最低,我们可以在`DataFrame`或`Series`对象上使用`agg`方法并指定多个聚合函数,代码和结果如下所示。
```Python
df.groupby('销售区域').销售额.agg(['sum', 'max', 'min'])
```
输出:
```
sum max min
销售区域
上海 11610489 116303 948
北京 12477717 133411 690
南京 1767301 87527 1089
安徽 895463 68502 1683
广东 1617949 120807 990
江苏 537079 114312 3383
浙江 687862 90909 3927
福建 10178227 87527 897
```
如果希望自定义聚合后的列的名字,可以使用如下所示的方法。
```Python
df.groupby('销售区域').销售额.agg(销售总额='sum', 单笔最高='max', 单笔最低='min')
```
输出:
```
销售总额 单笔最高 单笔最低
销售区域
上海 11610489 116303 948
北京 12477717 133411 690
南京 1767301 87527 1089
安徽 895463 68502 1683
广东 1617949 120807 990
江苏 537079 114312 3383
浙江 687862 90909 3927
福建 10178227 87527 897
```
如果需要对多个列使用不同的聚合函数,例如“统计每个销售区域销售额的平均值以及销售数量的最低值和最高值”,我们可以按照下面的方式来操作。
```Python
df.groupby('销售区域')[['销售额', '销售数量']].agg({
'销售额': 'mean', '销售数量': ['max', 'min']
})
```
输出:
```
销售额 销售数量
mean max min
销售区域
上海 20622.538188 100 10
北京 20125.350000 100 10
南京 22370.898734 100 11
安徽 26337.147059 98 16
广东 32358.980000 98 10
江苏 29837.722222 98 15
浙江 27514.480000 95 20
福建 18306.163669 100 10
```
##### 透视表和交叉表
上面的例子中,“统计每个销售区域每个月的销售总额”会产生一个看起来很长的结果,在实际工作中我们通常把那些行很多列很少的表成为“窄表”,如果我们不想得到这样的一个“窄表”,可以使用`DataFrame`的`pivot_table`方法或者是`pivot_table`函数来生成透视表。透视表的本质就是对数据进行分组聚合操作,**根据 A 列对 B 列进行统计**,如果大家有使用 Excel 的经验,相信对透视表这个概念一定不会陌生。例如,我们要“统计每个销售区域的销售总额”,那么“销售区域”就是我们的 A 列,而“销售额”就是我们的 B 列,在`pivot_table`函数中分别对应`index`和`values`参数,这两个参数都可以是单个列或者多个列。
```Python
pd.pivot_table(df, index='销售区域', values='销售额', aggfunc='sum')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211106180912.png" style="zoom:50%">
> **注意**:上面的结果操作跟之前用`groupby`的方式得到的结果有一些区别,`groupby`操作后,如果对单个列进行聚合,得到的结果是一个`Series`对象,而上面的结果是一个`DataFrame` 对象。
如果要统计每个销售区域每个月的销售总额,也可以使用`pivot_table`函数,代码如下所示。
```Python
pd.pivot_table(df, index=['销售区域', df['销售日期'].dt.month], values='销售额', aggfunc='sum')
```
上面的操作结果是一个`DataFrame`,但也是一个长长的“窄表”,如果希望做成一个行比较少列比较多的“宽表”,可以将`index`参数中的列放到`columns`参数中,代码如下所示。
```Python
pd.pivot_table(
df, index='销售区域', columns=df['销售日期'].dt.month,
values='销售额', aggfunc='sum', fill_value=0
)
```
> **说明**`pivot_table`函数的`fill_value=0`会将空值处理为`0`。
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211106104551.png" style="zoom:50%">
使用`pivot_table`函数时,还可以通过添加`margins`和`margins_name`参数对分组聚合的结果做一个汇总,具体的操作和效果如下所示。
```Python
df['月份'] = df['销售日期'].dt.month
pd.pivot_table(
df, index='销售区域', columns='月份',
values='销售额', aggfunc='sum', fill_value=0,
margins=True, margins_name='总计'
)
```
输出:
![image-20211106181707655](https://gitee.com/jackfrued/mypic/raw/master/20211106181707.png)
交叉表就是一种特殊的透视表,它不需要先构造一个`DataFrame`对象,而是直接通过数组或`Series`对象指定两个或多个因素进行运算得到统计结果。例如,我们要统计每个销售区域的销售总额,也可以按照如下所示的方式来完成,我们先准备三组数据。
```Python
sales_area, sales_month, sales_amount = df['销售区域'], df['月份'], df['销售额']
```
使用`crosstab`函数生成交叉表。
```Python
pd.crosstab(
index=sales_area, columns=sales_month, values=sales_amount, aggfunc='sum'
).fillna(0).applymap(int)
```
> **说明**:上面的代码使用了`DataFrame`对象的`fillna`方法将空值处理为0再使用`applymap`方法将数据类型处理成整数。
#### 数据可视化
一图胜千言,我们对数据进行透视的结果,最终要通过图表的方式呈现出来,因为图表具有极强的表现力,能够让我们迅速的解读数据中隐藏的价值。和`Series`一样,`DataFrame`对象提供了`plot`方法来支持绘图,底层仍然是通过`matplotlib`库实现图表的渲染。关于`matplotlib`的内容,我们在下一个章节进行详细的探讨,这里我们只简单的讲解`plot`方法的用法。
例如,我们想通过一张柱状图来比较“每个销售区域的销售总额”,可以直接在透视表上使用`plot`方法生成柱状图。我们先导入`matplotlib.pyplot`模块,通过修改绘图的参数使其支持中文显示。
```Python
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = 'FZJKai-Z03S'
```
> **说明**:上面的`FZJKai-Z03S`是我电脑上已经安装的一种支持中文的字体的名称,字体的名称可以通过查看用户主目录下`.matplotlib`文件夹下名为`fontlist-v330.json`的文件来获得,而这个文件在执行上面的命令后就会生成。
使用魔法指令配置生成矢量图。
```Python
%config InlineBackend.figure_format = 'svg'
```
绘制“每个销售区域销售总额”的柱状图。
```Python
temp = pd.pivot_table(df, index='销售区域', values='销售额', aggfunc='sum')
temp.plot(figsize=(8, 4), kind='bar')
plt.xticks(rotation=0)
plt.show()
```
> **说明**上面的第3行代码会将横轴刻度上的文字旋转到0度第4行代码会显示图像。
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211106195040.png" style="zoom:50%">
如果要绘制饼图,可以修改`plot`方法的`kind`参数为`pie`,然后使用定制饼图的参数对图表加以定制,代码如下所示。
```Python
temp.sort_values(by='销售额', ascending=False).plot(
figsize=(6, 6), kind='pie', y='销售额',
autopct='%.2f%%', pctdistance=0.8,
wedgeprops=dict(linewidth=1, width=0.35)
)
plt.legend(loc='center')
plt.show()
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211106201550.png" style="zoom:50%">

View File

@ -0,0 +1,448 @@
## Pandas的应用-5
### DataFrame的应用
#### 窗口计算
`DataFrame`对象的`rolling`方法允许我们将数据置于窗口中然后就可以使用函数对窗口中的数据进行运算和处理。例如我们获取了某只股票近期的数据想制作5日均线和10日均线那么就需要先设置窗口再进行运算。我们可以使用三方库`pandas-datareader`来获取指定的股票在某个时间段内的数据,具体的操作如下所示。
安装`pandas-datareader`三方库。
```Bash
pip install pandas-datareader
```
通过`pandas-datareader` 提供的`get_data_stooq`从 Stooq 网站获取百度股票代码BIDU近期股票数据。
```Python
import pandas_datareader as pdr
baidu_df = pdr.get_data_stooq('BIDU', start='2021-11-22', end='2021-12-7')
baidu_df.sort_index(inplace=True)
baidu_df
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208205710.png" style="zoom:38%;">
上面的`DataFrame`有`Open`、`High`、`Low`、`Close`、`Volume`五个列,分别代码股票的开盘价、最高价、最低价、收盘价和成交量,接下来我们对百度的股票数据进行窗口计算。
```Python
baidu_df.rolling(5).mean()
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208205932.png" style="zoom:38%;">
上面的`Close` 列的数据就是我们需要的5日均线当然我们也可以用下面的方法直接在`Close`列对应的`Series`对象上计算5日均线。
```Python
baidu_df.Close.rolling(5).mean()
```
输出:
```
Date
2021-11-22 NaN
2021-11-23 NaN
2021-11-24 NaN
2021-11-26 NaN
2021-11-29 150.608
2021-11-30 151.014
2021-12-01 150.682
2021-12-02 150.196
2021-12-03 147.062
2021-12-06 146.534
2021-12-07 146.544
Name: Close, dtype: float64
```
#### 相关性判定
在统计学中我们通常使用协方差covariance来衡量两个随机变量的联合变化程度。如果变量 $X$ 的较大值主要与另一个变量 $Y$ 的较大值相对应,而两者较小值也相对应,那么两个变量倾向于表现出相似的行为,协方差为正。如果一个变量的较大值主要对应于另一个变量的较小值,则两个变量倾向于表现出相反的行为,协方差为负。简单的说,协方差的正负号显示着两个变量的相关性。方差是协方差的一种特殊情况,即变量与自身的协方差。
$$
cov(X,Y) = E((X - \mu)(Y - \upsilon)) = E(X \cdot Y) - \mu\upsilon
$$
如果 $X$ 和 $Y$ 是统计独立的那么二者的协方差为0这是因为在 $X$ 和 $Y$ 独立的情况下:
$$
E(X \cdot Y) = E(X) \cdot E(Y) = \mu\upsilon
$$
协方差的数值大小取决于变量的大小,通常是不容易解释的,但是正态形式的协方差大小可以显示两变量线性关系的强弱。在统计学中,皮尔逊积矩相关系数就是正态形式的协方差,它用于度量两个变量 $X$ 和 $Y$ 之间的相关程度(线性相关),其值介于`-1`到`1`之间。
$$
\rho{X,Y} = \frac {cov(X, Y)} {\sigma_{X}\sigma_{Y}}
$$
估算样本的协方差和标准差,可以得到样本皮尔逊系数,通常用希腊字母 $\rho$ 表示。
$$
\rho = \frac {\sum_{i=1}^{n}(X_i - \bar{X})(Y_i - \bar{Y})} {\sqrt{\sum_{i=1}^{n}(X_i - \bar{X})^2} \sqrt{\sum_{i=1}^{n}(Y_i - \bar{Y})^2}}
$$
我们用 $\rho$ 值判断指标的相关性时遵循以下两个步骤。
1. 判断指标间是正相关、负相关,还是不相关。
- 当 $ \rho \gt 0 $,认为变量之间是正相关,也就是两者的趋势一致。
- 当 $ \rho \lt 0 $,认为变量之间是负相关,也就是两者的趋势相反。
- 当 $ \rho = 0 $,认为变量之间是不相关的,但并不代表两个指标是统计独立的。
2. 判断指标间的相关程度。
- 当 $ \rho $ 的绝对值在 $ [0.6,1] $ 之间,认为变量之间是强相关的。
- 当 $ \rho $ 的绝对值在 $ [0.1,0.6) $ 之间,认为变量之间是弱相关的。
- 当 $ \rho $ 的绝对值在 $ [0,0.1) $ 之间,认为变量之间没有相关性。
皮尔逊相关系数适用于:
1. 两个变量之间是线性关系,都是连续数据。
2. 两个变量的总体是正态分布,或接近正态的单峰分布。
3. 两个变量的观测值是成对的,每对观测值之间相互独立。
`DataFrame`对象的`cov`方法和`corr`方法分别用于计算协方差和相关系数,`corr`方法的第一个参数`method`的默认值是`pearson`,表示计算皮尔逊相关系数;除此之外,还可以指定`kendall`或`spearman`来获得肯德尔系数或斯皮尔曼等级相关系数。
接下来,我们从名为`boston_house_price.csv`的文件中获取著名的[波士顿房价数据集](https://www.heywhale.com/mw/dataset/590bd595812ede32b73f55f2)来创建一个`DataFrame`,我们通过`corr`方法计算可能影响房价的`13`个因素中,哪些跟房价是正相关或负相关的,代码如下所示。
```Python
boston_df = pd.read_csv('data/csv/boston_house_price.csv')
boston_df.corr()
```
> **说明**:如果需要上面例子中的 CSV 文件,可以通过下面的百度云盘地址进行获取,数据在《从零开始学数据分析》目录中。链接:<https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g>提取码e7b4。
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208213325.png">
斯皮尔曼相关系数对数据条件的要求没有皮尔逊相关系数严格,只要两个变量的观测值是成对的等级评定资料,或者是由连续变量观测资料转化得到的等级资料,不论两个变量的总体分布形态、样本容量的大小如何,都可以用斯皮尔曼等级相关系数来进行研究。我们通过下面的方式来计算斯皮尔曼相关系数。
```Python
boston_df.corr('spearman')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208213518.png">
在 Notebook 或 JupyterLab 中,我们可以为`PRICE`列添加渐变色,用颜色直观的展示出跟房价负相关、正相关、不相关的列,`DataFrame`对象`style`属性的`background_gradient`方法可以完成这个操作,代码如下所示。
```Python
boston_df.corr('spearman').style.background_gradient('RdYlBu', subset=['PRICE'])
```
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208215228.png">
上面代码中的`RdYlBu`代表的颜色如下所示,相关系数的数据值越接近`1`,颜色越接近红色;数据值越接近`1`,颜色越接近蓝色;数据值在`0`附件则是黄色。
```Python
plt.get_cmap('RdYlBu')
```
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208215057.png">
### Index的应用
我们再来看看`Index`类型,它为`Series`和`DataFrame`对象提供了索引服务,常用的`Index`有以下几种。
#### 范围索引RangeIndex
代码:
```Python
sales_data = np.random.randint(400, 1000, 12)
month_index = pd.RangeIndex(1, 13, name='月份')
ser = pd.Series(data=sales_data, index=month_index)
ser
```
输出:
```
月份
1 703
2 705
3 557
4 943
5 961
6 615
7 788
8 985
9 921
10 951
11 874
12 609
dtype: int64
```
#### 分类索引CategoricalIndex
代码:
```Python
cate_index = pd.CategoricalIndex(
['苹果', '香蕉', '苹果', '苹果', '桃子', '香蕉'],
ordered=True,
categories=['苹果', '香蕉', '桃子']
)
ser = pd.Series(data=amount, index=cate_index)
ser
```
输出:
```
苹果 6
香蕉 6
苹果 7
苹果 6
桃子 8
香蕉 6
dtype: int64
```
代码:
```Python
ser.groupby(level=0).sum()
```
输出:
```
苹果 19
香蕉 12
桃子 8
dtype: int64
```
#### 多级索引MultiIndex
代码:
```Python
ids = np.arange(1001, 1006)
sms = ['期中', '期末']
index = pd.MultiIndex.from_product((ids, sms), names=['学号', '学期'])
courses = ['语文', '数学', '英语']
scores = np.random.randint(60, 101, (10, 3))
df = pd.DataFrame(data=scores, columns=courses, index=index)
df
```
> **说明**:上面的代码使用了`MultiIndex`的类方法`from_product`,该方法通过`ids`和`sms`两组数据的笛卡尔积构造了多级索引。
输出:
```
语文 数学 英语
学号 学期
1001 期中 93 77 60
期末 93 98 84
1002 期中 64 78 71
期末 70 71 97
1003 期中 72 88 97
期末 99 100 63
1004 期中 80 71 61
期末 91 62 72
1005 期中 82 95 67
期末 84 78 86
```
代码:
```Python
# 计算每个学生的成绩期中占25%期末占75%
df.groupby(level=0).agg(lambda x: x.values[0] * 0.25 + x.values[1] * 0.75)
```
输出:
```
语文 数学 英语
学号
1001 93.00 92.75 78.00
1002 68.50 72.75 90.50
1003 92.25 97.00 71.50
1004 88.25 64.25 69.25
1005 83.50 82.25 81.25
```
#### 日期时间索引DatetimeIndex
1. 通过`date_range()`函数,我们可以创建日期时间索引,代码如下所示。
代码:
```Python
pd.date_range('2021-1-1', '2021-6-1', periods=10)
```
输出:
```
DatetimeIndex(['2021-01-01 00:00:00', '2021-01-17 18:40:00',
'2021-02-03 13:20:00', '2021-02-20 08:00:00',
'2021-03-09 02:40:00', '2021-03-25 21:20:00',
'2021-04-11 16:00:00', '2021-04-28 10:40:00',
'2021-05-15 05:20:00', '2021-06-01 00:00:00'],
dtype='datetime64[ns]', freq=None)
```
代码:
```Python
pd.date_range('2021-1-1', '2021-6-1', freq='W')
```
输出:
```
DatetimeIndex(['2021-01-03', '2021-01-10', '2021-01-17', '2021-01-24',
'2021-01-31', '2021-02-07', '2021-02-14', '2021-02-21',
'2021-02-28', '2021-03-07', '2021-03-14', '2021-03-21',
'2021-03-28', '2021-04-04', '2021-04-11', '2021-04-18',
'2021-04-25', '2021-05-02', '2021-05-09', '2021-05-16',
'2021-05-23', '2021-05-30'],
dtype='datetime64[ns]', freq='W-SUN')
```
2. 通过`DateOffset`类型,我们可以设置时间差并和`DatetimeIndex`进行运算,具体的操作如下所示。
代码:
```Python
index = pd.date_range('2021-1-1', '2021-6-1', freq='W')
index - pd.DateOffset(days=2)
```
输出:
```
DatetimeIndex(['2021-01-01', '2021-01-08', '2021-01-15', '2021-01-22',
'2021-01-29', '2021-02-05', '2021-02-12', '2021-02-19',
'2021-02-26', '2021-03-05', '2021-03-12', '2021-03-19',
'2021-03-26', '2021-04-02', '2021-04-09', '2021-04-16',
'2021-04-23', '2021-04-30', '2021-05-07', '2021-05-14',
'2021-05-21', '2021-05-28'],
dtype='datetime64[ns]', freq=None)
```
代码:
```Python
index + pd.DateOffset(days=2)
```
输出:
```
DatetimeIndex(['2021-01-05', '2021-01-12', '2021-01-19', '2021-01-26',
'2021-02-02', '2021-02-09', '2021-02-16', '2021-02-23',
'2021-03-02', '2021-03-09', '2021-03-16', '2021-03-23',
'2021-03-30', '2021-04-06', '2021-04-13', '2021-04-20',
'2021-04-27', '2021-05-04', '2021-05-11', '2021-05-18',
'2021-05-25', '2021-06-01'],
dtype='datetime64[ns]', freq=None)
```
4. 可以使用`DatatimeIndex`类型的相关方法来处理数据,具体包括:
- `shift()`方法:通过时间前移或后移数据,我们仍然以上面百度股票数据为例,代码如下所示。
代码:
```Python
baidu_df.shift(3, fill_value=0)
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208220551.png" style="zoom:150%;">
代码:
```Python
baidu_df.shift(-1, fill_value=0)
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208220713.png" style="zoom:150%;">
- `asfreq()`方法:指定一个时间频率抽取对应的数据,代码如下所示。
代码:
```Python
baidu_df.asfreq('5D')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221202.png">
代码:
```Python
baidu_df.asfreq('5D', method='ffill')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221249.png" style="zoom:150%;">
- `resample()`方法:基于时间对数据进行重采样,相当于根据时间周期对数据进行了分组操作,代码如下所示。
代码:
```Python
baidu_df.resample('1M').mean()
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221429.png">
> **说明**:上面的代码中,`W`表示一周,`5D`表示`5`天,`1M`表示`1`个月。
5. 时区转换
- 获取时区信息。
```Python
import pytz
pytz.common_timezones
```
- `tz_localize()`方法:将日期时间本地化。
代码:
```Python
baidu_df = baidu_df.tz_localize('Asia/Chongqing')
baidu_df
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208221947.png">
- `tz_convert()`方法:转换时区。
代码:
```Python
baidu_df.tz_convert('America/New_York')
```
输出:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20211208222404.png">

220
Day66-80/76.概率基础.md Normal file
View File

@ -0,0 +1,220 @@
## 概率基础
### 数据的集中趋势
我们经常会使用以下几个指标来描述一组数据的集中趋势:
1. 均值 - 均值代表某个数据集的整体水平,我们经常提到的客单价、平均访问时长、平均配送时长等指标都是均值。均值的缺点是容易受极值的影响,虽然可以使用加权平均值来消除极值的影响,但是可能事先并不清楚数据的权重;对于正数可以用几何平均值来替代算术平均值。
- 算术平均值:$$\bar{x}=\frac{\sum_{i=1}^{n}x_{i}}{n}=\frac{x_{1}+x_{2}+\cdots +x_{n}}{n}$$例如计算最近30天日均DAU、日均新增访客等都可以使用算术平均值。
- 几何平均值:$$\left(\prod_{i=1}^{n}x_{i}\right)^{\frac{1}{n}}={\sqrt[{n}]{x_{1}x_{2} \cdots x_{n}}}$$,例如计算不同渠道的平均转化率、不同客群的平均留存率、不同品类的平均付费率等,就可以使用几何平均值。
2. 中位数 - 将数据按照升序或降序排列后位于中间的数,它描述了数据的中等水平。
3. 众数 - 数据集合中出现频次最多的数据,它代表了数据的一般水平。数据的趋势越集中,众数的代表性就越好。众数不受极值的影响,但是无法保证唯一性和存在性。
例子有A和B两组数据。
```
A组5, 6, 6, 6, 6, 8, 10
B组3, 5, 5, 6, 6, 9, 12
```
A组的均值6.74中位数6众数6。
B组的均值6.57中位数6众数5, 6。
> **说明**在Excel中可以使用AVERAGE、MEDIAN、MODE函数分别计算均值、中位数和众数。求中位数也可以使用QUARTILE.EXC或QUARTILE.INC函数将第二个参数设置为2即可。
对A组的数据进行一些调整。
```
A组5, 6, 6, 6, 6, 8, 10, 200
B组3, 5, 5, 6, 6, 9, 12
```
A组的均值会大幅度提升但中位数和众数却没有变化。
> **思考**怎样判断上面的200到底是不是一个异常值
| | 优点 | 缺点 |
| ------ | -------------------------------- | ------------------------------------ |
| 均值 | 充分利用了所有数据,适应性强 | 容易收到极端值(异常值)的影响 |
| 中位数 | 能够避免被极端值(异常值)的影响 | 不敏感 |
| 众数 | 能够很好的反映数据的集中趋势 | 有可能不存在(数据没有明显集中趋势) |
> **练习1**:在“概率基础练习.xlsx”文件的表单“练习1”中有一组用户订单支付金额的数据计算订单的均值、中位数、众数。
>
> **练习2**在“概率基础练习.xlsx”文件的表单“练习2”中有一组商品销售量的数据现计划设定一个阈值对阈值以下的商品对应的分销商进行优化应该选择什么作为阈值比较合适
### 数据的离散趋势
如果说数据的集中趋势,说明了数据最主要的特征是什么;那么数据的离散趋势,则体现了这个特征的稳定性。例如 A 地区冬季平均气温`0`摄氏度,最低气温`-10`摄氏度B 地区冬季平均气温`-2`摄氏度,最低气温`-4`摄氏度;如果你是一个特别怕冷的人,在选择 A 和 B 两个区域作为工作和生活的城市时,你会做出怎样的选择?
1. 极值就是最大值maximum、最小值minimum代表着数据集的上限和下限。
> **说明**在Excel中计算极值的函数是MAX和MIN。
2. 极差:又称“全距”,是一组数据中的最大观测值和最小观测值之差,记作$R$。一般情况下,极差越大,离散程度越大,数据受极值的影响越严重。
3. 四分位距离:$ IQR = Q_3 - Q_1 $。
4. 方差:将每个值与均值的偏差进行平方,然后除以总数据量得到的值。简单来说就是表示数据与期望值的偏离程度。方差越大,就意味着数据越不稳定、波动越剧烈,因此代表着数据整体比较分散,呈现出离散的趋势;而方差越小,意味着数据越稳定、波动越平滑,因此代表着数据整体比较集中。
- 总体方差:$$ \sigma^2 = \frac {\sum_{i=1}^{N}(X_i - \mu)^2} {N} $$。
- 样本方差:$$ S^2 = \frac {\sum_{i=1}^{N}(X_i - \bar{X})^2} {N-1} $$。
> **说明**在Excel中计算总体方差和样本方差的函数分别是VAR.P和VAR.S。
5. 标准差:将方差进行平方根运算后的结果,与方差一样都是表示数据与期望值的偏离程度。
- 总体标准差:$$ \sigma = \sqrt{\frac{\sum_{i=1}^{N}(X_i - \mu)^2}{N}} $$。
- 样本标准差:$$ S = \sqrt{\frac{\sum_{i=1}^{N}(X_i - \bar{X})^2}{N-1}} $$。
> **说明**在Excel中计算标准差的函数分别是STDEV.P和STDEV.S。
> **练习3**:复制“概率基础练习.xlsx”文件的表单“练习1”将复制的表单命名为“练习3”计算订单支付金额的最大值、最小值、极差、方差和标准差。
### 数据的频数分析
频数分析是指用一定的方式将数据分组,然后统计每个分组中样本的数量,再辅以图表(如直方图)就可以更直观的展示数据分布趋势的一种方法。
频数分析的意义:
1. 大问题变小问题,迅速聚焦到需要关注的群体。
2. 找到合理的分类机制,有利于长期的数据分析(维度拆解)。
例如一个班有40个学生考试成绩如下所示
```
73, 87, 88, 65, 73, 76, 80, 95, 83, 69, 55, 67, 70, 94, 86, 81, 87, 95, 84, 92, 92, 76, 69, 97, 72, 90, 72, 85, 80, 83, 97, 95, 62, 92, 67, 73, 91, 95, 86, 77
```
用上面学过的知识,先解读学生考试成绩的数据。
均值81.275中位数83众数95。
最高分97最低分55极差42方差118.15标准差10.87。
但是仅仅依靠上面的数据是很难对一个数据集做出全面的解读我们可以把学生按照考试成绩进行分组如下所示大家可以自行尝试在Excel或用Python来完成这个操作。
| 分数段 | 学生人数 |
| -------- | -------- |
| <60 | 1 |
| [60, 65) | 1 |
| [65, 69) | 5 |
| [70, 75) | 6 |
| [75, 80) | 3 |
| [80, 85) | 6 |
| [85, 90) | 6 |
| [90, 95) | 6 |
| >=95 | 6 |
> **练习4**:在“概率基础练习.xlsx”文件的表单“练习4”中有某App首页版本迭代上线后的A/B测试数据数据代表了参与测试的用户7日的活跃天数请分析A组和B组的数据并判定哪组表现更优。
>
> **练习5**:在“概率基础练习.xlsx”文件的表单“练习5”中有某App某个功能迭代上线后的A/B测试数据数据代表了参与测试的用户30日的产品使用时长请分析A组和B组的数据并判定哪组表现更优。
### 数据的概率分布
#### 基本概念
1. 随机试验:在相同条件下对某种随机现象进行观测的试验。随机试验满足三个特点:
- 可以在相同条件下重复的进行。
- 每次试验的结果不止一个,事先可以明确指出全部可能的结果。
- 重复试验的结果以随机的方式出现(事先不确定会出现哪个结果)。
2. 随机变量:如果$X$指定给概率空间$S$中每一个事件$e$有一个实数$X(e)$,同时针对每一个实数$r$都有一个事件集合$A_r$与其相对应,其中$A_r=\{e: X(e) \le r\}$,那么$X$被称作随机变量。从这个定义看出,$X$的本质是一个实值函数,以给定事件为自变量的实值函数,因为函数在给定自变量时会产生因变量,所以将$X$称为随机变量。
- 离散型随机变量:数据可以一一列出。
- 连续型随机变量:数据不可以一一列出。
如果离散型随机变量的取值非常庞大时,可以近似看做连续型随机变量。
3. 概率质量函数/概率密度函数:概率质量函数是描述离散型随机变量为特定取值的概率的函数,通常缩写为**PMF**。概率密度函数是描述连续型随机变量在某个确定的取值点可能性的函数,通常缩写为**PDF**。二者的区别在于,概率密度函数本身不是概率,只有对概率密度函数在某区间内进行积分后才是概率。
#### 离散型分布
1. 伯努利分布(*Bernoulli distribution*):又名**两点分布**或者**0-1分布**是一个离散型概率分布。若伯努利试验成功则随机变量取值为1。若伯努利试验失败则随机变量取值为0。记其成功概率为$p (0 \le p \le 1)$,失败概率为$q=1-p$,则概率质量函数为:
$$ {\displaystyle f_{X}(x)=p^{x}(1-p)^{1-x}=\left\{{\begin{matrix}p&{\mbox{if }}x=1,\\q\ &{\mbox{if }}x=0.\\\end{matrix}}\right.} $$
2. 二项分布(*Binomial distribution*$n$个独立的是/非试验中成功的次数的离散概率分布,其中每次试验的成功概率为$p$。一般地,如果随机变量$X$服从参数为$n$和$p$的二项分布,记为$X\sim B(n,p)$。$n$次试验中正好得到$k$次成功的概率由概率质量函数给出,$\displaystyle f(k,n,p)=\Pr(X=k)={n \choose k}p^{k}(1-p)^{n-k}$,对于$k= 0, 1, 2, ..., n$,其中${n \choose k}={\frac {n!}{k!(n-k)!}}$。
3. 泊松分布(*Poisson distribution*适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内受到的服务请求的次数、汽车站台的候客人数、机器出现的故障数、自然灾害发生的次数、DNA序列的变异数、放射性原子核的衰变数等等。泊松分布的概率质量函数为$P(X=k)=\frac{e^{-\lambda}\lambda^k}{k!}$,泊松分布的参数$\lambda$是单位时间(或单位面积)内随机事件的平均发生率。
> **说明**:泊松分布是在没有计算机的年代,由于二项分布的运算量太大运算比较困难,为了减少运算量,数学家为二项分布提供的一种近似。
#### 分布函数和密度函数
对于连续型随机变量,我们不可能去罗列每一个值出现的概率,因此要引入分布函数的概念。
$$
F(x) = P\{X \le x\}
$$
如果将$ X $看成是数轴上的随机坐标,上面的分布函数表示了$ x $落在区间$ (-\infty, x) $中的概率。分布函数有以下性质:
1. $ F(x) $是一个单调不减的函数;
2. $ 0 \le F(x) \le 1$,且$ F(-\infty) = \lim_{x \to -\infty} F(x) = 0 $ $F(\infty) = \lim_{x \to \infty} F(x) = 1$
3. $ F(x) $是右连续的。
概率密度函数就是给分布函数求导的结果,简单的说就是:
$$
F(x) = \int_{- \infty}^{x} f(t)dt
$$
#### 连续型分布
1. 均匀分布(*Uniform distribution*):如果连续型随机变量$X$具有概率密度函数$f(x)=\begin{cases}{\frac{1}{b-a}} \quad &{a \leq x \leq b} \\ {0} \quad &{\mbox{other}}\end{cases}$,则称$X$服从$[a,b]$上的均匀分布,记作$X\sim U[a,b]$。
2. 指数分布(*Exponential distribution*):如果连续型随机变量$X$具有概率密度函数$f(x)=\begin{cases} \lambda e^{- \lambda x} \quad &{x \ge 0} \\ {0} \quad &{x \lt 0} \end{cases}$,则称$X$服从参数为$\lambda$的指数分布,记为$X \sim Exp(\lambda)$。指数分布可以用来表示独立随机事件发生的时间间隔,比如旅客进入机场的时间间隔、客服中心接入电话的时间间隔、知乎上出现新问题的时间间隔等等。指数分布的一个重要特征是无记忆性(无后效性),这表示如果一个随机变量呈指数分布,它的条件概率遵循:$P(T \gt s+t\ |\ T \gt t)=P(T \gt s), \forall s,t \ge 0$。
3. 正态分布(*Normal distribution*):又名**高斯分布***Gaussian distribution*),是一个非常常见的连续概率分布,经常用自然科学和社会科学中来代表一个不明的随机变量。若随机变量$X$服从一个位置参数为$\mu$、尺度参数为$\sigma$的正态分布,记为$X \sim N(\mu,\sigma^2)$,其概率密度函数为:$\displaystyle f(x)={\frac {1}{\sigma {\sqrt {2\pi }}}}e^{-{\frac {\left(x-\mu \right)^{2}}{2\sigma ^{2}}}}$。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210716155507.png" width="80%">
“3$\sigma$法则”:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210716155542.png" width="75%">
正态分布有一个非常重要的性质,**大量统计独立的随机变量的平均值的分布趋于正态分布**,这就是**中心极限定理**。中心极限定理的重要意义在于,我们可以用正态分布作为其他概率分布的近似。
一个例子:假设某校入学新生的智力测验平均分数与标准差分别为 100 与 12。那么随机抽取 50 个学生,他们智力测验平均分数大于 105 的概率是多少?小于 90 的概率是多少?
本例没有正态分布的假设还好中心极限定理提供一个可行解那就是当随机样本数量超过30样本平均数 近似于一个正态变量,标准正态变量$ Z = \frac {\bar{X} - \mu} {\sigma / \sqrt{n}} $。
平均分数大于 105 的概率为:$ P(Z \gt \frac{105 - 100}{12 / \sqrt{50}}) = P(Z \gt 5/1.7) = P(Z \gt 2.94) = 0.0016$。
平均分数小于 90 的概率为:$ P(Z \lt \frac{90-100}{12/\sqrt{50}}) = P(Z < -5.88) = 0.0000 $。
> **说明**:上面标准正态分布的概率值可以查表得到。
4. 伽马分布(*Gamma distribution*):假设$X_1, X_2, ... X_n$为连续发生事件的等候时间,且这$n$次等候时间为独立的,那么这$n$次等候时间之和$Y$$Y=X_1+X_2+...+X_n$)服从伽玛分布,即$Y \sim \Gamma(\alpha,\beta)$,其中$\alpha=n, \beta=\lambda$,这里的$\lambda$是连续发生事件的平均发生频率。
5. 卡方分布(*Chi-square distribution*):若$k$个随机变量$Z_1,Z_2,...,Z_k$是相互独立且符合标准正态分布数学期望为0方差为1的随机变量则随机变量$Z$的平方和$X=\sum_{i=1}^{k}Z_i^2$被称为服从自由度为$k$的卡方分布,记为$X \sim \chi^2(k)$。
### 其他内容
#### 条件概率和贝叶斯定理
**条件概率**是指事件A在事件B发生的条件下发生的概率通常记为$P(A|B)$。设A与B为样本空间$\Omega$中的两个事件,其中$P(B) \gt 0$。那么在事件B发生的条件下事件A发生的条件概率为$P(A|B)=\frac{P(A \cap B)}{P(B)}$,其中$P(A \cap B)$是联合概率即A和B两个事件共同发生的概率。
事件A在事件B已发生的条件下发生的概率与事件B在事件A已发生的条件下发生的概率是不一样的。然而这两者是有确定的关系的**贝叶斯定理**就是对这种关系的陈述,即:$P(A|B)=\frac{P(A)P(B|A)}{P(B)}$,其中:
- $P(A|B)$是已知B发生后A的条件概率也称为A的后验概率。
- $P(A)$是A的先验概率也称为边缘概率是不考虑B时A发生的概率。
- $P(B|A)$是已知A发生后B的条件概率称为B的似然性。
- $P(B)$是B的先验概率。
按照上面的描述,贝叶斯定理可以表述为:`后验概率 = (似然性 * 先验概率) / 标准化常量`​,简单的说就是后验概率与先验概率和相似度的乘积成正比。
#### 大数定理
样本数量越多,则其算术平均值就有越高的概率接近期望值。
1. 弱大数定律(辛钦定理):样本均值依概率收敛于期望值,即对于任意正数$\epsilon$,有:$\lim_{n \to \infty}P(|\bar{X_n}-\mu|>\epsilon)=0$。
2. 强大数定律样本均值以概率1收敛于期望值$P(\lim_{n \to \infty}\bar{X_n}=\mu)=1$。
#### 假设检验
假设检验就是通过抽取样本数据,并且通过**小概率反证法**去验证整体情况的方法。假设检验的核心思想是小概率反证法(首先假设想推翻的命题是成立的,然后试图找出矛盾,找出不合理的地方来证明命题为假命题),即在**零假设**null hypothesis的前提下估算某事件发生的可能性如果该事件是小概率事件在一次研究中本来是不可能发生的但现在却发生了这时候就可以推翻零假设接受**备择假设**alternative hypothesis。如果该事件不是小概率事件我们就找不到理由来拒绝之前的假设实际中可引申为接受所做的无效假设。
假设检验会存在两种错误情况,一种称为“拒真”,一种称为“取伪”。如果原假设是对的,但你拒绝了原假设,这种错误就叫作“拒真”,这个错误的概率也叫作显著性水平$\alpha$,或称为容忍度;如果原假设是错的,但你承认了原假设,这种错误就叫作“取伪”,这个错误的概率我们记为$\beta$。
### 总结
描述性统计通常用于研究表象,将现象用数据的方式描述出来(用整体的数据来描述整体的特征);推理性统计通常用于推测本质(通过样本数据特征去推理总体数据特征),也就是你看到的表象的东西有多大概率符合你对隐藏在表象后的本质的猜测。

View File

@ -0,0 +1,119 @@
## 相关和回归
我们知道,可以通过对指标的维度拆来解寻找指标变化的原因。当我们找到问题的原因时,自然会进一步思考一个问题:指标变化的原因这么多,其中的关键因素又是哪个呢?例如,我们在工作场景中时不时会讨论这些问题:
1. 电商类产品想知道哪个品类销售对整体销售贡献更大;
2. 渠道运营想知道哪个渠道的用户对整体活跃作用更大;
3. 负责留存的想知道哪个客群对整体的留存关系更大;
4. 产品想知道到底哪些维度(城市、年龄、接入设备等)会影响整体活跃。
还有很多类似的场景,在这种情况下我们不仅要要找到数据变化的原因,还需要明确出不同原因的重要性。因为实际工作中可用资源有限,只能集中优势资源解决核心问题。
### 相关分析基本概念
相关性分析,指对两个或多个指标进行分析,评估它们两两之间联系或相互影响的程度。相关性分析不仅可以分析出多个指标间是否存在相关关系,还能给出相关程度的量化值。在进行相关性分析时,我们会使用“相关系数”定量给出几个指标间联系和影响的程度,通常用 $ \rho $ 来表示,计算公式为:
$$
\rho = \frac {cov(X, Y)} {\sqrt{var(X) \cdot var(Y)}}
$$
需要注意的是,$ \rho $ 只能用来度量线性关系,它的取值在 $ [-1, 1] $ 之间。数据中的离群值会对 $ \rho $ 产生影响,在计算时必须先剔除,实际使用相关关系时,还需要**关注相关关系的稳定性**。
我们用 $ \rho $ 值判断指标的相关性时遵循以下两个步骤。
1. 判断指标间是正相关、负相关,还是不相关。
- 当 $ \rho \gt 0 $,认为指标间是正相关,也就是两者的趋势一致。如果指标 A 与指标 B 的 $ \rho \gt 0 $,那么指标 A 上涨,指标 B 也会上涨;反之亦然。
- 当 $ \rho \lt 0 $,认为指标间是负相关,也就是两者的趋势相反。如果指标 A 与指标 B 的 $ \rho \lt 0 $,那么指标 A 上涨,指标 B 会下降;反之亦然。
- 当 $ \rho = 0 $,认为指标间是不相关的,但并不代表两个指标是统计独立的。
2. 判断指标间的相关程度。
- 当 $ \rho $ 的值在 $ [0.5,1] $ 之间,认为指标间是强相关,指标间的业务联系非常紧密。
- 当 $ \rho $ 的值在 $ [0.1,0.5) $ 之间,认为指标间是弱相关,指标间的业务联系不太紧密。
- 当 $ \rho $ 的值在 $ [0,0.1) $ 之间,认为指标间是无相关,指标间的业务联系无任何联系,也就是说当我们去运营指标 A 时,指标 B 不会产生相应的变化。
### 相关分析应用场景
事实上,相关性分析的应用场景非常多,基本上当问到“这两个东西有什么关系”、“哪个指标的作用(贡献或价值)更大”、“我们应该重点解决哪个问题”这类问题时,都可以用相关性分析给出比较准确的回答,非常便于产品运营找到解决问题的核心抓手。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713095938.png" width="80%">
在使用相关分析时,应注意以下几个方面:
1. 业务意义当我们想知道A指标的情况时可以监控B指标。
2. 注意事项:千万不要将相关关系判断为因果关系,相关关系是伴随关系而不是因果关系。
3. 强相关关系才是有业务价值的,建议寻找相关系数在 0.6 以上甚至 0.8 以上的指标。
4. 相关关系的本质是 Y 的变化有多少能被 X 解释,跟 X 和 Y 之间的斜率大小无关。
### Excel计算相关系数
1. 方法一:使用 CORREL 函数。
2. 方法二:使用“数据分析”模块的“相关系数”功能。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713164021.png" width="75%">
### 相关分析案例
#### 分析哪个客群的留存对整体留存贡献更大
留存的运营中我们最常看的就是新客的留存和活跃客群的留存,用来评估哪个客群的留存与整体的留存联系更紧密,以便制定后续运营的策略。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210928214403.png" style="zoom:65%;">
利用Excel进行相关分析的结果如下所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210928214522.png" style="zoom:65%;">
可以看出,活跃访客的留存率与整体留存率的相关是强相关;而新增访客的留存率与整体留存率的相关是弱相关,所以如果要提升整体留存率,我们的产品运营资源应当更多地投放给活跃用户,以提升整体的留存率;而新增访客,虽然不会拿到很多运营资源,但是我们也要去深入分析为什么新增访客的留存的贡献比较小,适时做一些提升这部分客群与整体留存的策略。
#### 案例2找出对购买转化率贡献最高的渠道
基本上电商运营会同时部署多个渠道,包括线上电商平台以及线下的门店。由于现有某产品从各个渠道获客的用户在产品上的购买转化率,需要评估哪些渠道的用户对整体购买转化率贡献最大,后续将重点营销此渠道。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210928214725.png" style="zoom:65%;">
#### 案例3分析哪些因素对 DAU 的影响更大
我们分析 DAU 时常会将它拆解为各种维度来分析,这里我们分析与 DAU 联系最紧密的维度到底是哪些,以帮助我们制定针对性的运营策略,如下图所示。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210928215043.png" style="zoom:65%;">
对于这样的报表,我们需要找出到底是哪几个城市、哪个操作系统,以及哪个年龄段的用户对于 DAU 的影响最大。如果能找出来这个关系,那么后续要提升 DAU就有非常清晰的方向。
### 线性回归
如果只有一个自变量 X而且因变量 Y 和自变量 X 之间的数量变化关系呈现近似的线性关系,就可以建立一元线性回归方程,通过自变量 X 的值来预测因变量 Y 的值,这就是所谓的**一元线性回归预测**,回归方程如下所示:
$$
Y = aX + b
$$
我们可以通过历史数据(已知的 $ X $ 和 $ Y $ ),确定参数 $ a $ 和 $ b $ 的值,还原出回归方程,从而实现预测。很显然,$ a $和 $ b $ 的取值可以有很多种选择,那么什么才是最好的 $ a $ 和 $ b$ 呢?如果把真实值记为 $ y $,把预测值记为 $ \hat{y} $,那么让 $ SSR $ 值最小的 $ a $ 和 $ b $ 就是最好的 $ a $ 和 $ b $ ,称之为**最小二乘解**,其中$ SSR $ 值计算公式如下所示:
$$
SSR = \sum_{i=1}^{n}(y_i - \hat{y_i})^2
$$
损失函数是凹函数,找到使函数最小的`a`和`b`的值,可以通过向凹函数的拐点进行逼近的方式来找到更好的`a`和`b`的值,具体的公式如下所示:
$$
a^\prime = a + (-1) \times \frac {\partial loss(a, b)} {\partial a} \times \Delta \\
b^\prime = b + (-1) \times \frac {\partial loss(a, b)} {\partial b} \times \Delta
$$
对于上面的求 $ SSR $ 的函数来说,可以用下面的公式计算偏导数:
$$
f(a, b) = \frac {1} {N} \sum_{i=1}^{N}(y_i - (ax_i + b))^2 \\
\frac {\partial {f(a, b)}} {\partial {a}} = \frac {2} {N} \sum_{i=1}^{N}(-x_iy_i + x_i^2a + x_ib) \\
\frac {\partial {f(a, b)}} {\partial {b}} = \frac {2} {N} \sum_{i=1}^{N}(-y_i + x_ia + b)
$$
上面的方法称为**梯度下降法**。
在Excel中可以使用“数据分析”模块的“”来实现线性回归。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210714073655.png" width="75%">
对于回归分析最为重要的是评价回归的结果是否理想这关系到能否通过回归方程去预测将来我们先看看决定系数Multiple R-Squared通常称之为$ R^2 $)。在统计学习中,决定系数用于度量因变量的变化中可由自变量解释部分所占的比例,也就是你的回归模型的解释力是否良好,$ R^2 $ 的值越接近`1`越好。
$$
SS_{tot} = \sum_{i}(y_{i} - \bar {y})^2 \\
SS_{res} = \sum_{i}(y_{i} - \hat {y_i})^2 \\
R^2 = 1 - \frac {SS_{res}} {SS_{tot}}
$$
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210714074159.png" width="60%">
接下来我们还要对回归方程的显著性进行检验,主要包括 t 检验回归系数的检验和F检验回归方程的检验。对于F检验F-statistic的结果主要关注其 p-value ,如果 p-value 小于0.05,那么说明拟合效果是不错的。

View File

@ -0,0 +1,112 @@
## 方差分析和参数估计
### 方差分析
#### 基本概念
在产品运营中我们会遇到各种需要评估运营效果的场景包括促活的活动是否起到作用、A/B 测试的策略有无成效等等。具体例如,产品升级前的平均 DAU 是 155 万,产品升级后的平均 DAU 是 157 万,那么如何判断 DAU 提升的 2 万是正常的波动,还是升级带来的效果呢?对比同一组数据在实施某些策略前后的数据变化,判断数据波动是不是某一因素导致的,这种方法我们称之为方差分析。方差分析通常缩写为 ANOVAAnalysis of Variance也叫“F 检验”,用于两个及两个以上分组样本的差异性检验。简单的说,**分析差异的显著性是否明显的方法就是方差分析**。
举一个例子,如果我们需要分析优惠券的金额对用户的购买转化率是否能起到有效作用,我们可以将数据分成以下三个组:
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713085210.png" width="60%">
用户购买行为是随机的,购买率很高的不会很多,购买率极低的也不会很多,绝大部分用户的购买率都集中在某个值附近,这个值我们叫作整体购买率的平均值。如果每个客群分组自身的购买率均值与这个整体购买率平均值不一致,就会出现以下两种情况。
1. 第一种情况
蓝色分组的购买率平均值(蓝色线)比整体平均值(黑色线)要高,有可能是最右边那个很高的购买率把分组的均值抬升的,同时蓝色分组的数据分布很散(方差大),此时不能有十足把握说明该组用户的购买转化率很高。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713085506.png" width="50%">
2. 第二种情况
绿色分组的购买率平均值(绿色线)比整体平均值(黑色线)要高,但是绿色分组的数据非常集中,都集中在分组的平均值(绿色线)附近,此时我们可以认为该组的转化率平均值与整体有明显区别。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713085608.png" width="50%">
为了更好表述上面的问题,我们可以引入“组内方差”的概念,即描述每个分组内部数据分布的离散情况。如下图所示,对于上面蓝色和绿色分组的“组内方差”,显然蓝色的组内方差更大,绿色的组内方差更小。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713085808.png" width="75%">
综上所述,如果上面三个分组的用户购买率平均值不在中线(整体购买率)左右,而是有明显的偏高或偏低,并且该组内的每个转化率都紧紧围绕在该组购买率平均值的附近(即组内方差很小)。那么我们就可以断定:该组的购买率与整体不一致,是该组对应优惠金额的影响造成的。
#### 定量分析
如果要进行定量分析,可以使用 F 检验值和 F crit 临界值这两个指标。F 检验值用来精确表达这几组差异大小的F crit临界值是一个判断基线
- 当 F > F crit这几组之间的差异超过判断基准了认为不同优惠金额的分组间的购买率是不一样的优惠金额这个因素会对购买率产生影响也就是说通过运营优惠金额这个抓手是可以提升用户购买转化率的
- 当 F < F crit则认为不同优惠金额的分组间的购买率是一样的优惠金额这个因素不会对购买率产生影响也就是说需要继续寻找其他与购买转化率有关的抓手
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210713090505.png">
> **说明**:图中 SS 代表方差、df 代表指标自由度、MS 是均方差、P-value 是差异的显著性水平。
上图是用 Excel 得出的 A、B、C 三组的方差分析结果,如图所示 F < F crit所以从定量分析角度可以判定优惠金额不会对购买率产生影响
#### 实施方法
实施方差分析可以分为以下三步走:
1. 判断样本是否满足“方差分析”的前提条件
- 每个分组中的每个值都必须来自同一个总体样本;
- 方差分析只能分析满足正态分布的指标,事实上,在产品运营中大部分指标都是正态分布,例如:
- 几乎所有的转化率都满足正态分布:购买率、点击率、转化率、活跃率、留存率、复购率等。
- 几乎所有的业务量都满足正态分布:客单价、每日新增用户数、渠道引流的流量等。
- 几乎所有的用户画像指标都满足正态分布:年龄、城市、登录次数、使用时长等。
- 分析的样本必须是随机抽样
2. 计算 F 检验值和 F crit 临界值
3. 如果有差异,需要评估差异大小
我们用一个新的指标来表示:$ R^2=SSA / SST $,其中 $ R^2 $ 表示差异大小,$ SSA $ 是组间误差平方和,$ SST $ 是总误差平方和。
- 当 $ R^2 \gt 0.5 $,认为各个分组间的差异非常显著;
- 当 $ R^2 $ 在 $ [0.1, 0.5] $ 之间时,认为各个分组间的差异一般显著;
- 当 $ R^2 \lt 0.1 $ 时,认为各个分组间的差异微弱显著。
> **练习**:打开“方差分析练习.xlsx”文件完成练习1。
#### 多因素方差分析
上面的案例是针对一种策略来分析效果。我们把这种形式的方差分析叫作单因素方差分析,实际工作中,我们可能需要研究多种策略(例如运营中的渠道、活动、客群等)对结果的影响,我们称之为多因素方差分析。例如我们会在多个运营渠道上安排多种运营活动,评价各个渠道的转化率。此时,影响转化率的因素有渠道和活动两个因素,我们可以使用“无重复双因素方差分析”来检查数据。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210714125251.png" width="75%">
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210714130853.png" width="75%">
#### 应用场景
工作中遇到以下两类场景就可以使用方差分析:
1. 同一个客群在实施某个策略前后的指标对比。
2. 两个或多个客群对比同一指标,评估同一指标在不同客群上的差异。
<img src="https://gitee.com/jackfrued/mypic/raw/master/20210714131318.png" width="85%">
### 参数估计
在产品运营的工作中,数据分析常会遭遇诸多非常让人困扰的情况,例如:产品运营面对的数据量动辄百万级、千万级,带来的就是分析速度急剧下降,跑个数等一两天时间已经是很理想情况;另外,在很多场景下,我们都只能拿到部分数据(样本),而无法获取全量数据(总体)。在这种情况下我们就必须通过分析非常小量样本的特征,再用这些特征去评估海量总体数据的特征,可以称之为**样本检验**。
**推断型统计的核心就是用样本推测总体**。在实际生产环境中,可能无法获得所有的数据,或者即便获取了所有的数据,但是没有足够的资源来分析所有的数据,在这种情况下,我们都需要用非常小量的样本特征去评估总体数据的特征,这其中的一项工作就是参数估计。
参数估计应用的场景非常的多,例如:
1. 在产品侧我们可以用参数估计的方式评估A/B测试的效果。
2. 在运营侧,我们可以用参数估计的方式优化活动配置和推荐策略。
3. 在市场侧,我们可以用参数估计的方式制定广告投放策略。
#### 实施步骤
1. 确定分析的置信水平
2. 确定估计的参数类型
3. 计算参数估计的区间
- 数值型指标:$ A = z \times 样本标准差 / \sqrt{样本数量} $,其中 $ z $ 的值可以通过查表得到如果置信水平选择95%,那么 $ z $ 的值就是1.96。大部分运营指标都是数值型指标例如DAU、ARPU、转化率等。
- 占比型指标:$ A = z \times \sqrt{占比 \times (1 - 占比) / 样本数量} $$ z $ 值同上。占比型指标如性别占比、渠道占比、品类占比等。
最终得到的估计区间就是:$ [样本均值 - A, 样本均值 + A] $。

View File

@ -0,0 +1,2 @@
### 聚类和降维

View File

@ -0,0 +1,2 @@
## 数据分析方法论

214
Day66-80/code/Day68.ipynb Normal file
View File

@ -0,0 +1,214 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"%matplotlib inline\n",
"%config InlineBackend.figure_format='svg'"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"plt.rcParams['font.sans-serif'] = 'FZJKai-Z03S'\n",
"plt.rcParams['axes.unicode_minus'] = False"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser1 = pd.Series(data=[320, 180, 300, 405], index=['一季度', '二季度', '三季度', '四季度'])\n",
"ser1"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser2 = pd.Series({'一季度': 320, '二季度': 180, '三季度': 300, '四季度': 405})\n",
"ser2"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print(ser2[0], ser2[2], ser2[-1])\n",
"ser2[0], ser2[-1] = 350, 360 \n",
"print(ser2)"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print(ser2['一季度'], ser2['三季度'])\n",
"ser2['一季度'] = 380\n",
"print(ser2)"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print(ser2[1:3])\n",
"print(ser2['二季度': '四季度'])\n",
"ser2[1:3] = 400, 500\n",
"print(ser2)"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print(ser2[['二季度', '四季度']])\n",
"ser2[['二季度', '四季度']] = 500, 520\n",
"print(ser2)"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"print(ser2[ser2 >= 500])"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"# 求和\n",
"print(ser2.sum())\n",
"# 求均值\n",
"print(ser2.mean())\n",
"# 求最大\n",
"print(ser2.max())\n",
"# 求最小\n",
"print(ser2.min())\n",
"# 计数\n",
"print(ser2.count())\n",
"# 求标准差\n",
"print(ser2.std())\n",
"# 求方差\n",
"print(ser2.var())\n",
"# 求中位数\n",
"print(ser2.median())"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser2.describe()"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser3 = pd.Series(data=['apple', 'banana', 'apple', 'pitaya', 'apple', 'pitaya', 'durian'])\n",
"ser3.value_counts()"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser4 = pd.Series(data=[10, 20, np.NaN, 30, np.NaN])\n",
"ser4.dropna()"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser4.fillna(value=40)"
],
"outputs": [],
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
"source": [
"ser4.fillna(method='ffill')"
],
"outputs": [],
"metadata": {}
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.7"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

View File

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 277 KiB

View File

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

View File

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 217 KiB

After

Width:  |  Height:  |  Size: 217 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

View File

Before

Width:  |  Height:  |  Size: 474 KiB

After

Width:  |  Height:  |  Size: 474 KiB

View File

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 509 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Some files were not shown because too many files have changed in this diff Show More