</img>

目标与原则

以最小的复杂度展示足够多的信息

  1. 目标:如果不能为用户提供有用的信息,那么就没啥用;如果信息展现形式太复杂,那么就会被噪声干扰
  2. 原则:
    1. 对比(Contrast):让页面引人注目,避免页面上的元素太过相似。如果元素(字体、颜色、大小、线宽、形状、空间等)不相同,那就干脆让它们截然不同。
    2. 重复(Repetition):让设计中的视觉要素在整个作品中重复出现。既能增加条理性,还可以加强统一性。
    3. 对齐(Alignment):任何东西都不能在页面上随意安放。每个元素都应当与页面上的另一个元素有某种视觉联系,建立清晰、精巧而且清爽的外观。
    4. 亲密性(Proximity):彼此相关的项应当靠近,归组在一起。如果多个项相互之间存在很近的亲密性,它们就会成为一个视觉单元,而不是多个孤立的元素。这有助于组织信息,减少混乱,为读者提供清晰的结构。

美国教育家、设计师Robin Williams《The Non-Designer's Design Book(写给大家看的设计书)》

随着HTML5、SVG/Canvas普及,尤其是Mike Bostock于2010开源D3.js,python数据可视化受到冲击,Facebook于2013发布react.js后,Python数据可视化开始向web组件化发展,重点方向是机器学习与Web交互

首发年份 名称 简介
2003 matplotlib 基础绘图工具
2010 networkx 复杂网络与图算法工具
2012 seaborn 快速统计图
2012 bokeh 交互式
2012 plotly 交互式
2015 altair 声明式语义
2015 dash 基于plotly的web app
2015 tensorboard tensorflow and keras
2017 ipyvolume 3D交互
2018 Vaex 高性能渲染
2018 streamlit 机器学习web app
2018 volia notebook web app

pyviz网站整理了Python数据可视化工具

matplotlib基础图库

Matplotlib的设计哲学是让Python程序员完全控制可视化应用。Matplotlib中文字体显示问题,请参考中文设置方法

  1. 模仿MatLab,上手简单,理工科同学上手成本低
  2. 许多渲染接口
  3. 功能齐全、文档完整
  4. 测试容易、源代码质量高

</img>

%load_ext autoreload
%autoreload 2

%matplotlib inline
from matplotlib.font_manager import _rebuild

_rebuild()
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_style("whitegrid", {"font.sans-serif": ["SimHei", "Arial"]})

import pandas_alive
import pandas as pd
import numpy as np
df_covid = pd.read_json("3.data-viz/timeseries.json")
df_covid.index = pd.DatetimeIndex(df_covid.iloc[:, 0].apply(lambda _: _["date"]))
df_covid.index.name = "日期"
df_covid = df_covid.applymap(lambda _: int(_["confirmed"]))
df_covid.replace(0, np.nan, inplace=True)
top20 = df_covid.iloc[-1].sort_values().tail(20).index
df_covid = df_covid[top20]

绘图环境

  1. 在Jupyter(IPython) Notebook中画图:
    • %matplotlib notebook会在Notebook中启动交互式图形
    • %matplotlib inline会在Notebook中启动静态图形
  2. 在.py文件中画图:执行使用matplotlib的脚本后,会看到一个新窗口,里面会显示图形
  3. 在IPython shell中画图(在图形界面系统中启动):在IPython shell中启动ipython后使用%matplotlib魔法命令,每个plt命令都会自动打开一个图形窗口
%matplotlib notebook

x = np.linspace(0, 10, 100)
fig = plt.figure()
plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x))
plt.title('测试')

# plt.show()`会启动一个事件循环(event loop)
plt.show()
%matplotlib inline

x = np.linspace(0, 10, 100)

fig = plt.figure()
plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x))
plt.title('测试')

# plt.show()`会启动一个事件循环(event loop)
plt.show()
# 保存图形
fig.savefig('sin_cos.png')
ls -lh sin_cos.png
-rw-r--r--  1 toddtao  staff    23K May 28 16:13 sin_cos.png
# 用IPython的`Image`对象显示图形
from IPython.display import Image
Image('sin_cos.png')

