用matplotlib库绘制手涂风格桥表

2018-05-31  本文已影响0人  AIfred

0x00 缘起

最早接触到桥表是为了完成在某个Excel制图任务,当时用的Excel2010并不像如今的2016版本可以直接画出桥表来。可以说当时能用辅助图画出桥表,基本上也就明白了Excel画图的精髓,再难的图也可手起刀落。

学了Python后就挺想拿它来画图的,而matplotlib作为Python中最为重要的制图工具库之一,其风格真的跟MATLAB像极了。毕业那会儿,拿着MATLAB的参考书学画图的日子可谓历历在目,所以使用matplotlib库自然有种亲切感。

在网上可以找到一些使用Python画桥表,(比如该教程)但最大的问题在于,该图的风格不管数据的涨跌如何使用的图形颜色都是一样的,这样会让使用者不能一眼看穿数据变化的原因。在本教程中,参考Excel画桥表的方法来构建桥表。

另外,平时由于工作的关系,经常画的图都非常非常的严谨,画的多了就有点视觉疲劳,因而这次试试用matplotlib库的xkcd方法/函数来完成手绘风格的桥表。其全貌如下:

0x01 展示全貌

手绘风格.png

0x02 画图原理

假如,现在有如下的一组数据,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,
    })

如果忘记声明,得到图像中的中文将显示乱码,具体见下

中文缺失.png

0x05 读取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()
上一篇下一篇

猜你喜欢

热点阅读