突然看到一个问题,自动排班,想了一下,这主要是个逻辑问题,自行手动实现:

基于以下假设:

假设1.工厂,机器24小时不停转,人工作12小时换班,人不可连续上两个班;

一般一次排一周的表,排一个月感觉不太实际,Mon_1即表示周一白班,_2即周一夜班;

假设2.比如有10台机器,至少需要20个人,一般要多一点余量,我们这里以23人计;(后续思考,Every dog has its day ,其实一周最多应该六天,要么周日统一休息,稍微改下代码即可,如果抽空休息的话,得多加点判断,这里就不搞了)

假设3.本意是避免一个人周一上了夜班,以后一直上夜班,实际上在两班倒的情况下,10台机器23个人,只多3个,并不能很好地解决这个问题,最关键的是增加人手,代码实现上,优先排本周累计上班次数少的;

包含以下内容:

(1)实现伪造工号、姓名

(2)实现排班

(3)测试代码

(4)不分享完全实用代码,分享过程与思考

一、生成随机不重复姓名

import pandas as pd
from faker import Faker
import time,datetime
import random
from random import sample
import numpy as np
from collections import Counter
pd.set_option('display.max_columns', 20)

Faker可以帮助我们迅速生成,不过数量稍多,可能有重复的,加手机号是为了看起来更加真实而已,并不用手机号。

# 生成一个随机姓名
fake = Faker("zh_CN")
print('随便生成一个姓名')
print(fake.name())

# 生成一个随机手机号
print('随便生成一个随机手机号')
print(fake.phone_number())
随便生成一个姓名
范龙
随便生成一个随机手机号
13603006196
# 生成多个随机姓名
number = [i for i in range(1,301)]
names =[fake.name() for i in range(1,301)]
temp = dict(zip(number,names))
# print(dict(zip(number,names)))

# 检查名字是否有重复
check_dict={} # 临时字典
for key,value in temp.items():
    if value not in check_dict:
        check_dict[value]=1
    else:
        check_dict[value]+=1

for a,b in check_dict.items():
    if b >1:
        print('重复的名字有---------')
        print(a,b)
重复的名字有---------
王静 2
重复的名字有---------
吴建军 2
重复的名字有---------
李雷 2
重复的名字有---------
李利 2
重复的名字有---------
李鑫 2
重复的名字有---------
王桂英 2

果然人一多,就有重复的,现实中,人名重复很常见,实际上排班应该用工号,工号肯定是唯一的,但这里我们演示都用姓名,因为一堆工号会让看官看的懵逼;

1.1正确姿势

应该加点判断进去:

def make_a_name():
    """生成一个姓名"""
    fake = Faker("zh_CN") # 生成一个随机姓名
    return fake.name()

def make_a_phone():
    """生成一个手机号"""
    fake = Faker("zh_CN")
    return fake.phone_number()

def make_namelist(length=200):
    """生成指定个数姓名"""
    name_list=[]
    while len(name_list) < length: # 需要的人数
        if make_a_name() not in name_list: # 去重
            name_list.append(make_a_name())
    return name_list

def make_phonelist(length=200):
    """生成指定个数的手机号"""
    phone_list=[]
    while len(phone_list) < length: # 需要的人数
        if make_a_phone() not in phone_list: # 去重
            phone_list.append(make_a_phone())
    return phone_list

def make_id(length=200):
    """生成指定个数的不重复员工号"""
    id_list = []
    while len(id_list)<length:
        one_id = random.randint(100,1000) # 从100到1000随机生成一个
        if one_id not in id_list:
            id_list.append(one_id)
    return id_list

def combine_them():
    """合并数据并写入到excel中"""
    ii = make_id(length=200)
    nn = make_namelist(length=200)
    pp = make_phonelist(length=200)
    df = pd.DataFrame(data={'工号':ii,'姓名':nn,'手机号':pp})
#     print(df)
    df.to_excel('d:/fake_names.xlsx',sheet_name='Shit1',index=False)

1.2定义一个类:

我们直接修改下代码,