用markdown语法显示图形

绘图接口

  1. 图形结构:Artist(Figure、Axes、Axis)
  2. MATLAB风格接口:通过pyplot(plt)接口绘图,与MATLAB语法类似,plt是有状态的(stateful),会持续跟踪“当前的”图形和坐标轴,控制子图时比较麻烦
  3. 面向对象接口:通过FigureAxes方法控制图形,可以按照行列控制子图,操作非常灵活
    • figure:plt.Figure类的一个实例,是一个能够容纳各种坐标轴、图形、文字和标签的容器
    • axes:plt.Axes类的一个实例,是一个带有刻度和标签的矩形,包含所有可视化的图形元素

</img>

MATLAB风格接口

plt.figure()  # 创建图形

# 创建两个子图中的第一个,设置坐标轴
plt.subplot(2, 1, 1) # (行、列、子图编号)
plt.plot(x, np.sin(x))

# 创建两个子图中的第二个,设置坐标轴
plt.subplot(2, 1, 2)
plt.plot(x, np.cos(x));

面向对象接口

# 先创建图形网格
# ax是一个包含两个Axes对象的数组
fig, ax = plt.subplots(2)

# 在每个对象上调用`plot()`方法
ax[0].plot(x, np.sin(x))
ax[1].plot(x, np.cos(x));

图形配置

图形样式

plt.plot()函数设置颜色(color参数)与风格(linestyle参数)

plt.figure(figsize=(10, 5))
plt.plot(x, np.sin(x - 0), color='blue')        # 标准颜色名称
plt.plot(x, np.sin(x - 1), color='g')           # 缩写颜色代码(rgbcmyk)
plt.plot(x, np.sin(x - 2), color='0.75')        # 范围在0~1之间的灰度值
plt.plot(x, np.sin(x - 3), color='#FFDD44')     # 十六进制(RRGGBB,00~FF)
plt.show()
plt.figure(figsize=(10, 5))
plt.plot(x, np.sin(x - 0), linestyle='-')  # 实线
plt.plot(x, np.sin(x - 1), linestyle='--') # 虚线
plt.plot(x, np.sin(x - 2), linestyle='-.') # 点划线
plt.plot(x, np.sin(x - 3), linestyle=':');  # 实点线
plt.show()

可以将linestylecolor编码组合起来,作为plt.plot()函数的一个参数使用:

plt.figure(figsize=(10, 5))
plt.plot(x, np.sin(x - 0), '-g')  # 绿色实线
plt.plot(x, np.sin(x - 1), '--c') # 青色虚线
plt.plot(x, np.sin(x - 2), '-.k') # 黑色点划线
plt.plot(x, np.sin(x - 3), ':r');  # 红色实点线

图形标签

  1. 图标题
  2. 轴标题
  3. 图例
plt.figure(figsize=(10, 5))
plt.plot(x, np.sin(x), '-g', label='sin(x)')
plt.plot(x, np.cos(x), ':b', label='cos(x)')
plt.title("正弦余弦曲线")
plt.xlabel("x值")
plt.ylabel("三角函数值");
plt.legend();

画散点图

创建散点图可以用plt.plotplt.scatter。后者功能更强大,可以让每个散点具有不同的属性(大小、表面颜色、边框颜色等),实现多维度可视化,例如alpha参数来调整透明度:

plt.plot性能优于plt.scatter:由于plt.scatter会对每个散点进行单独渲染,因此渲染器会消耗更多的资源。而在plt.plot中,散点基本都彼此复制,因此整个数据集中所有点的颜色、尺寸只需要配置一次,因此处理几千个点的数据集时,plt.plot方法比plt.scatter方法性能好。

rng = np.random.RandomState(0)
x = rng.randn(100)
y = rng.randn(100)
colors = rng.rand(100)
sizes = 1000 * rng.rand(100)
plt.figure(figsize=(10, 10))
plt.scatter(x, y, c=colors, s=sizes, alpha=0.3, cmap="viridis")
# 显示颜色条
plt.colorbar();

