用 Python 绘制现金流量图

用 Python 绘制现金流量图

最近在学习工程经济学,经常要绘制现金流量图。希望能用 Python 更方便地绘制现金流量图。但是我在网上找了一圈,发现网上的教程画出来的现金流量图根课本里的不太一样。在网上看到的常见的教程里面告诉你的方法都是直接把现金流量图绘制成柱状图或者折线图的形式,但是学校非要我们把现金流量图画成课本上的箭头状

没办法,只好自己来写一下 Python 的实现了。也不知道这样搞是不是很有意义。

现金流量图是一种反映经济系统资金运动状态的图式,即把经济系统的现金流量绘入一时间坐标图中,表示出各现金流入、流出与相应时间的对应关系。运用现金流量图,就可全面、形象、直观地表达经济系统的资金运动状态。

现金流量图是描述现金流量作为时间函数的图形,它能表示资金在不同时间点流入与流出的情况。它是经济分析的有效工具,其重要有如力学计算中的结构力学图。

我们课本上的现金流量图是这个样子的:(图片来源:MBA 智库 · 百科 :现金流量图

image

显然,常见的绘图库,比如 MatPlotLib 或者 SeaBorn 里面根本没有提供这类图的现成的实现。

Python 实现

首先,我们先在 Jupyter 中导入相应的库(数据分析御三家)。

' 导入相应的库 '
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

然后来设置一下绘图选项:

plt.rcParams.update( {
        "font.sans-serif":'SimHei',  # 防止中文乱码
        "axes.unicode_minus":False,  # 中文负号显示
    } )

实现原理

我们画这个现金流量图,本质上就是要画箭头。

MatPlotLib 中提供了画箭头的方法 matplotlib.axes.Axes.arrow()。这个方法可用的参数非常多,常用到的参数如下:

参数 数据类型 默认值 含义
x, y float 绘制箭头的起点
dx, dy float 箭头的终点
fc, ec char 'black' 箭头和箭头杆的颜色
width float 0.001 箭头尾巴的宽度
length_includes_head bool False 计算长度的时候是否包含箭头部分
head_width float or None 3*width 箭头部分的总宽度
head_length float or None 1.5*head_width 箭头部分的长度
shape 'full' 箭头样式
overhang float 0 箭头角后掠的程度

调用这个方法的一个逻辑就是要把箭头杆和箭头尖尖当作两个部分分别加以处理。

具体代码

我们定义下面的函数来绘制现金流箭头。函数传入一个可迭代对象 cf 表示现金流,可以是 list 列表、numpy.array 数组或者 pandas.Series 序列。同时传入一个用于绘图的坐标系 ax

这里设置了一个参数 distance,用来表示绘制的图形的离散尺度。这是因为在实践中发现画图的时候这个箭头的大小、图的范畴很难控制,不是画得很大就是画得很小。于是干脆设置一个变量作为参数来手动控制,如果画的箭头太大就把 distance 改得小一点;如果箭头画得太小就把 distance 改得大一点。 一般情况下,当我们绘制一个特定的图 ax 的时候,会为 ax 设定一个统一的 distance

如果没有给定 distance,则 distance 取序列中元素最大值与序列长度的比值。

在每次循环中,使用ax.arrow函数绘制箭头。箭头起点位置为 (i, 0),(i 表示第 i 年,i 从 0 遍历到 n,n 为总年数);终点位置为 (i, cf[i])cf[i] 是第 i 年的现金流量),箭头颜色为传入的参数 arrow_color,箭头长度为 distance,箭头宽度为 0.1

  • 如果现金流元素大于 0,则使用 ax.text() 函数在箭头上方显示该现金流值。
  • 如果现金流元素小于 0,则使用 ax.text() 函数在箭头下方显示该现金流值
def plot_CashFlow_Arrow( 
        cf, # CashFlow,一段现金流
        distance = None, # 图表离散尺度
        arrow_color = 'black', # 箭头颜色
        ax = None # 绘图的坐标系
        ):
    cf = np.array(cf).flatten()
    if distance == None : # 如果图表元素离散尺度未给定
        distance = cf.max() / len(cf) # 就取现金流最大值除以总年份为尺度
    for i in range( 0, len(cf) ):
        ax.arrow(
            i, 0, 0, (cf[i]), 
            fc = arrow_color, ec = arrow_color, 
            length_includes_head = True,
            head_width = 0.1, 
            head_length = distance, 
            overhang = 0.5
        )
        if cf[i] > 0:
            ax.text(
                i + len(cf)*0.01, 
                cf[i] + distance, 
                str( round(cf[i], 2) )
            )
        elif cf[i] < 0:
            ax.text(
                i + len(cf)*0.01, 
                cf[i] - distance,
                str( round(cf[i], 2) )
            )            
    return ax

