본문 바로가기
인사 이야기

[인사-23006] 제약조건에 최적화된 교대제 자동 스케쥴링_Python

by 노동법의수호자 2023. 2. 12.
반응형

Ⅰ. 교대근무와 건강상의 위험성

을지대 의료경영학과 연구팀은 2014~2020년 질병관리청 국민건강영양조사에 참여한 만 19세 이상 근로자 1만3191명의 답변 자료를 활용해 근로자의 수면시간과 근무 형태가 우울감에 미치는 영향을 분석했다. 분석 결과 30대 이상 교대 근무자들이 주간 근무자들에 비하여 2~3배 이상 우울감을 느끼는 것으로 나타났다. 

 

한편, 고용노동부는 『교대작업과 건강』에서 교대근무가 근로자들에게 ⓐ생체리듬(Circadian Rhythm)의 부조화, ⓑ수면질의 저하, ⓒ위장장애, ⓓ심혈관계 질환, ⓔ임신, ⓕ기저질환의 악화 등과 같은 부정적인 영향을 미칠 수 있다고 보았다. 특히, 교대근무로 인한 안전사고가 증가하는 것을 지적하였다.

 

교대근무는 중장기적으로 근로자의 신체에 부담을 줄 수 있고, 안전사고 발생으로 기업 이미지 훼손 및 법적 이슈, 근로자 사기 저하 등 적지 않은 문제를 야기할 수 있으므로 교대 근무자를 활용하여야 하는 사업장은 근무 스케쥴링 시 이러한 부분까지 고려하여 근무표를 작성하는 것이 바람직하다.

 

그러나 현업 담당자는 항상 바쁘다. 근로자가 몇 백에서 몇 천명까지 되는 사업장에서 1주 52시간제와 같은 노동법상 이슈뿐만 아니라 위와 같은 건강상의 이슈까지 고려하며 근로자들의 근무표를 작성하라고 한다면 아마 퇴사하고 말 것이다. 따라서 이러한 문제를 해결하기 위해 아래와 같은 자동화된 근무 교대근무표를 작성하는 Tool를 만들어보고자 한다.

 

여기서는 코딩작업이 필요하므로 "Python"과  "pycharm"을 다운로드 받아야 한다. 자세한 내용은 아래 링크에 가면 설명해두었으니 사전 준비 작업에 참고하길 바란다.

Ⅱ. 최적의 선택은? : OR-Tools

예를 들어 아래 조건을 모두 만족하는 점심 메뉴를 시키고자 한다. 어떻게 해야 할까?

 

  • (조건1) 너무 단 거는 싫어! 당분은 평균 대비 30% 낮았으면 좋겠어
  • (조건2) 오늘은 좀 자극적인게 땡기네! 염도가 평균 대비 50%~80%였으면 좋겠어
  • (조건3) 면 요리라면 국물이 없었으면 좋겠어
  • (조건4) 기왕이면 음식점에 대한 평가가 500개 이상 달려 있으면서 평점이 4.5점 이상인 곳이였으면 좋겠어

이제 상기 조건을 만족하면서 칼로리가 가장 최소화할 수 있는 최적의 메뉴가 무엇인지 맞추면 된다. 보기만 해도 이 문제는 해결할 수 없을 것처럼 보인다. 하지만 유전 알고리즘(Genetic Algorithm), LP Solotion, CP Solotion 등 다양한 도구를 활용하면 해결할 수 있다. 우리는 최적의 교대제 근무표 작성 문제를 해결하여야 하므로 CP Solotion을 이용하여 문제를 해결해보고자 한다.

 

그렇다면 CP Solotion은 무엇일까? 검색을 하면 아래와 같이 나타난다.

제약 조건 최적화 또는 제약 조건 프로그래밍(CP)은 임의의 제약 조건 측면에서 문제를 모델링할 수 있는 수많은 후보 중에서 실행 가능한 솔루션을 식별하는 이름입니다. CP 문제는 많은 과학 및 공학 분야에서 발생합니다. '프로그래밍'이라는 단어는 약간 부적절한 명칭으로 '컴퓨터'가 '계산하는 사람'을 의미했던 것과 비슷합니다. 여기에서 '프로그래밍'이란 컴퓨터 언어로 프로그래밍하는 것이 아니라 계획의 구성을 의미합니다.