MNIST手写数字可视化

MNIST手写数字是机器学习图像识别经典示例,在Scikit-Learn里面,包含近2000份8×8的手写数字缩略图,每个图片都是8x8=64像素,展开成特征矩阵是64维空间。

from sklearn.datasets import load_digits
digits = load_digits()

fig, ax = plt.subplots(10, 10, figsize=(8, 8))
for i, axi in enumerate(ax.flat):
    axi.imshow(digits.images[i], cmap='binary')
    axi.set(xticks=[], yticks=[])

通过scikit-learn流形学习(manifold learning)最早的算法Isomap(Isometric Mapping)将64维空间降成2维平面实现可视化,对比PCA(主成分分析),Isomap可以学习到非线性特征

"流形学习"——中国拓扑学家江泽涵院士取自文天祥《正气歌》的“天地有正气,杂然赋流形”,表示“多样体”

from sklearn.manifold import Isomap

iso = Isomap(n_components=2).fit_transform(digits.data)
sns.set_style("dark", {"font.sans-serif": ["SimHei", "Arial"]})
plt.figure(figsize=(10, 10))
plt.scatter(
    iso[:, 0], iso[:, 1], lw=0.1, c=digits.target, cmap=plt.cm.get_cmap("cubehelix", 10),
)
plt.colorbar(ticks=range(10), label="数字值")
plt.clim(-0.5, 9.5);

子图

Matplotlib通过子图(subplot)的概念,实现多角度数据对比:

  1. plt.axes:手动创建子图
  2. plt.subplot:简易网格子图
  3. plt.subplots:用NumPy数组创建网格
  4. plt.GridSpec:自由排列网格

plt.axes:手动创建子图

plt.axes函数默认创建一个标准的坐标轴,填满整张图。其可选参数用4个值分别表示图形坐标的 [bottom, left, width, height](底坐标、左坐标、宽度、高度),数值的取值范围是左下角(原点)为0,右上角为1。

sns.set_style("white", {"font.sans-serif": ["SimHei", "Arial"]})
plt.figure(figsize=(10, 10))
ax1 = plt.axes()  # 默认坐标轴
ax2 = plt.axes([0.65, 0.65, 0.2, 0.2])

面向对象画图接口中类似的命令有fig.add_axes()。用这个命令创建两个竖直排列的坐标轴:

fig = plt.figure(figsize=(10, 10))
ax1 = fig.add_axes([0.1, 0.5, 0.8, 0.4], xticklabels=[], ylim=(-1.2, 1.2))
ax2 = fig.add_axes([0.1, 0.1, 0.8, 0.4], ylim=(-1.2, 1.2))

x = np.linspace(0, 10)
ax1.plot(np.sin(x))
ax2.plot(np.cos(x));

plt.subplot:简易网格子图

plt.subplot()在一个网格中创建一个子图。这个命令有3个整型参数——将要创建的网格子图行数、列数和索引值,索引值从1开始,从左上角到右下角依次增大:

plt.figure(figsize=(10, 10))

for i in range(1, 7):
    plt.subplot(2, 3, i)
    plt.text(0.5, 0.5, str((2, 3, i)), fontsize=18, ha="center")

plt.subplots_adjust命令可以调整子图之间的间隔。用面向对象接口的命令fig.add_subplot()可以取得同样的效果:

通过plt.subplots_adjusthspacewspace参数设置与图形高度与宽度一致的子图间距

fig = plt.figure(figsize=(10, 10))
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(1, 7):
    ax = fig.add_subplot(2, 3, i)
    ax.text(0.5, 0.5, str((2, 3, i)), fontsize=18, ha="center")

plt.subplots:用一行代码创建网格

plt.subplots()实用一行代码创建多个子图,并返回一个包含子图的NumPy数组。参数是行数与列数,以及可选参数sharexsharey,通过它们可以设置不同子图之间的关联关系。

fig, ax = plt.subplots(2, 3, sharex="col", sharey="row", figsize=(10, 10))
for i in range(2):
    for j in range(3):
        ax[i, j].text(0.5, 0.5, str((i, j)), fontsize=18, ha="center")

