Copyright ©2022 Zhang Tongshuai

图形语法与plotnine¶

张统帅 清华大学¶

2020.06.20¶

产生背景¶

产生背景¶

  • 统计图表自18世纪,逐渐得到广泛应用;
  • 制图理论的完备,催生了越来越多的图表;
  • 可视化图表绘制软件随之也越来越多;
  • 如何能以较小代价穷举尽可能多的图形?

一张图vs一句话¶

  • 一句话中的每个单词有语法上的定义;
  • 单词的改变可导致句子意思的巨大变化。

数据的“流式表达”¶

data_flow_expression

Leland Wilkinson 和《The Grammar of Graphics》¶

  • 90年代末,开发统计图形绘图工具 GPL;
  • 对无数统计图表分析研究后的理论总结;
  • 实现图形语法的软件架构细节。

用一套语法描述任意图形的方法诞生了!

图形语法元素¶

图表不是一个单独的实体,统计图形的定义依靠以下几个基础语法:

声明 描述
DATA 从数据集生成视觉编码的数据操作
TRANS 视觉编码变换(譬如rank)
SCALE 度量变换(譬如log)
COORD 定义坐标系(譬如极坐标)
ELEMENT 图形(譬如点图)及其视觉属性(譬如color)
GUIDE 辅助元素(譬如legend)

图形语法的实现¶

环境 实现
R ggplot2
JSON Vega
Tableau VuzQL
Javascript G2
Python plotnine/Bokeh

图形语法剖析(plotnine)¶

Plot(图)= data(数据集)+ Aesthetics(美学映射)+ Geometry(几何对象)

show_case

Story Telling Visualization¶

假设我们有以下数据:

City Region Price Volume Sales
0 Beijing North 11 8.04 88.44
1 Shanghai East 8 6.95 55.60
2 Guangzhou South 13 7.58 98.54
3 Shenzhen South 8 8.81 70.48
4 Tianjin South 11 9.33 102.63
5 Chongqing North 14 9.96 139.44

Layers 1-2-3 Data-Aesthetics-Geometries¶

  • Data 是基础,包含要绘制的元素
  • Aesthetics提供轴和映射关系
  • Geometries提供几何对象(统计图表)

Layers 1-2-3 Data-Aesthetics-Geometries¶

  1. 将Price映射到x,Sales映射到y
  2. 以points的形式绘制
In [50]:
from plotnine import ggplot, aes
from plotnine.geoms import *
In [51]:
ggplot(data, aes(x='Price', y='Sales')) +  geom_point()
Out[51]:
<ggplot: (149469665196)>

Layers 1-2-3 Data-Aesthetics-Geometries¶

我们将Region映射到color

In [52]:
ggplot(data, aes(x='Price', y='Sales', color='Region')) +  geom_point()
Out[52]:
<ggplot: (149468623954)>

Layers 1-2-3 Data-Aesthetics-Geometries¶

我们将Volume映射到size

In [53]:
(ggplot(data, aes(x='Price', y='Sales', 
                 color='Region', size='Volume')) 
 +  geom_point()
)
Out[53]:
<ggplot: (149462919075)>

Layer4 Facets¶

用分面facets对数据进行分组

  • 在一个画布上分布多幅图形
  • 先把数据划分为多个子集
  • 然后把每个子集依次绘制到画布的不同面板

Layer4 Facets¶

我们依据Region的不同进行分面

In [54]:
from plotnine.facets import *
ggplot(data, aes(x='Price', y='Sales', 
                 color='Region', size='Volume')) +\
    geom_point() +\
    facet_wrap('Region')
Out[54]:
<ggplot: (149461057034)>

Layer 5: Statistics¶

  • 统计变换是对数据进行统计
  • 通常以某种方式对数据做计算
  • 然后在图上显示变换后结果
  • 每种几何对象,默认对应一种统计变换;
  • 每种统计变换,默认对应一个几何对象。

Layer 5: Statistics¶

针对每一个价位,计算销量的均值

In [55]:
from plotnine.stats import *
import numpy as np
ggplot(data, aes(x='Price', y='Sales')) +\
    stat_summary(fun_y=np.mean, geom='bar')
Out[55]:
<ggplot: (149466799397)>

Layer 6: Coordinates¶

  • 坐标系统控制坐标轴,可以进行变换
  • 例如XY轴翻转、坐标系变换等
  • plotnine的坐标系统功能尚不完整

Layer 6: Coordinates¶

将x和y坐标翻转

In [56]:
from plotnine.coords import *
ggplot(data, aes(x='Price', y='Sales', 
                 color='Region', size='Volume')) +\
    geom_point() +\
    facet_wrap('Region') +\
    coord_flip()
Out[56]:
<ggplot: (149468624511)>