CP는 최적화 (최적의 솔루션 찾기)가 아닌 타당성 (실현 가능한 솔루션 찾기)을 기반으로 하며 객관적인 함수가 아닌 제약 조건 및 변수에 중점을 둡니다. 실제로 CP 문제에는 목표 함수가 없을 수도 있습니다. 문제에 대한 제약 조건을 추가하여 가능한 한 대규모 솔루션 집합을 보다 관리하기 쉬운 하위 집합으로 좁히는 것이 목표일 수 있습니다.

CP에 적합한 문제의 예로는 직원 예약이 있습니다. 이 문제는 공장과 같이 지속적으로 운영되는 회사가 직원을 위해 주간 일정을 만들어야 하는 경우에 발생합니다. 아주 간단한 예를 살펴보겠습니다. 회사는 매일 8시간 교대근무를 하고 4명의 직원 중 3명을 매일 다른 교대 근무에 배정하고 4일은 쉬는 날을 할당합니다.이처럼 작은 경우에도 가능한 가용한 일정 수는 매일 4! = 4 * 3 * 2 * 1 = 24개가 할당될 수 있으므로 가능한 주간 일정 수는 247개(40억 개 이상)입니다. 대개는 가능한 솔루션 수를 줄이는 다른 제약조건이 있습니다. 예를 들어 각 직원은 주당 최소 일수를 근무해야 합니다. CP 메서드는 새 제약 조건을 추가해도 어떤 솔루션을 계속 사용할 수 있는지 추적합니다. 따라서 대규모 실제 예약 문제를 해결하는 강력한 도구가 됩니다.

너무 어렵다. 간단히 설명하자면 그냥 무식하게 관련 문제를 둘러싼 모든 경우의 수를 다 돌려보고 거기서 우리가 설정한 제약조건과 목표에 최고로 근접하는 어떠한 경우를 우리에게 가져다 주는 것이라고 보면 된다. 상당히 흥미롭다!

 

우리는 상기 OR-Tools를 활용하여 여러 제약조건을 모두 만족하는 최적의 교대제 근무표를 도출하고자 한다. 물론, 아래에서 제시된 코드는 설명을 위해 제약조건을 간단하게 설정하였으며 단순한 가정을 두고 작성된 점을 감안하여야 한다.


반응형

Ⅲ. 자동 스케쥴링 근무표 Python code

1. 예시 데이터 생성

import pandas as pd
import numpy as np

# Create a list of 60 random names
names = ["Name_" + str(i) for i in range(60)]

# Create a list of 60 random ages from 20 to 60
ages = np.random.randint(20, 61, size=60)

# Create a list of 60 random values of 1 and 2 for the gender column
genders = np.random.randint(1, 3, size=60)

# Create a list of 60 random values from 1 to 100 for the proficiency column
proficiencies = np.random.randint(1, 101, size=60)

# Calculate the burden rate by subtracting the value obtained by multiplying the skill level by 0.3 after adding the gender and age
burden_rates = genders + ages + 0.1 * proficiencies

# Create a data frame with the lists
df = pd.DataFrame({'Name': names, 'Age': ages, 'Gender': genders, 'Proficiency': proficiencies, 'Burden Rate': burden_rates})

# Print the data frame
print(df)

위 샘플 데이터는 우리가 흔히 근로자들의 HR 데이터를 Excel 파일로 생성해서 관리하는 것을 감안하여 데이터 프레임 형식으로 구성해보았다. 60명의 연령과, 성별, 숙련도를 랜덤하게 만들고 나면, 이것들을 가지고 각 근로자별 작업 부담률을 계산한 뒤 열을 추가해준다. 작업 부담률이 높을 수록 향후 작업 시 건강이슈가 염려될 수 있는 근로자라고 가정한다.

 

2. 3조 2교대 근무표 작성

import calendar
from datetime import datetime

## 오늘이 속한 달의 총 일수

today = datetime.now()
num_days = calendar.monthrange(today.year, today.month)[1]

## 3개조 근무표 생성

# create Team_A schedule
team_A = [1, 1, 1.5, 1.5, 0, 0] ## 주주야야휴휴
sequence_index = 0
Team_A = []

for i in range(num_days):
    Team_A.append(team_A[sequence_index % len(team_A)])
    sequence_index += 1

# create Team_B schedule
team_B = [1.5, 1.5, 0, 0, 1, 1] ## 야야휴휴주주
sequence_index = 0
Team_B = []

