Оптимизация затрат на персонал
Я большой энтузиаст не только байесовской статистики, но и математической оптимизации [1]. К сожалению, публикации по применению этих методов в HR-аналитике практически не встречаются. Я стараюсь исправлять эту ситуацию. Статьи про байесовскую статистику в HR-аналитике я уже публиковал, а вот тема математической оптимизации сегодня будет обсуждаться впервые.
Простыми словами математическая оптимизация – это способ поиска наилучшего решения какой-либо задачи. Решение задач этим методом можно свести к выполнению следующего списка шагов:
- Формулирование проблемы.
- Определение целевой функции.
- Установление ограничений.
- Нахождение оптимального решения.
Пройдем все эти шаги на примере HR проблемы.
Формулирование проблемы
Производственная компания располагает двумя категориями персонала с разным уровнем экспертизы: базовой и опытной. Также компания производит три вида продукции: стандартный, классический и элитный. Каждую неделю в распоряжении компании 200 смен опытных сотрудников и 400 смен сотрудников базового уровня. При этом опытные сотрудники дороже: одна смена стоит 250 условных единиц, в то время как смена базовых сотрудников стоит 100. Известно, что в одну смену сотрудник базового уровня может произвести 3 штуки элитного, 4 штуки классического и 5 штук стандартного продукта. Опытные сотрудники могут произвести в два раза больше.
Перед HR-директором стоит задача минимизировать затраты на производственный персонал каждую неделю. Конечно, самым простым решением будет не выводить на работу никого, тогда затраты будут равны нулю. Но помимо требования к минимизации затрат есть требование к уровню производства. Каждую неделю компания должна производить не менее 500 штук элитного, 600 штук классического и 2000 штук стандартного продукта.
Чтобы нам было проще работать, давайте представим всю известную нам информацию в виде таблицы.
Пора перейти от текстового формулирования к математическому. Пусть переменная x – будет количеством штук товара, которые мы должны произвести. На уровне здравого смысла, мы понимаем, что это значение не может быть дробным или меньше нуля. Я стараюсь не уходить в детали, но правильно сказать, что мы решаем задачу линейного смешанного программирования. Нам понадобится это понимание, когда мы перейдем непосредственно к решению.
У нас есть множество сотрудников I = [«Базовая категория», «Опытная категория»] и множество продуктов J = [«Стандартный продукт», «Классический продукт», «Элитный продукт»].
Также в нашем распоряжении ряд параметров, которые мы представим следующим образом:
- Стоимость смены P = [100, 250].
- Доступно смен Sh = [400, 200].
- Эффективность сотрудников Ef = [(«Базовая категория», «Стандартный продукт») = 5, («Опытная категория», «Стандартный продукт») = 10 … и т.д.]
- Необходимо произвести продуктов Pr =[2000, 600, 500].
Определение целевой функции
Целевую функцию и ограничения принято выражать математическими формулами. Вам может показаться это избыточным, но поверьте мне, это сильно упрощает решение и помогает понять, какой код мы должны будем написать.
Итак, мы хотим минимизировать затраты на персонал в обеих категориях, работающих на производстве трех продуктов. Тогда функция принимает следующий компактный вид:
Установление ограничений
У нас с вами всего три ограничения:
- на имеющиеся в распоряжении смены;
- на количество продукции, которую необходимо произвести;
- на то, что переменная x – это целое неотрицательное число.
Выразим ограничение суммы смен, которое не может превышать лимиты по категориям персонала.
Ограничение на количество производимой продукции по типам, которое не может быть меньше установленный границы для каждого продукта.
Последнее ограничение касается переменной x. Напомню, что множество целых чисел отображается дважды начерченной буквой Z.
Нахождения оптимального решения
Математика — это очень круто! Но настала пора переходить к решению, которое мы выполним в Python. Для этого нам понадобится библиотека pyomo [2] – это очень удобный фреймворк для решения разных задач оптимизации. И вторая библиотека – это cplex [3], которая содержит в себе «солвер» (не знаю, как это называется по-русски, наверное, «решатель») для задач линейного смешанного программирования.
Если бы перед нами стояла другая задача, к примеру, нелинейное программирование, то мы могли продолжить использовать pyomo, как фреймворк, а вот солвер понадобился бы другой.
Устанавливаем необходимые библиотеки в первый раз и подключаем их.
# Установка при первом запуске !pip install pyomo !pip install cplex # Подключение библиотек import cplex import pyomo.environ as pyo from pyomo.opt import SolverFactory
Первым шагом мы задаем модель с помощью функции ConcreteModel()
.
# Создаем модель и записываем её в переменную model model = pyo.ConcreteModel()
Дальше весь код будет отражением нашим математических формул, которые в свою очередь отражение бизнес-логики. Последовательное выполнение описанных этапов помогает не запутаться и решать задачи структурировано.
# Категории персонала I model.i = pyo.Set(initialize = ['Базовая категория', 'Опытная категория']) # Виды продуктов J model.j = pyo.Set(initialize = ['Стандартный продукт', 'Классический продукт', 'Элитный продукт'])
По этим двум множествам мы будет итерировать всё остальное. Начнем с задания известных нам параметров. Для удобства дальнейшего итерирования принято задавать параметры и переменные модели в виде model.name, а затем дополнительно записывать их как name = model.name, далее вы увидите, где и как это помогает.
# Стоимость категорий сотрудников model.P = pyo.Param(model.i, initialize = {'Базовая категория': 100, 'Опытная категория': 250}) P = model.P # Доступное количество смен каждую неделю по категориям сотрудников model.Sh = pyo.Param(model.i, initialize = {'Базовая категория': 400, 'Опытная категория': 200}) Sh = model.Sh # Количество продуктов, которое необходимо производить еженедельно model.Pr = pyo.Param(model.j, initialize = {'Стандартный продукт':2000, 'Классический продукт':600, 'Элитный продукт':500}) Pr = model.Pr # Количество продуктов по видам, которые может произвести один сотрудник. # Обратите внимание, что здесь мы итерируем по двум множествам. model.Ef = pyo.Param(model.i, model.j, initialize = ({('Базовая категория','Стандартный продукт'):5, ('Базовая категория','Классический продукт'):4, ('Базовая категория','Элитный продукт'):3, ('Опытная категория','Стандартный продукт'):10, ('Опытная категория','Классический продукт'):8, ('Опытная категория','Элитный продукт'):6,})) Ef = model.Ef
Зададим переменную x, как целое неотрицательное число.
model.x = pyo.Var(model.i, model.j, domain = pyo.NonNegativeIntegers) x = model.x
Можно по-разному определять целевую функцию и ограничения, но мы будем использовать для этого самописные функции в Python. Давайте создадим нашу целевую функцию, которая должна минимизировать затраты на персонал.
# Название функции может быть любым. # Мы видим, что передача параметров в переменные, # которую мы выполнили ранее, позволяет записать код короче и понятнее. def Objective_function(model): return sum(P[i]*sum(x[i,j] for j in model.j) for i in model.i) # sense = pyo.minimize указывает на то, что мы хотим минимизировать сумму model.Objf = pyo.Objective(rule = Objective_function, sense = pyo.minimize)
Таким же образом установим ограничения. На уровне математики мы определили с вами, что их три, но ограничение на x мы уже реализовали путем создания переменной в модели, поэтому осталось только два.
Здесь стоит проявить внимание к аргументам функции, в этом нам вновь помогут наши математические формулы, в которых мы указали с помощью квантора всеобщности (перевернутая буква А), что ограничение на производимую продукцию применяется к каждому элементу множества J, а ограничение на количество смен, к каждому элементу множества I.
# Ограничение на производство необходимого количества продукции def ProductionRequirment(model, j): return sum(Ef[(i, j)] * x[i,j] for i in model.i) >= Pr[j] model.Const1 = pyo.Constraint(model.j, rule=ProductionRequirment) # Ограничение на имеющиеся в распоряжении количество смен def ShiftAvailability(model, i): return sum(x[i,j] for j in model.j) <= Sh[i] model.Const2 = pyo.Constraint(model.i, rule=ShiftAvailability)
Мы готовы к запуску решения, но бывает полезным перед этим проверить насколько верно мы все указали внутри модели, что можно сделать с помощью команды model.pprint()
.
# Задаем солвер Solver = SolverFactory('cplex_direct') # Применяем солвер к нашей модели и записываем результат в переменную results = Solver.solve(model, tee = True) # Посмотрим на то, какой же расход на персонал в неделю мы понесем print('Целевая функция = ', model.Objf())
Получаем результат минимизации Целевая функция = 79650.0
.
Но как ответить на вопрос: как распределить персонал между производством разных продуктов чтобы выполнить это бюджетное обещание? Очень просто, модель позволяет это увидеть тоже.
# Извлечем результаты по категориям сотрудников и видам продукции for i in model.i: for j in model.j: print('Количество смен',i, j, '=' ,x[i,j]())
Чтобы выполнить целевую функцию мы должны распределить сотрудников следующим образом:
Количество смен Базовая категория Стандартный продукт = 248.0 Количество смен Базовая категория Классический продукт = 150.0 Количество смен Базовая категория Элитный продукт = 1.0 Количество смен Опытная категория Стандартный продукт = 76.0 Количество смен Опытная категория Классический продукт = 0.0 Количество смен Опытная категория Элитный продукт = 83.0
Итак, с помощью математики и расчётов, которые делает за нас Python, мы с лёгкостью можем ответить на вопросы HR-директора, как нужно распределять персонал на производстве, чтобы минимизировать расходы, а также выполнить требуемые производственные показатели.