Layer 7: Themes¶

  • 对所有非数据的元素进行定制
  • 改变字体、坐标轴、背景等各种元素
  • 预置一些已经写好的主题

Layer 7: Themes¶

绘制xkcd(一种科学漫画)风格

In [57]:
from plotnine.themes import *

ggplot(data, aes(x='Price', y='Sales', 
                 color='Region', size='Volume')) +\
    geom_point() +\
    facet_wrap('Region') +\
    theme_xkcd()
Out[57]:
<ggplot: (149466845154)>

图形语法小结¶

Data 绘制所用数据(DataFrame)
Aesthetics 数据映射为图像属性
Geometries 用来表示数据的几何形状
Facets 对数据进行分组并绘制子图
Statistics 通过统计运算得到新数据
Coordinates 变换数据绘制的空间
Theme 对所有非数据元素进行定制
“+” 实现不同图层的叠加

Plotnine介绍¶

  • 根据图层语法开发
  • 借鉴R的 ggplot2包
  • 基于matplotlib
  • 与pandas配合良好
  • 目前仍在活跃开发中

语法格式¶

code_format

上帝的归上帝, 凯撒的归凯撒 plotnine_intro

准备工作¶

In [58]:
%matplotlib inline
import plotnine as p9
import pandas as pd
#导入plotnine包的绘图函数
from plotnine import * 
#导入plotnine自带的数据集
from plotnine.data import * 

p9.options.figure_size = (9, 4.5)

surveys_complete = pd.read_csv('../data/surveys.csv')
surveys_complete = surveys_complete.dropna()

基本用法¶

  • 将图与数据绑定,创建ggplot对象
  • 未定义任何图像属性,显示空白
In [59]:
(p9.ggplot(data=surveys_complete))
Out[59]:
<ggplot: (149464795844)>

数据驱动图表¶

  • 使用aes()建立数据变量与图中元素的映射
  • 可以理解为,哪一个变量驱动响应元素
In [60]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight', y='hindfoot_length')))
Out[60]:
<ggplot: (149462844013)>

数据驱动图表¶

aes()函数中常见的映射选项是:

  • x和y:用于指定x轴和y轴映射的变量
  • color:映射点或线的颜色
  • fill:映射填充区域的颜色
  • linetype:映射图形的线形(1=实线、2=虚线、3=点、4=点破折号、5=长破折号、6=双破折号)
  • size:点的尺寸和线的宽度
  • shape:映射点的形状
  • group:默认情况下ggplot2把所有观测点分为了一组, 如果需要把观测点按额外的离散变量进行分组处理, 必须修改默认的分组设置

encodings

探索式作图¶

  • 使用“+”进行图层的叠加
  • 允许修改现有ggplot对象
  • 可以建立图标的模板/原型
  • 从而探索各种类型的图表
In [61]:
# Create
surveys_plot = p9.ggplot(
    data=surveys_complete,
    mapping=p9.aes(x='weight', y='hindfoot_length'))

# Draw the plot
surveys_plot + p9.geom_point()
Out[61]:
<ggplot: (149469674406)>

尝试——柱状图(Bar)¶

  • 使用surveys_complete的plot-id列创建柱状图
  • 设置stat参数确定柱状图的高度:
    • "count",高度等于每组的数据个数
    • "bin",连续变量进行统计转换
    • "identity",高度表示数据数据的值
In [62]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='plot_id')) 
 + p9.geom_bar(stat='count')
)
Out[62]:
<ggplot: (149468567227)>
In [63]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='plot_id', y='weight')) 
 + p9.geom_bar(stat='identity')
)
Out[63]:
<ggplot: (149466039016)>
In [64]:
import matplotlib.pyplot as plt
plt.style.use('ggplot')
surveys_complete.groupby('plot_id').sum().plot(kind='bar', y='weight', figsize=(10,7))
plt.ylabel("weight")
plt.show()

Tips¶

  • ggplot()中的参数是全局的,可以被所有geom层看到
  • 对于每一层geom,可以单独设置aes()

迭代作图¶

  • plotnine作图是迭代过程
  • data、aes、geom是基本元素
In [65]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length'))
 + p9.geom_point()
)
Out[65]:
<ggplot: (149468412805)>
In [66]:
# 调整透明度
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight', y='hindfoot_length'))
 + p9.geom_point(alpha=0.1)
)
Out[66]:
<ggplot: (149460941948)>
In [67]:
# 设定所有点的颜色
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight', y='hindfoot_length'))
 + p9.geom_point(alpha=0.1, color='green')
)
Out[67]:
<ggplot: (149463554121)>
In [68]:
# 将`species_id`映射到颜色
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length'))
 + p9.geom_point(alpha=0.1, mapping=aes(color='species_id'))
)
Out[68]:
<ggplot: (149462884220)>

TIPS¶

