查看原文
其他

数据可视化 | Python绘制多维柱状图:一图展示西部各省人口变迁【附本文数据和代码】

快点关注→ 数据Seminar 2023-03-20

目录

一、前言

二、可视化成果预览

三、绘图准备工作

    1. 模块安装

    2. 数据预处理

四、绘图

    1. 全局设置

    2. 子图绘制

    3. 方案一图形完善

    4. 方案二效果展示

五、总结

六、同系列内容

七、免费Python教程

本文共12622字,阅读大约需要32分钟,欢迎指正!


💡 公众号对话框内发送关键词“多维柱状图”,即可获取文章中用到的样例数据和代码。

Part1前言

柱状图是利用柱子的高度来反映数据差异的统计图,与一维的柱状图相比,二维柱状图可以展示y轴上的变量含有两个指标的情况,如下图(图1)右图所示,多维柱状图的功能以此类推。

图1:一维柱状图(左)和  二维柱状图(右)

在数据维度较少的情况下,使用 Excel 绘制多维柱状图非常便捷,也能够清晰展示数据的变化。不过当数据的维度较多时,Excel在一张画布上绘制的多维柱状图并不方便观察数据变化趋势,但若是绘制在一张画布的多个统计图中,过程比较繁琐且效果图略显粗糙,此时就可以借助其他的可视化工具达到目的。

Python中的matplotlib模块具有绘制子图的功能,满足在一张画布上对多维数据进行可视化展示的需求,下图(图2)是一张画布包含了两个子图的统计图。


子图:一张画布上同时呈现多个图,每个图都称之为一个子图,子图与子图之间可以是不同类型的图, 比如折线图、柱状图 、饼图、散点图等。


图2:一行两列排列的子图

上图中的左侧子图为柱状图和折线图的组合图,右侧子图为折线图。不同的图形类型可以展示不同的数据特点,可以灵活搭配使用。matplotlib模块可以绘制的图像类型包括:柱状图、曲线图、散点图、等高线图、矩阵热图、直方图、3D图形等

2022年末,我国的常住人口数量在1962年后首次出现负增长现象,同时我国城镇化发展速度较快,有必要从乡村和城镇两个维度对近几年的常住人口变化进行数据分析,数据可视化能够更加直观地了解数据变化情况。本文讲述的是如何用 Python 在一张画布上用二维柱状图展示 “2017-2021年西部地区十二个省份乡村、城镇的常住人口变动情况” 。

Part2可视化成果预览

本文使用matplotlib模块绘制二维柱状图,对西部地区每个省份2017-2021年的乡村和城镇常住人口进行对比分析。

数据来源:国家统计局分省份乡村、城镇常住人口数据,官网中云南省的数据查询如下图(图3)所示:

图3:“国家统计局” 云南省人口数据查询页面

西部地区的省份共12个,下图(图4)是西部地区各个省份2017-2021年的乡村人口数据,显然可见,如果增加“城镇人口”维度的数据,数据的变化方向呈现不明显,可视化效果不太理想。

图4:西部地区各个省份2017-2021年的乡村人口数据

那么,上图(图4)仅仅展示了十二个省份“十三五”期间‘’乡村人口“维度的数据情况,如果我们想在一张图中同时展示”“乡村人口”和“城镇人口”两个维度的数据该怎么做呢?matplotlib模块可以实现一张画布绘制多个子图用以显示多个维度的数据变化的功能,绘图效果如下图(图5),其中柱体有图案填充的部分表示“城镇常住人口”指标,不含填充图案的为“乡村常住人口”指标。

图5:2017-2021年西部地区分省份乡村、城镇常住人口

Part3绘图准备工作

1模块安装

在一个已经安装Python环境的计算机中,安装matplotlib模块的步骤如下(以 Windows 系统为例):

  1. 使用Win+R打开命令提示符窗口,输入cmd命令,回车运行
  2. 在打开的命令提示符页面输入命令pip install matplotlib即可安装matplotlib模块 已安装的模块需要导入才能使用,在Python中导入matplotlib模块的代码如下:
import matplotlib    # 后续的字体设置使用
from matplotlib import pyplot as plt #绘图使用:导入matplotlib 的pyplot子模块,并简称为‘plt'

