查看原文
其他

数据可视化 | 用 Python 制作动感十足的动态柱状图

数据可视化 数据Seminar 2023-09-29

本文目录

一、前言

二、实现工具:Pynimate

三、可视化数据准备

四、实现代码

    (一)简易代码快速实现

    (二)动画优化

五、总结

六、相关文章

本文共7678个字,阅读大约需要20分钟,欢迎指正!

一、前言

厌倦了死板无趣的数据显示方式?或许你正需要一种新颖、有趣的方式去呈现那些沉重的数据,本期文章我们就来介绍一种别致的数据可视化图形——动态柱状图。

先来看一下动态柱状图长啥样:

全国各省(暂无港澳台数据)2008-2022年绿色产业专利申请数量

这种富有动感的图形常用于可视化一些具有时间跨度的统计数据,相比常见的静态柱状图,动态可视化更能直观地体现不同主体某项指标的变化速度和态势,不同主体之间你追我赶的动态效果也能增加图形的数据代入感,实在值得一学。

本教程基于 pandas 2.0.0 版本以及 pynimate 1.3.0 版本书写 。

本文中所有 Python 代码均在集成开发环境 Visual Studio Code (VScode) 中使用交互式开发环境 Jupyter Notebook 中编写,本文分享的代码请使用 Jupyter Notebook 打开。


💡后台回复关键词“20230922”即可获取本文所有演示代码以及演示用的数据。

二、实现工具:Pynimate

Pynimate 是一位 Python 达人编写并于 2023 年 1 月上线的一个第三方库,同其他 Python 第三方库一样,在终端中使用下面的命令即可快速安装它。

Pynimate 库仅支持 Python 3.9 及更高版本才能安装,如果你安装的 Python 版本低于 3.9,可以使用 conda 快速创建一个高版本的 Python 环境,这也是推荐使用 conda 的原因之一。如何安装 conda 工具可以回顾此文:数据治理 | Python 环境冲突烦?环境移植难?我用 Miniconda

1 pip install pynimate

官方网站:https://julkaar9.github.io/pynimate/guide/starter/

GituHub 项目地址:https://github.com/julkaar9/pynimate

三、可视化数据准备

动态柱状图适用于描述具有时间跨度的统计数据,本文用到的是全国各省绿色产业专利申请&授权数据(暂无港澳台数据)。绘图时仅使用了近 10 年的绿色发明专利申请数量这一指标。

全国各省绿色产业专利申请&授权数据来源于CPPGD中的绿色产业-特色统计库。

CPPGD(全称:中国公共政策与绿色发展数据库)是由企研数据携手浙江大学中国农村发展研究院和浙江工商大学经济学院联合发起,为助力国家围绕"碳达峰、碳中和"双碳目标做出的一系列重大战略部署,服务中国绿色发展及相关领域学术与政策研究而倾力打造的专题数据库。>>>点此查看CPPGD更多介绍

使用 Pynimate 库制作动态柱状图,需要提供类型为 DataFrame 类型的统计数据表,并将自变量(多数情况下是时间字段)设置为 DataFrame 的行索引,再将每一个主体的指标作为数据列,如下图所示。

而企研数据提供的原始数据则是展开后的详细数据,样例数据如下图所示。

该图来源于企研·社科大数据平台,网址:https://r.qiyandata.com/


因此需要使用 Pandas 对数据进行预处理,将原始数据转换为 Pynimate 支持的样式,处理后的近十年的统计数据的变量名为df,而全部年份统计数据的变量名为data,处理代码如下。(向右滑动查看完整代码)

1 import os, warnings; warnings.filterwarnings('ignore')
2 import pandas as pd
# 读取 excel 表,且只保留需要的字段
4 data = pd.read_excel('./绿色产业主体-专利数量-历年各省绿色产业企业专利数量统计(1985年-2022年)-230919173528.xlsx')\
5                     [['年份''省份(申请地)''专利类型''专利申请数量']]
# 只保留发明专利数据
7 data = data[data['专利类型'] == '发明专利'].drop('专利类型', axis=1)
# 完善年份字段,添加月份和日期,并转为日期类型
9 data['年份'] = data['年份'].apply(lambda x: f'{x}-01-01').astype('datetime64[ns]')
10 # 生成排序后的时间列表,留作后用
11 Year_list = sorted(list(set(data['年份'])))
12 # 分组聚合再转置,将一个地区全部年份的数据合并为一条,存放在一个列表中
13 data = data.groupby(data['省份(申请地)'])\
14             .apply(lambda x: pd.Series({'专利申请数量':list(x['专利申请数量'])}))\
15             .T.reset_index(drop=True)
16 # 首列位置插入一个年份列,存放先前生成的时间列表,这样就和指标列保持一致,后续一起展开
17 data.insert(0, '年份', [Year_list])
18 # 一次性对时间、指标进行展开,展开后将年份列设置为索引即可
19 data = data.explode(list(data.columns))
20 # 上述操作后,数据索引出现问题,不得不使用另类方式重置索引
21 data.to_csv('./test.csv', index=False)
22 data = pd.read_csv('./test.csv').set_index('年份')
23 os.remove('./test.csv')    # 删除写入的中间文件
24 # 只选取最后十行数据,即仅十年的统计数据来制作动态柱状图
25 # 注意:data 是全部年份的数据;df 是近十年的数据
26 df = data.tail(10)
27 df      # 可以输出查看处理后的数据,上文已经展示过