上述坐标系只是绘制某一个特定的现金流向上、向下的箭头。但与此同时,我们也可以看出这种形式的现金流量图具有以下的特点:

  1. x 轴及其坐标的标签是位于图中央的,没有四个边框
  2. 坐标值的间距均为 1,x 范围从 0 开始,到现金流量年份的最大值多一点点出头

我们编写下面的函数将坐标图的形式转化为我们想要的现金流量图的标准形式:

def set_ax_FlowChart_form( 
        ax, 
        length, 
        distance = None, 
        axis_color = "black" 
        ):
    if distance == None : # 如果图表元素离散尺度未给定
        distance = 10 * length # 就取 10 倍年份为 distance
    # 设置四个坐标轴不可见
    ax.spines['top'].set_visible(False) # 设置坐标轴,下同
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_visible(False)
    ax.spines['bottom'].set_visible(False)

    # 把 X 轴及其数据标签挪到图表当中
    ax.spines['bottom'].set_position(('data',0))
    plt.setp( ax.xaxis.get_majorticklabels(), ha="left" ) 
    # left 表示 X 坐标数据标签向左对齐
    # 否则箭头会挡住数字
    plt.arrow( # 中央 x 轴箭头
        -0.1, 0, length + 1.2, 0, 
        fc = axis_color, 
        ec = axis_color, 
        shape ="full", 
        head_width = distance*0.5, head_length=0.3, overhang=0.5)

    # 隐藏 y 坐标
    plt.yticks([])

    # 设置 X 轴的刻度为1
    x_major_locator = plt.MultipleLocator(1)    
    # 把x轴的刻度间隔设置为1,并存在变量里
    ax.xaxis.set_major_locator(x_major_locator) 
    # 把x轴的主刻度设置为1的倍数

    # 设置图表 X Y 范围,防止绘图区太大或太小
    # 0.1, 1.4 和 15 都是反复试出来的
    # 因为这样效果好,没什么原因
    ax.set_xlim(-0.1, length + 1.4)
    ax.set_ylim(-15 * distance, 15 * distance)
    return ax

使用示例 1:根据现金流量表绘制现金流量图

假设现在有如下现金流量表:

项目 – t年 0 1 2 3 4 5 6
投资 600
收入 350 350 450 450 450 450
经营成本 200 200 250 250 250 250

希望根据这一现金流量表绘制相应的先进流量图。

首先,我们利用字典读取现金流序列为 pandas.Dataframe,设置投资这一列的值为负值表示支出。数据也可以从 .csv 或者 .xlsx 之类的表格格式的文件中读取。

dict = { # 投资,收入和经营成本
    "invs":[ 600,    0,    0,    0,    0,    0 ],
    "incs":[   0,  350,  350,  450,  450,  450 ],
    "cost":[   0,  200,  200,  250,  250,  250 ]
}
df = pd.DataFrame.from_dict(dict, orient='index').T.astype(float)
df[['invs']] = - df[['invs']]
df
invs incs cost
0 -600.0 0.0 0.0
1 -0.0 350.0 200.0
2 -0.0 350.0 200.0
3 -0.0 450.0 250.0
4 -0.0 450.0 250.0
5 -0.0 450.0 250.0

接下来,我们根据表格中数值的大小估算设置一个 distance

# 图表元素离散程度
# 方便在数值变更的时候调整图表分布
distance = 50

遍历 DataFrame,将每一列作为 Series 传入写好的函数快速绘图:

fig, ax = plt.subplots()
ax = set_ax_FlowChart_form( ax, len(df), 
    distance = distance)
for columns in df:
    plot_CashFlow_Arrow( df[[columns]], ax = ax, 
        distance = distance,)
ax.set_ylabel("现金(万元)")
ax.set_title("现金流量图")
Text(0.5, 1.0, '现金流量图')

image

使用示例 2:绘制等额、等差、等比序列现金流量图

等额序列现金流量图

首先生成一个等额序列:

distance=2.5
# 用列表保存现金流量的值
A = []
A.append(-30)
for i in range(0, 7):
	A.append(10)
A
[-30, 10, 10, 10, 10, 10, 10, 10]

然后绘图:

fig, ax = plt.subplots()
ax = set_ax_FlowChart_form( ax, len(A), distance = distance )
plot_CashFlow_Arrow( cf = A, distance = distance, ax = ax )
# 画出水平线
x = np.arange(1,8)
y = 0*x + 10
plt.plot(x, y, c='r', ls='--') 
plt.title("等额序列现金流量图")
plt.ylabel("资金(万元)")
Text(0, 0.5, '资金(万元)')