2数据预处理

笔者从国家统计局官网得到的数据是一个面板数据,地区包括全国各个省份,时间从2016-2021年,含有“乡村常住人口”和“城镇常住人口”两个指标,在Python中导入数据后表示为变量data。因为我们只需要西部地区十二个省份2017-2021年的数据,所以需要根据地区和时间对data进行筛选,得到所需要的西部地区数据表示为变量df_west,操作如下:

# 1.导入需要用到的包
import pandas as pd #数据分析常用的pandas包
import numpy as np #数据分析常用的numpy包
# 2.读取数据
data = pd.read_excel("2016-2021年全国各省乡村、城镇常住人口数量.xlsx"# 数据data的时间跨度:2016-2021,地区:全国各个省份
# 3.根据 年份 和 地区 筛选出所需要的数据
data = data[data['年份'].isin([2017,2018,2019,2020,2021])]
df_west = data[data['地区'].isin(['内蒙古自治区',
                                   '广西壮族自治区',
                                   '重庆市',
                                   '四川省',
                                   '贵州省',
                                   '云南省',
                                   '西藏自治区',
                                   '陕西省',
                                   '甘肃省',
                                   '青海省',
                                   '宁夏回族自治区',
                                   '新疆维吾尔自治区'])]
df_west.head() #显示df_west的前五行

为了绘制多维柱状图,我们需要按照不同的指标,将面板数据进行数据形式的转变,处理为下面代码中的变量resultresult_0Excel绘制多维柱状图时也需要进行同等数据处理步骤。

#1.使用透视表函数进行数据形式的转变
#指标1:乡村人口
result = pd.pivot_table(data=df_west,
                          values='乡村人口(万人)',
                          index='年份',
                          columns='地区',
                          aggfunc=np.sum )
index = np.arange(len(result)) #用在后面的画图中
#指标2:城镇人口
result_0 = pd.pivot_table(data=df_west,
                          values='城镇人口(万人)',
                          index='年份',
                          columns='地区',
                          aggfunc=np.sum )
index_0 = np.arange(len(result_0))
result

关于“乡村人口“指标处理后的result变量的结果如下:

“城镇人口”指标处理后的result_0变量结果如下:

resultresult_0的结果可以看出,经过数据预处理后的“乡村人口”和“城镇人口”的数据具有相同的行名称(年份)和列名称(地区),只是根据指标不同,其表现的具体数据值不同。

Part4绘图

根据上文中提到的包含十二个省份的子图,可以看出绘制包含多个子图的二维柱状图可以分为以下几个部分:

  • 统计图全局的统一设置,如
    • 画布(尺寸,分辨率)
    • 子图(数量,排列布局,子图间是否共享坐标轴)
    • 字体(类型等)
  • 统计图子图的绘制,如
    • 柱子(宽度,颜色,填充样式)
    • 数值标签(字体:大小或类型等,放置位置)
  • 统计图的完善:如
    • 坐标轴刻度标签(刻度值数值、字体大小、刻度值位置)
    • 坐标轴标签(坐标轴标签值、字体大小,坐标轴标签值位置)
    • 图例(图例值、图例大小、图例位置)
    • 子图之间的间隙设定

1全局设置

在画图之前,我们首先要设定画布全局的字体,matplotlib模块在绘图时,画布上只显示英文,中文字体则显示为‘口’,如果不对中文字体进行设定,‘口’将会出现在图标题、图例、坐标轴标签等其他位置。同时使用plt.subplots()函数设置画布上子图的布局,参数dpifigsize可以设置画布整体的分辨率和尺寸,分辨率大小可以调整图的清晰度,画布大小可以一定程度上调整图的长宽比例,参数sharey表示子图之间是否共享y坐标轴的刻度,设置子图间共享坐标轴以达到在各个子图间进行数值比较的目的

# 1.设置中文字体
font = {'family':'SimHei' }
matplotlib.rc('font',**font)
# 2.设置画布大小和分辨率
fig,axes = plt.subplots(nrows=3,ncols=4,figsize=(20,10),sharey=True,dpi=500)
# #子图分布为3行4列,画布大小 figsize:20*10,dpi分辨率

上述代码中plt.subplots()函数中参数的具体含义如下表所示:

2子图绘制

绘制柱状图,所使用的函数是plt.bar(),后面的绘图过程只使用了xheightwidthcolorhatch五个参数,用以设置柱子的位置、高度、宽度、颜色以及填充图案等图形要素。如果我们想要在柱体上添加数值标签,可以使用plt.text()函数进行实现,参数hafontsize控制数值标签的水平对齐位置和字体大小,此函数还可以设置字体类型、字体风格和粗细等,有其他需要可自行搜索查询。针对于本文中的“乡村人口”和“城市人口”两个维度而言,在统计图中怎么进行区分呢?

  • 方案一:对两个维度定义相同的颜色,通过对其中一个维度的柱体填充阴影进行区分
  • 方案二:对两个维度定义不同的颜色进行区分

如果所绘制的统计图,最终需要黑白展示或者黑白打印,比如论文中的插图,那么方案一就比较适合。


上述两种方案的效果示例图如下图(图6)所示。

图6:方案一和方案二的效果对比

首先采用了方案一进行展示绘图过程,方案二的后续绘图过程与方案一相同。采用方案一绘制一张统计子图如下所示(图7):

# 1.绘图
# 设置画布和子图布局
fig,axes = plt.subplots(nrows=3,ncols=4,figsize=(20,10),sharey=True,dpi=500)
# 在第一个子图上画图
plt.subplot(3,4,1)
# 定义颜色
Color_1 = '#666699'
# 绘图(乡村维度)
plt.bar(x=index, height=result[result.columns[0]], color=Color_1, width=0.4)
# 绘图(城镇维度)
plt.bar(x=index+0.4, height=result_0[result_0.columns[0]], color=Color_1, width=0.4, hatch='./.')
# 2. 获取标签值
# #云南省 乡村人口2017年的数据
f01 = result['云南省'][2017]
# #云南省 城镇人口2017年的数据
f02 = result_0['云南省'][2017]
# # 其他年份的标签
f11 = result['云南省'][2018]; f12=result_0['云南省'][2018]
f21 = result['云南省'][2019]; f22=result_0['云南省'][2019]
f31 = result['云南省'][2020]; f32=result_0['云南省'][2020]
f41 = result['云南省'][2021]; f42=result_0['云南省'][2021]
# 3. 添加标签
# 3.1 添加“云南省 乡村2017年”标签
plt.text(x=index[0], y=f01+20, s=f01, ha='center', fontsize=9) 
# 3.2 添加“云南省 城镇2017年”标签
plt.text(x=index[0]+0.4, y=f02+20, s=f02, ha='center',fontsize=9)
## 添加其他年份的标签
plt.text(x=index[1], y=f11+20, s=f11, ha='center', fontsize=9)
plt.text(x=index[1]+0.4, y=f12, s=f12,ha='center',fontsize=9)
plt.text(x=index[2], y=f21+20, s=f21, ha='center', fontsize=9)
plt.text(x=index[2]+0.4, y=f22+20, s=f22,ha='center',fontsize=9)
plt.text(x=index[3], y=f31+20, s=f31, ha='center', fontsize=9)
plt.text(x=index[3]+0.4, y=f32+20, s=f32,ha='center',fontsize=9)
plt.text(x=index[4], y=f41+20, s=f41, ha='center', fontsize=9)
plt.text(x=index[4]+0.4, y=f42+20, s=f42,ha='center',fontsize=9)
# 显示图像
plt.show()
图7:单个子图

每个子图可以绘制不同类型的统计图,由于本文的十二个子图间的统计图存在规律,因此先在图7中绘制一个统计图查看效果,为了避免代码冗长,后面将采用循环的方式遍历每个子图将图形绘制完整。


上述代码所使用到的重要函数以及使用到的参数的介绍如下表所示:

以上的操作已经有了二维柱状图的雏形,但是缺少可以对图中数据进行具体说明的一些统计图要素,比如坐标轴标签、图例、图标题等,绘制要素完整的统计图可以进行以下的操作,效果图如下图(图8)所示。

# 1.绘图
# 设置画布和子图布局
fig,axes = plt.subplots(nrows=3,ncols=4,figsize=(20,10),sharey=True,dpi=500)
# 在第一个子图上画图
plt.subplot(3,4,1)
# 定义颜色
Color_1 = '#666699'
# 绘图(乡村维度)
plt.bar(x=index, height=result[result.columns[0]], color=Color_1, width=0.4)
# 绘图(城镇维度)
plt.bar(x=index+0.4, height=result_0[result_0.columns[0]], color=Color_1, width=0.4, hatch='./.')
# 2. 获取标签值
# #云南省 乡村人口2017年的数据
f01 = result['云南省'][2017]
# #云南省 城镇人口2017年的数据
f02 = result_0['云南省'][2017]
# # 其他年份的标签
f11 = result['云南省'][2018]; f12=result_0['云南省'][2018]
f21 = result['云南省'][2019]; f22=result_0['云南省'][2019]
f31 = result['云南省'][2020]; f32=result_0['云南省'][2020]
f41 = result['云南省'][2021]; f42=result_0['云南省'][2021]
# 3. 添加标签
# 3.1 添加“云南省 乡村2017年”标签
plt.text(x=index[0], y=f01+20, s=f01, ha='center', fontsize=9)
# 3.2 添加“云南省 城镇2017年”标签
plt.text(x=index[0]+0.4, y=f02+20, s=f02, ha='center',fontsize=9)
## 添加其他年份的标签
plt.text(x=index[1], y=f11+20, s=f11, ha='center', fontsize=9)
plt.text(x=index[1]+0.4, y=f12, s=f12,ha='center',fontsize=9)
plt.text(x=index[2], y=f21+20, s=f21, ha='center', fontsize=9)
plt.text(x=index[2]+0.4, y=f22+20, s=f22,ha='center',fontsize=9)
plt.text(x=index[3], y=f31+20, s=f31, ha='center', fontsize=9)
plt.text(x=index[3]+0.4, y=f32+20, s=f32,ha='center',fontsize=9)
plt.text(x=index[4], y=f41+20, s=f41, ha='center', fontsize=9)
plt.text(x=index[4]+0.4, y=f42+20, s=f42,ha='center',fontsize=9)
# 4.坐标轴刻度值的标签设定
plt.xticks(ticks=index+0.2, labels=result.index, fontsize=10)
plt.yticks(fontsize=10)#y轴刻度 标签值定义
# 5.添加 x、y轴的标签名称
plt.xlabel(xlabel="年份", fontsize=10, loc='right'#字体、位置设定,loc:{'left','center','right'}
plt.ylabel(ylabel="人口(万人)", fontsize=10, loc='top')# loc:{'top','center','bottom'}
# 6.图例设定
plt.legend(labels=['乡村','城镇'], ncol=2, loc='upper left', prop={'size': 10})
#ncol=2:图例设置为两列进行展示;loc:图例位于图的具体位置;prop:图例属性设置,size设置图例的大小
# 7.统计图的标题设定
plt.title(label='云南省')
# 显示图形
plt.show()
图8:要素完整的统计子图

增添图例、图名称和坐标轴标签等要素后,统计图已经成形,但是各个组成部分间稍显拥挤,由于我们绘制的是包含十二个子图的统计图,可以全部绘制完毕后进行布局调整,为了避免代码过于冗长,我们可以写成一个循环结构,遍历十二个省份,绘制在对应的子图中(循环的代码可在下文中找到),如下图(图9)

图9:统计图雏形

3方案一图形完善

上图(图9)虽然已经具备统计图的雏形,但是不太美观且表述不够清晰,例如存在图例挡住部分柱状图、各个子图间略显拥挤的情况,因此可以对图例的放置位置子图之间的排列间距进行稍微的调整,调整后就可以得到下图(图10)所示的统计图。

# 完整代码
# 设置画布和子图布局
fig,axes = plt.subplots(3, 4, figsize=(20,10),sharey = True,dpi=500)
# 自定义颜色,十二个颜色
Color=['#666699''#CCFFFF''#FFCCCC''#CCFF99''#CCCCFF''#FF9966''#CCFF33''#99CC99''#66CCCC''#99CC66''#6699CC','#FFFF66'
# 对十二个省份遍历,进行循环绘图
for i in np.arange(12):
    plt.subplot(3,4,i+1)
    #设置图片的右边框和上边框为不显示
    ax=plt.gca() 
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    # 绘制二维柱状图
    # #乡村维度
    plt.bar(x=index, height=result[result.columns[i]], color=Color[i], width=0.4)
    # #城市维度
    plt.bar(x=index+0.4, height=result_0[result_0.columns[i]], color=Color[i], width=0.4, hatch='./.')
    # 获得值标签
    f01 = result[result.columns[i]][2017]; f02=result_0[result_0.columns[i]][2017]
    f11 = result[result.columns[i]][2018]; f12=result_0[result_0.columns[i]][2018]
    f21 = result[result.columns[i]][2019]; f22=result_0[result_0.columns[i]][2019]
    f31 = result[result.columns[i]][2020]; f32=result_0[result_0.columns[i]][2020]
    f41 = result[result.columns[i]][2021]; f42=result_0[result_0.columns[i]][2021]
    # 添加柱形图数据值标签,设置字体大小和位置
    # # 添加“2017年”标签
    plt.text(x=index[0], y=f01+20, s=f01, ha='center',fontsize=9)     # 乡村维度
    plt.text(x=index[0]+0.4, y=f02+20, s=f02, ha='center',fontsize=9) # 城镇维度
    # # 添加其他年份的标签
    plt.text(x=index[1], y=f11+20, s=f11, ha='center', fontsize=9)
    plt.text(x=index[1]+0.4, y=f12, s=f12,ha='center',fontsize=9)
    plt.text(x=index[2], y=f21+20, s=f21, ha='center', fontsize=9)
    plt.text(x=index[2]+0.4, y=f22+20, s=f22,ha='center',fontsize=9)
    plt.text(x=index[3], y=f31+20, s=f31, ha='center', fontsize=9)
    plt.text(x=index[3]+0.4, y=f32+20, s=f32,ha='center',fontsize=9)
    plt.text(x=index[4], y=f41+20, s=f41, ha='center', fontsize=9)
    plt.text(x=index[4]+0.4, y=f42+20, s=f42,ha='center',fontsize=9)
    # 设置坐标轴刻度标签
    plt.xticks(ticks=index+0.2, labels=result.index, fontsize=10)
    plt.yticks(fontsize=10)
    # 设置坐标轴标签
    plt.xlabel(xlabel="年份", fontsize=10, loc='right')
    plt.ylabel(ylabel="人口(万人)", fontsize=10, loc='top')
    # 设置图例
    plt.legend(labels=['乡村','城镇'], ncol=2, loc='upper left', prop={'size': 10}, bbox_to_anchor=(0.01,1.15))
    # 设置图标题
    plt.title(label=result.columns[i], x=0.5, y=1.2)
# 设置子图的排列布局
plt.tight_layout(h_pad=2.3, w_pad=2.3)
#保存为图片,dpi为分辨率
plt.savefig(fname="pic_1.png",dpi=500) 
图10:方案一效果展示(同图5)

关于图例、坐标轴标签和图名称的设置,其函数以及相关参数如下表所示:

4方案二效果展示

在上文中方案一的实施过程中,我们使用阴影填充柱状图中城镇人口指标,在视觉上以此区分乡村人口数量和城镇人口数量这两个不同的指标。除了这种方式之外,我们可以使用更加直接的区分方式,比如上文中提到的方案二——对两个维度(乡村人口数量和城镇人口数量)定义不同的颜色进行区分

方案二绘图的具体代码和绘图效果如下图(图11)。

# 画布和子图设置
fig,axes = plt.subplots(3, 4, figsize=(20,10),sharey = True,dpi=800)
#自定义颜色
Color=['#666699''#CCFFFF''#FFCCCC''#CCFF99''#CCCCFF''#FF9966''#CCFF33''#99CC99''#66CCCC''#99CC66''#6699CC','#FFFF66'
for i in np.arange(12):
    # 对子图进行遍历
    plt.subplot(3,4,i+1)
    # gca:get current axis得到当前
    ax=plt.gca() 
    # 设置图片的右边框和上边框为不显示
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    # 绘制二维柱状图
    # #乡村维度
    plt.bar(x=index, height=result[result.columns[i]], width=0.4) # color=Color[i]
    # #城市维度
    plt.bar(x=index+0.4, height=result_0[result_0.columns[i]],  width=0.4) # hatch='./.'
    # 得到值标签
    f01 = result[result.columns[i]][2017]; f02=result_0[result_0.columns[i]][2017]
    f11 = result[result.columns[i]][2018]; f12=result_0[result_0.columns[i]][2018]
    f21 = result[result.columns[i]][2019]; f22=result_0[result_0.columns[i]][2019]
    f31 = result[result.columns[i]][2020]; f32=result_0[result_0.columns[i]][2020]
    f41 = result[result.columns[i]][2021]; f42=result_0[result_0.columns[i]][2021]
    # 添加柱形图数据值标签,设置字体大小和位置
    # # 添加“2017年”标签
    plt.text(x=index[0], y=f01+20, s=f01, ha='center',fontsize=9)     # 乡村维度
    plt.text(x=index[0]+0.4, y=f02+20, s=f02, ha='center',fontsize=9) # 城镇维度
    # # 添加其他年份的标签
    plt.text(x=index[1], y=f11+20, s=f11, ha='center', fontsize=9)
    plt.text(x=index[1]+0.4, y=f12, s=f12,ha='center',fontsize=9)
    plt.text(x=index[2], y=f21+20, s=f21, ha='center', fontsize=9)
    plt.text(x=index[2]+0.4, y=f22+20, s=f22,ha='center',fontsize=9)
    plt.text(x=index[3], y=f31+20, s=f31, ha='center', fontsize=9)
    plt.text(x=index[3]+0.4, y=f32+20, s=f32,ha='center',fontsize=9)
    plt.text(x=index[4], y=f41+20, s=f41, ha='center', fontsize=9)
    plt.text(x=index[4]+0.4, y=f42+20, s=f42,ha='center',fontsize=9)
    # 设置坐标轴刻度标签
    plt.xticks(ticks=index+0.2, labels=result.index, fontsize=10)
    plt.yticks(fontsize=10)
    # 设置坐标轴标签
    plt.xlabel(xlabel="年份",fontsize=10, loc='right')
    plt.ylabel(ylabel="人口(万人)",fontsize=10,loc='top')
    # 设置图例
    plt.legend(labels=['乡村','城镇'], ncol=2, loc='upper left', prop={'size': 10}, bbox_to_anchor=(0.01,1.15))
    # 设置图标题
    plt.title(label=result.columns[i], x=0.5, y=1.2)
# 设置子图的排列布局
plt.tight_layout(h_pad=2.3, w_pad=2.3)
# 保存图片
plt.savefig(fname="pic_6.png",dpi=1000)
图11:方案二效果展示图

上述代码与方案一最终代码几乎一样,唯一的区别在于方案一的代码在 plt.bar() 函数中对不同维度的柱体使用了相同的颜色(详见color参数),且在第二维度的柱体中添加了阴影(详见hatch参数)。而方案二的代码则直接不设置color参数和hatch参数,让系统调用默认的参数,达到不同维度的柱体显示不同颜色且没有阴影的效果,正如上图所示。

Part5总结

Python中,matplotlib模块作为数据可视化的重要绘图工具之一,其功能强大,使用灵活,绘图自由度很高。相对于Excel的常规绘图功能而言,本文从另一角度出发,着重讲解了将单一多维柱状图拆分为含有多个子图的多维柱状图的过程,为大家展示了一种新的数据可视化思路,并提供了执行方案。如果大家对 matplotlib模块感兴趣,可以移步官网查看更多数据可视化内容(https://matplotlib.org/)。


💡 公众号对话框内发送关键词“多维柱状图”,即可获取文章中用到的样例数据和代码。

Part6同系列内容

Part7免费 Python 教程




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

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

往期推荐


老姚书角|李井奎:遵杨奇明嘱序老姚书角

大数据应用 | 陈云松:大数据的功能

Python教学 | Python 中的循环结构(下)【附本文代码和数据】

Python教学 | Python 中的循环结构(上)【附本文代码和数据】

Python教学 | Python 中的分支结构(判断语句)【附本文代码和数据】

数据治理 | 进阶版Git:使用Git分支特性进行多人协作





数据Seminar




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


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


    欢迎扫描👇二维码添加关注    

点击下方“阅读全文”了解更多

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

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