## 定义一个类
class MakeFakes():
    """
    其实只需要工号、姓名即可,但是为了看起来很真,
    强行加了手机号
    随机生成一个姓名、手机号定义为静态方法,内部调用
    """
    def __init__(self,length ,path):
        self.length = length # 需要的人数
        self.path = path # 输出路径
        
    @staticmethod
    def make_a_name():
        """生成一个姓名"""
        fake = Faker("zh_CN") # 生成一个随机姓名
        return fake.name()
    @staticmethod
    def make_a_phone():
        """生成一个手机号"""
        fake = Faker("zh_CN")
        return fake.phone_number()

    def make_namelist(self):
        """生成指定长度姓名"""
        name_list=[]
        while len(name_list) < self.length: # 需要的人数
            if MakeFakes.make_a_name() not in name_list: # 去重
                name_list.append(MakeFakes.make_a_name())
        return name_list

    def make_phonelist(self):
        """生成指定长度手机号"""
        phone_list=[]
        while len(phone_list) < self.length: # 需要的人数
            if MakeFakes.make_a_phone() not in phone_list: # 去重
                phone_list.append(MakeFakes.make_a_phone())
        return phone_list

    def make_id(self):
        """生成不重复员工号"""
        id_list = []
        while len(id_list)<self.length:
            one_id = random.randint(100,1000) # 从100到1000随机生成一个
            if one_id not in id_list:
                id_list.append(one_id)
        return id_list

    def combine_them(self):
        """合并数据并写入到excel中"""
        ii = self.make_id()
        nn = self.make_namelist()
        pp = self.make_phonelist()
        df = pd.DataFrame(data={'工号':ii,'姓名':nn,'手机号':pp})
        df.to_excel(self.path,sheet_name='Shit1',index=False)
        return df

这生成随机姓名,还是要稍微运行一下,并不是瞬间就行;

使用一下,我帮各位看了,没有有重复的;

ceshi = MakeFakes(230,'d:/names_230.xlsx')
ceshi.combine_them()

二、排班

先搞简单点,比如10台机器,23个人,多的3个是留着应对突发状况的;

ceshi2 = MakeFakes(23,'d:/names_23.xlsx')
ceshi2.combine_them()

 

后续想了一下,10*2*7=140,140/6=23.3,实际上应该要24个,这样才能保证每个人最多上六天班,不过这里只是演示;

我们在D盘生成了一个23人的先看看效果,230人的,用于验证;

2.1逻辑演示

# 以姓名演示
data = pd.read_excel('d:/names_230.xlsx')
id_list=data['姓名']
# print(id_list)
# 100台机器
machines = list(range(1,101))
cols = ['Mon_1','Mon_2','Tue_1','Tue_2','Wed_1','Wed_2','Thu_1','Thu_2','Fri_1','Fri_2','Sat_1','Sat_2','Sun_1','Sun_2']
work_df = pd.DataFrame(index=list(range(1,101)),columns=cols)

这里展示下逻辑:

# 可以上班的名单,必须转先为列表
available_names = id_list.values.tolist()
# 第一天白,随便抽100个人,变量无贬义
dogs = sample(available_names,100)
work_df.loc[:,'Mon_1'] = dogs
# 第一天夜班,当天上白班的不能再上了
for value in dogs:
    available_names.remove(value)
dogs =sample(available_names,100)
work_df.loc[:,'Mon_2'] = dogs
# 第二天白,重置可上班的
# 要去除上一轮夜班
available_names = id_list.values.tolist()
for value in dogs:
    available_names.remove(value)
dogs =sample(available_names,100)
work_df.loc[:,'Tue_1'] = dogs

