Помощь в оптимизации, включающая матричные операции и ограничения - PullRequest
7 голосов
/ 09 апреля 2020

Я так далеко от своей лиги в этом, так что я надеюсь, что кто-то может направить меня в правильном направлении. Я думаю, что это проблема оптимизации, но я был сбит с толку scipy.optimize и его сочетанием с целлюлозой. Кроме того, математика математики поражает мой разум. Поэтому эта проблема действительно замедляет меня, не спрашивая.

Постановка задачи: У меня есть набор данных клиентов. Для каждого клиента я могу выбрать 3 варианта или ни одного. Итак 4 варианта. Также для каждого покупателя у меня есть цифра c, которая говорит, насколько «хорош» каждый выбор. Вы можете представить это значение как the probability of the Choice to create a future sale.

# fake data for the internet
data = {'customerid':[101,102,103,104,105,106,107,108,109,110],
        'prob_CHOICEA':[0.00317,0.00629,0.00242,0.00253,0.00421,0.00414,0.00739,0.00549,0.00658,0.00852],
        'prob_CHOICEB':[0.061,0.087,0.055,0.027,0.022,0.094,0.099,0.072,0.018,0.052],
        'prob_CHOICEC':[0.024,0.013,0.091,0.047,0.071,0.077,0.067,0.046,0.077,0.044]
       } 

# Creates pandas DataFrame 
df = pd.DataFrame(data) 
df = df.reset_index(drop=True).set_index(['customerid'])
+------------+--------------+--------------+--------------+
| customerid | prob_CHOICEA | prob_CHOICEB | prob_CHOICEC |
+------------+--------------+--------------+--------------+
|        101 |      0.00317 |        0.061 |        0.024 |
|        102 |      0.00629 |        0.087 |        0.013 |
|        103 |      0.00242 |        0.055 |        0.091 |
|        104 |      0.00253 |        0.027 |        0.047 |
|        105 |      0.00421 |        0.022 |        0.071 |
|        106 |      0.00414 |        0.094 |        0.077 |
|        107 |      0.00739 |        0.099 |        0.067 |
|        108 |      0.00549 |        0.072 |        0.046 |
|        109 |      0.00658 |        0.018 |        0.077 |
|        110 |      0.00852 |        0.052 |        0.044 |
+------------+--------------+--------------+--------------+

Я начал с объединения этих элементов в один массив для каждого клиента:

# combine all values into 1 array
list_to_combine = ['prob_CHOICEA', 'prob_CHOICEB','prob_CHOICEC']

df['probs_A_B_C']= df[list_to_combine].values.tolist()

df.drop(list_to_combine, axis=1, inplace=True)
+------------+-------------------------+
| customerid |       probs_A_B_C       |
+------------+-------------------------+
|        101 | [0.00317, 0.061, 0.024] |
|        102 | [0.00629, 0.087, 0.013] |
|        103 | [0.00242, 0.055, 0.091] |
|        104 | [0.00253, 0.027, 0.047] |
|        105 | [0.00421, 0.022, 0.071] |
|        106 | [0.00414, 0.094, 0.077] |
|        107 | [0.00739, 0.099, 0.067] |
|        108 | [0.00549, 0.072, 0.046] |
|        109 | [0.00658, 0.018, 0.077] |
|        110 | [0.00852, 0.052, 0.044] |
+------------+-------------------------+

Для каждого клиент, у меня есть только 4 варианта действий:

choices = [
    [0,0,0],
    [1,0,0],
    [0,1,0],
    [0,0,1]
]

Для каждого клиента я хочу выбрать лучший выбор для каждого клиента. На первый взгляд это было легко - просто выберите наибольшее число. Однако это начинает поражать меня, как только я начинаю добавлять ограничения.

Например, что делать, если я хочу выбрать лучший выбор для каждого клиента, но с ограничением, что сумма выбранных вариантов = 5

