Анализ чувствительности модели с помощью Python

Знакомство

В реальном сценарии результат зависит от множества различных входных переменных, и это очень затрудняет достижение вывода или уверенное принятие решения. Еще более рискованно, если результаты анализа имеют финансовые последствия. Для того, чтобы справиться с такой неопределенностью в данных и прогнозировать с достаточной уверенностью, проводится анализ чувствительности и моделирования. Анализ чувствительности позволяет определить влиятельные переменные и оценить величину влияния каждой переменной. Это широко используется для принятия решений, основанных на данных, и для проверки предположений, сделанных в ходе анализа. Моделирование используется для проверки надежности модели и определения пороговых значений, за пределами которых результат будет иметь положительное или отрицательное влияние.

В этой статье мы определим проблему и проведем анализ чувствительности вместе с моделированием. Мы будем изучать следующие темы в этой статье

  • Определение задачи линейного программирования (LPP)
  • Использование PuLP для решения LPP
  • Что такое анализ чувствительности?
  • Как интерпретировать слабые и ценовые переменные?
  • Моделирование с помощью итертулов и анализатора чувствительности
  • Визуализация результатов

Определение задачи линейного программирования (LPP)

Компания производит два продукта — Продукт А и Продукт Б. Наша цель состоит в том, чтобы максимизировать прибыль в соответствии с тремя определенными ограничениями.

Целевая функция:

Прибыль = 30 * A + 45 * B

Ограничения целостности:

  • Ограничение 1: 3 * A + 12 * B < = 150
  • Ограничение 2: 4 * A + 3 * B < = 47
  • Ограничение 3: 5 * A + 2 * B < = 60
  • A, B >= 0

Использование PuLP для решения LPP

В этой статье мы будем использовать PuLP, который является пакетом с открытым исходным кодом на python. Начнем с импорта библиотек.

from pulp import *

Мы переведем определенную задачу в код.

Шаг 1: Инициализируйте класс. Мы будем использовать класс LpMaximize для оптимизации

model = LpProblem("Product_Profits",LpMaximize)

Шаг 2: Определение переменных

A = LpVariable('A', lowBound=0)
B = LpVariable('B', lowBound=0)

Шаг 3: Определение цели — прибыль от продуктов А и В

model += 30 * A + 45 * B

Шаг 4: Определите ограничения, мы добавим три ограничения

# Constraint 1
model += 3 * A + 12 * B <= 150
# Constraint 2
model += 4 * A + 3 * B <= 47
# Constraint 3
model += 5 * A + 2 * B <= 60

Шаг 5: Решите для оптимизации

model.solve()
print("Model Status:{}".format(LpStatus[model.status]))
print("Objective = ", round(value(model.objective),3))

Result:
Model Status:Optimal
Objective = 617.307

Шаг 6: Давайте проверим оптимальное значение A и B

for var in model.variables():
print(var.name,"=", var.varValue)

Result:
A = 2.9230769
B = 11.769231

Общие сведения об анализе чувствительности

Мы нашли оптимальное решение для нашей линейной задачи. Вот несколько сценариев, на которые мы найдем ответы.

  • Как это скажется на прибыли, если RHS ограничения 1 изменится со 150 на 160?
  • Как это скажется на прибыли, если RHS ограничения 2 изменится с 47 на 46 или 45?
  • Существуют ли значения для ограничения 3, где нет влияния на прибыль? Если да, то каково пороговое значение?

Эти вопросы (или аналогичные вопросы) важны для лучшего понимания производительности модели. Мы извлечем цену и слабые значения из модели.

res = [{'Name':name,'Constraint':const,'Price':const.pi,'Slack': const.slack}
for name, const in model.constraints.items()]
print(pd.DataFrame(res))
Result:
Name Constraint Price Slack
0 _C1 {A: 3, B: 12} 2.307692 -0.000000
1 _C2 {A: 4, B: 3} 5.769231 -0.000000
2 _C3 {A: 5, B: 2} -0.000000 21.846154

Как интерпретировать слабые и ценовые значения?

Цена: Цена ограничений 1 равна 2,30, а 2 — 5,76. Это означает, что если произойдет изменение единицы в RHS ограничений 1 и 2, это повлияет на оптимальное значение на 2,30 и 5,76.

Slack: Значение slack ограничения 1 и ограничения 2 равно нулю, а для ограничения 3 оно равно 21,84, т.е. изменения полосы пропускания 21,84 не будут влиять на оптимальное значение. Разберемся в этом на примере. Мы изменим RHS ограничения 3 с 60 на 55 и отметим влияние этого на результирующее значение.

Вот полный код до сих пор.