可以看到,第一天白班抽100个人,第一夜,需先重置可上班人员名单,再删除刚上完班的,循环下去(该方法有漏洞,往下看,本文主打一个草履虫可懂

2.2初次尝试

# 可以看到,后面的基本是重复上一轮的
# 写出方法
def renew(dog,available):
    for value in dog:
        available.remove(value)
    return available

def make_work_schedule(machines,excel_path):
    cols = ['Mon_1','Mon_2','Tue_1','Tue_2','Wed_1','Wed_2','Thu_1','Thu_2','Fri_1','Fri_2','Sat_1','Sat_2','Sun_1','Sun_2']
    work_df = pd.DataFrame(index=machines,columns=cols)
    # 获取工号
    data = pd.read_excel(excel_path)
    id_list=data['姓名']
    # 所有工号
    available_names = id_list.values.tolist()
    for c in cols:
        if c !='Mon_1':
            available_names = id_list.values.tolist()
            # 更新下次可上班的
            available_names = renew(dogs,available_names)
        # 第一天白
        dogs = sample(available_names,10)
        work_df.loc[:,c] = dogs
        
        
    return work_df

此处得思考了,会不会分配不均;比如计件的工厂,上班少了工资会低,固定工资的工厂,上班少的高兴,被安排上七天班的,肯定要很不公平,那我们来看看:

# 查看情况
def get_work_times(df):
    temp_list =[]
    name_list,time_list=[],[]
    "好像只能行、列循环判断,用pandas的负面影响"
    for col in range(0,df.shape[1]):
        for row in range(0,df.shape[0]):
            value = df.iloc[row,col]
            if not pd.isna(value): 
                temp_list.append(value)
    cc = Counter(temp_list)
    for k,v in cc.items():
        name_list.append(k)
        time_list.append(v)
    return name_list,time_list

a,b = get_work_times(schedule1)
dict(zip(a,b))

可以看到,有的只有4天,有的是7天,所以有问题,要改一下:

2.3 改进

自己看代码,注释写了很多:

def renew(dog,available):
    """该方法不变"""
    for value in dog:
        available.remove(value)
    return available

def make_work_schedule2(machines,excel_path):
    cols = ['Mon_1','Mon_2','Tue_1','Tue_2','Wed_1','Wed_2','Thu_1','Thu_2','Fri_1','Fri_2','Sat_1','Sat_2','Sun_1','Sun_2']
    work_df = pd.DataFrame(index=machines,columns=cols)
    # 获取工号
    data = pd.read_excel(excel_path)
    id_list=data['姓名']
    # 所有工号
    available_names = id_list.values.tolist() # 初始化,用于第一次判定
    iter_count = 0 # 记录执行了多少次,后面要用
    n = 0 # 记录多少个被抽到较少的,提前安排上班
    for c in cols:
        iter_count+=1 
        if iter_count>1: # 第二轮循环开始要先重置该list,不然减没了
            all_names = id_list.values.tolist() # 所有员工名单
            available_names = renew(dogs,all_names) # 返回去除刚上完班的
        if iter_count==3: 
            """
            在第三轮,就把每抽到的立马先安排上班
            指定第三轮,是怕出现极特殊情况,很多轮就是抽不中他
            eager_names 即优先安排上班的员工
            """
            names,times = get_work_times(work_df)
            # 差集,挑出两轮没被抽到的
            eager_names  = list(set(all_names)-set(names)) 
            n = len(eager_names)           
        if iter_count>3:
            """
            从第四轮开始,时刻注意将被抽到较少的提前排班,
            经过三次排班,所有人至少被抽到一次
            """
            names,times = get_work_times(work_df)
            # 找到被抽到次数最少的,可能会>10
            less_names = [names for names, x in zip(names, times) if x == min(times)]
            # 求交集,刚下班的不可能让其上班
            eager_names = list(set(less_names)&(set(available_names)))  
            n = len(eager_names)

        if 10>n>0:
            # 先安排上班少的
            dogs = sample(eager_names,n) 
            # 再安排可上班的,挑满10个
            temp = sample(list(set(available_names)-(set(dogs))),10-n)
            dogs.extend(temp) 
            work_df.loc[:,c] = dogs
        if n>=10:
            "会出现n>10的情况,那就挑10个咯"
            dogs = sample(eager_names,10)
            work_df.loc[:,c] = dogs
        if n ==0:
            # 基本是第一、二次才会执行
            dogs = sample(available_names,10)
            work_df.loc[:,c] = dogs
        
    return work_df

直接测试:

# 生成数据
machines = list(range(1,11))
excel_path='d:/names_23.xlsx'
schedule2 = make_work_schedule2(machines,excel_path)
schedule2

# 查看是否不均
aa,bb = get_work_times(schedule2)
dict(zip(aa,bb))

循环100次,每次1个周的班,即排了100周的班,看不均程度:

min(result_dict.values()),max(result_dict.values())

# (603, 616)

import matplotlib.pyplot as plt
import seaborn as sns
sns.barplot(x=list(result_dict.keys()),y=list(result_dict.values()))
plt.xticks(range(0,len(result_dict)),labels=result_dict.keys(),color='red',rotation=90)

次数多了,还是会有区别,因为我们本意是只排一周的班,不过100周起码是两年多的时间,差10来次,其实可以接受;

不过此处有个问题,并未考虑 下一周的班和上一周班的关系,比如我上一周白班上得多,下一周是不是夜班要多一点?

2.4封装

class Gaoshiqing():
    def __init__(self,human_numbers,machine_numbers,judge_col,excel_path):
        self.human_numbers = human_numbers # 需要的人数
        self.machine_numbers = machine_numbers # 机器数
        self.excel_path = excel_path # 文件路径
        self.judge_col = judge_col # 用工号还是姓名判定
        
    def get_work_times(self,df):
#         df = pd.read_excel(self.excel_path)
        temp_list =[]
        name_list,time_list=[],[]
        for col in range(0,df.shape[1]):
            for row in range(0,df.shape[0]):
                value = df.iloc[row,col]
                if not pd.isna(value): 
                    temp_list.append(value)
        cc = Counter(temp_list)
        for k,v in cc.items():
            name_list.append(k)
            time_list.append(v)
        return name_list,time_list

    @staticmethod
    def renew(dog,available):
        for value in dog:
            available.remove(value)
        return available

    def make_work_schedule(self):
        machines = range(1,self.machine_numbers+1)
        cols = ['Mon_1','Mon_2','Tue_1','Tue_2','Wed_1','Wed_2','Thu_1','Thu_2','Fri_1','Fri_2','Sat_1','Sat_2','Sun_1','Sun_2']
        work_df = pd.DataFrame(index=machines,columns=cols)
        # 可用工号、姓名列判定
        data = pd.read_excel(self.excel_path)
        id_list=data[self.judge_col]
        # 所有姓名
        available_names = id_list.values.tolist() # 初始化,用于第一次判定
        iter_count = 0 # 记录执行了多少次,后面要用
        n = 0 # 记录多少个被抽到较少的,提前安排上班
        for c in cols:
            iter_count+=1 
            if iter_count>1: # 第二轮循环开始要先重置该list,不然减没了
                all_names = id_list.values.tolist() # 所有员工名单
                available_names = Gaoshiqing.renew(dogs,all_names) # 返回去除刚上完班的
            if iter_count==3: 
                """
                在第三轮,就把每抽到的立马先安排上班
                指定第三轮,是怕出现极特殊情况,很多轮就是抽不中他
                eager_names 即优先安排上班的员工
                """
                names,times = self.get_work_times(work_df)
                # 差集,挑出两轮没被抽到的
                eager_names  = list(set(all_names)-set(names)) 
                n = len(eager_names)           
            if iter_count>3:
                """
                从第四轮开始,时刻注意将被抽到较少的提前排班,
                经过三次排班,所有人至少被抽到一次
                """
                names,times = self.get_work_times(work_df)
                # 找到被抽到次数最少的,可能会>10
                less_names = [names for names, x in zip(names, times) if x == min(times)]
                # 求交集,刚下班的不可能让其上班
                eager_names = list(set(less_names)&(set(available_names)))  
                n = len(eager_names)

            if self.machine_numbers>n>0:
                # 先安排上班少的
                dogs = sample(eager_names,n) 
                # 再安排可上班的,挑满
                temp = sample(list(set(available_names)-(set(dogs))),self.machine_numbers-n)
                dogs.extend(temp) 
                work_df.loc[:,c] = dogs
            if n>=self.machine_numbers:
                dogs = sample(eager_names,self.machine_numbers)
                work_df.loc[:,c] = dogs
            if n ==0:
                # 基本是第一、二次才会执行
                dogs = sample(available_names,self.machine_numbers)
                work_df.loc[:,c] = dogs      
        return work_df

我们用230人,100台机器来测试下:

 循环一百次后,人多了,差异会变大:

改进1:看来还是要考虑已经上过多少次班,比如将本月、本季度上过多少次班的,统计一下,先将数量较少的优先排,即方法中的eager_names,要加一点条件;

改进2:白班、夜班的不同讲道理每个人白班、夜班要稍微分一分,不能一直上夜班或者上白班,由于我们富裕的人并不多,所以一旦某A周一上白班,某A这周上白班的次数必然较多,可以再排周一白班的时候,加入判断条件;

思考1:正常来讲,每周至少得放一天假吧,所以我们的假设不一定完全成立;

思考2:如果要插空放假,把每个人在每周挑一天放假,可用正则表达式选定周几,在available_names里面调整,由于脑子抽风,写的是血汗工厂的排班情况,就不能再往下了,指不定哪个人就抄袭用上了,因为上面的代码,要实用还需要改。

思考3:比如一台机器要1个人操作,一台机器不停一天要两个班次,一个人不能连续上班,每个人每周至少要放一天假(当天不可上白、夜班),则比如m=10台机器,一周需要10*2*7=140班次,n至少为24人,基于人要请假、休息、生病等因素,实际人数肯定得比这多;

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