+------------+-------------------------+-------------+
| customerid |       probs_A_B_C       | best_choice |
+------------+-------------------------+-------------+
|        101 | [0.00317, 0.061, 0.024] | [0,0,0]     |
|        102 | [0.00629, 0.087, 0.013] | [0,1,0]     |
|        103 | [0.00242, 0.055, 0.091] | [0,0,1]     |
|        104 | [0.00253, 0.027, 0.047] | [0,0,0]     |
|        105 | [0.00421, 0.022, 0.071] | [0,0,0]     |
|        106 | [0.00414, 0.094, 0.077] | [0,1,0]     |
|        107 | [0.00739, 0.099, 0.067] | [0,1,0]     |
|        108 | [0.00549, 0.072, 0.046] | [0,0,0]     |
|        109 | [0.00658, 0.018, 0.077] | [0,0,1]     |
|        110 | [0.00852, 0.052, 0.044] | [0,0,0]     |
+------------+-------------------------+-------------+

Я даже не придумал, как это сделать, я просто посмотрел на это вручную в иллюстративных целях.

В идеале я хотел бы добавить несколько ограничений одновременно:

  • Общая сумма best_choice = N
  • Общая сумма CHOICEA (первый элемент best_choice)> = M
  • Общая сумма CHOICEB (второй элемент best_choice) <= 10 </li>

Есть идеи, с чего начать?

Ответы [ 2 ]

5 голосов
/ 15 апреля 2020

Эта проблема может быть решена с помощью линейного программирования (LP), но самая сложная часть - это не знание того, что вы должны использовать LP, это преобразование вашей проблемы в проблему LP-optimization, и я покажу вам, как это сделать. который. Прежде чем продолжить, я изменю данные примера, предоставленные вами для упрощения (из-за огромного количества сгенерированных переменных), поэтому предположим, что у нас есть следующие входные данные:

+------------+---------------------+
| customerid |  prob A  |  prob B  |
+------------+---------------------+
|        101 |  0.00317 |   0.061  |
|        102 |  0.00629 |   0.087  |
+------------+---------------------+

Предположим, что размер входных данных проблема в N, где N представляет количество вариантов:

+---------------------+
|  prob A  |  prob B  |
+---------------------+
|  0.00317 |   0.061  |
|  0.00629 |   0.087  |
+------------+--------+

Поскольку у нас есть 4 варианта выбора, N=4 (не имеет значения, что некоторые из них являются взаимоисключающими, такие характеристики будут отображаться по ограничениям). В LP мы имеем дело со следующими вещами:

  • Целевая функция C (размеры 1x[at least N], это массив строк ),
  • Матрица A ограничений (размеры зависят от того, сколько ограничений вы хотите добавить, у вас также может быть больше ограничений, чем для переменных) и
  • правая сторона (которую мы будем вызов b, его размеры [number of rows in A]x1, столбец-массив ).

Соответственно, проблема максимизации LP будет иметь следующий формат:

Max Cx

subject to:
    Ax <= b
    x >= 0

Обратите внимание, что с этого момента мы создадим переменные LP для представления входных данных, которые у нас есть, поэтому предположим следующее отображение между xi и input data:

+-------------------------------+
| LP variable | Customer | prob |
+-------------------------------+
|     x0      |    101   |   A  |
|     x1      |    101   |   B  |
|     x2      |    102   |   A  |
|     x3      |    102   |   B  |
+-------------------------------+

Давайте начнем с параллельного заполнения матрицы ограничений A и RHS b, мы должны создать матрицу, образованную объединение столбцов two NxN матриц идентификаторов :

                           A               
      +-----------------------------------------------------+ 
      |  x0  |  x1  |  x2  |  x3  |  x4  |  x5 |  x6  |  x7 |       b
      +-----------------------------------------------------+    +-----+
row 0 |   1  |   0  |   0  |   0  |   1  |  0  |   0  |  0  |    |  1  |
row 1 |   0  |   1  |   0  |   0  |   0  |  1  |   0  |  0  |    |  1  |
row 2 |   0  |   0  |   1  |   0  |   0  |  0  |   1  |  0  |    |  1  |
row 3 |   0  |   0  |   0  |   1  |   0  |  0  |   0  |  1  |    |  1  |
      +-----------------------------------------------------+    +-----+