from pulp import *model = LpProblem("Product_Profits",LpMaximize)
# Define variables
A = LpVariable('A', lowBound=0)
B = LpVariable('B', lowBound=0)
# Define Objetive Function: Profit on Product A and B
model += 30 * A + 45 * B
# Constraint 1
model += 3 * A + 12 * B <= 150
# Constraint 2
model += 4 * A + 3 * B <= 47
# Constraint 3
model += 5 * A + 2 * B <= 55 # <-- the value was changed from 60 to 55
# Solve Model
model.solve()
print("Model Status:{}".format(LpStatus[model.status]))
print("Objective = ", round(value(model.objective),3))Result:
Model Status: Optimal
Objective = 617.308

Оптимальное значение функции не изменилось и по-прежнему составляет 617,308. Теперь изменим значение ограничения 3 на 38.

Result:
Model Status: Optimal
Objective = 616.667

На этот раз оптимальное значение функции изменилось на 616,667. Аналогично, для значений 3635 и 34 мы получаем оптимальные значения 608,33604,16 и 600,00 соответственно. Если вы заметили, разница между оптимальными значениями составляет 4,17. Это означает, что при каждом изменении единицы значения слабины ниже 38 оптимальное значение функции будет изменяться на 4,17

Теперь давайте попробуем то же самое со значением цены. Мы изменим RHS ограничения 1 для разных значений и сохраним другие ограничения одинаковыми.

# Constraint 1
model += 3 * A + 12 * B <= 149Result: For 149
Model Status: Optimal
Objective = 615.000Result: For 148
Model Status: Optimal
Objective = 612.692Result: For 147
Model Status: Optimal
Objective = 610.385

Обратите внимание, что разница между оптимальным значением составляет 2,30. Это означает, что при каждом изменении значения цены ограничения 1 ниже 150 оптимальное значение функции будет изменяться на 2,30

Теперь мы изменим RHS ограничения 2 для разных значений и сохраним другие ограничения одинаковыми.

# Constraint 2
model += 4 * A + 3 * B <= 47Result: For 46
Model Status: Optimal
Objective = 611.538Result: For 45
Model Status: Optimal
Objective = 605.769Result: For 44
Model Status: Optimal
Objective = 600.000

Обратите внимание, что разница между оптимальным значением составляет 5,76. Это означает, что при каждом изменении значения цены ограничения 2 ниже 47 оптимальное значение функции будет изменяться на 5,76

Построение симуляции

Мы экспериментировали с различными значениями ограничений и отмечали наши наблюдения, но это утомительный процесс, если количество ограничений увеличивается. Будет хорошо иметь комбинации ограничений и результирующих оптимальных значений в таблице для готовой справки. Мы достигнем этого, обернув нашу логику в функцию с последующим моделированием значений RHS для каждого из ограничений и генерацией оптимальных значений. Вот полный код.

lst_constraint1 = list(range(140,151,1))
lst_constraint2 = list(range(45,48,1))
lst_constraint3 = list(range(35,39,1))
def sensitivity_table(lst_constraint1, lst_constraint2, lst_constraint3):
 
    """_summary_
    Args:
        L1 (List): Range of values of constraint 1
        L2 (List): Range of values of constraint 2
        L3 (List): Range of values of constraint 3Returns:
        Int: Returns the optimal value i.e profit
    """
    
    try:
        # Initialize Class, Define Vars., and Objective
        model = LpProblem("Product_Profits",LpMaximize)# Define variables
        A = LpVariable('A', lowBound=0)
        B = LpVariable('B', lowBound=0)# Define Objetive Function: Profit on Product A and B
        model += 30 * A + 45 * B# Constraint 1
        model += 3 * A + 12 * B <= lst_constraint1# Constraint 2
        model += 4 * A + 3 * B <= lst_constraint2# Constraint 3
        model += 5 * A + 2 * B <= lst_constraint3# Solve Model
        model.solve()print("Model Status:{}".format(LpStatus[model.status]))
        print("Objective = ", round(value(model.objective),3))
        
        for var in model.variables():
            print(var.name,"=", var.varValue)
            print(f'"lst_constraint1" = {lst_constraint1}, "lst_constraint2" = {lst_constraint2}, "lst_constraint3" = {lst_constraint1}')
        res = [{'Name':name,'Constraint':const,'Price':const.pi,'Slack': const.slack} for name, const in model.constraints.items()]
        print(pd.DataFrame(res))
        return round(value(model.objective),2)
        
    except Exception as e:
        print(f'Simulation error: {e}')res = [(p3, p2, p1, sensitivity_table(p1, p2, p3)) for p3 in lst_constraint3 for p2 in lst_constraint2 for p1 in lst_constraint1]df = pd.DataFrame(res, columns= ['Constraint 3','Constraint 2', 'Constraint 1', 'Objective'])
df_pivot = df.pivot(index = 'Constraint 1', columns = ['Constraint 2', 'Constraint 3'], values = 'Objective')
df_pivot

