用 Python 制作子弹图也这么简单,爱了~

众所周知,Python 的应用是非常广泛的,今天我们就通过 matplotlib 库学习下如何制作精美的子弹图 ## 什么是子弹图 一个子弹图约定俗成的定义 > 子弹图使用长度/高度、位置和颜色对数据进行编码,以显示与目标和性能带相比的实际情况 我们先来看下子弹图大概张什么样子 ![](https://upload-images.jianshu.io/upload_images/5803165-70577c0d54bcbd64.png) 子弹图具有单一的主要度量(例如,当前年初至今的收入),将该度量与一个或多个其他度量进行比较以丰富其含义(例如,与目标相比),并将其显示在性能的定性范围的背景,例如差、满意和好。 定性范围显示为单一色调的不同强度,使色盲者可以辨别它们,并将仪表板上的颜色使用限制在最低限度 好了,差不多这就是子弹图的应用场景和绘制标准了,下面我们就开始制作吧 ## 构建图表 思路大致是,可以使用堆叠条形图来表示各种范围,并使用另一个较小的条形图来表示值,最后,用一条垂直线标记目标 可以看出,我们需要多个组件图层,使用 matplotlib 来实现会比较方便 ```Python import matplotlib.pyplot as plt import seaborn as sns from matplotlib.ticker import FuncFormatter %matplotlib inline ``` 这里我们还导入了 Seaborn,是因为 Seaborn 有一些非常有用的工具来管理调色板,利用这种功能比尝试以其他方式复制它更容易 我们需要生成调色板的主要原因是我们很可能希望为各种定性范围生成视觉上吸引人的配色方案,直接使用 seaborn 来完成会方便很多 在下面的例子中,我们可以使用 palplot 便利函数来显示 5 种绿色色调的调板 ```Python sns.palplot(sns.light_palette("green", 5)) ``` ![](https://upload-images.jianshu.io/upload_images/5803165-2a2fbcd158781c54.png) ```Python sns.palplot(sns.light_palette("purple",8, reverse=True)) ``` 以相反的顺序制作 8 种不同深浅的紫色 ![](https://upload-images.jianshu.io/upload_images/5803165-4871afc9f6db4c55.png) 我们现在知道了如何设置调色板,接下来让我们使用 Matplotlib 根据上面列出的原则创建一个简单的子弹图 首先,定义我们想要绘制的值 ```Python limits = [80, 100, 150] data_to_plot = ("Example 1", 105, 120) ``` 这个将创建 3 个范围:0-80、81-100、101-150 和一个值为 105 和目标线为 120 的“示例”线 接下来,构建一个蓝色调色板: ```Python palette = sns.color_palette("Blues_r", len(limits)) ``` 接下来是构建范围的堆积条形图: ```Python 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 ``` ![](https://upload-images.jianshu.io/upload_images/5803165-66b110678db36aa1.png) 然后我们可以添加一个较小的条形图来表示 105 的值: ```Python ax.barh([1], data_to_plot[1], color='black', height=5) ``` ![](https://upload-images.jianshu.io/upload_images/5803165-71ba33083cf23a4a.png) 已经初见雏形了 最后一步是使用 axvline 添加目标标记: ```Python ax.axvline(data_to_plot[2], color="gray", ymin=0.10, ymax=0.9) ``` ![](https://upload-images.jianshu.io/upload_images/5803165-d44dbf9cd323702f.png) 上面我就完成了子弹图的简单制作,但是我们所有的测试数值都是写死的,下面我们编写一个可以填写任意数值的代码 ## 最终代码 ```Python 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 = List of labels, measures and targets limits = list of range valules labels = list of descriptions of the limit ranges axis_label = string describing x axis title = string title of plot size = tuple for plot size palette = a seaborn palette formatter = matplotlib formatter object for x axis target_color = color string for the target line bar_color = color string for the small bar label_color = color string for the limit label text Returns: a matplotlib figure """ # Determine the max value for adjusting the bar height # Dividing by 10 seems to work pretty well h = limits[-1] / 10 # Use the green palette as a sensible default if palette is None: palette = sns.light_palette("green", len(limits), reverse=False) # Must be able to handle one or many data sets via multiple subplots if len(data) == 1: fig, ax = plt.subplots(figsize=size, sharex=True) else: fig, axarr = plt.subplots(len(data), figsize=size, sharex=True) # Add each bullet graph bar to a subplot for idx, item in enumerate(data): # Get the axis from the array of axes returned when the plot is created if len(data) > 1: ax = axarr[idx] # Formatting to get rid of extra marking clutter 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): # Draw the bar ax.barh([1], lim - prev_limit, left=prev_limit, height=h, color=palette[idx2]) prev_limit = lim rects = ax.patches # The last item in the list is the value we're measuring # Draw the value we're measuring ax.barh([1], item[1], height=(h / 3), color=bar_color) # Need the ymin and max in order to make sure the target marker # fits ymin, ymax = ax.get_ylim() ax.vlines( item[2], ymin * .9, ymax * .9, linewidth=1.5, color=target_color) # Now make some labels 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) ``` 代码虽然看起来有点长,但是其实都是上面步骤的叠加,都比较简单,就不再重复说明了 我们直接调用一下看看效果 ```Python 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="Performance Measure", label_color="black", bar_color="#252525", target_color='#f7f7f7', title="销售代表表现") ``` ![](https://upload-images.jianshu.io/upload_images/5803165-f27b61fffeccc216.png) 我们还可以进行一些优化,格式化 x 轴以便更一致地显示信息 在下面这个例子中,我们可以衡量一家假设公司的营销预算绩效 ```Python def money(x, pos): 'The two args are the value and tick position' return "${:,.0f}".format(x) money_fmt = FuncFormatter(money) data_to_plot3 = [("HR", 50000, 60000), ("Marketing", 75000, 65000), ("Sales", 125000, 80000), ("R&D", 195000, 115000)] 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="营销渠道预算绩效", formatter=money_fmt) ``` ![](https://upload-images.jianshu.io/upload_images/5803165-31957d3482dbf0a0.png) 看起来效果都不错哦,怎么样,一起跟着做起来吧! 本文由[mdnice](https://mdnice.com/?platform=6)多平台发布
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容