for i in range(num_days):
    Team_B.append(team_B[sequence_index % len(team_B)])
    sequence_index += 1

# create Team_C schedule
team_C = [0, 0, 1, 1, 1.5, 1.5] ## 야야휴휴주주
sequence_index = 0
Team_C = []

for i in range(num_days):
    Team_C.append(team_C[sequence_index % len(team_C)])
    sequence_index += 1

AAA = [sum(Team_A), sum(Team_B), sum(Team_C)]

교대조는 3개 조로 구성되어 있으며 각각, 주간근무는 "1", 야간근무는 "1.5", 휴무는 "0"으로 표시하였고 1주 최소 4번에서 최대 5번 근무만 가능하므로 주간근무와 야간근무 모두 10.4시간 정도로 수행하고 1.6시간은 휴게 시간으로 구성한다면 1주 52시간을 초과하지 않는 3조 2교대 근무가 가능해진다. 

 

오늘 날짜가 속한 그 달의 총 일수에 ⓐ주간근무, 주간근무, 야간근무, 야간근무, 휴무, 휴무 근무패턴과 ⓑ야간근무, 야간근무, 휴무, 휴무, 주간근무, 주간근무 근무패턴, ⓒ휴무, 휴무, 주간근무, 주간근무, 야간근무, 야간근무 근무패턴를 각각 입력하여 ⓐ조, ⓑ조, ⓒ조로 교대 근무조를 구성한다.

 

ⓐ조의 그 달의 모든 근무량을 합산하여 Team_A로 지정, Team_B와 Team_C 역시 마찬가지로 작업을 해준 뒤 AAA에 리스트 형식으로 입력한다.

 

야간근무에 1.5를 입력한 이유는 주간근무 시 앞서 계산한 작업 부담 정도보다 야간근무 시 50% 이상 더 고될 것이라고 판단하였기 때문이다.
(사실 야간근무가 어느 정도 힘들지는 각 사업장에서 알아서 판단하면 될 것으로 보인다.)

 

3. 모델 구축과 제약조건 설정

from ortools.sat.python import cp_model

model = cp_model.CpModel()

## <변수 설정>
worker_vars = {}
for worker in range(df.shape[0]):
    for team in range(3):
        worker_vars[worker, team] = model.NewBoolVar(f'x[{worker},{team}]')

## <제약 조건>
for worker in range(df.shape[0]):
    model.AddAtMostOne(worker_vars[worker, team] for team in range(3))

for team in range(3):
     model.Add(sum(worker_vars[worker, team] for worker in range(df.shape[0])) == df.shape[0] // 3) 
     # constraint that the number of people in each team must be equally distributed.

우선 변수 설정단계는 60명의 근로자가 각 팀에 배정받는 모든 경우의 수를 입력하는 것이라고 보면 된다. 가령, A라는 근로자가 1조(ⓐ조)에 있을 수도 있고 2조(ⓑ조)에 있을 수도 있고 3조(ⓒ조)에 있을 수도 있다. 즉 A라는 근로자한테는 3가지 경우의 수가 존재한다. 60명의 근로자 모두에게 모든 조합가능한 경우의 수를 worker_vars[worker, team]에 지정한다.

 

그러고 나면 이제 해답을 찾기 위해 우리가 제시하는 제약 조건을 설정하여야 한다. 여기서는 아래와 같은 제약조건을 컴퓨터에게 제공하였다.

 

  1. 근로자는 여러 개의 조에 동시에 할당될 수 없다.
  2. 각 교대 근무조는 근로자들이 동일한 비중으로 존재하여야 한다.

상기 제약 조건을 입력한 CP_Model이 일차적으로 만들어진 상태이다. 이제 다음 단계로 목표함수를 정하여야 한다.

 


반응형

4. 목표 함수 설정과 최적해 도출

# Set the objective function
objective_terms = []
for worker in range(df.shape[0]):
     for team in range(3):
         objective_terms.append(worker_vars[worker,team] * AAA[team] * df.loc[worker,"Burden Rate"])
model.Minimize(sum(objective_terms))

# Solve the model
solver = cp_model.CpSolver()
status = solver.Solve(model)

# Collect the results

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f'Total burden_rates = {solver.ObjectiveValue()}\n')
    for worker in range(df.shape[0]):
        for team in range(3):
            if solver.BooleanValue(worker_vars[worker, team]):
                print(f'Worker {worker} assigned to team {team}.' +
                      f' burden_rates = {df["Burden Rate"][worker]*AAA[team]}')
