0x00 缘起
最早接触到桥表是为了完成在某个Excel制图任务,当时用的Excel2010并不像如今的2016版本可以直接画出桥表来。可以说当时能用辅助图画出桥表,基本上也就明白了Excel画图的精髓,再难的图也可手起刀落。
学了Python后就挺想拿它来画图的,而matplotlib作为Python中最为重要的制图工具库之一,其风格真的跟MATLAB像极了。毕业那会儿,拿着MATLAB的参考书学画图的日子可谓历历在目,所以使用matplotlib库自然有种亲切感。
在网上可以找到一些使用Python画桥表,(比如该教程)但最大的问题在于,该图的风格不管数据的涨跌如何使用的图形颜色都是一样的,这样会让使用者不能一眼看穿数据变化的原因。在本教程中,参考Excel画桥表的方法来构建桥表。
另外,平时由于工作的关系,经常画的图都非常非常的严谨,画的多了就有点视觉疲劳,因而这次试试用matplotlib库的xkcd方法/函数来完成手绘风格的桥表。其全貌如下:
0x01 展示全貌
手绘风格.png0x02 画图原理
假如,现在有如下的一组数据,main表示一个起始位置数据,factor1~8表示从起始到终点的8个影响因素。
举例而言,main表示17年年末的库存数量,factor1~8,我们表示未来8年的整体库存增减变动。
''' 数据示例savedata.csv
item,value
main,230000
factor1,-30000
factor2,-47000
factor3,90000
factor4,37200
factor5,-42700
factor6,25000
factor7,-30000
factor8,-45000
'''
桥表的本质其实就是堆积柱状图,其他的教程,可以用两组数即可展示整个图形。我们要画的桥表,展示增减变化要使用不同颜色,因此,增加的点要用一组柱表示,相应下跌要用另一组柱表示。而为了风格更具变化,我们使用五组数据来画图,分别是:
起始数据组(Begin)
结尾数据组(End)
支架数据组(Cum)
下跌数据组(Down)
上涨数据组(Up)
其中支架数据组,只对数进行站位,最终将被隐藏。
0x03 导入相关库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
0x04 设置风格
首先碰到的一个问题是,matplotlib对中文支持的不是非常好,而xkcd函数查看其源代码可知(注意return语句),它会覆盖任何在其之前的中文代码设置,因此一开始我们需要对字体进行申明。(关于如何让matplotlib使用中文字体,可以查看该教程)
# 风格设置
# 使用xkcd(手绘)风格
plt.xkcd()
# 为了使得表格能显示中文,需要更改font.family SimHei即是黑体
plt.rcParams['font.family'] = ['SimHei', 'xkcd', 'Humor Sans', 'Comic Sans MS']
# xkcd源码
def xkcd(scale=1, length=100, randomness=2):
if rcParams['text.usetex']:
raise RuntimeError(
"xkcd mode is not compatible with text.usetex = True")
from matplotlib import patheffects
return rc_context({
'font.family': ['xkcd', 'Humor Sans', 'Comic Sans MS'],
'font.size': 14.0,
'path.sketch': (scale, length, randomness),
'path.effects': [patheffects.withStroke(linewidth=4, foreground="w")],
'axes.linewidth': 1.5,
'lines.linewidth': 2.0,
'figure.facecolor': 'white',
'grid.linewidth': 0.0,
'axes.grid': False,
'axes.unicode_minus': False,
'axes.edgecolor': 'black',
'xtick.major.size': 8,
'xtick.major.width': 3,
'ytick.major.size': 8,
'ytick.major.width': 3,
})
如果忘记声明,得到图像中的中文将显示乱码,具体见下
中文缺失.png0x05 读取csv数据
读取csv数据,判断数据尾部是否含有汇总结果的数据,即期初加上各因素变化的所有值
# 从本地读取源数据
data = pd.read_csv('savedata.csv', index_col='item')
# 如果元数据中没有汇总结果,生成result行标签
if data.index[-1].lower() not in ['result', 'r', '结果']:
data.loc['result'] = data.sum()
0x06 对数据进行处理,生成辅助数据
# 生成辅助数据
begin, end, cum, up, down = [np.arange(data.size) * np.nan for i in range(5)]
# 使用pandas来处理数据
bridge_chart = pd.DataFrame({'Raw': data['value'],
'Begin': begin,
'End': end,
'Up': up,
'Down': down,
'Cum': cum}, index=data.index)
# 定义起始位置
bridge_chart.loc[bridge_chart.index[0], 'Begin'] = bridge_chart.loc[bridge_chart.index[0], 'Raw']
# 定义最终位置
bridge_chart.loc[bridge_chart.index[-1], 'End'] = bridge_chart.loc[bridge_chart.index[-1], 'Raw']
# 定义上涨因素
bridge_chart['Up'] = bridge_chart.loc[
(bridge_chart['Raw'] > 0) & ~(bridge_chart.index.isin([bridge_chart.index[0],bridge_chart.index[-1]])),
'Raw']
# 定义下跌因素
bridge_chart['Down'] = -bridge_chart.loc[
(bridge_chart['Raw'] < 0) & ~(bridge_chart.index.isin([bridge_chart.index[0], bridge_chart.index[-1]])),
'Raw']
# 定义辅助位置 结果为 Begin(current) + End(current) -Down(current) + Up(Next)
bridge_chart['Cum'] = bridge_chart['Begin'].fillna(0) + bridge_chart['End'].fillna(0) -\
bridge_chart['Down'].fillna(0) + bridge_chart['Up'].shift(1).fillna(0)
# 对辅助位置进行累加
bridge_chart['Cum'] = bridge_chart['Cum'].cumsum()
# 辅助位置数据掐头去尾
bridge_chart.loc[[bridge_chart.index[0], bridge_chart.index[-1]], 'Cum'] = np.nan
0x07 生成辅助线数组
# 定义辅助细线
line_index = [i for i in ind for n in range(3)]
line_value = [i for i in bridge_chart['Raw'].cumsum() for n in range(3)]
line = pd.DataFrame(line_value, index=line_index, columns=['Value'])
line = line.shift(1)
line[1::3] = np.nan
line.iloc[[0, -1]] = np.nan
0x08 设置相关数据并开始画图
值得注意的一点是,如果用main,factor1-8直接作为x轴坐标标签,会导致数据位置错乱,并最终使得桥表绘制失败。
因此ind的预先定义尤为重要,并在之后用plt.xticks(ind, bridge_chart.index)将坐标与标签进行映射。
另外,使用currency函数其目的是使得显示的数据仅保留千位,让整个Y坐标显得比较紧凑。如果实际中不需要可以用其他的函数进行替代。
# 相关设置
N = len(data)
# 预先定义x轴的刻度,整数
ind = np.arange(N)
# 格柱之间的间隔
width = 0.7
# 进行画图
p_Begin = plt.bar(ind, bridge_chart['Begin'], width, color='#999999')
p_End = plt.bar(ind, bridge_chart['End'], width, color='#726DD1')
p_Cum = plt.bar(ind, bridge_chart['Cum'], width, color='w')
p_Down = plt.bar(ind, bridge_chart['Down'], width, bottom=bridge_chart['Cum'], color='#00cd00')
p_Up = plt.bar(ind, bridge_chart['Up'], width, bottom=bridge_chart['Cum'], color='#cd0000')
# 画出辅助线
p_line = plt.plot(line_index, line['Value'], linewidth=0.51, color='k')
# 图表格式设置
plt.title(u'手绘风格桥表')
plt.xlabel('因素')
plt.ylabel('金额')
plt.xticks(ind, bridge_chart.index)
def currency(x, pos):
return "{:,.0f} K".format(x/1000.0)
# 对Y轴刻度进行重新设置
ax = plt.gca()
ax.yaxis.set_major_formatter(plt.FuncFormatter(currency))
plt.show()
0x09 强迫症的福音
虽然上面的手绘风格偶尔为之可能还是有点新奇的地方。对于强迫症患者估计是接受不了的,要使得图形恢复正常,只需要注释掉开头的代码即可:
# 风格设置
# 使用xkcd(手绘)风格
# plt.xkcd()
正常风格桥表.png
0x0a 全部代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 风格设置
# 使用xkcd(手绘)风格
plt.xkcd()
# 为了使得表格能显示中文,需要更改font.family
plt.rcParams['font.family'] = ['SimHei', 'xkcd', 'Humor Sans', 'Comic Sans MS']
# 从本地读取源数据
data = pd.read_csv('savedata.csv', index_col='item')
''' 数据示例
item,value
main,230000
factor1,-30000
factor2,-47000
factor3,90000
factor4,37200
factor5,-42700
factor6,25000
factor7,-30000
factor8,-45000
'''
# 如果元数据中没有汇总结果,生成result行标签
if data.index[-1].lower() not in ['result', 'r', '结果']:
data.loc['result'] = data.sum()
# 生成辅助数据
begin, end, cum, up, down = [np.arange(data.size) * np.nan for i in range(5)]
# 使用pandas来处理数据
bridge_chart = pd.DataFrame({'Raw': data['value'],
'Begin': begin,
'End': end,
'Up': up,
'Down': down,
'Cum': cum}, index=data.index)
# 定义起始位置
bridge_chart.loc[bridge_chart.index[0], 'Begin'] = bridge_chart.loc[bridge_chart.index[0], 'Raw']
# 定义最终位置
bridge_chart.loc[bridge_chart.index[-1], 'End'] = bridge_chart.loc[bridge_chart.index[-1], 'Raw']
# 定义上涨因素
bridge_chart['Up'] = bridge_chart.loc[
(bridge_chart['Raw'] > 0) & ~(bridge_chart.index.isin([bridge_chart.index[0],bridge_chart.index[-1]])),
'Raw']
# 定义下跌因素
bridge_chart['Down'] = -bridge_chart.loc[
(bridge_chart['Raw'] < 0) & ~(bridge_chart.index.isin([bridge_chart.index[0], bridge_chart.index[-1]])),
'Raw']
# 定义辅助位置 结果为 Begin(current) + End(current) -Down(current) + Up(Next)
bridge_chart['Cum'] = bridge_chart['Begin'].fillna(0) + bridge_chart['End'].fillna(0) -\
bridge_chart['Down'].fillna(0) + bridge_chart['Up'].shift(1).fillna(0)
# 对辅助位置进行累加
bridge_chart['Cum'] = bridge_chart['Cum'].cumsum()
# 辅助位置数据掐头去尾
bridge_chart.loc[[bridge_chart.index[0], bridge_chart.index[-1]], 'Cum'] = np.nan
# 定义辅助细线
line_index = [i for i in ind for n in range(3)]
line_value = [i for i in bridge_chart['Raw'].cumsum() for n in range(3)]
line = pd.DataFrame(line_value, index=line_index, columns=['Value'])
line = line.shift(1)
line[1::3] = np.nan
line.iloc[[0, -1]] = np.nan
# 相关设置
N = len(data)
# 预先定义x轴的刻度,整数
ind = np.arange(N)
# 格柱之间的间隔
width = 0.7
# 进行画图
p_Begin = plt.bar(ind, bridge_chart['Begin'], width, color='#999999')
p_End = plt.bar(ind, bridge_chart['End'], width, color='#726DD1')
p_Cum = plt.bar(ind, bridge_chart['Cum'], width, color='w')
p_Down = plt.bar(ind, bridge_chart['Down'], width, bottom=bridge_chart['Cum'], color='#00cd00')
p_Up = plt.bar(ind, bridge_chart['Up'], width, bottom=bridge_chart['Cum'], color='#cd0000')
# 画出辅助线
p_line = plt.plot(line_index, line['Value'], linewidth=0.51, color='k')
# 图表格式设置
plt.title(u'手绘风格桥表')
plt.xlabel('因素')
plt.ylabel('金额')
plt.xticks(ind, bridge_chart.index)
def currency(x, pos):
return "{:,.0f} K".format(x/1000.0)
# 对Y轴刻度进行重新设置
ax = plt.gca()
ax.yaxis.set_major_formatter(plt.FuncFormatter(currency))
plt.show()
网友评论