Мы также должны убедиться, что для каждого клиента выбрана не более одной переменной (строка наших входных данных), поэтому мы также создайте одну дополнительную переменную для каждого клиента, в данном случае x8 и x9, и установите для них значение 1 в соответствующих новых строках 2, которые мы создадим для A. Кроме того, новые строки также должны иметь 1 в переменных, отображаемых для каждого клиента (просто посмотрите, какие переменные присутствуют в желаемом клиенте). Таким образом, мы добавляем следующие 2 строки в матрицу A и массив столбцов b:

                                  A
      +------------------------------------------------------------------+
      |  x0  |  x1  |  x2  |  x3  |  x4  |  x5 |  x6  |  x7 |  x8  | x9  |       b
      +------------------------------------------------------------------+    +-----+
row 4 |   1  |   1  |   0  |   0  |   0  |  0  |   0  |  0  |   1  |  0  |    |  1  |
row 5 |   0  |   0  |   1  |   1  |   0  |  0  |   0  |  0  |   0  |  1  |    |  1  |
      +------------------------------------------------------------------+    +-----+

Теперь A становится:

                                  A
      +------------------------------------------------------------------+
      |  x0  |  x1  |  x2  |  x3  |  x4  |  x5 |  x6  |  x7 |  x8  | x9  |       b
      +------------------------------------------------------------------+    +-----+
row 0 |   1  |   0  |   0  |   0  |   1  |  0  |   0  |  0  |   0  |  0  |    |  1  |
row 1 |   0  |   1  |   0  |   0  |   0  |  1  |   0  |  0  |   0  |  0  |    |  1  |
row 2 |   0  |   0  |   1  |   0  |   0  |  0  |   1  |  0  |   0  |  0  |    |  1  |
row 3 |   0  |   0  |   0  |   1  |   0  |  0  |   0  |  1  |   0  |  0  |    |  1  |
row 4 |   1  |   1  |   0  |   0  |   0  |  0  |   0  |  0  |   1  |  0  |    |  1  |
row 5 |   0  |   0  |   1  |   1  |   0  |  0  |   0  |  0  |   0  |  1  |    |  1  |
      +------------------------------------------------------------------+    +-----+

Предположим, мы также хотим добавить ограничение, обеспечивающее, что всего будет сделано 2 выбор проб, затем мы добавляем строку 6 и столбец x10 в A, устанавливая 1 переменную от x0 до x3, а также x10:

                                  A
      +------------------------------------------------------------------------+
      |  x0  |  x1  |  x2  |  x3  |  x4  |  x5 |  x6  |  x7 |  x8  | x9  | x10 |         b
      +------------------------------------------------------------------------+      +-----+
row 0 |   1  |   0  |   0  |   0  |   1  |  0  |   0  |  0  |   0  |  0  |  0  |      |  1  |
row 1 |   0  |   1  |   0  |   0  |   0  |  1  |   0  |  0  |   0  |  0  |  0  |      |  1  |
row 2 |   0  |   0  |   1  |   0  |   0  |  0  |   1  |  0  |   0  |  0  |  0  |      |  1  |
row 3 |   0  |   0  |   0  |   1  |   0  |  0  |   0  |  1  |   0  |  0  |  0  |      |  1  |
row 4 |   1  |   1  |   0  |   0  |   0  |  0  |   0  |  0  |   1  |  0  |  0  |      |  1  |
row 5 |   0  |   0  |   1  |   1  |   0  |  0  |   0  |  0  |   0  |  1  |  0  |      |  1  |
row 6 |   1  |   1  |   1  |   1  |   0  |  0  |   0  |  0  |   0  |  0  |  1  |      |  2  |
      +------------------------------------------------------------------------+      +-----+

Обратите внимание, что в этом простом примере ограничение количества вариантов выбора не более 2 не влияет на конечный результат.

Наконец, мы строим целевую функцию:

                               C
+-----------------------------------------------------------------------------------+
|    x0    |    x1   |    x2   |    x3   |  x4 |  x5 | x6  |  x7 |  x8 |  x9 |  x10 |
+-----------------------------------------------------------------------------------+
|  0.00317 |   0.061 | 0.00629 |   0.087 |  0  |  0  |  0  |  0  |  0  |  0  |   0  |
+-----------------------------------------------------------------------------------+

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

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

