场景编程集锦 - 英雄排座次

时间:2023-02-10 16:05:36

场景描述

“英雄不问出处”这句话在银行绩效考评中得到了充分的诠释。每个季度的银行绩效点评会的核心议题就是对经营机构绩效考核指标排名进行宣讲,对每项指标的完成情况逐一进行点评,并提出提升KPI指标的方法和路径。这样的会议常常使支行行长倍感压力,体验到冰火两重天的巨大反差,可谓是几家欢乐几家愁。关于会议座次的安排,根据惯例以经营机构/支行的绩效排名来安排支行负责人的座位。绩效指标完成好的支行行长在会场前排就座,而绩效完成较差的支行行长则是坐在会场后排。如果支行连续3次在分行季度考评中排名垫底,支行行长就要面临末位淘汰而下课的风险。

接下来,我们将以某全国性股份制商业银行重庆分行为例,通过使用Python编程方式自动生成全行绩效点评报告,还原和再现银行季度绩效点评场景。 下面绩效点评报告其中的一页内容:

场景编程集锦 - 英雄排座次


编程思路

2.1 考核指标体系

某银行绩效考评实现千分制考核。考核指标共有10项包括:公司存款、零售存款、贷款规模、资产质量、小微业务、住房贷款、代销基金、中收业务、客户新增和网银开户数等等,每项指标满分100分,共计1000分,因此也称之为千分制考核。分行管理部门根据每家经营机构业务经营数据,按照具体的考核办法转换为考核指标得分,我们把所有经营机构的考核指标得分,存放在KPIs.xlsx电子表格文件中。该文件格式如下所示:

场景编程集锦 - 英雄排座次

2.2 建议措施知识库

关于提升考核指标的有关建议和措施的专家知识库,目前库中包含800+条知识。我们暂且存放在文本文件suggestions.txt中,这个文件是以utf-8的格式保存的。

2.3 几个关键技术

实现Excel文件读入,可以使用pd.read_excel()实现,生成dataframe结构数据保存支行考核指标的得分内容;

生成支行考核指标排位数据,可以使用dataframe.rank()实现这个功能;

自动生成每个支行考核指标排位的总体情况,针对排名垫底的3项指标,自动生成指标提升的建议和措施;

自动生成全行绩效点评报告,以word文档的形式存储。因为这个自动生成的报告可为有关人员对绩效报告进行修改和完善,以增强绩效报告的增强灵活性和实用性;

我们可以使用word文件另存为功能,把word格式的绩效点评报告转换为pdf格式文件,作为绩效点评会中使用的最终文档。


代码实现

整个程序由3个模块组成。make_chart.py主要用于绘制统计图形;make_report.py用于生成绩效点评报告数据,以及一些基础性功能;print_report.py生成word格式的绩效点评报告。具体代码如下所示:

3.1 模块 make_report.py

"""
make_report.py : 生成绩效报告数据
"""
from random import sample
import pandas as pd
from make_chart import *

# 定义模块公共接口
__all__ = ['make_report', 'load_suggestion', 'get_performance', 'get_kpi', 'get_rank', 'get_lower_kpi']

def load_suggestion(filename):
"""
功能: 加载建议措施的文件内容
返回: 字典形式存放的建议措施
"""
suggestion_dic = {}
with open(filename, encoding='utf8') as f:
lines = f.readlines()

for line in lines:
line = line.strip()
if line:
if line.startswith('[') and line.endswith(']'): # 判断是否指标名称
key = line[1:-1] # 去除方括号,提取指标名称
suggestion_dic[key] = []
else:
suggestion_dic[key] += [line] # 将建议措施添加字典中
return suggestion_dic

def get_kpi(filename):
"""
功能:获取考核指标得分
"""
# 读取KPI考核指标得分(1000分制)
dataset = pd.read_excel(filename, index_col=0)
# 生成总分列
dataset['总分'] = dataset.sum(axis=1)
return dataset

def get_rank(dataset):
"""
功能: 获取考核指标排名
"""
rank_dataset = dataset.copy()
for col_name in dataset.columns:
rank_dataset[col_name] = dataset[col_name].rank(method='dense', ascending=False)
return rank_dataset


