上篇:数据采集
说到数据采集,那最大名鼎鼎的方式就是“爬虫”啦,让我们来看看百媚生带给我们的“爬虫”利器吧,是不是真如传言的“见血封喉”呢?
Requests
啥?为什么 requests
是“爬虫”?
可不要小瞧了它!虽说 requests
是网络请求库,但它却如高手手中的「木剑」一般,用好了,一样招招致命。
使用 requests
发起攻击(请求),犹如疾风般迅速,犹如落叶般轻盈。
>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
'{"type":"User"...'
>>> r.json()
{'private_gists': 419, 'total_private_repos': 77, ...}
复制代码
这就完了?
如果对方是返回 Json 格式的 API 服务,是的,这就完了。我们已经拿到数据了。
如果对方是返回 XML 格式的 API 服务,那么,我们再搭配上原生的 xml
或者 lxml
解析器,灭敌于百步之外。
"""
content 是 xml 格式的字符串,即 r.text
例如
<?xml version="1.0"?>
<data>
<country name="a"></country>
<country name="b"></country>
<country name="c"></country>
</data>
"""
import xml.etree.ElementTree as ET
tree = ET.parse(content)
root = tree.getroot()
# 遍历节点
for child in root:
print(child.tag, child.attrib)
复制代码
而 lxml
更快更凶残。
from lxml import etree
root = etree.XML(content)
for element in root.iter():
print("%s - %s" % (element.tag, element.text))
复制代码
lxml
更是支持强大的 xpath
和 xlst
语法(语法文档详见参考)。
# 使用 xpath 语法快速定位节点,提取数据
r = root.xpath('country')
text = root.xpath('country/text()')
复制代码
xlst
进行快速转换。
xslt_root = etree.XML('''\
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<foo><xsl:value-of select="/a/b/text()" /></foo>
</xsl:template>
</xsl:stylesheet>''')
transform = etree.XSLT(xslt_root)
f = StringIO('<a><b>Text</b></a>')
doc = etree.parse(f)
result_tree = transform(doc)
复制代码
对手更凶残了,是 HTML 文档!这下就需要 BeautifulSoup
或 lxml
解析器出马了。BeautifulSoup
虽然速度不快,好在利于理解。
from bs4 import BeautifulSoup
# content 即 html 字符串, requests 返回的文本 text
soup = BeautifulSoup(content, 'html.parser')
print(soup.title)
print(soup.title.name)
print(soup.find_all('a'))
print(soup.find(id="link3"))
for link in soup.find_all('a'):
print(link.get('href'))
复制代码
上房揭瓦(解析网页),那是手到擒来。
而用 lxml
还是那么干净利落。
html = etree.HTML(content)
result = etree.tostring(html, pretty_print=True, method="html")
print(result)
# 接下来就是 xpath 的表演时间
复制代码
可见,木剑虽朴实,在高手手中,也能变化无穷。如果是“接骨木”,那更是了不得。最快速便捷的数据采集神兵,非 requests
莫属!
Scrapy
接下来让我们看看数据采集的百变神兵 —— Scrapy,分分钟让我们全副武装。
# 创建一个项目
scrapy startproject tutorial
cd tutorial
# 创建一个爬虫
scrapy genspider quotes quotes.toscrape.com
复制代码
然后编辑项目下 spiders/quotes.py
爬虫文件。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
"""
生成初始请求。
"""
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
"""
处理请求返回的响应。
"""
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)
复制代码
然后就是启动爬虫。
scrapy crawl quotes
复制代码
这还没有发挥 Scrapy
的能力呢!
解析网页
# CSS 解析
response.css('title::text').getall()
# xpath 解析
response.css('//title/text()').getall()
复制代码
自动生成结果文件
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
def parse(self, response):
# parse 函数直接返回字典或者 Item 对象。
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
复制代码
在爬取的命令上加上 -o
参数,即可快速将结果保存到文件,支持多种格式(csv,json,json lines,xml),也可方便地扩展自己的格式。
scrapy crawl quotes -o quotes.json
复制代码
数据分页了,还有下一页怎么办?抛出请求,让 Scrapy 自己去处理。
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
"""
parse 函数 yield 字典或者 Item 对象,则视为结果,
yield 请求对象(follow 方法即是跟随链接,快速生成对应的请求对象)即继续爬取。
"""
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a').get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
复制代码
这就完了吗?当然不会,Scrapy 还提供了多种数据采集需要用到的功能。
- 强大的扩展能力,快速编写扩展和中间件。
- 灵活的配置,并发控制,限速控制等。
- 自定义的爬取对象处理流水线。
- 自定义的爬取对象存储。
- 自动统计数据。
- 整合邮件。
- Telnet 控制台等等。
这只是核心功能,还没见到它的社区能力呢!
- Scrapyd:工程化部署爬虫。
- Scrapy-Splash:为 Scrapy 提供了 JS 渲染能力。
- Scrapy Jsonrpc:Json RPC 服务控制爬虫。
- Gerapy:Web 爬虫管理平台。
- ScrapyWeb:另一个 Web 爬虫管理平台。
- ScrapyKeeper:还是一个 Web 爬虫管理平台。
- Portia:无需编码的交互式爬虫平台。
这些就不再展开了。
快速而又强大的数据采集利器,当属 Scrapy
!
Pyspider
强大的瑞士军刀 —— Pyspider。
Pyspider 可不得了,它提供了一整套完整的数据采集解决方案,堪称爬虫界的“瑞士军刀”。
- 原生提供 Web 管理界面,支持任务监控、项目管理、结果查看等等。
- 原生支持众多的数据库后端,如 MySQL、MongoDB、SQLite、Elasticsearch、Postgresql。
- 原生支持多种消息队列,如 RabbitMQ,Beanstalk、Redis、Kombu。
- 支持任务优先级、自动重试、定时任务、支持 JS 渲染等功能。
- 分布式架构。
爬虫,就是这么简单!
from pyspider.libs.base_handler import *
class Handler(BaseHandler):
crawl_config = {
}
@every(minutes=24 * 60)
def on_start(self):
self.crawl('http://scrapy.org/', callback=self.index_page)
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('a[href^="http"]').items():
self.crawl(each.attr.href, callback=self.detail_page)
def detail_page(self, response):
return {
"url": response.url,
"title": response.doc('title').text(),
}
复制代码
启动爬虫框架。
pyspider
复制代码
然后,我们就可以通过 http://localhost:5000/
进行爬虫的管理和运行了。
我们可以使用 css 选择器快速提取网页信息。
def index_page(self, response):
for each in response.doc('a[href^="http"]').items():
if re.match("http://www.imdb.com/title/tt\d+/$", each.attr.href):
self.crawl(each.attr.href, callback=self.detail_page)
self.crawl(response.doc('#right a').attr.href, callback=self.index_page)
def detail_page(self, response):
return {
"url": response.url,
"title": response.doc('.header > [itemprop="name"]').text(),
"rating": response.doc('.star-box-giga-star').text(),
"director": [x.text() for x in response.doc('[itemprop="director"] span').items()],
}
复制代码
启用 PhantomJS
来渲染网页上的 JS。
pyspider phantomjs
复制代码
使用 fetch_type='js'
。
class Handler(BaseHandler):
def on_start(self):
self.crawl('http://www.twitch.tv/directory/game/Dota%202',
fetch_type='js', callback=self.index_page)
def index_page(self, response):
return {
"url": response.url,
"channels": [{
"title": x('.title').text(),
"viewers": x('.info').contents()[2],
"name": x('.info a').text(),
} for x in response.doc('.stream.item').items()]
}
复制代码
还能执行一段 JS 代码,来获取那些动态生成的网页内容。
class Handler(BaseHandler):
def on_start(self):
self.crawl('http://www.pinterest.com/categories/popular/',
fetch_type='js', js_script="""
function() {
window.scrollTo(0,document.body.scrollHeight);
}
""", callback=self.index_page)
def index_page(self, response):
return {
"url": response.url,
"images": [{
"title": x('.richPinGridTitle').text(),
"img": x('.pinImg').attr('src'),
"author": x('.creditName').text(),
} for x in response.doc('.item').items() if x('.pinImg')]
}
复制代码
好了,接下来我知道,问题就是 Pyspider
和 Scrapy
选哪个?
简单说下它们的对比。
Scrapy 有更强大的扩展能力,社区更活跃,周边更丰富。而 Pyspider 本身功能更全,但扩展能力较弱。许多 Scrapy 需要扩展实现的功能,如 Web 界面、JS 渲染等,Pyspider 原生都提供了。
Pyspider 的整套生态上手更容易,实现更快速。Scrapy 对复杂的场景有更多的选择余地,更灵活。
所以,诸位选哪款?
成年人需要做选择吗?
参考
中篇:数据处理
说到数据处理,最大名鼎鼎的应该就是“Excel”了,它的大名,可谓无人不知,无人不晓。有了 Excel,即使毫无编程经验的人,都能对数据耍上两把。而我们今天就来看看,仅仅一点点代码,就能实现的更为强大的功能。
Numpy
第一把交椅,必须是“Numpy”了。不少人可能听说过,但是可能从未用过,因为 Numpy 是 Python 数据处理的基石,是其他不少相关工具的依赖。
Numpy 的功能非常纯粹,它为 Python 提供了一个非常强大且灵活的“数组”数据结构。
但是 Python 不是有列表(list)了吗?为什么还要其他的“数组”结构?
Numpy 主要有以下几个特点:
- list 仅仅是一个“链表”,而 Numpy 提供了真正的多维数组
- 提供了众多的数组相关操作
- 提供了许多线性代数等数学函数
- 提供了广播功能,像操作标量一样操作多维数组
- 底层使用 C 实现,非常快,适合大量数据处理
Numpy 提供了非常方便的方法生成多维数组,例如从 list 转换而来。
import numpy as np
a = np.array([1,2,3,4])
复制代码
第一行引入 Numpy,这是一般推荐的做法,用 as 别名为 np。不是必须的,但建议遵循。
还有众多的生成函数。
# 生成一个 3 * 4 的矩阵,每个元素都是 0
np.zeros((3,4))
# 生成一个 2 * 3 的矩阵,每个元素都是 1
np.ones( (2,3) )
# 等差数列生成,从 10 到 30,5 递增
np.arange(10, 30, 5)
复制代码
不同于 list,数组的每个元素必须有一致的数据类型。
>>> a.dtype.name
'int64'
复制代码
可以在创建时通过 dtype 参数指定,例如 int32,int64,float64 等。
Numpy 的数组有几个比较重要的属性。
- ndim:轴的数量,也就是维度
- shape:数组大小的元祖
- size:总数据个数
- dtype:数据类型
itemsize:每个数组元素占的字节数
# 创建一个 1 到 14 到数组,并改成 3 * 5 的矩阵
>>> a = np.arange(15).reshape(3, 5)
>>> a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> a.ndim
2
>>> a.shape
(3, 5)
>>> a.size
15
>>> a.dtype.name
'int64'
>>> a.itemsize
8
复制代码
这还是牛刀小试,让我们来见识 Numpy 的实力吧,像操作普通标量一样操作数组。
# 数组每个元素乘以 2
>>> b = a * 2
>>> b
array([[ 0, 2, 4, 6, 8],
[10, 12, 14, 16, 18],
[20, 22, 24, 26, 28]])
# 数组对应元素相加
>>> a + b
array([[ 0, 3, 6, 9, 12],
[15, 18, 21, 24, 27],
[30, 33, 36, 39, 42]])
# 数组对应元素相乘
>>> a * b
array([[ 0, 2, 8, 18, 32],
[ 50, 72, 98, 128, 162],
[200, 242, 288, 338, 392]])
# 逻辑判断,每个元素是否小于 8
>>> a < 8
array([[ True, True, True, True, True],
[ True, True, True, False, False],
[False, False, False, False, False]], dtype=bool)
复制代码
基本的函数操作
>>> a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
# 纵向求和(轴索引为 0)
>>> a.sum(axis=0)
array([15, 18, 21, 24, 27])
# 横向求和(轴索引为 1)
>>> a.sum(axis=1)
array([10, 35, 60])
# 每行的最小值(轴索引为 1)
>>> a.min(axis=1)
array([ 0, 5, 10])
复制代码
取数据
# 取索引 0,0 处的元素,也就是第一行第一个
>>> a[0,0]
0
# 取索引 2,3 处的元素
>>> a[2,3]
13
复制代码
当然也可以切片操作
# 取索引为 1 到 2 (不含)的行,也就是第 2 行
>>> a[1:2]
array([[5, 6, 7, 8, 9]])
# 取索引为 1 到 3 (不含)的行,索引为 2 到 5(不含)的列
>>> a[1:3,2:5]
array([[ 7, 8, 9],
[12, 13, 14]]))
复制代码
还有其他的数组转换、线性代数计算等函数,就不详细展开了。Numpy 就是我们进行数据处理的基本容器,有了 Numpy 提供的数组结构,后面的操作就游刃有余了。
Scipy
Scipy 是数学、工程、科学计算相关的函数库,本身是基于 Numpy 的数组的,也是和 Numpy 师出同门。
Scipy 有许多子模块,每个子模块提供各个领域特定的功能。cluster:聚类算法
- constants:物理和数学的常量
- fftpack:快速傅立叶变换
- integrate:微积分方程求解
- interpolate:插值计算
- io:输入输出
- linalg:线性代数
- ndimage:n 维图像处理
- ord:正交距离回归
- optimize:优化
- signal:信号处理
- sparse:稀疏矩阵
- spatial:空间数据结构和算法
- special:特殊函数
- stats:统计分布函数
各个函数的使用需要一定的理论基础,scipy 已经帮助我们实现,只需要调用即可。
例如,我们计算如下的积分方程
>>> import scipy.integrate as integrate
>>> import scipy.special as special
>>> result = integrate.quad(lambda x: special.jv(2.5,x), 0, 4.5)
>>> result
(1.1178179380783249, 7.8663172481899801e-09)
复制代码
Pandas
Pandas 不是一只普通的熊猫。Pandas 是基于 Numpy 的高级的数据处理工具库。Pandas 最擅长的就是处理表格数据,也就是我们熟悉的 Excel 类型的数据,Pandas 提供的方法能让我们简单的几行代码快速处理类 Excel 或者关系型数据表的数据。
一个最简单的例子。
>>> import pandas as pd # 和 nunpy 一样的约定,一般引入后别名为 pd
>>> df = pd.read_csv('test.csv') # 读取 csv 文件
>>> df # 显示读取的内容,为两列三行的数据
name price
0 apple 5
1 banana 3
2 pear 2
>>> df.describe() # 查看数据统计,自动找出数据列(price),计算相关统计量
price
count 3.000000
mean 3.333333
std 1.527525
min 2.000000
25% 2.500000
50% 3.000000
75% 4.000000
max 5.000000
>>> df.price = df.price * 2 # 所有价格都乘以 2
>>> df
name price
0 apple 10
1 banana 6
2 pear 4
>>> df.to_csv('out.csv', index=False) # 导出数据到 csv 文件
复制代码
Pandas 可以用来做数据清洗、转换、聚合等操作,为下一步数据可视化及机器学习准备数据。
Pandas 有多种数据类型,其中就常见的两种就是 Series 和 DataFrame。
DataFrame 可以理解为一个数据表格,行有行的标签(索引),列有列的标签(索引),是不是和 Excel 很像?
而 Series 即是这个数据表格的一列或者一行。所以一个 DataFrame 也可看作是一个 Series 数组。
Pandas 提供了一系列的方法快速的从其他数据源导入导出数据。
Pandas 也能非常方便的“挑选”特定的数据。
通过 Matplotlib 库,Pandas 可以方便的绘制可视化图表。
对于基本的数据抽取、转换、清洗等工作,Pandas 完全能胜任,掌握了 Pandas 就有了应对各种敌人的百变战甲。
Scikit-Learn
Scikit-Learn 是 Python 的机器学习库,也是在 Numpy 和 Scipy 的基础上,提供了强大的机器学习算法的实现。例如分类算法(SVM,K 近邻,随机森林等),回归算法(线性回归、SVR 等),聚类算法(K-means、谱聚类等)、降维、模型选择、数据预处理等等。
Scikit-Learn 通过对算法的高度抽象,对于一个基本的算法调用过程,基本上就简化为了:初始化->数据预处理->训练->预测,流程中的几个步骤。
一个简单的线性回归的例子。
>>> from sklearn import linear_model
>>> reg = linear_model.LinearRegression()
>>> reg.fit([[0, 0], [1, 1], [2, 2]], [0, 1, 2])
LinearRegression()
>>> reg.coef_
array([0.5, 0.5])
复制代码
除了基本的算法实现,scikit-learn 提供的数据预处理也极其强大,基本能涵盖数据预处理的许多方面。
例如,数据归一化,快速的 Z-score 归一化,使数据满足正态分布。
>>> from sklearn import preprocessing
>>> import numpy as np
>>> X_train = np.array([[ 1., -1., 2.],
... [ 2., 0., 0.],
... [ 0., 1., -1.]])
>>> X_scaled = preprocessing.scale(X_train)
>>> X_scaled
array([[ 0. ..., -1.22..., 1.33...],
[ 1.22..., 0. ..., -0.26...],
[-1.22..., 1.22..., -1.06...]])
复制代码
数据编码,可以将枚举的字符串数据编码为数值型,方便后续计算。
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OrdinalEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari']])
array([[0., 1., 1.]])
复制代码
One Hot 编码,将枚举字符串数据转换为多列的布尔数据(0 / 1)。
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari'],
... ['male', 'from Europe', 'uses Safari']]).toarray()
array([[1., 0., 0., 1., 0., 1.],
[0., 1., 1., 0., 0., 1.]])
复制代码
缺失值处理。
>>> import numpy as np
>>> from sklearn.impute import SimpleImputer
>>> imp = SimpleImputer(missing_values=np.nan, strategy='mean')
>>> imp.fit([[1, 2], [np.nan, 3], [7, 6]])
SimpleImputer()
>>> X = [[np.nan, 2], [6, np.nan], [7, 6]]
>>> print(imp.transform(X))
[[4. 2. ]
[6. 3.666...]
[7. 6. ]]
复制代码
如果想要看一个实战的项目,这里有一个基因型预测身高的项目例子。该项目根据用户在多个基因位点的基因型和身高数据进行模型训练,然后可根据训练的模型预测其他用户的身高。我们先不管这个预测的理论可行性和准确性,不过整个数据预处理和模型训练的操作是一个学习 scikit-learn 的过程。
Statsmodels
statsmodels 是一个 Python 的统计模型库,提供了很多统计相关的函数和操作,例如线性回归、统计检验、时间序列分析等。statsmodels 也是构建在 Numpy、SciPy 和 Pandas 之上的。
线性回归模型
import statsmodels.api as sm
data = sm.datasets.scotland.load(as_pandas=False) # 载入测试数据
data.exog = sm.add_constant(data.exog)
# 实例化伽马模型
>>> gamma_model = sm.GLM(data.endog, data.exog, family=sm.families.Gamma())
>>> gamma_results = gamma_model.fit()
>>> print(gamma_results.summary())
Generalized Linear Model Regression Results
==============================================================================
Dep. Variable: y No. Observations: 32
Model: GLM Df Residuals: 24
Model Family: Gamma Df Model: 7
Link Function: inverse_power Scale: 0.0035843
Method: IRLS Log-Likelihood: -83.017
Date: Fri, 21 Feb 2020 Deviance: 0.087389
Time: 13:59:13 Pearson chi2: 0.0860
No. Iterations: 6
Covariance Type: nonrobust
==============================================================================
coef std err z P>|z| [0.025 0.975]
------------------------------------------------------------------------------
const -0.0178 0.011 -1.548 0.122 -0.040 0.005
x1 4.962e-05 1.62e-05 3.060 0.002 1.78e-05 8.14e-05
x2 0.0020 0.001 3.824 0.000 0.001 0.003
x3 -7.181e-05 2.71e-05 -2.648 0.008 -0.000 -1.87e-05
x4 0.0001 4.06e-05 2.757 0.006 3.23e-05 0.000
x5 -1.468e-07 1.24e-07 -1.187 0.235 -3.89e-07 9.56e-08
x6 -0.0005 0.000 -2.159 0.031 -0.001 -4.78e-05
x7 -2.427e-06 7.46e-07 -3.253 0.001 -3.89e-06 -9.65e-07
==============================================================================
复制代码
方差分析
>>> import statsmodels.api as sm
>>> from statsmodels.formula.api import ols
>>> moore = sm.datasets.get_rdataset("Moore", "carData", cache=True) # 载入测试数据
>>> data = moore.data
>>> data = data.rename(columns={"partner.status": "partner_status"}) # 重命名
>>> moore_lm = ols('conformity ~ C(fcategory, Sum)*C(partner_status, Sum)', data=data).fit()
>>> table = sm.stats.anova_lm(moore_lm, typ=2) # Type 2 ANOVA DataFrame
>>> print(table)
sum_sq df F PR(>F)
C(fcategory, Sum) 11.614700 2.0 0.276958 0.759564
C(partner_status, Sum) 212.213778 1.0 10.120692 0.002874
C(fcategory, Sum):C(partner_status, Sum) 175.488928 2.0 4.184623 0.022572
Residual 817.763961 39.0 NaN NaN
复制代码
XGBoost
XGBoost 是一个改进的梯度增强算法(gradient boosting),基本的梯度增强算法(GBDT)已经在 scikit-learn 中有了实现。而 XGBoost 改进了该算法,提供了分布式的能力,更快速,更灵活,更便携。由于 XGBoost 的简单易学,性能高效和表现优异,常常被用于各种数据科学算法大赛。例如,著名的 Kaggle 大赛,XGBoost 是许多竞赛的夺冠热门算法。
XGBoost 是一个算法的名称,但是提供众多语言的实现,例如 Python, R, Java, Scala, C++ 等,同时也支持 Hadoop,Spark 等大数据生态。
如果需要详细了解 GBDT 和 XGBoost 的前世今生,可以戳这篇文章。
这里还有几个 Kaggle 竞赛项目的具体例子,可以供大家直接学习。
参考
- Numpy
- Scipy
- Pandas
- Scikit-Learn
- 身高预测
- Statsmodels
- XGBoost
- XGBoost Docs
- GBDT、XGBoost、LightGBM 的使用及参数调优
- Kaggle Getting Started
下篇:数据可视化
数据可视化,顾名思义就是将繁多的数据转换成利于展示,便于人理解的图表。而这其中,最为常见的就是几种图:饼图、折线图、柱状图、雷达图、箱线图等等。下面我们来看看 Python 中众多的绘图库,如何来绘制这些图表吧。
Matplotlib
Matplotlib 是 Python 非常著名的绘图库,受到了 Matlab 的启发,使用方式和接口都非常像 Matlab。先来看看用 Matplotlib 绘制的图,
比较中规中矩,非常适合科学计算领域。 Matplotlib 一般以如下的方式引用。
import matplotlib.pyplot as plt
复制代码
Matplotlib 中有几个核心概念。
- Figure:面板,所有图像都是位于 figure 对象中,一个图像只能有一个 figure 对象。
- Subplot:子图,figure 对象下创建一个或多个 subplot 对象(即坐标系)用于绘制图像。
- Axis:坐标轴,即每个子图或坐标系中的一条坐标轴。
绘制柱状图的例子。
import matplotlib.pyplot as plt
labels = ['G1', 'G2', 'G3', 'G4', 'G5']
men_means = [20, 35, 30, 35, 27]
women_means = [25, 32, 34, 20, 25]
men_std = [2, 3, 4, 1, 2]
women_std = [3, 5, 2, 3, 3]
width = 0.35
fig, ax = plt.subplots() # 即 Figure,subplot 对象
# 绘制柱状图
ax.bar(labels, men_means, width, yerr=men_std, label='Men')
ax.bar(labels, women_means, width, yerr=women_std, bottom=men_means, label='Women')
# 设置标签、标题、图例
ax.set_ylabel('Scores')
ax.set_title('Scores by group and gender')
ax.legend()
# 显示图表
plt.show()
复制代码
会显示如下所示的图形。
Matplotlib 配置的方式比较直观,同时也支持 Numpy 数组作为输入。
fig = plt.figure(2) # 新开一个窗口
ax1 = fig.add_subplot(1, 2, 1, polar=True) # 启动一个极坐标子图
theta = np.arange(0, 2 * np.pi, 0.02) # 角度数列值
ax1.plot(theta, 2 * np.ones_like(theta), lw=2) # 画图,参数:角度,半径,lw线宽
ax1.plot(theta, theta / 6, linestyle='--', lw=2) # 画图,参数:角度,半径,linestyle样式,lw线宽
# 启动一个极坐标子图
ax2 = fig.add_subplot(1, 2, 2, polar=True)
ax2.plot(theta, np.cos(5 * theta), linestyle='--', lw=2)
ax2.plot(theta, 2 * np.cos(4 * theta), lw=2)
# 设置网格轴的距离和角度
ax2.set_rgrids(np.arange(0.2, 2, 0.2), angle=45)
ax2.set_thetagrids([0, 45, 90])
plt.show()
复制代码
Matplotlib 是 Python 绘图库中功能强大,但是偏底层的,对于绘图的控制能力比较强,但是配置项也比较多。
Seaborn
Seaborn 是基于 Matplotlib 开发的绘图库,提供了更高级和简洁的语法。通过 Seaborn 的高级 API,能够快速绘图图形,且无需配置就提供了一些好看的样式。
先上一个折线图。
import seaborn as sns
sns.set(style="ticks")
# 载入测试数据集
df = sns.load_dataset("anscombe")
# 为每个数据集绘制折线图并展示线性回归线
sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df,
col_wrap=2, ci=None, palette="muted", height=4,
scatter_kws={"s": 50, "alpha": 1})
复制代码
绘图只有一行代码,是不是非常的酷!
Seaborn 有一套统一的绘图 API,要求原始数据的输入类型为 Pandas 的 Dataframe 或 Numpy 数组,API 形式如下:
- sns.图名(x=’X轴列名’, y=’Y轴列名’, data=原始数据df对象)
- sns.图名(x=’X轴列名’, y=’Y轴列名’, hue=’分组绘图参数’, data=原始数据df对象)
- sns.图名(x=np.array, y=np.array[, …])
也来绘制一个柱状图。
import seaborn as sns
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 生成数据
x = np.arange(8)
y = np.array([1,5,3,6,2,4,5,6])
df = pd.DataFrame({"x-axis": x, "y-axis": y})
# 绘制柱状图
sns.barplot("x-axis", "y-axis", palette="RdBu_r", data=df)
# 调用 Matplotlib 的底层 API
plt.xticks(rotation=90)
plt.show()
复制代码
Seaborn 提供了一个高层的简便的 API,可以和 Matplotlib 结合使用,大大简化了 Matplotlib 的配置操作。
plotnine/ggplot
plotnine 和 ggplot 的灵感都来源于 R 语言的 ggplot2,和 Matplotlib 的设计思路完全不同,是图层叠加的思想,即一层层叠加绘制。不过它们仍旧是基于 Matplotlib 开发的。输入数据需要是 Pandas 的 DataFrame 类型。两个库的语法类似,ggplot 最近已经不更新,而 plotnine 是最近比较活跃的。
下面的代码来自 plotnine,绘制柱状图。
import pandas as pd
import numpy as np
from plotnine import *
from plotnine.data import *
ggplot(mpg) + geom_bar(aes(x='class'))
复制代码
通过 + 把一个个图层叠加起来,一般会有数据层、几何图形层和美化层组成。
然后加一些色彩。
ggplot(mpg) + geom_bar(aes(x='class', fill='drv'))
复制代码
再进行一些变换。
(
ggplot(mpg)
+ geom_bar(aes(x='class', fill='drv'))
+ coord_flip()
+ theme_classic()
)
复制代码
plotnine / ggplot 的语法非常简洁,非常适合于喜欢或习惯 R 语言绘图的人。
Bokeh
Bokeh 是 Python 中一个非常强大的交互式图表库,即通过 JS 代码生成可以交互的网页端图表,非常适合嵌入到前端应用中。Bokeh 绘制的图表非常好看,并且有不错的交互体验。
左侧的选择框可实时影响中间的数据渲染,图表右侧还有交互式的控制栏。同时数据点也能响应鼠标事件,例如鼠标移动上去后显示具体数值。
上述图表的代码如下:
import pandas as pd
from bokeh.layouts import column, row
from bokeh.models import Select
from bokeh.palettes import Spectral5
from bokeh.plotting import curdoc, figure
from bokeh.sampledata.autompg import autompg_clean as df
df = df.copy()
SIZES = list(range(6, 22, 3))
COLORS = Spectral5
N_SIZES = len(SIZES)
N_COLORS = len(COLORS)
# 数据清理
df.cyl = df.cyl.astype(str)
df.yr = df.yr.astype(str)
del df['name']
columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]
def create_figure():
xs = df[x.value].values
ys = df[y.value].values
x_title = x.value.title()
y_title = y.value.title()
kw = dict()
if x.value in discrete:
kw['x_range'] = sorted(set(xs))
if y.value in discrete:
kw['y_range'] = sorted(set(ys))
kw['title'] = "%s vs %s" % (x_title, y_title)
p = figure(plot_height=600, plot_width=800, tools='pan,box_zoom,hover,reset', **kw)
p.xaxis.axis_label = x_title
p.yaxis.axis_label = y_title
if x.value in discrete:
p.xaxis.major_label_orientation = pd.np.pi / 4
sz = 9
if size.value != 'None':
if len(set(df[size.value])) > N_SIZES:
groups = pd.qcut(df[size.value].values, N_SIZES, duplicates='drop')
else:
groups = pd.Categorical(df[size.value])
sz = [SIZES[xx] for xx in groups.codes]
c = "#31AADE"
if color.value != 'None':
if len(set(df[color.value])) > N_COLORS:
groups = pd.qcut(df[color.value].values, N_COLORS, duplicates='drop')
else:
groups = pd.Categorical(df[color.value])
c = [COLORS[xx] for xx in groups.codes]
p.circle(x=xs, y=ys, color=c, size=sz, line_color="white", alpha=0.6, hover_color='white', hover_alpha=0.5)
return p
def update(attr, old, new):
layout.children[1] = create_figure()
x = Select(title='X-Axis', value='mpg', options=columns)
x.on_change('value', update)
y = Select(title='Y-Axis', value='hp', options=columns)
y.on_change('value', update)
size = Select(title='Size', value='None', options=['None'] + continuous)
size.on_change('value', update)
color = Select(title='Color', value='None', options=['None'] + continuous)
color.on_change('value', update)
controls = column(x, y, color, size, width=200)
layout = row(controls, create_figure())
curdoc().add_root(layout)
curdoc().title = "Crossfilter"
复制代码
由于 Bokeh 生成的是 Html 网页,无法直接看到图片,可以使用 Bokeh 服务器运行查看,当然也可以整合到自己的前端服务中。
bokeh serve --show crossfilter
复制代码
Pygal
Pygal 是一个 SVG 绘图库,SVG 是一种矢量图,也可嵌入前端网页中做交互式展示。不过和 Bokeh 不同的是,SVG 只是图表交互,并不包含 JS 代码来动态修改数据和样式。
Pygal 绘制的图表也是可以通过鼠标交互的,也可是隐藏或显示某个系列数据。
绘制柱状图。
bar_chart = pygal.Bar()
# 添加数据
bar_chart.add('Fibonacci', [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55])
# 生成 SVG
bar_chart.render_to_file('bar_chart.svg')
复制代码
只能展示静态的截图。
多个系列的数据。
import pygal
# 配置图表
line_chart = pygal.Bar()
line_chart.title = 'Browser usage evolution (in %)'
line_chart.x_labels = map(str, range(2002, 2013))
# 添加数据
line_chart.add('Firefox', [None, None, 0, 16.6, 25, 31, 36.4, 45.5, 46.3, 42.8, 37.1])
line_chart.add('Chrome', [None, None, None, None, None, None, 0, 3.9, 10.8, 23.8, 35.3])
line_chart.add('IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6, 54.7, 44.8, 36.2, 26.6, 20.1])
line_chart.add('Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4, 8.9, 5.8, 6.7, 6.8, 7.5])
line_chart.render()
复制代码
Pygal 语法简单,且能生成交互式 SVG 图表,适合有前端简单交互需求的场景使用。
Plotly
Plotly 是针对科学计算和机器学习可视化开发的图表库,同时它们还有 Dash 框架,用于快速开发机器学习或数据科学的应用。Plotly 绘制的图表非常美观,而且也是前端交互式的图表。Plotly 是 Plotly 公司的产品,不过它仍旧是开源且免费使用的,无需联网或注册账户,不过它们也提供企业版本。
还是绘制柱状图。
import plotly.express as px
data_canada = px.data.gapminder().query("country == 'Canada'")
fig = px.bar(data_canada, x='year', y='pop')
fig.show()
复制代码
图表也是可以交互的,且右上角有菜单栏。
简单的代码美化效果。
import plotly.express as px
data = px.data.gapminder()
data_canada = data[data.country == 'Canada']
fig = px.bar(data_canada, x='year', y='pop',
hover_data=['lifeExp', 'gdpPercap'], color='lifeExp',
labels={'pop':'population of Canada'}, height=400)
fig.show()
复制代码
Plotly 非常适合构建前端交互式图表,同时可以使用 Dash 框架快速构建数据分析应用,使用上也非常简单明了。
Altair
Altair 是基于 Vega 和 Vega-Lite 语法的声明式绘图语言,可以使用简单的语法绘制交互式的图表,效果还是比较好的。
照例来绘制柱状图。
import altair as alt
import pandas as pd
source = pd.DataFrame({
'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
})
alt.Chart(source).mark_bar().encode(
x='a',
y='b'
)
复制代码
图表也是交互式的。
import altair as alt
from vega_datasets import data
source = data.wheat()
bar = alt.Chart(source).mark_bar().encode(
x='year:O',
y='wheat:Q'
)
# 添加均值线
rule = alt.Chart(source).mark_rule(color='red').encode(
y='mean(wheat):Q'
)
(bar + rule).properties(width=600)
复制代码
Altair 的使用也非常简单,且可以绘制强大的交互式图表,不过需要学习 Vega 或 Vega-lite 绘图语法。