После запуска предпочитаемого солвера вы должны интерпретировать выходной результат. Результатом должен быть массив из одной строки, содержащий значение каждого xi. Результат вышеприведенного примера, который я привел, будет:

+------------------------------------------------------------------+
|  x0 |  x1 |  x2 |  x3 |  x4 |  x5 | x6  |  x7 |  x8 |  x9 |  x10 |
+------------------------------------------------------------------+
|  0  |  1  |  0  |  1  |  0  |  0  |  0  |  0  |  0  |  0  |   0  |
+------------------------------------------------------------------+

Приведенный выше результат означает, что вы должны выбрать элемент, представленный переменными x1 и x3, поскольку они установлены в 1, то есть клиент 101 выбирает prob B и клиент 102 также выбирает пробу B.

Post Scriptum:

  • После использования scipy.optimze.linprog lib для выполнения работы, просто убедитесь, что вы используете параметр "Aeq" вместо «Aub» для ограничений, если вы используете вышеупомянутое моделирование;
  • Я не углубился в математику за этой конкретной проблемой c, чтобы доказать это, однако, кажется, что целочисленный LP никогда не будет быть обязательным из-за характера ограничений, которые можно построить из этой проблемы;
  • Коэффициенты из целевой функции C могут принимать любое действительное значение, включая отрицательное и 0; и
  • Я предложил инструмент LP scipy, потому что я работал с ним раньше и работает как шарм, тем не менее, есть другие бесплатные потрясающие реализации, такие как glpk, которые могут предоставить более продвинутые инструменты для любых дальнейших потребностей в ваша проблема.
5 голосов
/ 14 апреля 2020

Вы можете использовать scipy.optimize.linprog для решения этой задачи линейной оптимизации. Требуется настроить граничные условия в виде матричных произведений, как указано в документации. Существует два типа граничных условий: неравенства вида A @ x <= b и равенства A @ x == b. Проблема может быть смоделирована следующим образом:

  • Результирующий вектор x имеет длину N*C, где N - количество клиентов, а C - количество вариантов; он представляет варианты для каждого пользовательского элемента в линейном макете: [c1_A, c1_B, c1_C, c2_A, c2_B, c2_C, ..., cN_A, cN_B, cN_C].
  • Поскольку каждый клиент может сделать не более одного выбора, мы имеем неравенство для каждого клиента, которое суммирует все соответствующие варианты, то есть матрицу, в которой строки представляют клиентов, а столбцы представляют все варианты. В матрице есть записи 1, если выбор соответствует покупателю, и ноль в противном случае (рисунок см. Ниже).
  • Опция A должна быть выбрана как минимум в M раз; поскольку у нас есть только неравенства вида A @ x <= b, мы можем инвертировать значения и использовать -1 записи в A, которые соответствуют опции A и -M в b.
  • Опция B должна быть выбирается не более 10 раз; это можно смоделировать аналогично предыдущему ограничению, используя записи 1 и положительные 10 (поскольку оно уже имеет форму <=).
  • Сумма всех вариантов должна составлять N , Это может быть смоделировано ограничением равенства, когда матрица суммирует все варианты в x, а результат должен быть равен N.

Это иллюстрация вышеуказанных ограничений:

# Max. one choice per customer.
# A =                                           # b =
[[1, 1, 1,   0, 0, 0,   ...,   0, 0, 0],        [1,
 [0, 0, 0,   1, 1, 1,   ...,   0, 0, 0],         1,
 ...                                             ...
 [0, 0, 0,   0, 0, 0,   ...,   1, 1, 1]]         1]

# Min. M choices for option A.
# A =                                              # b =
[[-1, 0, 0,   -1, 0, 0,   ...,   -1, 0, 0]]        [[-M]]

# Max. 10 choices for option B.
# A =                                           # b =
[[0, 1, 0,   0, 1, 0,   ...,   0, 1, 0]]        [[10]]

# Total number of choices equals N.
# A =                                           # b = 
[[1, 1, 1,   1, 1, 1,   ...,   1, 1, 1]]        [[N]]

Вот пример кода для настройки ограничений и запуска оптимизации:

