對比學(xué)習(xí),用Excel和Python繪制「子彈圖」
大家好,我是寶器!
今天給大家?guī)硪黄容^有意思的可視化圖——子彈圖詳細(xì)繪圖教程。對比Excel與Pyhton,手把手教你繪制高大上的子彈圖。
P.S. 本文使用Excel for Mac作為演示,Windows Excel操作稍有不同,差異大的地方文中有額外解釋,總體繪圖步驟和思想是一致的,不影響理解和閱讀。
子彈圖
子彈圖的樣子很像子彈射出后帶出的軌道,所以稱為子彈圖(英文名:Bullet Graph)。子彈圖的發(fā)明是為了取代儀表盤上常見的那種里程表,時速表等基于圓形的信息表達(dá)方式。

子彈圖的特點如下:
每一個單元的子彈圖只能顯示單一的數(shù)據(jù)信息源 通過添加合理的度量標(biāo)尺可以顯示更精確的階段性數(shù)據(jù)信息 通過優(yōu)化設(shè)計還能夠用于表達(dá)多項同類數(shù)據(jù)的對比 可以表達(dá)一項數(shù)據(jù)與不同目標(biāo)的校對結(jié)果
子彈圖無修飾的線性表達(dá)方式使我們能夠在狹小的空間中表達(dá)豐富的數(shù)據(jù)信息,線性的信息表達(dá)方式與我們習(xí)以為常的文字閱讀相似,相對于圓形構(gòu)圖的信息表達(dá),在信息傳遞上有更大的效能優(yōu)勢。
子彈圖的構(gòu)成
下圖為子彈圖的結(jié)構(gòu),以及與柱形圖的對比

主要數(shù)據(jù)值由圖表中間主條形的長度所表示,稱為功能度量(Feature Measure);而與圖表方向垂直的直線標(biāo)記則稱為比較度量(Comparative Measure),用來與功能度量所得數(shù)值進(jìn)行比較。如果主條形長度超越比較度量標(biāo)記的位置,則代表數(shù)據(jù)達(dá)標(biāo)。
功能度量背后的分段顏色條形用來顯示定性范圍得分。每種色調(diào)(如上面示例中三種不同深度的灰色)表示不同表現(xiàn)范圍等級,如欠佳、平均和良好。當(dāng)使用子彈圖時,建議最多使用五個等級。
子彈圖和柱狀圖對比
柱狀圖主要用于多個分類間的數(shù)據(jù)(大小、數(shù)值)的對比。
子彈圖主要用于各個分類間各自的數(shù)值所處狀態(tài)與測量標(biāo)記的對比,突出的是每個分類自身的情況,沒有分類間的比較,用于展示各個分類的子彈圖單元相對獨立。
Excel繪制子彈圖
子彈圖分為橫向子彈圖和縱向子彈圖。兩者繪制方法有所差異。!
縱向子彈圖
用Excel繪制縱向子彈圖較為簡單。選擇需要繪制的數(shù)據(jù),插入簇狀柱形圖,然后更改圖表類型。將"目標(biāo)值"與"銷售額"更改為次坐標(biāo)軸,圖表類型分別為"帶數(shù)據(jù)標(biāo)記的折線圖"和"簇狀柱形圖","合格值"與"挑戰(zhàn)值"為"堆積柱形圖"。
最后更改實際銷售額"簇狀柱形圖"的柱子寬度(調(diào)整間隙寬度),和目標(biāo)值的"帶數(shù)據(jù)標(biāo)記的折線圖"標(biāo)記類型(-)和大小(18)。

橫向子彈圖
橫向子彈圖與縱向子彈圖不同,它的繪制方法較為復(fù)雜,下面我們一步步帶你操作。

數(shù)據(jù)準(zhǔn)備
已有字段:
地區(qū),目標(biāo)銷售額,實際銷售額,挑戰(zhàn)銷售額,合格銷售額五個字段。
添加字段:
挑戰(zhàn)=挑戰(zhàn)值-合格值 y-次軸,以0.5為開始值,級差為0.5的等差數(shù)列,用于散點圖的y軸
Step01
選擇數(shù)據(jù),繪制橫行堆積條形圖。

Step02
插入一個銷售額列,并選擇對應(yīng)x/y軸數(shù)據(jù)。

選擇上步添加的銷售額堆積條形圖,修改圖表類型為散點圖,設(shè)置坐標(biāo)軸類型為【次要坐標(biāo)軸】

Step03
選擇合格值數(shù)據(jù)條,設(shè)置【間隙寬度】為50%。并按照銷售額的方法添加目標(biāo)值散點圖。

Step04
實際銷售額與目標(biāo)銷售額的繪制。這一步上win和mac兩者差異較大,因此在這里分別說明了步驟。
mac方法:
單擊【添加圖表元素】-【誤差線】-【標(biāo)準(zhǔn)誤差線】,點擊并刪除垂直誤差線,選擇“水平誤差線”并設(shè)置誤差線格式,方向為【負(fù)偏差】,末端樣式為【無線端】,誤差量為【自定義】,【指定值】數(shù)據(jù)來源為銷售額單元格。如下圖所示。