Приведенная выше таблица помогает нам получить значения из комбинаций всех трех ограничений, которые мы определили. Вот несколько наблюдений.

  • Если значение ограничения 1 равно 150, то ограничение 2 равно 47, а ограничение 3 равно 39. В результате оптимальное значение составляет 617,31. Любое значение ниже этих значений будет влиять на оптимальное значение.
  • Ранее мы видели, что изменение единицы в значении ограничения 3 повлияет на значение 4,17, т.е. 616,67–612,50 = 4,17 (желтые поля).
  • Аналогичным образом, ограничение 2 повлияет на значение 5,76 (зеленые поля) и ограничение 1 на 2,30 (красные квадраты).

Моделирование с помощью itertools

В предыдущем разделе мы сделали довольно много кодирования с вложенными циклами и итерациями для создания таблицы моделирования.

[(p3, p2, p1, sense(p1, p2, p3)) for p3 in lst_constraint3
for p2 in lst_constraint2
for p1 in lst_constraint1]

У нас было три вложенных цикла с тремя ограничениями, и это может стать более сложным, если количество ограничений увеличится. В этом разделе мы постараемся получить тот же результат гораздо более простым способом, используя библиотеку itertools. Мы будем использовать функцию продукта от itertools, которая даст нам все возможные комбинации ограничений и оптимальных значений без хлопот вложенных циклов

import itertools
...
[(var[0], var[1], var[2], sense(var[0], var[1], var[2]))
for var in itertools.product(lst_constraint1, lst_constraint2, lst_constraint3)][(140, 45, 35, 573.61),
(140, 45, 36, 577.78),
(140, 45, 37, 581.94),
(140, 45, 38, 582.69),
(140, 45, 39, 582.69)
............
(150, 47, 35, 604.17),
(150, 47, 36, 608.33),
(150, 47, 37, 612.5),
(150, 47, 38, 616.67),
(150, 47, 39, 617.31)]

Это вышло довольно аккуратно. Мы можем использовать этот список и сгенерировать сводную таблицу.

df_res_list = pd.DataFrame(res_list,
columns= ['Constraint 3','Constraint 2', 'Constraint 1', 'Objective'])
df_res_list_pivot = df.pivot(index = 'Constraint 1',
columns = ['Constraint 2', 'Constraint 3'], values = 'Objective')

Моделирование с помощью SenstivityAnalyzer

Мы упростили наш код, чтобы получить тот же результат, но нам все равно пришлось итерировать и извлекать значения. Теперь давайте используем возможности библиотеки SensitivityAnalayzer и упростим процесс еще больше. Мы создадим словарь ограничений и позволим библиотеке сделать всю тяжелую работу за нас.

from sensitivity import SensitivityAnalyzer
# creating a dictionary of constraints
sensitivity_dict = {
'lst_constraint1' : lst_constraint1,
'lst_constraint2' : lst_constraint2,
'lst_constraint3' : lst_constraint3
}
sa = SensitivityAnalyzer(sensitivity_dict, sensitivity_table)
sa.dfResult:
lst_constraint1 lst_constraint2 lst_constraint3 Result
140 45 35 573.610000
140 45 36 577.780000
140 45 37 581.940000
140 45 38 582.690000
140 45 39 582.690000
140 46 35 573.6100
................................
141 47 35 576.670000
141 47 36 580.830000
141 47 37 585.000000
141 47 38 589.170000
141 47 39 593.330000
142 45 35 579.720000
142 45 36 583.890000
142 45 37 587.310000

Мы можем использовать этот список и сгенерировать сводную таблицу. Давайте возьмем его на ступеньку выше и визуализируем результаты с помощью всего одной строки кода.

plot = sa.plot()

Заключение

Целью статьи было внедрение анализа чувствительности с помощью python, код и подход могут быть шаблонными и расширенными для оптимизации конкретных задач. В этой статье мы начали с понимания чувствительности, а затем определили задачу линейного программирования (LPP) с ограничениями, которые мы оптимизировали с помощью библиотеки PuLP. Затем мы глубоко погрузились в изучение переменной цены и слабости, а также ее роли и проанализировали влияние изменений переменных на оптимальное значение функции. Затем мы изучили способы создания таблиц моделирования с помощью библиотек Python — itertools и SensitivityAnalyzer.

Некоторые практические применения анализа чувствительности

  • Оптимизация цен на продукцию
  • Определите допущения и проверьте допуск
  • Эффективное управление распределением ресурсов, например: Производственные предприятия
  • Широко используется в финансовой отрасли экономистами, например: Оптимизация портфеля

Надеюсь, вам понравилась статья и она нашла ее полезной.

Вы можете найти код для справки — Github

Ссылки

https://pypi.org/project/PuLP/

https://pypi.org/project/sensitivity/

https://unsplash.com/

Источник