def get_performance(branch, dataset, rank):
"""
功能:获取总体情况文本内容
dataset : 考核指标得分表
rank : 考核指标排名表
branch : 支行名称
"""
result = branch + '综合排名第' + str(int(rank['总分'][branch])) + '名。具体指标排名:'
for kpi in rank.columns[:-1]: # 去掉总分列
result += kpi+'('+str(int(rank[kpi][branch])) + ');' # 把指标名称与排名拼接
return result[:-1]+'。' # 字符串结尾的分号替换为句号


def get_suggestion(kpi, suggestions):
"""
功能:获取建议措施
kpi:考核指标
suggestions : 建议措施字典
"""
suggest_lst = suggestions[kpi]
measures = sample(suggest_lst, 3) # 随机选取3条措施
return ';'.join(measures)+'。'


def get_lower_kpi(branch, kpi_rank, n):
"""
功能:取排名最低n个指标名称
kpi_rank : 指标排名表
branch : 支行名称
"""
branch_rank = kpi_rank.loc[branch][:-1] # 某支行kpi排位数据且不含总分列
lower_kpi = branch_rank.sort_values(ascending=False, inplace=False)[:n] # 取排位最低的n项指标
kpi_lst = lower_kpi.index
return kpi_lst

def make_report():
"""
功能:按支行生成报告数据
"""
suggestions = load_suggestion('data/suggestions.txt') # ①
kpi_dataset = get_kpi('data/KPIs.xlsx') # ②
rank_dataset = get_rank(kpi_dataset) # ③

make_chart(kpi_dataset) # ④

performance_dic = {}
for branch in rank_dataset.index:
performance_dic[branch] = []
chart = 'charts/'+branch+'.jpg'
performance = get_performance(branch, kpi_dataset, rank_dataset) # ⑤
measures = []
lower_kpi = get_lower_kpi(branch, rank_dataset, 3) # ⑥

for i, kpi in enumerate(lower_kpi): # ⑦
measure = get_suggestion(kpi, suggestions)
measures += [str(i+1)+'. ' + kpi + ':' + measure]
performance_dic[branch] = (chart, performance, measures) # ⑧
return performance_dic

重要函数说明如下:

函数load_suggestion(filename):主要用途是从文件filename中加载建议措施知识库,并以字典形式来存放知识。

函数get_kpi(filename):从Excel文件filename中读取所有支行/经营机构的考核指标得分数据,新增总分列,以pandas的dataframe存放数据。

函数get_rank(dataset):计算所有支行考核指标的排名。这里的dataset是所有支行/经营机构的考核指标和总分的得分。

函数get_performance(branch, dataset, rank):获取支行/经营机构的“总体情况”的文本描述。

函数get_suggestion(kpi, suggestions):获取提升考核指标kpi的建议措施文本描述。

函数make_report():按支行生成绩效点评报告的内容,包括:雷电分析图的文件名、总体情况文本和建议措施文本。这些数据是以字典方式存放,字典的键是支行名称,字典的值是一个列表,其每一个列表元素是一个元组,元组内容包括雷达图文件名、总体情况的描述文本和建议措施的描述文本这3个元素。

重要语句说明如下:

语句①实现了从目录data中读入文本文件data/suggestions.txt的数据,用于创建字典suggestions,保存考核指标提升的建议和措施。

语句②从Excel文件data/KPIs.xlsx中读入支行考核指标得分数据,在此基础上增加了“总分”一列,生成dataframe对象kpi_dataset。

语句③获取考核指标排位数据,生成dataframe对象rank_dataset。

语句④自动生成支行雷达分析图,这些jpg格式的图片文件保存在目录charts之中,每个支行对应一个jpg文件,供后续使用。

语句⑤用于生成某支行考核指标排位总体情况的描述文本。

语句⑥获取某支行排位最低的3项考核指标的指标名称。

语句⑦这个循环语句是要生成3项考核指标(排位最低)提升的建议措施文本。

