目录
最近应朋友邀请,帮他写一个催收案件的平均分配小程序,当他给我说出需求时,我人麻了,直接告辞好吧,但最终经不起美食的诱惑,答应了下来了,哈哈哈哈。以下是他提出关于案件分配的需求整理:
1 | 案件数量为m,其中掺杂有m12-案件和m12+案件 |
2 | 待分配人员数量为n,与之对应的是各人员的最终分配量l |
3 | 可能存在有部分案件不进行分配,此程序必须兼容这个需求 |
4 | 各人员的持案量可能各不相同,会存在好几个梯次,比如有人分配案件1000户,有人需要分配800户,有人只需要分配599户等等 |
5 | m12-案件需要做均匀分配,不考虑梯次问题,直接均分就行 |
6 | 有很多共债案件,所以一个客户的案件必须由一个人去跟进,不能多人跟进一个客户 |
7 | 最重要的一点是最终的分案结果必须保证公平公正,各梯次人员的案件金额必须要差不多,每个人的案件均值必须也要差不多 |
ps:
m12-:是指案件的逾期账龄小于等于360天;
m12+:指案件的逾期账龄大于360天;
持案量:每个人的案件持有量
共债:一个客户存在有多笔订单时,这些案件就被称为共债案件
综上整理,可以大概得出一个概念,根据一个excel表的数据,去进行读取并清洗,最后返回一个已分配好案件的表格出来。
首先要规范待分配案件的表格模板:要有两个底表,底表的命名是“Sheet1”和“Sheet2”,Sheet1内存放待分配的案件,A列为案件编号,B列为逾期账龄,C列为委案金额,Sheet2内存放的是调解员们的持案量,A列为调解员姓名,B列为调解员的持案量
简单说一下我对于实现分案的基本思路,将案件编号根据案件的金额进行降序排列,然后依次填入调解员的姓名,会得出一个调解员的列表,这个调解员的列表也就与案件编号一一对应上了,自然就实现了案件的分配,难点在于我该把这个调解员的姓名与哪个案件编号一一对应呢,这就需要去进行判断了。整个代码写了好多的变量,我自己都快晕乎乎了,所以自己的思路必须要清楚,变量的取名也一定要清晰,这一点是非常重要的。
一、分案前的准备阶段
读取excel表格信息
对于分案采用的第三方库有pandas
首先需要读取给出的excel表格信息,直接采用pandas里的read_excel方法,并把读取出的人员和持案量分别用列表保存,案件信息同理取出。
import pandas as pd
# 获取调解员和持案量数据
primitive_mediator_datas = pd.read_excel('D:/案件分配/待分配案件表.xlsx', sheet_name='Sheet2').values
mediator = [] # 调解员列表
case_load = [] # 调解员持案量列表
for primitive_mediator_data in primitive_mediator_datas:
mediator.append(str(primitive_mediator_data[0]))
case_load.append(int(primitive_mediator_data[1]))
# 将调解员和持案量组装成字典
mediator_case_load_dict = dict(zip(mediator, case_load))
# 获取案件的相关数据:案件编号、案件的逾期天数、案件的金额
primitive_case_datas = pd.read_excel('D:/案件分配/待分配案件表.xlsx', sheet_name='Sheet1').values
case_number = [] # 案件编号列表
case_days_overdue = [] # 案件逾期天数列表
case_amount = [] # 案件金额列表
for primitive_case_data in primitive_case_datas:
case_number.append(str(primitive_case_data[0]))
case_days_overdue.append(int(primitive_case_data[1]))
case_amount.append(float(primitive_case_data[2]))
区分m12-案件和m12+案件
按照需求,在分配案件的过程中需要区分m12-案件和m12+案件,所以需要对整个案件做划分,然后依次对两个账龄段的案件各自分配,最后汇总即可得到最终的案件分配结果。
区分m12-和m12+案件以逾期天数的360天做界限,但存在很多共债案件,一个客户逾期有好几笔订单,这些订单里可能有逾期360天以上的案件,也有逾期天数小于360天的,这类客户的订单该怎么定义呢?后来去问他,最终给的结果简单粗暴,哈哈哈,这些共债案件里但凡有一笔订单的逾期天数大于360天,都给归纳为m12+案件,哦豁,甲方最大嘛,那我们就照着他的需求来码代码吧!
a_m12down_case_number = [] # m12-案件编号列表
a_m12down_case_amount = [] # m12-案件金额列表
m12up_case_number = [] # m12+案件编号列表
m12up_case_amount = [] # m12+案件金额列表
for index1, days in enumerate(case_days_overdue):
# 通过if函数判断m12-案件和m12+案件
if days <= 360:
a_m12down_case_number.append(case_number[index1])
a_m12down_case_amount.append(case_amount[index1])
else:
m12up_case_number.append(case_number[index1])
m12up_case_amount.append(case_amount[index1])
# 判断m12-案件里是否有m12+案件,有的话需要从m12-案件里剔除,并将其追加进m12+案件里,最终得到真正的m12+案件列表
m12down_case_number = []
m12dowm_case_amount = []
# 遍历m12-案件编号,并判断该案件编号是否在m12+案件里
for index2, case in enumerate(a_m12down_case_number):
if case in m12up_case_number:
m12up_case_number.append(case)
m12up_case_amount.append(a_m12down_case_amount[index2])
else:
m12down_case_number.append(case)
m12dowm_case_amount.append(a_m12down_case_amount[index2])
通过以上代码就可以区分开m12-案件和m12+案件,案件的编号与金额一一对应
数据透视:对数据去重,并按照金额进行降序排列
一开始有说,催收的案件是存在很多共债的,案件分配时必须要一个客户只能一个调解员去跟进,所以必不可少的一步是对案件编号去重,同时分案的依据是按照案件金额去分配的,既可得出最佳的方法是对数据进行数据透视,得出一个案件编号的总金额是多少,然后按照总金额去分配案件。所以需要用到的是pandas中的DataFrame数据分析和pivot_table数据透视最后在加一个sort_values排序。为了将代码简化,这一步直接用函数封装起来
# 创建函数,传参数据为案件编号和案件金额
def get_perspective_data(m12_case_number, m12_case_amount):
# 进行数据透视并按照案件金额降序排列
m12_perspective_data = pd.DataFrame({
'案件编号': m12_case_number,
'案件金额': m12_case_amount},
columns=['案件编号', '案件金额']).pivot_table(
index='案件编号', values='案件金额', aggfunc='sum').sort_values(
by='案件金额', ascending=False)
m12_pers_case_number = [] # 用列表去接收已经透视好的案件编号
for m12_case_number in m12_perspective_data.index:
m12_pers_case_number.append(m12_case_number)
m12_pers_case_amount = [] # 用列表去接收已经透视好的案件金额
for m12_case_amount in m12_perspective_data.values:
# 因为透视后的values返回值是列表,所以要加个索引[0]
m12_pers_case_amount.append(m12_case_amount[0])
return m12_pers_case_number, m12_pers_case_amount
用m12-和m12+的案件编号及金额列表进行传参,即可得出透视后的数据
二、m12-案件分配
按照他所提出的需求,m12-的案件分配是最简单的,案件的金额降序排列之后,把调解员列表循环填入,即可完成m12-案件分配。如果真的按照这一个流程去执行,必然出现一个很明显的错误,就是当m12-的案件数量要少于调解员的数量时,程序自然要报错了,所以我们要加上一个判断。依旧采用函数包装,最后返回一个字典和调解员列表,字典的key是案件编号,values是m12-、案件金额和调解员姓名组成的列表。为什么要返回一个已分配的调解员列表呢?那是为了接下来计算每个调解员m12+的案件还需要分配多少而统计的呢。
def m12down_case_allocation(m12down_case_numbers, m12down_case_amounts, mediators):
m12down_mediator = [] # 分配的调解员列表
# 首先进行判断m12-的案件数量是否小于调解员的数量
if len(m12down_case_numbers) <= len(mediators):
m12down_mediator = mediators[: len(m12down_case_numbers)]
else:
num = 1
while num <= len(m12down_case_numbers) // len(mediators):
for med1 in mediators:
m12down_mediator.append(med1)
num += 1
for med2 in range(len(m12down_case_numbers) % len(mediators)):
m12down_mediator.append(mediators[med2])
m12down_end_distribution_result = {} # 最后的分配结果用字典包装
for index3, m12d_case_num in enumerate(m12down_case_numbers):
try:
m12down_end_distribution_result[m12d_case_num].append(['m12-', m12down_case_amounts[index3], m12down_mediator[index3]])
except:
m12down_end_distribution_result[m12d_case_num] = []
m12down_end_distribution_result[m12d_case_num].append(['m12-', m12down_case_amounts[index3], m12down_mediator[index3]])
return m12down_end_distribution_result, m12down_mediator
三、m12+案件分配前的准备阶段
再次数据透视:得出m12+案件持有量及梯次
对于整个案件的分配,要区分m12-和m12+,因为我们是先进行的m12-的案件分配,(ps:谁叫这个阶段的案件分配最简单呢)所以就要计算出m12+案件每个人还需要分配多少,这里要用到减法,一开始每个人需要分配的案件量减掉m12-每个人分配的案件量。依旧继续使用pandas里的数据透视。整个代码的变量命名可能有些复杂,耐下心来仔细看会发现其实很简单的思路。有些人会说一个函数没必要返回这么多的数据,可能是我个人的习惯,哈哈哈,如果你不是很喜欢,也可以稍加修改哦。
对于m12+的案件分配,需要引入一个概念:echelon梯次,因为每个人的案件分配量是不均衡的,所以要把各梯次下的调解员们给他整理出来,方便我们后面的分案。比如有10个调解员,他们需要分配的案件量分别是【1000,1000,1000,800,800,700,700,500,300,300】,那他们的梯次就有5个,分别是【1000,800,700,500,300】。
创建用于数据透视m12+案件持有量的函数,接收传参有:1、m12-案件列表;2、m12-案件已分配好的调解员列表;3、mediator_case_load_dict(一开始从excel读取到的调解员和各调解员的案件持有量组成的字典)。返回值有:1、m12+案件每人的持案量组成的列表;2、m12+案件持案量的梯次组成的列表;3、各梯次及各梯次下的调解员们组成的字典,其中key是梯次,values是该梯次下的调解员们组成的列表。
def m12up_case_mediator_load(m12down_case_numbers, m12down_mediator_group, mediator_holding_dict):
# 数据透视计算出m12-每个人的案件持有量
m12down_mediator_case_hold_pers = pd.DataFrame({
'案件编号': m12down_case_numbers,
'调解员': m12down_mediator_group},
columns=['案件编号', '调解员']).pivot_table(
index='调解员', values='调解员', aggfunc='count')
m12down_pers_mediator = m12down_mediator_case_hold_pers.index
m12down_pers_case_holding = [] # m12-每个人的案件持有量列表
for case_holding in m12down_mediator_case_hold_pers.values:
m12down_pers_case_holding.append(int(case_holding[0]))
# 根据统计出的每位调解员的m12-案件持有量计算出m12+案件持有量
m12up_case_holding = []
for med3 in mediator_holding_dict:
for index4, m12down_pers_med in enumerate(m12down_pers_mediator):
if med3 == m12down_pers_med:
m12up_case_holding.append(mediator_holding_dict[med3] - m12down_pers_case_holding[index4])
# 将m12+案件的调解员与对应的持案量组装成字典
mediator_holding = dict(zip(mediator, m12up_case_holding))
# 利用列表推导式统计出m12+案件持案量梯次
m12up_holding_echelon = []
[m12up_holding_echelon.append(e) for e in m12up_case_holding if e not in m12up_holding_echelon]
# 需要将各梯次与各梯次下的调解员们组装成字典
echelon_mediator = {}
for m12up_case_hold in m12up_holding_echelon:
for mediator_hold in mediator_holding:
if mediator_holding[mediator_hold] == m12up_case_hold:
try:
echelon_mediator[m12up_case_hold].append(mediator_hold)
except:
echelon_mediator[m12up_case_hold] = []
echelon_mediator[m12up_case_hold].append(mediator_hold)
return m12up_case_holding, m12up_holding_echelon, echelon_mediator
四、m12+案件分配
思路分析
到目前为止,前面的都是相对简单,真正比较困难的来了,因为要考虑梯次问题,所以会有点复杂。实现对案件的公平分配,我们要考虑以下几点:1、不可以把大的金额全部分给同一个人;2、同理,不可以把小的金额全部分给同一个人;3、要保证每个人的案件金额均衡,大金额小金额都要有,而且尽可能的保证每个人的大金额数量小金额数量差不多。
例:案件金额从100元到20000元,以100元为一个阶段,最大限度保证每个调解员每个阶段的案件都有。
解决办法
1、如何解决梯次的案件分配问题呢?
对此,我们需要再引入一个新的概念:人员基数
什么是人员基数?比如说有一批案件,数量为800户,我们需要分配给5个人去跟进,每个人分的100户,那人员基数就是800 / 100 = 8,8就是该批案件分配所需要用到的人员基数,然后按照8个人的基准去进行案件分配,最后挑5个人的案件量出来,就可以将此批案件公平分配给5个人了。
同理将此概念带入m12+的案件分配中,因为m12+案件存在多个梯次,最佳的分配方法就是挨个梯次的分配,每当分配一个梯次时,我们就需要计算出该梯次的人员基数,并按照此基数去进行案件分配,该梯次下有几位调解员,我们就取几个数量就行,这样就可以完成该梯次的案件分配,以此类推,即可完成所有的案件分配。在这之中,别忘了每当分配完一个梯次的案件后就要计算出剩余的待分配案件哦。
2、如何实现大金额小金额的均匀分配问题?
每分配一个调解员时,我们从大金额里取一个,小金额里取一个,当此调解员的持案量为奇数时,我们在从中间取一个给他,自然就解决了这个问题,哈哈哈,是不是很简单,有时候问题说的好复杂,其实仔细一想,好像实现起来并不难呢,瞬间想给他一个大逼兜。
因为要区分大金额小金额,我们要把案件按照金额去划分,因为之前的代码已经实现了案件降序排列,所以我们直接拦腰斩,用列表的切片方法,当案件数量为奇数时,多出的一个案件放在小金额内。为了公平起见,大金额从头取,小金额从尾取,调解员的持案量为奇数时,多出的一个案件从小金额的头部取。为了方便分案,我们用列表的下标索引方式去进行分案。
创建函数,接收传参数据有:1、m12+案件编号组成的列表;2、m12+各梯次及各梯次下的调解员们组成的字典;3、m12+案件持案量梯次组成的列表;4、m12+案件每人持案量组成的列表;5、m12+案件金额组成的列表。返回值为最终的分案结果(字典):【m12+案件编号: 'm12+', m12+案件金额, 调解员】
def m12up_case_allocation(m12up_case_numbers, echelon_meds, m12up_holding_echelons, m12up_pers_case_holdings, m12up_case_amounts):
# 区分大金额和小金额
big_all_m12up_case = m12up_case_numbers[:len(m12up_case_numbers) // 2]
small_all_m12up_case = m12up_case_numbers[len(m12up_case_numbers) // 2:]
# 将m12+的案件按照大金额和小金额,用下标的方法将其定为出来,大金额下标为正整数,小金额下标为负数,两个下标列表汇总也就是m12+所有待分配案件的下标了
big_case_subscript = [x for x in range(len(big_all_m12up_case))]
small_case_subscript = [x for x in range(-len(small_all_m12up_case), 0)]
# 为了方便接下来的分案,我们将小金额的下标们做升序排列,起始也就是-1
small_case_subscript.sort(reverse=True)
distribution_result = {} # 最终的分案结果用字典接收,key是案件下标,values是调解员
# 遍历持案量的梯次(字典)
for echelon in echelon_meds:
# 进行判断,当遍历到最后一个梯次时,剩下的案件全部是最后一位调解员的了
if echelon == m12up_holding_echelons[-1]:
for sub in big_case_subscript:
distribution_result[sub] = echelon_meds[echelon]
for sub in small_case_subscript:
distribution_result[sub] = echelon_meds[echelon]
else:
big_small_allocation = echelon // 2 # 大金额和小金额应该需要分配的案件量
reference = len(big_case_subscript) // big_small_allocation # 该梯次的人员基数
echelon_quantity = m12up_pers_case_holdings.count(echelon) # 计算出该梯次下的调解员人数
# 用列表推导式汇总出该梯次下的第一位调解员应该分得的案件下标
subscript_allocation = [x for x in range(0, reference * big_small_allocation, reference)]
# 用while循环对该梯次下的每个调解员进行分案
n = 0
while n < echelon_quantity:
# 遍历第一位调解员的案件下标,依次加1,即可得出该梯次下的所有调解员的案件下标,自然就可以完成案件分配
for allocation in subscript_allocation:
distribution_result[big_case_subscript[allocation + n]] = echelon_meds[echelon][n]
distribution_result[small_case_subscript[allocation + n]] = echelon_meds[echelon][n]
n += 1
# 判断该梯次的持案量是否为奇数,如果是,则需要在已降序排列小金额案件下标列表的尾部取一个案件进行分配
if echelon % 2 != 0:
a = 0
while a < echelon_quantity:
distribution_result[small_case_subscript[-1 - a]] = echelon_meds[echelon][n]
a += 1
# 到此为止,该梯次的案件也就分完了,所以我们需要去除已经分配好的案件,便于下个梯次的案件分配
for result in distribution_result:
if result in big_case_subscript:
big_case_subscript.remove(result)
elif result in small_case_subscript:
small_case_subscript.remove(result)
m12up_end_distribution_result = {} # 汇总最后的分配结果
for dis_result in distribution_result:
try:
m12up_end_distribution_result[m12up_case_numbers[dis_result]].append(['m12+', m12up_case_amounts[dis_result], distribution_result[dis_result]])
except:
m12up_end_distribution_result[m12up_case_numbers[dis_result]] = []
m12up_end_distribution_result[m12up_case_numbers[dis_result]].append(['m12+', m12up_case_amounts[dis_result], distribution_result[dis_result]])
return m12up_end_distribution_result
终于终于把案件给他分完了,啊啊啊
五、将数据保存到excel
将分案的结果保存至excel我们采用的第三方库是openpyxl
最后的分案结果是字典形式:{案件编号: [账龄, 案件金额, 调解员]},依次填入excel表格,第一列是案件编号,第二列是账龄,第三列是案件金额,第四列是调解员。
import openpyxl
def save_excel(datas):
book = openpyxl.Workbook()
sheet = book.create_sheet('案件分配结果')
sheet.cell(1, 1, '案件编号')
sheet.cell(1, 2, '账龄')
sheet.cell(1, 3, '案件金额')
sheet.cell(1, 4, '调解员')
for ind, data in enumerate(datas):
sheet.cell(ind + 2, 1, data)
sheet.cell(ind + 2, 2, datas[data][0][0])
sheet.cell(ind + 2, 3, '%.2f' % datas[data][0][1])
sheet.cell(ind + 2, 4, datas[data][0][2])
book.save('D:/案件分配/案件分配表.xlsx')
六、运行
if __name__ == '__main__':
# 得到m12-和m12+数据透视后的案件编号和金额
m12down_pers_case_number = get_perspective_data(m12down_case_number, m12down_case_amount)[0]
m12down_pers_case_amount = get_perspective_data(m12down_case_number, m12down_case_amount)[1]
m12up_pers_case_number = get_perspective_data(m12up_case_number, m12up_case_amount)[0]
m12up_pers_case_amount = get_perspective_data(m12up_case_number, m12up_case_amount)[1]
# 得到m12-案件分配的调解员列表
m12down_mediator = m12down_case_allocation(m12down_pers_case_number, m12down_pers_case_amount, mediator)[1]
# 得到m12+案件每人的持案量组成的列表
m12up_pers_case_holding = m12up_case_mediator_load(m12down_pers_case_number, m12down_mediator, mediator_case_load_dict)[0]
# 得到m12+案件持案量的梯次组成的列表
m12up_holding_echelon = m12up_case_mediator_load(m12down_pers_case_number, m12down_mediator, mediator_case_load_dict)[1]
# 得到各梯次及各梯次下的调解员们组成的字典
echelon_mediator = m12up_case_mediator_load(m12down_pers_case_number, m12down_mediator, mediator_case_load_dict)[2]
# 最后得到m12-和m12+案件的分配结果
m12down_end_case_distribution_result = m12down_case_allocation(m12down_pers_case_number, m12down_pers_case_amount, mediator)[0]
m12up_end_case_distribution_result = m12up_case_allocation(
m12up_pers_case_number,
echelon_mediator,
m12up_holding_echelon,
m12up_pers_case_holding,
m12up_pers_case_amount)
# 将m12-和m12+案件的最终分配结果合并
all_case_distribution_result = {}
for m12down in m12down_end_case_distribution_result:
all_case_distribution_result[m12down] = m12down_end_case_distribution_result[m12down]
for m12up in m12up_end_case_distribution_result:
all_case_distribution_result[m12up] = m12up_end_case_distribution_result[m12up]
# 保存excel
save_excel(all_case_distribution_result)
至此,整个代码就结束了
七、py文件打包成exe可执行文件
因为最终是要给到朋友去办公使用,总不能要求他在电脑按照python环境吧,所以我们需要将程序打包成exe可执行文件,方便使用。打包所用到的第三方库是pyinstaller,首先win+r调用windows的命令提示符,输入cmd进入终端,为了提升下载速度,可以切换国内清华源网来加速。
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyinstaller
下载完第三方库后,复制该py文件的绝对路径,我这里就是:D:案件分配/案件分配.py,接着输入以下命令,即可完成对程序的打包
Pyinstaller -F D:案件分配/案件分配.py
在文件打包的过程中会显示打包后文件的存放路径,我们复制路径去找到我们打包后的exe可执行文件就可以了
如果打包后的文件内存太大,可以创建虚拟环境去打包,社区里就有很多大佬发布了相关的博客,在这里就不多加赘述了。
八、总结
1、整个的代码其实并不难,主要是要逻辑清晰,并且对变量的命名要简单易懂;
2、该程序只区分m12-案件和m12+案件的均匀分配,如果要再区分别的账龄,也可以自己再加,其中m12-的案件分配需求没有太复杂,重点是m12+的案件分配,对于此案件的分配,我们引入了两个概念:梯次和人员基数,经过多次实验,事实证明用这两个概念去分案是可行的,并无限趋近与公平公正,需要注意的是,每当分配完一个梯次的案件后别忘了计算剩余待分配案件的数量哦;
3、整个的分案程序是基于原始待分配案件表的样式来的,所以必须规范待分配案件表的模板,否则程序无法进行。