注意,映射和设定是不同的:

  • 映射是将一个变量中离散或连续的数据与一个图形属性中以不同的参数来相互关联,
  • 而设定能够将这个变量中所有的数据统一为一个图形属性。

设置标度(Scales)¶

  • 标度控制着数据到图形属性的映射
  • 通过标度可以修改坐标轴和图例的参数
  • 常用标度:
    • 标签
    • 坐标轴
    • 图形选项(颜色、size、形状、线形等)

标度——标签¶

可以通过函数labs(name=value)来指定图形的标题(title),子标题(subtitle),坐标轴的标签(x,y)等,并可以指定标签的美学选项:

  • 参数是美学(aesthetic)选项
  • 使用name=value模式
  • 可以使用的选项是:
    • 指定文字:title、subtitle、caption、x和y
    • 指定美学选项:color、size等

标度——坐标轴¶

  • 标度是区分离散和连续变量的
  • 连续型、离散型和日期-时间型变量
  • 将以上类型变量映射构造对应的坐标轴。
函数 说明
scale_x_log10() x轴以log10的格式设定
scale_x_reverse() 将x坐标轴反转至y坐标轴
scale_x_sqrt() 将将x轴以sqrrt的格式设

常用scale函数¶

函数 说明
scale_*_continuous() 将连续型数值映射
scale_*_discrete() 将离散型数值映射
scale_*_identity() 将时间型数值映射
scale_*_manual(values = ()) 自定义将离散型数值映射
scale_*_date(date_labels = "%m/%d"),
date_breaks = "2 weeks")
将数据设定为时间型
scale_*_datetime() 将x轴数据设定为时间型

标度设置示例¶

In [69]:
p9.options.figure_size = (9, 6)
# 改变X轴坐标
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length'))
 + p9.geom_point(alpha=0.1, 
                 mapping=aes(color='species_id'))
 + p9.xlab("Weight (g)")
)
Out[69]:
<ggplot: (149460848457)>
In [70]:
# 采用对数坐标轴
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length'))
 + p9.geom_point(alpha=0.1, mapping=aes(color='species_id'))
 + p9.scale_x_log10()
)
Out[70]:
<ggplot: (149469660657)>

练习——柱状图颜色填充¶

在前面柱状图的基础上,在柱子内按照性别的比例填充两种不同的颜色。

In [71]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='plot_id',
                          fill='sex'))
    + p9.geom_bar()
    + p9.scale_fill_manual(["blue", "orange"])
)
Out[71]:
<ggplot: (149460700651)>

绘制分布图¶

  • 绘制箱型图
  • 在箱型图上叠加数据点
  • 绘制概率密度图
  • 在小提琴图上叠加数据点

绘制箱型图¶

对于每一种species_id,查看weight的分布

In [72]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='species_id',
                          y='weight'))
    + p9.geom_boxplot()
)
Out[72]:
<ggplot: (149469665259)>

箱型图叠加数据点¶

In [73]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='species_id',
                          y='weight'))
 + p9.geom_jitter(alpha=0.2) # 消除点的重合
 + p9.geom_boxplot(alpha=0, outlier_color = "red")
)
Out[73]:
<ggplot: (149463529707)>

绘制概率密度图¶

按照species_id绘制weight的概率密度图

In [74]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          fill='species_id'
                         ))
 + p9.geom_density(alpha=0.2)
)
Out[74]:
<ggplot: (149463630574)>

采用小提琴图叠加数据点¶

In [75]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='species_id',
                          y='weight',
                          color='factor(plot_id)'))
    + p9.geom_jitter(alpha=0.3)
    + p9.geom_violin(alpha=0, color="0.7")
    + p9.scale_y_log10()
)
Out[75]:
<ggplot: (149463691102)>

绘制时间序列数据¶

对于每一个species_id统计每年的数目,并绘图。

In [76]:
# 按照 species_id和year进行聚合
yearly_counts = surveys_complete.groupby(['year', 'species_id'])['species_id'].count()
# 重置索引
yearly_counts = yearly_counts.reset_index(name='counts')
yearly_counts.head()
Out[76]:
year species_id counts
0 1977 DM 181
1 1977 DO 12
2 1977 DS 29
3 1977 OL 1
4 1977 OX 2
In [77]:
(p9.ggplot(data=yearly_counts,
           mapping=p9.aes(x='year',
                          y='counts',
                          color='species_id'))
    + p9.geom_line()
)
Out[77]:
<ggplot: (149460896193)>

分面绘制多个子图¶

两种方式,分别使用 facet_wrap 或 facet_grid 函数。

  • facet_warp,用一个标准进行分类,按从左到右从上到下的顺序进行排列:
  • facet_grid,可以用2个维度分组,按照横向/纵向排列