win方法:
單擊【添加圖表元素】-【誤差線】-【其他誤差線選項】,在打開的窗格中選擇“水平誤差線”,方向為【負(fù)偏差】,末端樣式為【無線端】,誤差量為【自定義】,在打開的【自定義錯誤欄】中選【負(fù)錯誤值】,數(shù)據(jù)來源為銷售額單元格。
Step05
先選擇銷售額水平誤差線,增加線條的粗細(xì)至合適寬度,標(biāo)記點設(shè)為無。
再按照上一步方法設(shè)置目標(biāo)值,添加誤差線,方向為垂直方向,無線端,誤差量為【固定值】至合適寬度。

Python繪制子彈圖
Python繪制子彈圖方法也不復(fù)雜,這里需要借助seaborn調(diào)色板繪制子彈圖的背景顏色。配合使用matplotlib繪制條形圖和豎線條繪制實際值和目標(biāo)值即可。
導(dǎo)入相應(yīng)模塊
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.ticker import FuncFormatter
%matplotlib inline
Seaborn調(diào)色板
sns.palplot(sns.light_palette("green", 5))

增加調(diào)色板長度及顏色
sns.palplot(sns.light_palette("purple",8, reverse=True))

設(shè)置要繪制的數(shù)據(jù)
limits = [80, 100, 150]
data_to_plot = ("Example 1", 105, 120)
palette = sns.color_palette("Blues_r", len(limits))
嘗試構(gòu)建第一個堆疊條形圖
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)

添加目標(biāo)垂直線
ax.axvline(data_to_plot[2], color="blue",
ymin=0.10, ymax=0.9)

定義完整的函數(shù)
函數(shù)參數(shù)
data: 標(biāo)簽、測量和目標(biāo)的列表
limits: 范圍值列表
labels: 限制范圍的描述列表
axis_label: 描述x軸的字符串
title: 圖標(biāo)題
size: 繪圖尺寸元組
palette: seaborn調(diào)色板
formatter: matplotlib formatter 對象的x軸
target_color: 目標(biāo)行顏色字符串
bar_color: 小條的顏色字符串
label_color: 限制標(biāo)簽文本的顏色字符串
bulletgraph(data=None, limits=None, labels=None, axis_label=None,
title=None, size=(5, 3), palette=None, formatter=None,
target_color="gray", bar_color="black", label_color="gray")
子彈圖01
# 數(shù)據(jù)準(zhǔn)備
data_to_plot2 = [("張三", 105, 120),
("李斯", 99, 110),
("王武", 109, 125),
("侯奇", 135, 123),
("巴蜀", 45, 105)]
# 繪制圖
bulletgraph(data_to_plot2, limits=[20, 60, 100, 160],
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)
# 數(shù)據(jù)準(zhǔn)備
data_to_plot3 = [("Print", 50000, 60000),
("Billboards", 75000, 65000),
("Radio", 125000, 80000),
("Online", 195000, 115000)]
# 設(shè)置調(diào)色板
palette = sns.light_palette("grey", 3, reverse=False)
bulletgraph(data_to_plot3,
limits=[50000, 125000, 200000],
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)

子彈圖特點
總結(jié)一下,子彈圖有以下特點:
有設(shè)置定性的數(shù)據(jù)范圍,采用同一色系中不同深淺的顏色來表示。比如上圖中有Bad、Good、Excellent三個定性的數(shù)值范圍(也可以采用更多個,但是不建議過多,一般3~5個即可)。 主體數(shù)據(jù)條柱,一般用較深的顏色表示,可以吸引人的眼球,比如我們用來表達(dá)實際完成情況。這個柱子與定性范圍的柱子是重疊在一起的,但是比它們要窄。 有一個橫線(豎線)作為主要標(biāo)記標(biāo)識,可以用來表示目標(biāo),方便直觀地對比是否達(dá)成目標(biāo)。 刻度量表,可以清晰地表達(dá)具體的數(shù)值,這里我們就用坐標(biāo)軸表示。 文本標(biāo)簽,可以用來表示圖表的信息內(nèi)容。
附錄子彈圖完整函數(shù)
def bulletgraph(data=None, limits=None, labels=None, axis_label=None, title=None,
size=(5, 3), palette=None, formatter=None, target_color="gray",
bar_color="black", label_color="gray"):
""" Build out a bullet graph image
Args:
data = 標(biāo)簽、測量和目標(biāo)的列表
limits = 范圍值列表
labels = 限制范圍的描述列表
axis_label = 描述x軸的字符串
title = 圖標(biāo)題
size = 繪圖尺寸元組
palette = seaborn調(diào)色板
formatter = matplotlib formatter 對象的x軸
target_color = 目標(biāo)行顏色字符串
bar_color = 小條的顏色字符串
label_color = 限制標(biāo)簽文本的顏色字符串
Returns:
a matplotlib figure
"""
# 確定調(diào)整工具條高度的最大值
# 除以10似乎很有效
h = limits[-1] / 10
# 使用綠色調(diào)色板作為合理的默認(rèn)設(shè)置
if palette is None:
palette = sns.light_palette("green", len(limits), reverse=False)
# 必須能夠通過多個子圖處理一個或多個數(shù)據(jù)集
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):
# 從創(chuàng)建繪圖時返回的軸數(shù)組中獲取軸
if len(data) > 1:
ax = axarr[idx]
# 格式化以消除額外的標(biāo)記混亂
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,以確保目標(biāo)標(biāo)記適合
ymin, ymax = ax.get_ylim()
ax.vlines(
item[2], ymin * .9, ymax * .9, linewidth=1.5, color=target_color)
# 現(xiàn)在做一些標(biāo)簽
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)