语句⑧构建支行绩效点评报告字典:performance_dic。它包含支行名称、雷达分析图存放路径和文件名、考核指标排位的总体情况、以及提升考核指标的方法和措施等内容。

3.2 模块 make_chart.py

# make_chart.py : 制作业务指标雷达图
import numpy as np
import matplotlib.pyplot as plt
import matplotlib

# 定义模块公共接口
__all__ = ['make_chart'] # ①

chart_fonts = {'黑体': 'SimHei',
'宋体': 'SimSun',
'楷体': 'KaiTi',
'仿宋': 'FangSong',
'微软雅黑': 'Microsoft YaHei',
'微软正黑体': 'Microsoft JhengHei',
}

def radar_chart(name, labels, values, values1):
""" 绘制雷达图 """
matplotlib.rcParams['font.family'] = [chart_fonts['微软雅黑']]
matplotlib.rcParams['font.sans-serif'] = [chart_fonts['微软雅黑']]
# 极坐标等分
angles = np.linspace(0, 2*np.pi, len(labels), endpoint=False)
# 雷达图数据封闭
score_a = np.concatenate((values, [values[0]]))
score_b = np.concatenate((values1, [values1[0]]))
angles = np.concatenate((angles, [angles[0]]))
labels = np.concatenate((labels, [labels[0]]))
# 设置画布尺寸
fig = plt.figure(figsize=(8, 6), dpi=100)
# 新建子图
ax = plt.subplot(111, polar=True)
# 绘制雷达图
ax.plot(angles, score_a, 'bo-', linewidth=2)
ax.plot(angles, score_b, 'go-', linewidth=1)
ax.fill(angles, score_b, facecolor='g', alpha=0.25)
# 设置标签显示
ax.set_thetagrids(angles*180/np.pi, labels)
# 0 度起始位置
ax.set_theta_zero_location('N')
# 坐标刻度范围
ax.set_rlim(0, 100)
plt.grid(True, c='grey', linestyle='--', )
# plt.grid(True)
# 雷达图的坐标值显示角度,相对于起始角度的偏移量
ax.set_rlabel_position(270)
ax.set_title("考核指标雷达图", pad=15)
# 把图例放在图的外侧
plt.legend([name, '全行平均'], bbox_to_anchor=(1.05, 1), loc=3, borderaxespad=0)
# 调节标签的位置
set_label_pos(ax, angles)
plt.savefig('charts/'+name.strip()+'.jpg')

def set_label_pos(ax, angles):
""" 调节标签显示位置 """
rstep = int(ax.get_theta_direction())
if rstep > 0:
rmin = 0
rmax = len(angles)
else:
rmin = len(angles) - 1
rmax = -1

for label, i in zip(ax.get_xticklabels(), range(rmin, rmax, rstep)):
angle_rad = angles[i] + ax.get_theta_offset()
if angle_rad <= np.pi / 2:
ha = 'left'
va = "bottom"
elif np.pi / 2 < angle_rad <= np.pi:
ha = 'right'
va = "bottom"
elif np.pi < angle_rad <= (3 * np.pi / 2):
ha = 'right'
va = "top"
else:
ha = 'left'
va = "top"
label.set_verticalalignment(va)
label.set_horizontalalignment(ha)

def make_chart(dataset):
""" 生成所有支行雷达图的图片文件 """

mean_series = dataset.loc[:, '公司存款':'网银开户'].mean() # ②

names = dataset.index # ③

label_lst = dataset.columns[:10] # ④

values1 = mean_series.values # ⑤

for name in names:
values = dataset.loc[name, '公司存款':'网银开户'].values # ⑥
radar_chart(name, label_lst, values, values1) # ⑦

重要函数说明如下:

函数make_chart(dataset):解析考核指标dataset内容,生成所有支行雷达图的图片文件。

函数radar_chart(name, labels, values, values1):绘制雷达分析图,并以图片文件jpg的方式保存分析图表。

重要语句说明如下:

语句①定义本模块对外开放的程序接口,只有在__all__列表中定义的函数,才能供其他模块调用访问。

语句②计算所有考核指标平均值,但是不包含总分列。

语句③获取所有支行/经营机构的名称列表。