image

等差序列现金流量图

生成一个等差序列:

distance=2.5
# 用列表保存现金流量的值
A = []
A.append(-30)
# 生成差差序列
for i in range(0, 7):
    A.append(10 + 2*i)
A
[-30, 10, 12, 14, 16, 18, 20, 22]

同理,绘图:

fig, ax = plt.subplots()
ax = set_ax_FlowChart_form( ax, len(A), distance = distance )
plot_CashFlow_Arrow( cf = A, distance = distance, ax = ax )
# 画出等差线
x = np.arange( 1, 8 )
y = 10 + 2*(x - 1) # 第一年没挣钱,要 -1
plt.plot(x, y, c='b', ls='--') 
plt.title("等差序列现金流量图")
plt.ylabel("资金(万元)")
Text(0, 0.5, '资金(万元)')

image

等比序列现金流量图

对于等比序列现金流量图的绘制,可以使用类似下面的循环,可以用循环生成等比序列,并添加到列表 A 的第一行,同时绘制一条等比曲线。

distance=2.5
# 用列表保存现金流量的值
A = []
A.append(-30)
# 生成等比序列
for i in range(0, 7):
	A.append(10 * (1.2) ** i)
A
[-30,
 10.0,
 12.0,
 14.399999999999999,
 17.279999999999998,
 20.735999999999997,
 24.883199999999995,
 29.85983999999999]

绘图:

fig, ax = plt.subplots()
ax = set_ax_FlowChart_form( ax, len(A), distance = distance )
plot_CashFlow_Arrow( cf = A, distance = distance, ax = ax )
# 画出等比曲线
x = np.arange(1,8)
y = 10*(1.2**(x-1))
plt.plot(x, y, c='g', ls='--') 
plt.title("等比序列现金流量图")
plt.ylabel("资金(万元)")
Text(0, 0.5, '资金(万元)')

image

3. 结合各种绘图方法绘制更加复杂的现金流量图

假设我们现在存在一个两年后更新设备的现金流方案,我们写出其现金流量,根据这一现金流量进行绘图:

dist = {
    "旧设备收入" : [       0,  80000,   80000,       0 ],
    "旧设备成本" : [ -100000, -30000,  -30000,       0 ],
    "旧设备残值" : [       0,      0,   30000,       0 ],
    "新设备成本" : [       0,      0, -300000,  -50000 ],
    "新设备收入" : [       0,      0,       0,  150000 ],
    "新设备残值" : [       0,      0,       0,  240000 ]
}
A = pd.DataFrame(dist).astype(float)
A
旧设备收入 旧设备成本 旧设备残值 新设备成本 新设备收入 新设备残值
0 0.0 -100000.0 0.0 0.0 0.0 0.0
1 80000.0 -30000.0 0.0 0.0 0.0 0.0
2 80000.0 -30000.0 30000.0 -300000.0 0.0 0.0
3 0.0 0.0 0.0 -50000.0 150000.0 240000.0

我们在这里设置一个颜色列表,colorlist,用文本形式保存每一列想要的颜色。添加一个循环变量 i,让变量 i 遍历列表的下标,将列表元素作为函数的参数 arrow_color 传入,给每一列着色。

因为我们这里调用的不是一个典型的 MatPlotLib 绘图方法,所以无法自动调整图例。这里我们需要自行地强行设置图例项内容

distance = 20000
colorlist = [ 'green', 'green', 'green', 'red', 'red', 'red' ]
fig, ax = plt.subplots()
ax = set_ax_FlowChart_form( ax, 4, distance = distance )
i = 0
for columns in A:
    plot_CashFlow_Arrow( 
        A[columns], 
        distance = distance, 
        arrow_color = colorlist[i], 
        ax = ax 
        )
    i = i + 1
plt.title("方案三:2年后更新设备的现金流量图")
plt.ylabel("资金(元)")
## 自行设置图例
# plt.plot 返回值为元组
# 需要在 line1 line2 后添加逗号
# 表示这是一个只有一个元素的元组
line1, = plt.plot(1,1, 'g', label='旧设备现金流')
line2, = plt.plot(2,2, 'r', label='新设备现金流')
plt.legend(handles=[line1, line2], loc='lower right')
<matplotlib.legend.Legend at 0x27c66c4aeb0>

image

posted @ 2023-09-27 12:36  多玩我的世界盒子  阅读(175)  评论(0编辑  收藏  举报