对比Excel学习Python绘制「子弹图」

俊红的数据分析之路

共 13105字,需浏览 27分钟

 · 2021-07-13

今天给大家带来一篇比较有意思的可视化图——子弹图详细绘图教程。对比Excel与Pyhton,手把手教你绘制高大上的子弹图。


P.S. 本文使用Excel for Mac作为演示,Windows Excel操作稍有不同,差异大的地方文中有额外解释,总体绘图步骤和思想是一致的,不影响理解和阅读。

子弹图

子弹图的样子很像子弹射出后带出的轨道,所以称为子弹图(英文名:Bullet Graph)。子弹图的发明是为了取代仪表盘上常见的那种里程表,时速表等基于圆形的信息表达方式。

子弹图的特点如下:

  • 每一个单元的子弹图只能显示单一的数据信息源
  • 通过添加合理的度量标尺可以显示更精确的阶段性数据信息
  • 通过优化设计还能够用于表达多项同类数据的对比
  • 可以表达一项数据与不同目标的校对结果

子弹图无修饰的线性表达方式使我们能够在狭小的空间中表达丰富的数据信息,线性的信息表达方式与我们习以为常的文字阅读相似,相对于圆形构图的信息表达,在信息传递上有更大的效能优势。

子弹图的构成

下图为子弹图的结构,以及与柱形图的对比

主要数据值由图表中间主条形的长度所表示,称为功能度量(Feature Measure);而与图表方向垂直的直线标记则称为比较度量(Comparative Measure),用来与功能度量所得数值进行比较。如果主条形长度超越比较度量标记的位置,则代表数据达标。

功能度量背后的分段颜色条形用来显示定性范围得分。每种色调(如上面示例中三种不同深度的灰色)表示不同表现范围等级,如欠佳、平均和良好。当使用子弹图时,建议最多使用五个等级。

子弹图和柱状图对比

柱状图主要用于多个分类间的数据(大小、数值)的对比。

子弹图主要用于各个分类间各自的数值所处状态与测量标记的对比,突出的是每个分类自身的情况,没有分类间的比较,用于展示各个分类的子弹图单元相对独立。

Excel绘制子弹图

子弹图分为横向子弹图和纵向子弹图。两者绘制方法有所差异。!

纵向子弹图

用Excel绘制纵向子弹图较为简单。选择需要绘制的数据,插入簇状柱形图,然后更改图表类型。将"目标值"与"销售额"更改为次坐标轴,图表类型分别为"带数据标记的折线图"和"簇状柱形图","合格值"与"挑战值"为"堆积柱形图"。

最后更改实际销售额"簇状柱形图"的柱子宽度(调整间隙宽度),和目标值的"带数据标记的折线图"标记类型(-)和大小(18)。

横向子弹图

横向子弹图与纵向子弹图不同,它的绘制方法较为复杂,下面我们一步步带你操作。

数据准备

已有字段:
地区,目标销售额,实际销售额,挑战销售额,合格销售额五个字段。

添加字段:

  • 挑战=挑战值-合格值
  • y-次轴,以0.5为开始值,级差为0.5的等差数列,用于散点图的y轴

Step01

选择数据,绘制横行堆积条形图。

Step02

插入一个销售额列,并选择对应x/y轴数据。

选择上步添加的销售额堆积条形图,修改图表类型为散点图,设置坐标轴类型为【次要坐标轴】

Step03

选择合格值数据条,设置【间隙宽度】为50%。并按照销售额的方法添加目标值散点图。

Step04

实际销售额与目标销售额的绘制。这一步上win和mac两者差异较大,因此在这里分别说明了步骤。

mac方法:

单击【添加图表元素】-【误差线】-【标准误差线】,点击并删除垂直误差线,选择“水平误差线”并设置误差线格式,方向为【负偏差】,末端样式为【无线端】,误差量为【自定义】,【指定值】数据来源为销售额单元格。如下图所示。


win方法:

单击【添加图表元素】-【误差线】-【其他误差线选项】,在打开的窗格中选择“水平误差线”,方向为【负偏差】,末端样式为【无线端】,误差量为【自定义】,在打开的【自定义错误栏】中选【负错误值】,数据来源为销售额单元格。

Step05

先选择销售额水平误差线,增加线条的粗细至合适宽度,标记点设为无。

再按照上一步方法设置目标值,添加误差线,方向为垂直方向,无线端,误差量为【固定值】至合适宽度。

Python绘制子弹图

Python绘制子弹图方法也不复杂,这里需要借助seaborn调色板绘制子弹图的背景颜色。配合使用matplotlib绘制条形图和竖线条绘制实际值和目标值即可。

导入相应模块

import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.ticker import FuncFormatter
%matplotlib inline

Seaborn调色板

sns.palplot(sns.light_palette("green"5))

增加调色板长度及颜色

sns.palplot(sns.light_palette("purple",8, reverse=True))

设置要绘制的数据

limits = [80100150]
data_to_plot = ("Example 1"105120)
palette = sns.color_palette("Blues_r", len(limits))

尝试构建第一个堆叠条形图

fig, ax = plt.subplots()
ax.set_aspect('equal')
ax.set_yticks([1])
ax.set_yticklabels([data_to_plot[0]])

prev_limit = 0
for idx, lim in enumerate(limits):
    ax.barh([1], lim-prev_limit, left=prev_limit, height=15, color=palette[idx])
    prev_limit = lim

增加测量值

# 画出我们要测量的值
ax.barh([1], data_to_plot[1], color='black', height=5)

添加目标垂直线