语句④获取考核指标名称列表,这里是通过剔除列名列表中的总分列实现的。

语句⑤获取Series数据结构中的指标平均值。

语句⑥取出一个支行所有考核指标的得分。

语句⑦调用radar_chart()绘制雷达分析图,并生成一个支行的图片文件。 

3.3 模块 print_report.py

"""
print_report.py : 生成绩效点评报告
"""
import docx
from make_report import *

def main():
performance_dic = make_report() # ①
doc = docx.Document() # ②
doc.styles['Normal'].font.name = '宋体'
doc.styles['Normal']._element.rPr.rFonts.set(docx.oxml.ns.qn('w:eastAsia'), '宋体')
doc.styles['Normal'].font.size = docx.shared.Pt(10)

for branch in performance_dic:
p = doc.add_paragraph(branch) # ③
run = p.runs[0]
run.font.name = "微软雅黑"
run.element.rPr.rFonts.set(docx.oxml.ns.qn('w:eastAsia'), '微软雅黑')
run.font.size = docx.shared.Pt(18)
doc.add_picture(performance_dic[branch][0],
width=docx.shared.Inches(6), height=docx.shared.Inches(4.5)) # ④

# 生成总体情况文本
p = doc.add_paragraph('总体情况')
p.runs[0].bold = True
doc.add_paragraph(performance_dic[branch][1]) # ⑤

p = doc.add_paragraph('建议措施')
p.runs[0].bold = True
for measure in performance_dic[branch][2]: # ⑥
p = doc.add_paragraph(measure)

p.runs[0].add_break(docx.enum.text.WD_BREAK.PAGE) # ⑦
doc.save('reports/report.docx') # ⑧

if __name__ == '__main__':
main()

重要语句说明如下:

语句①调用函数make_report(),获取所有支行绩效指标数据。

语句②创建一个空白的word文档。

语句③在word文档中添加一个段落,实际上这里是支行名称。

语句④在word文档中添加图片,这里是一个支行的雷达分析图。

语句⑤在word文档中添加一个段落,这里是总体情况的描述文本。

语句⑥添加建议措施的描述文本段落。

语句⑦在word文档中添加一个换页符。

语句⑧把创建的word文档保存到目录reports中,取名report.docx。


执行效果

4.1 程序目录结构

D:\cases\梁山英雄排座次>dir
2022/12/07 09:44 <DIR> charts
2022/12/06 15:00 <DIR> data
2022/12/07 09:37 3,280 make_chart.py
2022/12/07 09:48 3,851 make_report.py
2022/12/07 09:42 1,430 print_report.py
2022/12/07 09:46 <DIR> reports
D:\cases\梁山英雄排座次>cd data
D:\cases\梁山英雄排座次\data>dir

2022/04/28 20:01 11,249 KPIs.xlsx
2022/12/06 19:28 5,901 suggestions.txt
D:\cases\梁山英雄排座次\data>

4.2 执行程序效果

D:\cases\梁山英雄排座次>python print_report.py

将在目录reports下生成word文档:report.docx

我们可以利用word的文件另存为功能,把report.docx转换为report.pdf文件。下面是我截取report.pdf文件其中一页内容展示如下:

场景编程集锦 - 英雄排座次

程序优化

到目前为止,此版本的程序功能也比较完善了。但是还是有一些值得优化的地方和空间。例如:

在总体情况部分,现在只要考核指标的排名展示,还可以考虑增加考核指标的得分显示的功能。

在输出的绩效点评报告中,添加一个文档的封面或者首页、增加页面的页码、还可以添加文档的页眉页脚等内容事项。当然这个内容你也可以在word中手动进行添加或许更加灵活,可以自行权衡处理。

你还可以考虑一步到位的方式生成报告内容,也就是说在Python程序中直接输出PDF格式的绩效点评报告。Python提供了多种方法处理PDF文件,作为一种可行的方案,你可以使用第三方库reportlab来处理和生成PDF格式文档。

    关于以上程序优化建议,有兴趣的读者可以亲自动手,或许你将面对新的挑战和取得新的收获。