else:
    print('No solution found.')

목표함수로 각 개별 근무자가 작업 부담률이 최소화될 수 있는 방향으로 목표를 설정하였다. 개별 근로자들의 부담률은 본인의 연령, 성별, 숙련 수준에 따라 다르고 야간 근무시 그러한 부담률이 50% 이상 가중되도록 앞서 설정하였기 때문에, 여기서는 [어떤 근로자가 어떤 근무조에 투입이 되었을 경우] × [그 근무조의 한달 작업량] × [해당 근로자의 업무 부담률]을 계산하여 이 값이 최소화되도록 최적 값을 찾아달라고 컴퓨터에게 요청한 것이다. 이렇게 코드를 입력하였을때 컴퓨터가 내놓는 결과값은 다음과 같다.

(우리가 앞서 랜덤하게 샘플 데이터를 구성하였기에 아래 결과값가 여러분들이 돌린 결과값을 다를 수 있다.)
(참고로 team 0은 ⓐ조, team 1은 ⓑ조, team 2는 ⓒ조를 의미한다.)

(아! 지금까지 읽어보신 분들은 맥이 빠질 수 있겠으나 이것은 Excel에서는 더 쉽게 구현할 수 있다..! 물론 데이터가 많아지거나 수식이 복잡해지면 불가능하겠지만... 허허)

Total burden_rates = 65056.3

Worker 0 assigned to team 0. burden_rates = 987.5
Worker 1 assigned to team 1. burden_rates = 1074.1000000000001
Worker 2 assigned to team 1. burden_rates = 1104.0
Worker 3 assigned to team 2. burden_rates = 1300.2
Worker 4 assigned to team 1. burden_rates = 1074.1000000000001
Worker 5 assigned to team 1. burden_rates = 1097.1000000000001
Worker 6 assigned to team 0. burden_rates = 927.5
Worker 7 assigned to team 0. burden_rates = 915.0
Worker 8 assigned to team 2. burden_rates = 1504.8000000000002
Worker 9 assigned to team 0. burden_rates = 765.0
Worker 10 assigned to team 2. burden_rates = 1476.1999999999998
Worker 11 assigned to team 2. burden_rates = 1443.1999999999998
Worker 12 assigned to team 1. burden_rates = 961.4
Worker 13 assigned to team 1. burden_rates = 1219.0
Worker 14 assigned to team 1. burden_rates = 1198.3
Worker 15 assigned to team 0. burden_rates = 960.0
Worker 16 assigned to team 1. burden_rates = 1069.5
Worker 17 assigned to team 2. burden_rates = 1412.4
Worker 18 assigned to team 1. burden_rates = 1055.7
Worker 19 assigned to team 2. burden_rates = 1174.8
Worker 20 assigned to team 2. burden_rates = 1449.8000000000002
Worker 21 assigned to team 0. burden_rates = 937.5
Worker 22 assigned to team 0. burden_rates = 785.0
Worker 23 assigned to team 0. burden_rates = 737.5
Worker 24 assigned to team 1. burden_rates = 920.0
Worker 25 assigned to team 2. burden_rates = 1214.4
Worker 26 assigned to team 2. burden_rates = 1460.8000000000002
Worker 27 assigned to team 0. burden_rates = 662.5
Worker 28 assigned to team 1. burden_rates = 915.4
Worker 29 assigned to team 2. burden_rates = 1196.8
Worker 30 assigned to team 2. burden_rates = 1295.8
Worker 31 assigned to team 2. burden_rates = 1278.2
Worker 32 assigned to team 2. burden_rates = 1214.4
Worker 33 assigned to team 0. burden_rates = 855.0000000000001 
**** <이하 생략> ****

사실 이 기술은 의외로 적용 가능성이 넓고 해외에서는 이미 각종 HR이슈를 해결하기 위하여 이러한 기술을 적용하거나 연구한 사례를 심심치 않게 찾아볼 수 있다. 그러나 안타깝게도 국내에서는 이러한 최적화 고민이 HR 영역에서 많이 연구되고 있지 않은 것으로 보인다. 아마 HR 데이터 분석의 역사가 국내에서는 그리 길지 않고 나아가 아직 이것의 가치와 효용성이 국내 기업에서는 평가절하되고 있기 때문일 것이다.

 

728x90
반응형

댓글