import numpy as np
import pandas as pd
from scipy.optimize import linprog

data = {'customerid':[101,102,103,104,105,106,107,108,109,110],
        'prob_CHOICEA':[0.00317,0.00629,0.00242,0.00253,0.00421,0.00414,0.00739,0.00549,0.00658,0.00852],
        'prob_CHOICEB':[0.061,0.087,0.055,0.027,0.022,0.094,0.099,0.072,0.018,0.052],
        'prob_CHOICEC':[0.024,0.013,0.091,0.047,0.071,0.077,0.067,0.046,0.077,0.044]
       } 

# Creates pandas DataFrame 
df = pd.DataFrame(data) 
df = df.reset_index(drop=True).set_index(['customerid'])
print(df, end='\n\n')

nc = df.shape[1]  # number of options

data = df.to_numpy().ravel()

# Max. choices per customer is 1.
A_ub_1 = np.zeros((len(df), len(data)))
for i in range(len(A_ub_1)):
    A_ub_1[i, nc*i:nc*(i+1)] = 1
b_ub_1 = np.ones(len(df))

# Min. choices for option A is 3.
A_ub_2 = np.zeros((1, len(data)))
A_ub_2[0, ::nc] = -1  # invert, since this defines an upper boundary
b_ub_2 = np.array([-3])

# Max. choices for option B is 2.
A_ub_3 = np.zeros((1, len(data)))
A_ub_3[0, 1::nc] = 1
b_ub_3 = np.array([2])

# Total sum of choices is 7.
A_eq = np.ones((1, len(data)))
b_eq = np.array([7])

result = linprog(
    -1 * data,  # linprog aims to minimize the value
    A_eq=A_eq, b_eq=b_eq,
    A_ub=np.concatenate((A_ub_1, A_ub_2, A_ub_3), axis=0),
    b_ub=np.concatenate((b_ub_1, b_ub_2, b_ub_3), axis=0),
    bounds=(0, 1)
)
print(result, end='\n\n')

choices = (result.x.reshape(-1, 3) > 1e-6).astype(int)
print('Choices:', choices, sep='\n')

Он дает следующие результаты:

            prob_CHOICEA  prob_CHOICEB  prob_CHOICEC
customerid                                          
101              0.00317         0.061         0.024
102              0.00629         0.087         0.013
103              0.00242         0.055         0.091
104              0.00253         0.027         0.047
105              0.00421         0.022         0.071
106              0.00414         0.094         0.077
107              0.00739         0.099         0.067
108              0.00549         0.072         0.046
109              0.00658         0.018         0.077
110              0.00852         0.052         0.044

     con: array([-1.30002675e-11])
     fun: -0.3812999999903971
 message: 'Optimization terminated successfully.'
     nit: 7
   slack: array([1.00000000e+00, 7.99305067e-11, 1.47325485e-11, 1.00000000e+00,
       1.00000000e+00, 2.49527066e-11, 2.42738052e-11, 5.84235438e-10,
       4.23596713e-11, 5.77714543e-11, 8.80984175e-12, 1.46305190e-11])
  status: 0
 success: True
       x: array([2.89971936e-10, 1.32732722e-11, 6.97732845e-12, 1.00000000e+00,
       3.28055311e-10, 5.72702383e-12, 1.80418885e-11, 4.61391860e-12,
       1.00000000e+00, 2.01674011e-10, 4.58311340e-12, 1.29599793e-11,
       2.95298295e-10, 4.34109315e-12, 1.21776975e-11, 3.39951283e-11,
       1.00000000e+00, 2.55262044e-10, 4.94703751e-11, 1.00000000e+00,
       1.57932544e-11, 9.99999999e-01, 2.21487598e-11, 1.33679145e-11,
       2.30514296e-10, 3.91129933e-12, 1.00000000e+00, 1.00000000e+00,
       8.19015577e-12, 1.07293976e-11])

Choices:
[[0 0 0]
 [1 0 0]
 [0 0 1]
 [0 0 0]
 [0 0 0]
 [0 1 0]
 [0 1 0]
 [1 0 0]
 [0 0 1]
 [1 0 0]]
...