ax.axvline(data_to_plot[2], color="blue"
           ymin=0.10, ymax=0.9)

定义完整的函数

函数参数

data: 标签、测量和目标的列表
limits: 范围值列表
labels: 限制范围的描述列表
axis_label: 描述x轴的字符串
title: 图标题
size: 绘图尺寸元组
palette: seaborn调色板
formatter: matplotlib formatter 对象的x轴
target_color: 目标行颜色字符串
bar_color: 小条的颜色字符串
label_color: 限制标签文本的颜色字符串

bulletgraph(data=None, limits=None, labels=None, axis_label=None,
            title=None, size=(53), palette=None, formatter=None,
            target_color="gray", bar_color="black", label_color="gray")

子弹图01

# 数据准备
data_to_plot2 = [("张三"105120),
                 ("李斯"99110),
                 ("王武"109125),
                 ("侯奇"135123),
                 ("巴蜀"45105)]
# 绘制图
bulletgraph(data_to_plot2, limits=[2060100160], 
            labels=["Poor""OK""Good""Excellent"], 
            size=(8,5), axis_label="绩效考核"
            label_color="black", bar_color="#252525"
            target_color='#f7f7f7',
            title="销售代表绩效")

子弹图02

money_fmt = FuncFormatter(money)
# 数据准备
data_to_plot3 = [("Print"5000060000),
                 ("Billboards"7500065000),
                 ("Radio"12500080000),
                 ("Online"195000115000)]
# 设置调色板
palette = sns.light_palette("grey"3, reverse=False)
bulletgraph(data_to_plot3, 
            limits=[50000125000200000], 
            labels=["Below""On Target""Above"], 
            size=(10,5), axis_label="Annual Budget"
            label_color="black", bar_color="#252525"
            target_color='#f7f7f7', palette=palette,
            title="Marketing Channel Budget Performance"
            formatter=money_fmt)

子弹图特点

总结一下,子弹图有以下特点:

  1. 有设置定性的数据范围,采用同一色系中不同深浅的颜色来表示。比如上图中有Bad、Good、Excellent三个定性的数值范围(也可以采用更多个,但是不建议过多,一般3~5个即可)。
  2. 主体数据条柱,一般用较深的颜色表示,可以吸引人的眼球,比如我们用来表达实际完成情况。这个柱子与定性范围的柱子是重叠在一起的,但是比它们要窄。
  3. 有一个横线(竖线)作为主要标记标识,可以用来表示目标,方便直观地对比是否达成目标。
  4. 刻度量表,可以清晰地表达具体的数值,这里我们就用坐标轴表示。
  5. 文本标签,可以用来表示图表的信息内容。

附录子弹图完整函数

def bulletgraph(data=None, limits=None, labels=None, axis_label=None, title=None,
                size=(53), palette=None, formatter=None, target_color="gray",
                bar_color="black", label_color="gray")
:

    """ Build out a bullet graph image
        Args:
            data = 标签、测量和目标的列表
            limits = 范围值列表
            labels = 限制范围的描述列表
            axis_label = 描述x轴的字符串
            title = 图标题
            size = 绘图尺寸元组
            palette = seaborn调色板
            formatter = matplotlib formatter 对象的x轴
            target_color = 目标行颜色字符串
            bar_color = 小条的颜色字符串
            label_color = 限制标签文本的颜色字符串
        Returns:
            a matplotlib figure
    """

    # 确定调整工具条高度的最大值
    # 除以10似乎很有效
    h = limits[-1] / 10

    # 使用绿色调色板作为合理的默认设置
    if palette is None:
        palette = sns.light_palette("green", len(limits), reverse=False)

    # 必须能够通过多个子图处理一个或多个数据集
    if len(data) == 1:
        fig, ax = plt.subplots(figsize=size, sharex=True)
    else:
        fig, axarr = plt.subplots(len(data), figsize=size, sharex=True)

    # 将每个项目符号图形条添加到副图中
    for idx, item in enumerate(data):

        # 从创建绘图时返回的轴数组中获取轴
        if len(data) > 1:
            ax = axarr[idx]

        # 格式化以消除额外的标记混乱
        ax.set_aspect('equal')
        ax.set_yticklabels([item[0]])
        ax.set_yticks([1])
        ax.spines['bottom'].set_visible(False)
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.spines['left'].set_visible(False)

        prev_limit = 0
        for idx2, lim in enumerate(limits):
            # 绘制条形图
            ax.barh([1], lim - prev_limit, left=prev_limit, height=h,
                    color=palette[idx2])
            prev_limit = lim
        rects = ax.patches
        # 列表中的最后一项是我们要测量的值 
        # 绘制我们正在测量的值
        ax.barh([1], item[1], height=(h / 3), color=bar_color)

        # 需要ymin和max,以确保目标标记适合
        ymin, ymax = ax.get_ylim()
        ax.vlines(
            item[2], ymin * .9, ymax * .9, linewidth=1.5, color=target_color)

    # 现在做一些标签
    if labels is not None:
        for rect, label in zip(rects, labels):
            height = rect.get_height()
            ax.text(
                rect.get_x() + rect.get_width() / 2,
                -height * .4,
                label,
                ha='center',
                va='bottom',
                color=label_color)
    if formatter:
        ax.xaxis.set_major_formatter(formatter)
    if axis_label:
        ax.set_xlabel(axis_label)
    if title:
        fig.suptitle(title, fontsize=14)
    fig.subplots_adjust(hspace=0)
点分享
点收藏
点点赞
点在看
浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报