楔子
Python 在数据可视化方面有非常多的第三方库,比如 matplotlib, pyecharts, bokeh 等等,但个人最喜欢的莫过于 plotly 这个库。plotly 被称为数据可视化神器,首先它支持很多很多种图表,并且参数可以自由设置,最关键的是画出来的图非常漂亮。毕竟在数据可视化方面,图表的颜值也是很重要的。
很多人认为,pandas 和 plotly 这两个库所带来的影响力甚至超越了 Python 这门语言本身。
下面我们就来全方位介绍一下 plotly 的用法,不过首先要安装它,直接 pip 安装即可,非常方便。
对于学习数据可视化框架而言,本质上就是学习如何使用它来绘制图表。plotly 支持很多图表,包括 3D 动态图等等,这里我们先介绍一些常用的,至于更复杂的图表后续会说。
import plotly.graph_objs as go import numpy as np import pandas as pd
上面几个模块先导入就行,我们的代码会在 jupyter notebook 上运行,可以直接显示图表。至于图表如何保存成图片,我们后续再说。
为了更好地理解 plotly,我们要记住两个核心概念,分别是轨迹和画布。上面导入的 graph_objs 专门用来绘制图表,比如 go.Scatter 是散点图,在 plotly 中,图表被称为轨迹(trace)。而轨迹如果想显示,那么必须显示在画布上,当然一个画布可以显示多个轨迹。
所以结论如下:我们根据自己的需要来创建轨迹,然后再创建一张画布,它是用于展示轨迹(图表)所不可或缺的舞台,然后将轨迹展示在画布上即可。
而我们也可以调整轨迹、画布的一些属性,让它们整体看起来更加的完美,也就是我们说的设置属性参数。plotly 里面图表、画布的属性参数非常多,但我们都会说。而且也正因为如此,图表中任何位置,只要我们不满意都可以进行设置。
下面就来介绍一些常见的图表。
散点图
散点图(scatter)是将数据以点的形式展现在平面直角坐标系上的统计图表,它至少需要两个不同变量,一个沿 X 轴绘制,另一个沿 Y 轴绘制。每个点在 X、Y 轴上都有一个确定的位置。众多的散点叠加后,即可展示数据集的整体景观,从而帮助我们分析两个变量之间的相关性,或找出趋势和规律。此外,我们还可以添加附加的变量,来给散点分组、着色、确定透明度等等。
# 生成100个点 x = np.linspace(0, 2, 100) y0 = np.random.randn(100) + 5 y1 = np.random.randn(100) y2 = np.random.randn(100) - 5 # 里面的参数一会儿解释 trace0 = go.Scatter( x=x, # x 轴的坐标 y=y0, # y 轴的坐标 mode="markers", # 纯散点绘图 name="散点" # 曲线名称 ) trace1 = go.Scatter( x=x, y=y1, # 散点 + 线段绘图 mode="markers + lines", name="散点 + 线段" ) trace2 = go.Scatter( x=x, y=y2, mode="lines", # 线段绘图 name="线段" ) # 我们看到比较神奇的地方是,Scatter 居然也可以绘制线段 # 是的,如果不指定 mode 为 "markers",默认绘制的就是线段 # 以上就创建了三条轨迹,下面该干什么了?对,创建画布 # 将轨迹组合成列表传进去,因为一张画布是可以显示多条轨迹的 fig = go.Figure(data=[trace0, trace1, trace2]) # 在 notebook 中,直接通过 fig 即可显示图表 fig
看一下画出来的图是什么样子?
可以看到比 matplotlib 漂亮多了,在绘制轨迹的时候我们使用了 4 个参数,回顾一下:
- x:很好理解,就是 x 轴的坐标;
- y:很好理解,就是 y 轴的坐标;
- name:轨迹的名称,就是显示在画布右上方的那个;
- mode:轨迹的种类,主要有三种,"markers" 表示纯散点,"markers+lines" 表示散点加上线段,"lines" 是线段;
然后再来看一个参数 marker,它接收一个字典,用来设置散点的样式。
x = np.linspace(0, 2, 100) y0 = np.random.randn(100) trace0 = go.Scatter( x=x, y=y0, mode="markers", name="散点图", marker={ # 点的大小 "size": 8, # 点的颜色 "color": "rgba(102, 198, 147, 0.7)", # 除此之外,还可以设置点的轮廓 "line": { # 线条大小 "width": 10, # 线条的颜色 "color": "rgba(1, 170, 118, 0.3)" } } ) fig = go.Figure(data=[trace0]) fig
这里我们没有指定 name 参数,所以右上角不会显示轨迹的名称。再来总结一下 marker 参数:
marker = { # 点的大小 "size": n, # 点的颜色,可以是 rgba,可以是 rgb; # 可以是颜色的英文名,比如 green,yellow; # 可以是一个 16 进制颜色码,比如:#FF6A04 # 以及它还可以是一个元素个数与轨迹的点的个数相同的数组 # 对应着数组中相同的值的多个点,会被标记为同一种颜色 # 在机器学习里面进行聚类的时候,非常常见 "color": "rgba(n1, n2, n3, n4)", # 点的线条、轮廓 "line": {"width": n, "color": "green"} # 是否在右侧显示一个颜色条,默认为 False,不显示 "showscale": True }
基于以上参数,我们可以将散点图的样式绘制得非常漂亮。
问题来了,如果我想添加标题以及坐标轴名称该怎么办?
# 其它代码不变 fig = go.Figure( data=[trace0], layout={ "title": "这是标题", "xaxis_title": "这是x轴", "yaxis_title": "这是y轴", # x轴坐标倾斜60度 "xaxis": {"tickangle": 60} } ) fig
我们看到指定 layout 参数即可给画布设置一些额外属性,该参数可以接收一个字典,通过里面的 key 来指定画布属性,比如:
- title:标题;
- xaxis_title:x 轴标题;
- yaxis_title:y 轴标题;
- xaxis:x 坐标轴的属性,可以传入一个字典,来设置坐标轴,比如:tickangle 就是将坐标倾斜。尤其在坐标值比较长的时候,我们就可以通过倾斜的方式来避免堆叠在一起。角度大于0顺时针,小于0逆时针。
- yaxis:y 坐标轴的属性,和 x 坐标轴一样,可以设置非常多的属性。具体能设置哪些,后面单独罗列出来;
- width:画布的宽度;
- height:画布的高度;
- template:画布的风格,可以选择 ggplot2, seaborn, simple_white, plotly, plotly_white, plotly_dark, presentation, xgridoff, ygridoff, gridon, none,默认是 plotly;
散点图的存在意义
散点图常被用于分析变量之间的相关性,如果两个变量的散点看上去都在某条直线附近波动,则称变量之间是线性相关的;如果所有点看上去都在某条曲线(非直线)附近波动,则称变量之间是非线形相关的;如果所有点在图中没有显示任何关系,则称变量之间是不相关的。
如果散点图呈现出一个集中的大致趋势,并且该趋势可以用一条光滑的曲线来近似,那么这个近似的过程就是曲线拟合,而这条曲线则被称为最佳拟合线或趋势线。如果图中存在个别远离集中区域的数据点,那么这样的点被称为离群点或异常值。
折线图
折线图(Line)是一个由笛卡尔坐标系、一些点、以及线段组成的统计图表,常用来表示数值随连续时间间隔或有序类别的变化。在折线图中,x 轴通常用作连续时间间隔或有序类别(比如阶段1、阶段2、阶段3);y 轴用于量化的数据,如果为负值则绘制于 y 轴下方;而连线用于连接两个相邻的数据点。
# covid_19_deaths 是数据集,这个不需要关心 # 你完全可以使用其他的数据集代替 trace0 = go.Scatter( x=covid_19_deaths["date"], y=covid_19_deaths["count"], ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark", "title": "当前死亡人数"}) fig
虽然用的是 go.Scatter,但默认绘制的是折线图。还记得怎么绘制散点图吗?对的,将 mode 指定为 "markers" 即可。
然后是给折线添加样式,我们给散点图添加样式是通过 marker 参数,而给折线添加样式也有对应的参数。
trace0 = go.Scatter( x=covid_19_deaths["date"], y=covid_19_deaths["count"], line={ # 折线的宽度和颜色 "width": 3, "color": "rgba(255, 30, 186, 1)" } ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark", "title": "当前死亡人数"}) fig
此时折线的样式就改变了,但是我们记得之前在设置散点图的时候好像也用到了 line。没错,只不过那个 line 是传给 marker 参数的字典里面的一个 key,用来设置点的线条、或者轮廓的样式。而这里的 line 它是一个参数,和 marker 参数是同级别的,是用来设置折线样式的。
参数 line 里面还可以指定折线的种类,是虚线、实线等等之类的。
trace0 = go.Scatter( x=covid_19_deaths["date"], y=covid_19_deaths["count"], line={ "width": 3, "color": "rgba(255, 30, 186, 1)", "dash": "dot" # 指定为虚线 } ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark", "title": "当前死亡人数"}) fig
"dash" 表示线条的种类,可选的 value 如下:
- "dot":由点组成的虚线;
- "dash":由短线组成的虚线;
- "dashdot":由点和短线组成的虚线;
绘制直线的时候,数据可能不是连续的,那么绘制出来的图像就会出现断层。这个时候我们可以指定 connectgaps 参数,举个例子:
x = np.array([1, 2, np.nan, 4, 5]) y1 = x * 3 + 1 y2 = x * 3 + 2 trace0 = go.Scatter( x=x, y=y1, ) trace1 = go.Scatter( x=x, y=y2, connectgaps=True ) fig = go.Figure(data=[trace0, trace1], layout={"template": "plotly_dark"}) fig
当数据出现了空值的时候,那么折线默认是会出现断层的,而将 connectgaps 指定为 True ,那么会将缺失值两端的点直接相连,使得直线是完整的。
折线图的存在意义
折线图用于分析事物随时间或有序类别而变化的趋势,如果有多组数据,则用于分析多组数据随时间变化或有序类别的相互作用和影响。折线的方向表示正 / 负变化,折线的斜率表示变化的程度。
柱状图
柱状图,是一种使用矩形条,对不同类别进行数值比较的统计图表。最基础的柱形图,需要一个分类变量和一个数值变量。在柱状图上,分类变量的每个实体都被表示为一个矩形(通俗来讲就是柱子),而数值则决定了柱子的高度。
trace0 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[17, 400, 16], ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
这是最简单的柱状图,很明显,即便我们什么参数都不加,样式就已经把 matplotlib 给秒杀了。所以先不管 plotly 这个框架的功能如何(很强),光图表的颜值就足以让我们去学习它。
当然我们也可以绘制多个柱状图:
trace0 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[17, 400, 16], name="bar1" ) trace1 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[86, 90, 96], name="bar2" ) fig = go.Figure(data=[trace0, trace1], layout={"template": "plotly_dark"}) fig
可以看到柱状图并排显示在一起了,此外我们还可以设置层叠柱状图。
trace0 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[17, 400, 16], name="bar1" ) trace1 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[86, 90, 96], name="bar2" ) # 指定 barmode 为 stack,会将柱状图堆叠在一起 # 这里又多了一个 barmode,所以可调节的属性非常多 # 但是不同的属性适用于不同的轨迹,我们需要哪个就设置哪个即可 fig = go.Figure(data=[trace0, trace1], layout={"template": "plotly_dark", "barmode": "stack"}) fig
所以层叠柱状图是在画布里面设置的,因为两个轨迹要显示在画布上。至于 Bar 里面的参数是设置轨迹本身, 比如我们还可以调节颜色什么的,通过参数 marker 指定:
trace0 = go.Bar( x=["古明地觉", "芙兰朵露", "古明地恋"], y=[87, 97, 85], name="bar1", marker={ # 除了rgba,还可以通过颜色的名称指定 "color": "pink", # 指定透明度 # 这里指定透明度的方式,同样适用于Scatter "opacity": 0.5, "line": { "width": 3, # 轮廓的宽度 "color": "cyan", # 轮廓的颜色 } } ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
柱状图如果想调节样式,也是通过 marker 参数,和 Scatter 里面 marker 一样。
柱状图的存在意义
柱状图最适合对分类的数据进行比较,尤其是当数值比较接近时,由于人眼对于高度的感知优于其它视觉元素(如面积、角度等),因此使用柱状图更加合适。
作为人们最常用的图表之一,柱状图也衍生出多种多样的图表形式。例如,将多个并列的类别聚类、形成一组,再在组与组之间进行比较,这种图表叫做分组柱状图或簇状柱形图。还可以将类别拆分成多个子类别,形成堆叠柱状图。再比如将柱形图和折线图结合起来,共同绘制在一张图上,俗称双轴图等等。
水平柱状图
水平柱状图也叫条形图,和柱状图类似,也是通过 go.Bar 来绘制,只不过需要多加一个参数。
trace0 = go.Bar( # 方向变了,所以 x 轴和 y 轴的数据也要调换位置 y=["古明地觉", "芙兰朵露", "古明地恋"], x=[87, 97, 85], marker={ "color": "pink", "opacity": 0.5, "line": { "width": 3, "color": "cyan", } }, # 指定为水平方向即可 orientation="h" ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
柱状图有多高取决于 y 轴,而水平柱状图有多长就取决于 x 轴了,所以此时 x 轴就是代表数值的那一方。至于设置颜色、轮廓等参数,和之前的柱状图一样。
面积图
面积图,又称区域图,是一种随有序变量的变化,反映数值变化的统计图表,原理与折线图相似。而面积图的特点在于,折线与自变量坐标轴之间的区域,会用颜色或者纹理填充。
绘制面积图依旧使用 Scatter,只需要指定一个 fill 参数即可。
x = np.linspace(0, 2, 100) y0 = np.cos(x) y1 = np.sin(x) trace0 = go.Scatter( x=x, y=y0, name="cos", mode="markers", fill="tozeroy" ) trace1 = go.Scatter( x=x, y=y1, name="sin", # mode还可以设置为none,表示将线段隐藏 mode="none", fill="tozeroy" ) fig = go.Figure(data=[trace0, trace1], layout={"title": "面积图", "template": "plotly_dark"}) fig
我们看到面积图就是将轨迹和 x 轴围成的部分涂上颜色,仔细观察橘色的 sin,线段没了,所以线段或者此时的面积图的边界线就相当于被隐藏掉了。如果不指定 fill,直接指定 mode="none" 也是可以隐藏的,只不过此时绘制的线段你就看不到了。另外我们看到即使是散点,也是可以围成面积图的,会用线将点拟合起来,再绘制围成的面积图。
然后注意 fill 参数,它的可选值如下:
- tozeroy:与 x 轴围成的面积;
- tozerox:与 y 轴围成的面积;
- toself:与自身围成的面积;
除了这三个可选值之外,其实还有几个,不过一般用不到。我们主要用的还是 tozeroy,绘制和 x 轴围成的面积。
x = np.array([1, 2, 3, 4, 5]) y0 = np.array([2, 6, 9, 7, 3]) y1 = y0 + 1 y2 = y0 + 2 y3 = y0 + 3 trace0 = go.Scatter( x=x, y=y0, name="y0", fill="tonexty" ) trace1 = go.Scatter( x=x, y=y1, name="y1", fill="tonexty" ) trace2 = go.Scatter( x=x, y=y2, name="y2", fill="tonexty" ) trace3 = go.Scatter( x=x, y=y3, name="y3", fill="tonexty" ) fig = go.Figure(data=[trace0, trace1, trace2, trace3], layout={"title": "面积图", "template": "plotly_dark"}) fig
还是很简单的。
面积图的存在意义
面积图可用于多个系列数据的比较,此时面积图的外观看上去类似层叠的山脉,在错落有致的外形下表达数据的总量和趋势。相较于折线图,面积图不仅可以清晰地反映出数据的趋势变化,也能够强调不同类别的数据间的差距对比。
但它的劣势在于,填充会让形状互相遮盖,反而看不清变化。我们上面的数据弄的比较规整,所以看起来很清晰,但实际工作中绘制出来的面积图就不一定了。而一种解决方法是使用有透明度的颜色,来让出覆盖区域。
甘特图
甘特图又称横道图,是用来显示项目进度等与时间相关的数据的。直接看个例子就很好理解了:
# 创建甘特图,使用plotly.figure_factory # 然后调用内部的create_gantt即可 import plotly.figure_factory as ff tasks = [ {"Task": "任务A", "Start": "2018-1-1", "Finish": "2018-3-1"}, {"Task": "任务B", "Start": "2018-2-1", "Finish": "2018-5-1"}, {"Task": "任务C", "Start": "2018-2-1", "Finish": "2018-6-1"}, {"Task": "任务D", "Start": "2018-4-1", "Finish": "2018-8-1"}, {"Task": "任务E", "Start": "2018-8-1", "Finish": "2019-1-1"} ] # 但是数据格式有要求,里面是一个列表,列表里面是字典 # 字典包含至少三个键值对,分别是 Task、Start、Finish # 表示任务、开始时间、结束时间,不能是其它的名字 fig = ff.create_gantt(tasks, title="这是甘特图") # 此时会直接返回画布,想调整属性的话 # 通过 fig.layout.update 即可 fig.layout.update({"template": "plotly_dark"}) fig
创建甘特图的时候,除了传递一个包含字典的列表,还可以传递 DataFrame,当然列名同样必须是 Task, Start, Finish。
此外我们还可以指定颜色,以及进度。
# 创建甘特图,使用plotly.figure_factory # 然后调用内部的create_gantt即可 import plotly.figure_factory as ff tasks = [ {"Task": "任务A", "Start": "2018-1-1", "Finish": "2018-3-1", "Complete": "干了一小半"}, {"Task": "任务B", "Start": "2018-2-1", "Finish": "2018-5-1", "Complete": "干了一半"}, {"Task": "任务C", "Start": "2018-2-1", "Finish": "2018-6-1", "Complete": "干了一半"}, {"Task": "任务D", "Start": "2018-4-1", "Finish": "2018-8-1", "Complete": "干了一大半"}, {"Task": "任务E", "Start": "2018-8-1", "Finish": "2019-1-1", "Complete": "全干完了"} ] # 为不同的进度赋予不同的颜色 colors = { "干了一小半": "rgb(125, 135, 144)", "干了一半": "rgb(187, 20, 168)", "干了一大半": "rgb(14, 199, 250)", "全干完了": "rgb(250, 1, 144)" } fig = ff.create_gantt(tasks, # 为不同的进度赋予不同的颜色 colors=colors, # 表示进度,名字任意 # 这里我们叫 "Complete" index_col="Complete", # 显示颜色条 show_colorbar=True) # fig.data 获取所有的轨迹 # fig.layout 获取画布的属性 fig = go.Figure(fig.data, layout={"template": "plotly_dark", "title": "我的甘特图"}) fig
甘特图感觉在工作中用的不是很多,了解一下就好。
直方图
直方图,又称质量分布图,用于表示数据的分布情况,是一种常见的统计图表。一般用横轴表示数据区间,纵轴表示分布情况,柱子越高,则落在该区间的数量越大。根据数据分布状况不同,直方图展示的数据有不同的模式,包括对称单峰、偏左单峰、偏右单峰、双峰、多峰以及对称多峰。
构建直方图,首先要确定组距、对数值的范围进行分区,通俗的说就是划定有几根柱子(例如 0-100 分,每隔20分划一个区间,共5个区间)。接着,对落在每个区间的数值进行频次计算(如落在 80-100 分的 10 人,60-80 分的 20 人,以此类推)。最后,绘制矩形,高度由频数决定。
直方图与上面介绍的柱状图有点相像,但其实是完全不同的。前者反映数据分布情况,后者则不具备此功能,只能对数值进行比较。从数据结构来说,柱状图需要 1 个分类变量,是离散的(如一班、二班、三班),因此柱子间有空隙。但直方图的数据均为连续的数值变量(如成绩),因此柱子间是没有空隙的。
x = np.random.randint(1, 20, 1000) trace0 = go.Histogram(x=x) fig = go.Figure(data=[trace0], layout={"title": "直方图", "template": "plotly_dark"}) fig
横坐标是数值,纵坐标是出现的次数。但是有些不完美,就是跨度太大了,所以我们可以指定 xaxis 属性,让跨度小一些。
x = np.random.randint(1, 20, 1000) trace0 = go.Histogram( x=x, # 指定histnorm为probability # 那么y轴将显示出现的次数所占的比例 histnorm="probability" ) fig = go.Figure(data=[trace0], layout={"title": "直方图", "template": "plotly_dark", # range表示坐标范围 # dtick表示相邻坐标之间的差值 # 这里是 2,所以就是 0 2 4 6... "xaxis": {"dtick": 2, "range": [0, 20]} }) fig
忘记说了,其实所有的图表对应的类里面,很多参数都是相同的,比如 marker 设置图表本身的颜色透明度、以及轮廓的颜色、大小,name 设置图表的名称等等,这些参数我们不再赘述,而是会直接使用。
然后我们也可以绘制多个直方图:
x0 = np.random.randn(1000) x1 = np.random.chisquare(5, 1000) trace0 = go.Histogram( x=x0, histnorm="probability", marker={ "opacity": 0.75 } ) trace1 = go.Histogram( x=x1, histnorm="probability", marker={ "opacity": 0.75 } ) fig = go.Figure(data=[trace0, trace1], layout={"title": "直方图", "template": "plotly_dark", "xaxis": {"dtick": 2, "range": [1, 20]} }) fig
但是我们发现这个直方图貌似有些不对劲,因为 plotly 将多个直方图强制变窄了,我们需要将 barmode 指定为 "overlay"。
x0 = np.random.randn(1000) x1 = np.random.chisquare(5, 1000) trace0 = go.Histogram( x=x0, histnorm="probability", marker={ "opacity": 0.75 } ) trace1 = go.Histogram( x=x1, histnorm="probability", marker={ "opacity": 0.75 } ) fig = go.Figure(data=[trace0, trace1], layout={"title": "直方图", "template": "plotly_dark", "barmode": "overlay"}) fig
如果将 barmode 改成 "stack",那么就会创建堆叠直方图。
x0 = np.random.randn(1000) x1 = np.random.randn(1000) trace0 = go.Histogram( x=x0, histnorm="probability", marker={ "opacity": 0.75 } ) trace1 = go.Histogram( x=x1, histnorm="probability", marker={ "opacity": 0.75 } ) fig = go.Figure(data=[trace0, trace1], layout={"title": "堆叠直方图", "template": "plotly_dark", "barmode": "stack"}) fig
最后是累计直方图,它的特点是:第n+1个区间的样本数是第n-1个区间的样本数加上第n个区间的样本数。
x0 = np.random.randn(1000) trace0 = go.Histogram( x=x0, histnorm="probability", marker={ "opacity": 0.75 }, # 指定 cumulative 即可 cumulative={"enabled": True} ) fig = go.Figure(data=[trace0], layout={"title": "累计直方图", "template": "plotly_dark", "barmode": "stack"}) fig
直方图的种类还是蛮多的,但说实话,感觉直方图用的也不是很多。如果只是做简单的报表统计,那么几乎用不到直方图。但我们接下来要介绍的饼图,使用就很广泛了。
饼图
饼图是一种划分为几个扇形的圆形统计图表。在饼图中,每个扇形的弧长(以及圆心角和面积)大小,表示该种类占总体的比例,并且这些扇形合在一起刚好是一个完全的圆形。
饼图最显著的功能在于表现占比。习惯上,人们也用饼图来比较扇形的大小,从而获得对数据的认知。但是,由于人类对角度的感知能力不如长度,因此在需要准确的表达数值(尤其是当数值接近、或数值很多)时,饼图常常不能胜任,建议用柱状图代替。
# 饼图就很简单了,使用 Pie 这个类 trace0 = go.Pie( labels=["古明地觉", "芙兰朵露", "古明地恋", "雾雨魔理沙", "紫妈"], values=[10, 25, 5, 35, 41] ) fig = go.Figure(data=[trace0], layout={"title": "饼图", "template": "plotly_dark"}) fig
我们还可以设置环形饼图,就是在中间挖一个洞。
trace0 = go.Pie( labels=["古明地觉", "芙兰朵露", "古明地恋", "雾雨魔理沙", "紫妈"], values=[10, 25, 5, 35, 41], hole=0.7 # 从中心挖掉百分之70的部分 ) fig = go.Figure(data=[trace0], layout={"title": "饼图", "template": "plotly_dark"}) fig
我们还可以将饼图旋转一定角度,因为最上方的两个部分明显是垂直分隔的,另外也可以使某一个部分突出。
trace0 = go.Pie( labels=["古明地觉", "芙兰朵露", "古明地恋", "雾雨魔理沙", "紫妈"], values=[10, 25, 5, 35, 41], pull=[0, 0, 0, 0, 0.1], # 突出最后一个 rotation=30, # 旋转30度 marker={ # 饼图也有marker参数,基本上所有图表都有marker参数 # 散点图可以指定一种颜色来让所有的点都呈现相同的颜色 # 但是饼图的每一部分应该是不同的颜色,这才符合饼图这种图形的意义 # 所以我们要传入一个列表,而数据有五个,那么也要指定五种颜色 # 但是即便不指定五个、或者颜色重复也是可以的 # 如果颜色不够,plotly会帮你补充,颜色多了,会只选列表的前五个 "colors": ["yellow", "green", "cyan", "pink", "blue"], # 并且这里不叫 color 了,而是叫 colors,因为多个颜色 "line": { "width": 3, "color": "white", # 轮廓颜色 } } ) fig = go.Figure(data=[trace0], layout={"title": "饼图", "template": "plotly_dark"}) fig
作为最常见的图表之一,饼图大量应用于各行各业的报告中。比如一家公司有着 5 个业务,现在要看每个业务的营收占据总营收的比例,那么饼图再合适不过了。
气泡图
气泡图是一种多变量的统计图表,由笛卡尔坐标系和大小不一的圆组成,可以看作是散点图的变形。在气泡图中,每个气泡都代表着一组三个维度的数据,其中前两个维度决定了气泡在笛卡尔坐标系中的位置(即x,y轴上的值),另外一个则通过气泡的大小来表示。例如,x 轴表示产品销量,y 轴表示产品利润,气泡大小代表产品市场份额百分比。
当然,气泡图也可以容纳更多维的数据,例如用第4个变量决定气泡的颜色、透明度等。
# 生成100个点,x 是横坐标 x = np.linspace(0, 2, 50) # y 是纵坐标 y = np.random.uniform(1, 8, 50) # x 和 y 确定了气泡的位置 # 而数值的大小,则确定气泡的大小 trace0 = go.Scatter( x=x, y=y, mode="markers", marker={ # size 还可以是一个数组 # 手动指定每一个气泡的大小 "size": 13 * y / np.min(y), # color 同样可以是数组 # 值相同的气泡会使用同一种颜色 "color": y, # 显示颜色条 "showscale": True } ) fig = go.Figure(data=[trace0], layout={"title": "气泡图", "template": "plotly_dark"}) fig
气泡图通常用于展示和比较数据之间的关系和分布,通过比较气泡位置和大小来分析数据维度之间的相关性。
气泡图也可以用于二维数据,即 y 轴和气泡大小使用同一维度的数据(y 轴和气泡大小的双视觉编码)。这种情况下,它实际上是作为柱状图的替代,用于对比分类数据,但比柱状图更加简洁美观。
旭日图
旭日图是一种表现层级数据的图表,它以父子层次结构来显示数据,并构成一个同心圆,因此又被成为称为多层饼图。离原点越近,数据的层级越高。换句话说,相邻的两层,内层是外层的父。
同时,一个父可以有多个子,即某一个环形又可以被分割成多个环形,环形的弧度由它在父层中所占比例来决定。若无占比关系,则处理成均分。
labels=["河南省", "信阳市", "郑州市", "洛阳市", "山东省", "济南市", "青岛市", "烟台市", "安徽省", "凤阳市", "合肥市", "芜湖市"] parents=["", "河南省", "河南省", "河南省", "", "山东省", "山东省", "山东省", "", "安徽省", "安徽省", "安徽省"] # values 为出生了多少万人(数字瞎编的) values=[0, 30, 20, 10, 0, 50, 40, 10, 0, 20, 15, 15] trace0 = go.Sunburst( # 注意:每一个层级都要表示 labels=labels, parents=parents, values=values ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
市的父级是省,基于每个市的总人数可以算出省的人数,因此内层的圆对应每个省人数所占的比例。然后每个省根据比例再对应外层的圆的一片扇形区域,每个区域再根据比例划分为不同的子区域。
我们可以绘制很多层,目前有两层。但要注意的是,我们需要将每一层的对应关系都描述出来,比如市的父级是省,那么省的父级呢?这里我们假设省为最高级,那么它的父级就是空字符串,父级的值就写成 0。
然后我们改一下,我们将省的父级设置为中国。
labels=["河南省", "信阳市", "郑州市", "洛阳市", "山东省", "济南市", "青岛市", "烟台市", "安徽省", "凤阳市", "合肥市", "芜湖市"] parents=["中国", "河南省", "河南省", "河南省", "中国", "山东省", "山东省", "山东省", "中国", "安徽省", "安徽省", "安徽省"] values=[ 0, 30, 20, 10, 0, 50, 40, 10, 0, 20, 15, 15] trace0 = go.Sunburst( labels=labels, parents=parents, values=values ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
然后再来关注 values 里面的几个 0,它们表示啥含义呢?
labels=["河南省", "信阳市", "郑州市", "洛阳市", "山东省", "济南市", "青岛市", "烟台市", "安徽省", "凤阳市", "合肥市", "芜湖市"] parents=["中国", "河南省", "河南省", "河南省", "中国", "山东省", "山东省", "山东省", "中国", "安徽省", "安徽省", "安徽省"] # 河南省、山东省分别还有 10 万人、20 万人没有统计 # 这些人可能来自于别的市 values=[ 10, 30, 20, 10, 20, 50, 40, 10, 0, 20, 15, 15] trace0 = go.Sunburst( labels=labels, parents=parents, values=values, ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
旭日图的本质是树状关系,与树图是等价的,因此也被称为极坐标下的矩形树图。它可以在承载大量数据的同时,清晰的显示数据间的结构关系。在许多工具中,旭日图被赋予交互功能,方便读图者自行探索。如支持鼠标悬浮高亮、筛选、显示当前层级等等。
桑基图
桑基图 (Sankey Diagram),是一种表现流程的示意图,用于描述一组值到另一组值的流向,分支的宽度对应了数据流量的大小。
1869年,查尔斯米纳德绘制了1812年拿破仑征俄图,描绘了拿破仑大军在东进时,兵力是如何一步步削弱的,这也是目前公认较早的桑基图。
假设有上面这种数据,我们来看看如何将它绘制成桑基图。
trace0 = go.Sankey( node={ "pad": 85, "thickness": 40, "line": {"color": "green", "width": 0.5}, "label": ["女", "男", "狮子座", "双子座", "巨蟹座"], "color": ["#f8ade8", "#aff8e9", "#eaf863", "#6fe2f8", "#f8d47d"], }, link={ # source 和 target 中的元素都表示上面 label 的索引 # 两者都是一一对应的,比如只看三个数组中的第一个元素 # (0, 2, 5) 就表示 ("女", "狮子座", 5) "source": [0, 0, 0, 1, 1, 1], "target": [2, 3, 4, 2, 3, 4], "value": [5, 2, 3, 4, 2, 2], "color": ["rgba(172, 163, 105, 0.6)", "rgba(196, 166, 255, 0.6)", "rgba(252, 255, 212, 0.6)", "rgba(188, 255, 199, 0.6)", "rgba(255, 185, 217, 0.6)", "rgba(255, 182, 129, 0.6)"], }, ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
绘制出来就长这个样子,我们来解释一下这张图。
Sankey 里面的参数 node 就表示这几个节点,键 "pad" 表示节点之间的距离,"thickness" 表示节点宽度,"line" 表示线条,"label" 表示节点的名称,"color" 表示节点的颜色。
参数 link 表示节点之间的连接情况,source 和 target 决定了流向,value 表示每个方向的流量,color 表示颜色。
当然我们这里只有一个流出和一个流入,但其实一个流入还可以作为下一个流入的流出,比如:
以上便是桑基图,适合表现分配情况、归类情况,以及变化和流动情况。
漏斗图
漏斗图,形如漏斗,用于单流程分析,在开始和结束之间由N个流程环节组成。漏斗图的起始总是100%,并在各个环节依次减少,每个环节用一个梯形来表示,整体形如漏斗。一般来说,所有梯形的高度应是一致的,这会有助人们辨别数值间的差异。
trace0 = go.Funnel( y = ["浏览量", "注册用户", "Vip", "SVip"], x = [20000, 15000, 8000, 2000], # 以前指定透明度的时候,是通过 marker 参数实现的 # 在 marker 参数里面指定一个 "opacity" # 但有的图表不行,需要单独使用 opacity 参数 opacity = 0.8, marker={ # 漏斗每部分的颜色 "color": ["deepskyblue", "tan", "teal", "silver"], # 轮廓 "line": { "width": 4, "color": ["wheat", "wheat", "blue", "wheat"] } }, # 漏斗之间的连接部分 connector={"fillcolor": "pink", "line": {"width": 2, "dash": "dot"}} ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
同样的,你也可以绘制多个漏斗图,比如:
需要注意的是,漏斗图的各个环节,有逻辑上的顺序关系,同时漏斗图的所有环节的流量都应该使用同一个度量。漏斗图最适宜用来呈现业务流程的推进情况,如用户的转化情况、订单的处理情况、招聘的录用情况等。通过漏斗图,可以较直观的看出流程中各部分的占比、发现流程中的问题,进而做出决策。
雷达图
雷达图是一种显示多变量数据的图形方法,通常从同一中心点开始等角度间隔地射出三个以上的轴,每个轴代表一个定量变量,各轴上的点依次连接成线或几何图形。
雷达图可以用来在变量间进行对比,或者查看变量中有没有异常值。另外,多幅雷达图之间或者雷达图的多层数据线之间,还可以进行总体数值情况的对比。
trace0 = go.Scatterpolar( r=[3, 5, 3, 4, 5], theta=["速度", "力量", "技巧", "反应", "心态"], fill='toself' ) fig = go.Figure(data=[trace0], layout={"template": "plotly_dark"}) fig
使用雷达图时,特征类别不能过多,并且特征之间要归一化,或者按照统一标准来标准化。
热力图
热力图是一种通过对色块着色来显示数据的统计图表,绘图时需指定颜色映射的规则。例如,较大的值由较深的颜色表示,较小的值由较浅的颜色表示;或者较大的值由较暖的颜色表示,较小的值由较冷的颜色表示等等。
trace0 = go.Heatmap( x=["星期一", "星期二", "星期三", "星期四", "星期五"], y=["早上", "中午", "晚上"], z=[ # 星期一到星期五的早上 [12, 13, 17, 14, 15], # 星期一到星期五的中午 [32, 33, 37, 34, 35], # 星期一到星期五的晚上 [22, 23, 27, 24, 25], ] ) fig = go.Figure( data=[trace0], layout={"template": "plotly_dark"}, ) fig
我们还可以调整颜色:
trace0 = go.Heatmap( x=["星期一", "星期二", "星期三", "星期四", "星期五"], y=["早上", "中午", "晚上"], z=[[12, 13, 17, 14, 15], [32, 33, 37, 34, 35], [22, 23, 27, 24, 25], ], colorscale = 'Viridis' )
通过指定 colorscale,即可选择不同风格的色块,它的可选值如下:
从数据结构来划分,热力图需要 2 个分类字段和 1 个数值字段,分类字段确定 x、y 轴,将图表划分为规整的矩形块,而数值字段决定了矩形块的颜色。
热力图适合用于查看总体的情况、发现异常值、显示多个变量之间的差异,以及检测它们之间是否存在任何相关性。它的优势在于空间利用率高,可以容纳较为庞大的数据。热力图不仅有助于发现数据间的关系、找出极值,也常用于刻画数据的整体样貌,方便在数据集之间进行比较。
但热力图也有缺陷,尽管热力图能够容纳较多的数据,然而人们很难将其中的色块转换为精确的数字。因此当需要清楚知道数值的时候,可能需要额外的标注,plotly 支持这一点,当把鼠标放到色块上时,会显示相应的数值。
其它轨迹
我们上面已经介绍了工作中常用的图表,如果你还想知道更多的图表,可以查看 plotly 官网。
画布属性 layout
重点来了,我们创建画布的时候用的是 Figure 类,它里面接收两个参数。第一个参数 data 负责接收轨迹,第二个参数 layout 负责调整画布属性。我们创建完画布之后,也可以通过 fig.data 或 fig.layout 拿到相应的轨迹和画布。
然后我们来看看画布都支持哪些属性。
title
标题,可以是一个字符串,也可以是一个字典。
layout = { "title": "这是标题" } # 或者传递一个字典,指定标题的同时 # 还可以指定字体、位置 layout = { "title": {"text": "这是标题", # 字体(一会单独说) "font": {"family": "STKaiti", "size": 30}, # 水平位置,0.5 表示居中 "x": 0.5} }
xaxis_title、yaxis_title
x 轴名称和 y 轴名称,同样支持字符串和字典。
x = np.linspace(0, 2, 100) y = np.random.randn(100) trace0 = go.Scatter( x=x, y=y, mode="markers" ) fig = go.Figure( data=[trace0], layout={"template": "plotly_dark", "title": {"text": "我是散点图", "font": {"family": "STKaiti", "size": 30}, "x": 0.5}, "xaxis_title": "我是 x 轴", "yaxis_title": {"text": "我是 y 轴", "font": {"family": "STKaiti", "size": 20, "color": "pink"}}, } ) fig
我们看到标题和 y 轴的字体变了,但 x 轴没有变。因为 x 轴传递的字符串,它只能表示标题内容,样式则使用默认的。
这些用表格罗列出来的,就不演示了,可以自己测试一下,看看这些参数的效果。
font
字体,和 titlefont 属性一致,通过传递一个字典来指定颜色、大小、种类等等。但 titlefont 的效果只会作用于标题,而这里的 font 会作用于所有地方,比如标题、x 轴和 y轴的名称、坐标刻度等等。
x = np.linspace(0, 2, 100) y0 = np.random.randn(100) + 5 y1 = np.random.randn(100) - 5 trace0 = go.Scatter( x=x, y=y0, name="one" ) trace1 = go.Scatter( x=x, y=y1, name="two" ) fig = go.Figure( data=[trace0, trace1], layout={"font": {"color": "cyan", "size": 20, "family": "stcaiyun"}, "template": "plotly_dark" } ) fig
所有能看到字的部分,其字体的颜色、大小、类型都变了。
legend
设置展示轨迹名称时的属性。
x = np.linspace(0, 2, 100) y0 = np.random.randn(100) + 5 y1 = np.random.randn(100) - 5 trace0 = go.Scatter( x=x, y=y0, name="one" ) trace1 = go.Scatter( x=x, y=y1, name="two" ) fig = go.Figure( data=[trace0, trace1], layout={"legend": {"bgcolor": "orange", "font": {"size": 30, "color": "blue"}}, "template": "plotly_dark" } ) fig
paper_bgcolor、plot_bgcolor
分别表示画布背景颜色、和图表背景颜色。
x = np.linspace(0, 2, 100) y0 = np.random.randn(100) + 5 y1 = np.random.randn(100) - 5 trace0 = go.Scatter( x=x, y=y0, name="one" ) trace1 = go.Scatter( x=x, y=y1, name="two" ) fig = go.Figure( data=[trace0, trace1], layout={"paper_bgcolor": "yellow", "plot_bgcolor": "cyan" } ) fig
images
我个人觉得最牛的一个功能,就是可以将图片作为背景。
from PIL import Image x = np.linspace(0, 2, 100) y0 = np.random.randn(100) + 5 y1 = np.random.randn(100) - 5 # 使用 PIL 打开图片 im = Image.open("../lemu.jpg") trace0 = go.Scatter(x=x, y=y0, name="one") trace1 = go.Scatter(x=x, y=y1, name="two") fig = go.Figure( data=[trace0, trace1], layout={ "images": [{ # 可以是PIL读取的Image对象,也可以是base64字符 "source": im, "sizex": 1, "sizey": 1, "yanchor": "bottom", "opacity": 0.3, }], "showlegend": False, "template": "plotly_dark" }) fig
以上就是画布 layout 支持的一些属性,但还有两个最重要没有说,就是 xaxis 和 yaxis。
xaxis 和 yaxis
xaxis 和 yaxis 非常重要,关键的东西我们留到最后,由于它们支持的属性是一样的,都是坐标轴,就一起说了。首先 xaxis 和 yaxis 同样是传递给 layout 的字典里面的 key,但关键的是它们对应的 value 也是一个字典,这个字典里面同样可以支持很多的属性。
color:
颜色,针对于当前坐标轴的刻度
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"color": "red"}, "template": "plotly_dark" }) fig
坐标轴刻度变成了红色。
gridcolor、gridwidth
网格线颜色和网格线宽度。
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"gridwidth": 5, "gridcolor": "cyan"}, "template": "plotly_dark", }) fig
linecolor、linewidth
坐标轴线条颜色与宽度
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"linewidth": 5, "linecolor": "cyan"}, "template": "plotly_dark", }) fig
rangeslider
范围滑块
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"rangeslider": {"bgcolor": "cyan"}}, "template": "plotly_dark" }) fig
下方有一个滑块,可以拖动,然后显示对应的图形部分。
showgrid
是否显示网格。
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"showgrid": False}, "yaxis": {"showgrid": False}, "template": "plotly_dark" }) fig
side
设置坐标轴刻度位置
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"side": "top"}, "yaxis": {"side": "right"}, "template": "plotly_dark" }) fig
showline:显示线条开关;
spikecolor:峰值数据颜色;
tickangle:刻度倾斜角度,之前用过的。
tickprefix、ticksuffix:刻度前缀和后缀
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": {"tickprefix": "<<", "ticksuffix": ">>"}, "template": "plotly_dark" }) fig
title、titlefont
设置坐标轴名称,类似于 layout 中的 title 和 titlefont,当然坐标轴名称还可以通过 xaxis_title 和 yaxis_title 指定。
x = np.linspace(0, 2, 100) y = np.random.randn(100) + 5 trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ # title 也可以是一个字典 # 在里面通过 "font" 指定字体 "xaxis": {"title": "我是 x 轴", "titlefont": {"color": "cyan", "size": 20}}, "template": "plotly_dark" }) fig
type:刻度类型
可以是 '-', 'linear', 'log', 'date', 'category', 'multicategory' 之一。
zeroline:是否显示零线、就是值全部为 0 的线
对于 xaxis 就是 y 轴,对于 yaxis 就是 x轴。
zerolinecolor、zerolinewidth
零线的颜色和宽度
x = np.linspace(0, 2, 100) y = np.random.randn(100) trace0 = go.Scatter(x=x, y=y) fig = go.Figure( data=[trace0], layout={ "xaxis": { "zerolinecolor": "cyan", "zerolinewidth": 5 }, "yaxis": { "zerolinecolor": "pink", "zerolinewidth": 5 }, "template": "plotly_dark" }) fig
以上就是两个坐标轴支持的属性设置,可以看到支持的属性非常多,而且都可以自定制,只不过很多我们都用不到。
小结
plotly 真的是一个绘图神器,支持大量的图表,当然我们只介绍了一部分,不过它们都是最常用的。在使用 plotly 的时候要记住轨迹和画布两个概念,我们说图表在 plotly 中就是轨迹,然后轨迹需要展示在画布上,而画布有很多属性,这些我们上面介绍的比较详细了,虽然很多都用不到。
然后就是 plotly 的图表保存问题,我这里都是直接在 jupyter 上生成,然后下载下来的。至于如何用 plotly 的 API 来生成图片,以及多图绘制,我们下一篇文章再说。
申明:本文中对一些图表所做的概念上的解析,来自于网站 "图之典" ,这是由一些爱好数据可视化的人创建的。里面对图表进行了概念上的详细描述,包括来源、适用场景、以及不适用场景等等,都做了详细的说明。如果你喜欢数据可视化,那么这个网站你一定不能错过。
写在最后,截止到 2022年11月20号,我的公众号关注人数突破 3000 了(喜大普奔)。尽管和其他号主相比,我连人家的二十分之一、三十分之一都不到,但也很开心了。其实我涨粉的主要途径也全靠同行转发,在其他号主转发我文章的时候吸一波粉。
总之,今后我也会努力地输出更多内容,认真写好每一篇文章。