plt.GridSpec`:实现更复杂的排列方式

plt.GridSpec()可以实现不规则的多行多列子图网格。例如,一个带行列间距的2x3网格的配置代码如下所示:

plt.figure(figsize=(10, 10))
grid = plt.GridSpec(2, 3, wspace=0.4, hspace=0.3)
plt.subplot(grid[0, 0])
plt.subplot(grid[0, 1:])
plt.subplot(grid[1, :2])
plt.subplot(grid[1, 2]);

用Matplotlib画三维图

可以用ax.plot3Dax.scatter3D函数来创建三维坐标点构成的线图与散点图,需要用%matplotlib notebook实现交互

ipyvolume:通过WebGL在Jupyter notebook实现3D交互,支持百万散点的页面渲染和交互

%matplotlib notebook
from mpl_toolkits import mplot3d

fig = plt.figure(figsize=(10, 10))
ax = plt.axes(projection="3d")

y = 15
# 三维曲线
zline = np.linspace(0, y, 1000)
xline = np.sin(zline)
yline = np.cos(zline)
ax.plot3D(xline, yline, zline, "gray")

# 三维散点
zdata = y * np.random.random(100)
xdata = np.sin(zdata) + 0.1 * np.random.randn(100)
ydata = np.cos(zdata) + 0.1 * np.random.randn(100)
ax.scatter3D(xdata, ydata, zdata, c=zdata, cmap="Greens");

ax.plot_surface演示一个三维正弦函数画的三维等高曲面图,要求X,Y,Z都是二维网格数据的形式:

x = np.linspace(-6, 6, 30)
y = np.linspace(-6, 6, 30)

X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X ** 2 + Y ** 2))


fig = plt.figure(figsize=(10, 10))
ax = plt.axes(projection="3d")
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap="viridis", edgecolor="none")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
Text(0.5, 0, 'z')

pandas plotpandas-alive

Pandas以Matplotlib实现了plot接口(Matlab风格),可以快速实现Serise与Datafram的可视化,pandas-alive增加了时间序列的动态图效果

%matplotlib inline
!head -n 20 timeseries.json
{
  "Afghanistan": [
    {
      "date": "2020-1-22",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    },
    {
      "date": "2020-1-23",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    },
    {
      "date": "2020-1-24",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    },
df_covid.diff().hist(figsize=(20,15), sharey=True);
spain = df_covid['Spain'].diff()
spain[spain<0]
日期
2020-04-24   -10034.0
Name: Spain, dtype: float64
df_covid.loc["2020-04-20":"2020-04-30", "Spain"]
日期
2020-04-20    200210.0
2020-04-21    204178.0
2020-04-22    208389.0
2020-04-23    213024.0
2020-04-24    202990.0
2020-04-25    205905.0
2020-04-26    207634.0
2020-04-27    209465.0
2020-04-28    210773.0
2020-04-29    212917.0
2020-04-30    213435.0
Name: Spain, dtype: float64
def current_total(values):
    total = values.sum()
    s = f"总数 : {int(total)}"
    return {"x": 0.85, "y": 0.2, "s": s, "ha": "right", "size": 11}


animated_html = df_covid.tail(60).plot_animated(period_summary_func=current_total)
Generating BarChartRace, plotting ['Netherlands', 'Pakistan', 'Belgium', 'Chile', 'Mexico', 'Saudi Arabia', 'Canada', 'China', 'Peru', 'India', 'Iran', 'Turkey', 'Germany', 'France', 'Italy', 'Spain', 'United Kingdom', 'Brazil', 'Russia', 'US']
/Users/toddtao/opt/anaconda3/lib/python3.7/site-packages/pandas_alive/charts.py:70: UserWarning: Plotting too many bars may result in undesirable output, use `n_visible=5 to limit number of bars
  "Plotting too many bars may result in undesirable output, use `n_visible=5 to limit number of bars"
from IPython.display import display, Video

display(Video('3.data-viz/covid19.mp4'))