In [78]:
# 基于前面的例子
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length',
                          color='species_id'))
    + p9.geom_point(alpha=0.1)
)
Out[78]:
<ggplot: (149466015091)>
In [79]:
# 按照性别分为两个子图
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length',
                          color='species_id'))
    + p9.geom_point(alpha=0.1)
    + p9.facet_wrap("sex")
)
Out[79]:
<ggplot: (149468409004)>
In [80]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length',
                          color='species_id'))
    + p9.geom_point(alpha=0.1)
    + p9.facet_wrap("plot_id")
)
Out[80]:
<ggplot: (149461355819)>
In [81]:
# only select the years of interest
survey_2000 = surveys_complete[surveys_complete["year"].isin([2000, 2001])]

(p9.ggplot(data=survey_2000,
           mapping=p9.aes(x='weight',
                          y='hindfoot_length',
                          color='species_id'))
    + p9.geom_point(alpha=0.1)
    + p9.facet_grid("year ~ sex")
)
Out[81]:
<ggplot: (149466095165)>

练习¶

按照性别,绘制子图展示平均weight随时间的变化

yearly_weight = surveys_complete.groupby(['year', 'sex'])['weight'].mean().reset_index()
In [82]:
yearly_weight = surveys_complete.groupby(['year', 'sex'])['weight'].mean().reset_index()
(p9.ggplot(data=yearly_weight,
           mapping=p9.aes(x='year',
                          y='weight'))
    + p9.geom_line()
    + p9.facet_wrap("sex")
)
Out[82]:
<ggplot: (149463597692)>

练习¶

在上图基础上,比较不同species_id的变化趋势。

In [83]:
yearly_weight = surveys_complete.groupby(['year', 'species_id', 'sex'])['weight'].mean().reset_index()
(p9.ggplot(data=yearly_weight, mapping=p9.aes(x='year', y='weight', color='species_id')) + p9.geom_line() + p9.facet_wrap('sex') )
Out[83]:
<ggplot: (149462516104)>

进一步定制化¶

  • 将year作为分类变量(categorical),统计各年数目
  • 有什么问题?
In [84]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='factor(year)'))
    + p9.geom_bar()
)
Out[84]:
<ggplot: (149464795784)>
In [85]:
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='factor(year)'))
    + p9.geom_bar()
    + p9.theme_bw()
    + p9.theme(axis_text_x = p9.element_text(angle=90))
)
Out[85]:
<ggplot: (149463144060)>

保存自定义主题¶

  • 自定义的主题设置可以保存为对象
  • 在画图时作为图层叠加
In [86]:
my_custom_theme = p9.theme(axis_text_x = p9.element_text(color="grey", size=10,
                                                         angle=90, hjust=.5),
                           axis_text_y = p9.element_text(color="grey", size=10))
(p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='factor(year)'))
    + p9.geom_bar()
    + my_custom_theme
)
Out[86]:
<ggplot: (149463541435)>

保存图片¶

In [87]:
my_plot = (p9.ggplot(data=surveys_complete,
           mapping=p9.aes(x='weight', y='hindfoot_length'))
    + p9.geom_point()
)
my_plot.save("scatterplot.png", width=4, height=2, dpi=300)
from PIL import Image
im = Image.open('scatterplot.png')
im
C:\ProgramData\Anaconda3\envs\study\lib\site-packages\plotnine\ggplot.py:727: PlotnineWarning: Saving 4 x 2 in image.
C:\ProgramData\Anaconda3\envs\study\lib\site-packages\plotnine\ggplot.py:730: PlotnineWarning: Filename: scatterplot.png
Out[87]:

qplot快速作图¶

  • 用法与传统plot类似,
  • 前两个参数分别是X轴变量和y轴变量
  • 可以用data指定数据集
  • 通过参数设置图形样式 -可以添加geom
In [88]:
surveys_plot = p9.qplot(x=surveys_complete['weight'], y=surveys_complete['hindfoot_length'])
surveys_plot
Out[88]:
<ggplot: (149460691288)>
In [89]:
surveys_plot = p9.qplot(data=surveys_complete,
                        x='weight', y='hindfoot_length')
surveys_plot
Out[89]:
<ggplot: (149460999503)>
In [90]:
surveys_plot = p9.qplot(data=surveys_complete,
                        x='weight', y='hindfoot_length',
                        color='weight')
surveys_plot
Out[90]:
<ggplot: (149463573194)>
In [91]:
surveys_plot = p9.qplot(data=surveys_complete,
                        x='weight', y='hindfoot_length',
                        geom = ["point", "bin2d"])
surveys_plot
Out[91]:
<ggplot: (149463874130)>

总结¶

  • plotnine采用图形语法
  • API与R ggplot2高度相似
  • 作图效果好于matplotlib
  • 适用于探索性数据分析

不足:

  • 功能不完全或者存在bug
  • 有一定的学习使用门槛
  • 教程资料较少