四、实现代码

使用 Pandas 预处理统计数据后,就可以开始制作动态柱状图了。如果只是想简单画个图,图个乐儿,你只需要使用十来行代码就可以达到目的。但如果想要美化可视化图形,则需要使用更多的代码来调整细节,下面给出了两种方式的对应代码。

1. 简易代码快速实现

直接上代码:(向右滑动查看完整代码)

1 import matplotlib as mpl   # 导入 matplotlib ,并设置字体来显示中文
2 from matplotlib import pyplot as plt
3 mpl.rcParams['font.family'] = 'Simsun'
4 plt.rcParams['axes.unicode_minus'] = False 
5 import pynimate as nim     # 导入 pynimate 库
6
7 cnv = nim.Canvas()      # 创建一个画布
# 创建一个动态柱状图
9 bar = nim.Barplot(data=df, time_format="%Y-%m-%d", ip_freq="30d", rounded_edges=True, n_bars=31)
10 # 设置柱状图中的柱体,使之与统计数据中的列名对应起来,并设置插帧频率和柱体数量
11 bar.set_time(callback=lambda i, datafier: datafier.data.index[i].year)
12 cnv.add_plot(bar)   # 将柱状图添加到画布
13 cnv.animate()       # 开始创建动画
14 # 将动画结果保存为 gif 格式, 文件名为 Result_1.gif,保存在当前工作路径下
15 cnv.save("Result_1", 24, "gif"

虽说代码短小精悍,但其中隐藏的东西却不少,上述第 9 行代码中pynimate.barplot()函数下的前三个参数datatime_formatip_freq就是几个至关重要的参数,其中参数data指的是符合要求的 DataFrame 类型数据,这个在上文已经解决,第二个参数则是数据中日期类型的格式对照,下面我们着重分析一下ip_freq参数。

显然,原始数据只有十条,即 10 个时间点下各省的绿色专利(发明专利)申请数量,但做出的动画却十分丝滑,其中必然保留了成百上千个时间点的信息,这就要归功于 pynimate 库的线性动画插帧,而控制插帧频率的就是ip_freq参数。例如原始数据只有 10 条,我们在上述最后一行代码将动画保存为动图时,设置保存为 24 帧的动画,也就是一秒钟出现 24 个静态柱形图,如果不进行插帧(设置参数ip_freq=None即可),那么动画将只持续 10/24 秒,且每两帧之间不会有任何过渡,整个动画看起来就会十分生硬且播放速度过快,就像下面这样。

所以上述情形只适合可视化时间差非常密集的统计数据。而在上文代码中,我们将插帧参数ip_freq设置为'30d',其含义是 30 天,即每 30 天将会进行一次插帧,而插帧的数量是有另一个参数确定的,默认情况下每一个插帧周期将会插入 2 帧,那么从一年到下一年将会存在 24 帧(其中 23 帧为线性过渡帧,另外,程序中一年一般按 360 天计算,故每年为 24 帧),这样一来,如果再保存为 24 帧的动画,那么总时长大约就是 9 秒(最后一条数据仅含 1 帧)。清楚上述原理后,我们就可以控制插帧频率参数 ip_freq 和动画帧数这两个参数来控制动画的时长和动画连续性,同时需要注意,动画帧数越高,插帧越频繁,动画文件就会越大,保存时程序的运行时间也会越长。

此外,仔细观察上述两个动画(一个插帧,一个不插帧),你会发现插帧后的动画中柱体上的数值是小数,实际上这是不合理的,因为专利的数量只能是整数。不过这种异常是由线性插帧引起的,只是为了让动画尽可能变得线性、丝滑,因此在插帧取中间值时得到了浮点数,无法考虑到数据的使用场景。而未插帧的动画不需要线性插值,自然不会凭空多出那么多小数。

2. 动画优化

上一节中的动画虽然制作简单,但整体看来还是缺乏美感,下面我们将使用更多的代码来对动画的细节进行优化,各个部分代码的功能尽数写在注释中,记得多看注释哦~(向右滑动查看完整代码)

1### 注意:这一步将会对各省所有时间点(1985-2022)的绿色发明专利进行可视化
2 from matplotlib import pyplot as plt  # 导入matplotlib库
3 import pandas as pd  # 导入pandas库
4 import pynimate as nim  # 导入pynimate库
5
6 plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
7 plt.rcParams['font.family'] = "simsun"   # 设置默认字体为宋体,支持显示汉字

# 定义回调函数,用于设置条形图的样式
10 def post_update(ax, i, datafier, bar_attr):
11     # 设置柱形图的画布上下左右边线不可见(隐藏柱状图四条边线)
12     ax.spines["top"].set_visible(False)
13     ax.spines["right"].set_visible(False)
14     ax.spines["bottom"].set_visible(False)
15     ax.spines["left"].set_visible(False)
16     # 设置柱形图画布内部填充颜色
17     ax.set_facecolor("#001219")  # 暗黑色
18    
19 # 创建一个画布对象 cnv,设置画布长宽和背景颜色
20 cnv = nim.Canvas(figsize=(17, 10), facecolor="#001219")  # 暗黑色,与柱形图填充色保持一致
21
22 # 创建一个Barplot对象bar,设置数据、时间格式、回调函数、圆角边框、网格、横轴变化等样式和效果
23 bar = nim.Barplot(
24     # 注意这里提供的是 1985-2022 完整的统计数据
25     data=data,          # 动画描述的统计数据,注意字段的类型,指标字段须是数值类型
26     time_format ="%Y-%m-%d",  # 原始数据中日期的显示方式
27     ip_freq='90d',    # 插帧的频率,30d 代表 30 天
28     ip_frac=0.5,      # 插值分数,默认值为 0.5, 此时应该表示每一个插帧频率下插入两帧
29     fixed_xlim=False, # 为 False时,横轴会在每一帧都根据数据值发生变化,避免因数值过小导致柱体不可见
30                       # 若此参数设置为 False,那么 ip_frac 参数值将固定为 1
31     post_update=post_update, # 调用回调函数空值柱状图样式
32     rounded_edges=True, # 柱形是否设置圆角,只有当数值足够小时该参数才会生效
33                         # 只要统计数据中存在数据超过了某个阈值,柱体圆角参数就不会再生效
34                         # 真是一个巨坑!害我找了半天
35     grid=True,          # 是否显示网格线, 如果参数 fixed_xlim 设置为 False,那么简易将此参数设置为 True
36     n_bars=31,          # 柱体的数量,如果不写,最多出现十个柱体
37 )
38
39 # 可以通过set_bar_color传入字典来指定每一个柱体的颜色
40 # bar.set_bar_color(bar_cols)  # 这里不设置,使用默认的色彩风格。
41                                # 若要设置主题颜色,须主动创建柱体名称与颜色(16进制表示法)的映射
42
43 # 设置图表标题的内容和字体颜色、字体大小;w 代表 white
44 bar.set_title("全国各省(暂无港澳台数据)历年(1985-2022)绿色产业企业发明专利申请数量统计图",
45                 color="w",
46                 size=21)
47                
48 # 设置横轴名称、字体颜色和大小
49 bar.set_xlabel("绿 色 产 业 企 业 申 请 发 明 专 利 数 量", color="w", size=15,
50                x=0.30,      # 横轴名称的坐标位置在横轴上的映射点,取 0-1
51                y=-0.08,     # 日期位置在纵轴上的映射点,取负值表示在 y 轴下方
52                )
53
54 # 设置柱状图右下方时间的显示方式、字体颜色、大小
55 bar.set_time(callback=lambda i, datafier: \
56             datafier.data.index[i].strftime("%Y.%m"),  # 日期显示方式,%Y 表示年份;%m 表示月份
57             x=0.95,      # 日期位置在横轴上的映射点,取 0-1
58             y=0.30,      # 日期位置在纵轴上的映射点,取 0-1
59             color="w",   # 日期文字颜色 white
60             size=65)     # 日期文字字体大小
61             
62 # 设置辅助文本的内容和样式
63 bar.set_text(
64     "sum",   # text 参数,没搞懂这个参数,欢迎大神赐教
65     # 下面参数表示要添加到图中的文字,其中使用字符串格式化方式在每一帧都计算出当前时间点的数值总和
66     callback=lambda i, datafier: \
67             f"Total : {int(datafier.data.iloc[i].sum())}\nDesigned by 数据Seminar",
68     size=25,    # 字体大小
69     x=0.72,     # 文字位置在横轴上的映射点,取 0-1
70     y=0.18,     # 文字位置在纵轴上的映射点,取 0-1
71     color="w",  # 字体颜色,white
72 )
73
74 # 设置柱体注释(柱体顶部的文字)的样式
75 bar.set_bar_annots(color="w", size=13)
76
77 # 设置x轴和y轴刻度和文字的样式
78 bar.set_xticks(colors="w",   # 横轴刻度的颜色
79                 length=0,    # 横轴刻度线向下延伸的长度,不宜过长
80                 labelsize=13) # 横轴刻度字体的大小
81 bar.set_yticks(colors="w", labelsize=13)  # 同上,描述的是纵轴
82
83 # 设置柱体的样式,当统计数据中存在较大的数值时,便不会再生效,这里就不会生效
84 bar.set_bar_border_props(
85     edge_color="black"
86     pad=0.1, 
87     mutation_aspect=1, 
88     radius=0.2, 
89     mutation_scale=0.6
90 )
91 
92 # 将设置好的 Barplot 对象 bar 添加到画布 Canvas 对象 cnv 中
93 cnv.add_plot(bar)
94 # 生成动画
95 cnv.animate()
96 
97 # 保存动画图表为 gif 格式
98 cnv.save(filename="Result_perfect",    # 保存后的文件名称
99             fps=24,          # 动画的帧数,即每一秒静态图像的个数
100            extension="gif"# 保存为 gif 格式

上图为笔者使用特意调整后的参数制作的动态柱状图,由于在上述第 29行代码指定了参数fixed_xlimFalse,因此动态柱状图的横轴会随着数据值的变化而逐帧变化。这样做是为了避免动态图在统计数据值过小的时间段内,柱体小到无法显示的尴尬状态。不过这样也有一定弊端,由于横坐标轴不停地改变,且第一个柱体始终保持最长状态,这就导致整体数据的增长速率和爆发时间点得不到完美体现。说明这个第三方库还有很多能够优化的空间。

另外,如果调用上述第 40 行代码为每一个柱体定制颜色,还可以让动态图更具有特色(上图没有设置色彩,将使用默认的配色),动画效果正如本文引言的用图,这里就不拿出来消耗大家的流量了,毕竟一个几分钟的动图占用了不小空间。

顺便推荐一个可以免费压缩 GIF 动画但又不怎么影响画质的网站:https://gifcompressor.com/zh/

笔者没有将创建和使用新色彩的代码放在文中,如果你对此感兴趣,在公众号后台回复关键词“20230922”即可获取完整代码。



💡  由于公众号平台只允许上传总帧数不超过 300 帧的动图,所以笔者为了呈现完整的数据,不得不将插帧频率降低到 90 d,所以上图播放速度偏快。而前言中的动图的插帧频率则为 30 d,但展示的数据是从 2008 年开始的。


五、总结

如果你想学习各种 Python 编程技巧,提升个人竞争力,那么就加入我们的数据 Seminar 交流群吧,欢迎大家在社群内交流、探索、学习,一起进步!同时您也可以分享通过数据 Seminar 学到的技能以及得到的成果(例如本期文章中的动态可视化)。

长按扫码,加入数据seminar-Python交流学习群

六、相关文章

conda 环境问题:数据治理 | Python 环境冲突烦?环境移植难?我用 Miniconda

数据可视化字体、色彩、配色问题:数据可视化 | 讲究!用 Python 制作词云图学问多着呢



星标⭐我们不迷路!想要文章及时到,文末“在看”少不了!

点击搜索你感兴趣的内容吧

往期推荐


质量检测 | 对一份中国工商企业注册数据库的质量考察

ChatGPT在指尖跳舞: open-interpreter实现本地数据采集、处理一条龙

Python 实战 | 使用正则表达式从文本中提取指标

Python 教学 | 一文搞懂面向对象中的“类和实例”

数据可视化 | 没错!在 Python 中也能像 ggplot2 一样绘图

Python 实战 | ChatGPT + Python 实现全自动数据处理/可视化





数据Seminar




这里是大数据、分析技术与学术研究的三叉路口


文 | 《社科领域大数据治理实务手册》


    欢迎扫描👇二维码添加关注    
点击下方“阅读全文”了解更多

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存