Логистика. Часть 6. Что такое нестинг? / Хабр

Нестинг

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

Простейшие варианты нестинга и его оптимизация

В гражданской авиации цена ассоциируется с классом бронирования, или подклассом, которые обозначаются заглавными латинскими буквами. Класс обслуживания и класс бронирования — разные понятия. Например, «БИЗНЕС» — это класс обслуживания, который авиакомпания может разбить на три подкласса бронирования: J, C и D, при этом вовсе не значит, что в этих трех подклассах различный уровень комфорта или сервиса. Единственное, чем они могут отличаться, так это ценой (тарифами, в которых и указывается стоимость предоставления услуги по перевозке). В тарифе также можно прописать и некоторые другие условия: например, подкласс J может продаваться только за 3 дня до вылета, а подкласс C — только в период с 7 до 3 дней до вылета.

Выходит, что авиакомпании не могут использовать динамическое ценообразование в полной мере, потому что изменение тарифов (в которых указывается цена билетов) не может быть выполнено мгновенно. Это заставляет авиакомпании планировать продажи заранее. Например, авиакомпания может оценить спрос и решить, что:

  • в период c 10-ти до 3-х дней до вылета нужно продавать 18 билетов подкласса D по цене 110 у.е.;
  • в период c 3-х до 1-го дней до вылета нужно продавать 14 билетов подкласса C по цене 120 у.е.;
  • за день до вылета нужно продать 8 билетов подкласса J по цене 140 у.е.

В общем план опирается на классическое определение спроса и должен включать в себя:

  • периоды, которые могут быть указаны в днях:
    • ;
    • ;
    • .
    • D = 110 у.е.;
    • C = 120 у.е.;
    • J = 140 у.е..
    • D = 18 билетов;
    • С = 14 билетов;
    • J = 8 билетов.

    На простом примере выше было показано, что введение квот действительно целесообразно и приводит к максимизации прибыли. Однако возникающие остатки необходимо как-то перераспределять между подклассами: в случае появления остатков мест в подклассе D эти самые остатки можно отдать, например, в подкласс C или J. С другой стороны, корректирующие цены также являются очень полезными, а значит, несмотря на наличие всего трех периодов, авиакомпания может ввести больше подклассов, например, к имеющимся трем добавить еще подклассы I и Z. Какие-то подклассы могут быть не использованы вовсе, но так как у них тоже есть квоты, то необходимо указать какому подклассу будут отданы их места в этом случае.

    Чтобы справиться с перераспределением остатков между подклассами и вести перерасчет мест, авиакомпании используют нестинг. Упрощенно нестинг можно представить как механизм, который определяет иерархию подклассов (цен) и правила перерасчета квот, т.е. доступных к продаже мест по определенной цене в случае, если в подклассах появляются остатки или какие-то подклассы не используются.

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

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

    Допустим, имеется всего три периода: , , и три кривых спроса: , , :

    price = np.round(price, 1) mu1 = np.array([22, 20, 17, 14, 8]) mu2 = np.array([24, 21, 19, 15, 11]) mu3 = np.array([21, 19, 18, 17, 16]) plt.plot(price, mu1, 'bo-', label=r'$T_
    ) plt.plot(price, mu2, 'ro-', label=r'$T_
    ) plt.plot(price, mu3, 'go-', label=r'$T_
    ) plt.xlabel('price') plt.ylabel('demand') plt.title('Dependence of demand on price and period') plt.legend();

    Данные функции показывают матожидания количества проданных билетов по определенной цене в каждом периоде. Количество проданных билетов — случайная величина, которая может испытывать отклонения. Если бы авиакомпания могла выполнить повтор третьего периода бесчисленное количество раз и продавать в нем билеты по цене 1.2, то все возможные количества проданных билетов могли бы быть описаны некоторым распределением, скажем, распределением Пуассона:

    mu = 18 Q = np.arange(5, 35) plt.stem(Q, poisson.pmf(Q, mu), label='poisson pmf') plt.xlabel('Demand') plt.ylabel('Probability') plt.title(r'Distribution of demand in the $T_$ at price 1.2') plt.legend();

    Причем для разных цен эти распределения будут разными:

    P1 = poisson.pmf(Q, mu1.reshape(5, 1)) P2 = poisson.pmf(Q, mu2.reshape(5, 1)) P3 = poisson.pmf(Q, mu3.reshape(5, 1)) for t in range(5): plt.plot(Q, P3[t], 'o-', ms=4, label=str(price[t])) plt.legend(title='Price:') plt.xlabel('Demand') plt.ylabel('Probability') plt.title('Demand distributions in the third period');

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

    fig, ax = plt.subplots(1, 3, figsize=(12, 4), sharex=True, sharey=True) cbar_ax = fig.add_axes([.15, -.07, .7, .05]) cbar_ax.set_title('Probability', y=-2.3) P1_df = pd.DataFrame(P1.T[::-1], index=Q[::-1], columns=price) sns.heatmap(P1_df, cmap='copper', cbar_ax=cbar_ax, cbar_kws=, ax=ax[0]) ax[0].plot(np.r_[0.5:5.5:1], 35 - mu1, 'o-', c='darkred', label='f1') ax[0].set_xlabel('Price') ax[0].set_ylabel('Demand') ax[0].legend() ax[0].set_title(r'Demand distributions in the $T_
    ) P2_df = pd.DataFrame(P2.T[::-1], index=Q[::-1], columns=price) sns.heatmap(P2_df, cmap='copper', cbar=False, ax=ax[1]) ax[1].plot(np.r_[0.5:5.5:1], 35 - mu2, 'o-', c='darkred', label='f2') ax[1].set_xlabel('Price') ax[1].set_ylabel('Demand') ax[1].legend() ax[1].set_title(r'Demand distributions in the $T_
    ) P3_df = pd.DataFrame(P3.T[::-1], index=Q[::-1], columns=price) sns.heatmap(P3_df, cmap='copper', cbar=False, ax=ax[2]) ax[2].plot(np.r_[0.5:5.5:1], 35 - mu3, 'o-', c='darkred', label='f3') ax[2].set_xlabel('Price') ax[2].set_ylabel('Demand') ax[2].legend() ax[2].set_title(r'Demand distributions in the $T_
    );

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

    В данном примере случайные отклонения спроса ничем не объясняются и могут восприниматься как шум. Отклонения могут быть вызваны самыми разными факторами (например, ценой на туристические путевки). Включение некоторого фактора переводит модель из трехмерного пространства в четырехмерное, а точки гиперповерхности будут испытывать меньшие отклонения, поскольку какая-то часть отклонений теперь объяснена этим новым фактором. Нетрудно догадаться, что чем меньше отклонения (дисперсия, неопределенность), тем меньше вычислений для последующей оптимизации.

    Создание модели спроса — это задача машинного обучения, качество которой зависит от данных. Тут действует старое правило: мусор на входе — мусор на выходе. Плохая модель может свести на нет все попытки последующей оптимизации.

    Другая особенность модели связана с тем, что авиакомпания не может себе позволить эксперименты с ценами, чтобы получить больше данных для уточнения модели. Фреквентистский подход в данном случае — это плохая идея. Он плох тем, что оперирует неопределенностью напрямую, посредством обработки экспериментов: чем больше экспериментов, тем лучше информация о неопределенности (распределении возможных отклонений спроса). Информация о неопределенности спроса при этом — это главное «топливо» для последующей оптимизации.

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

    Еще одна особенность связана с тем, что модель зависит от интервалов времени, а не от времени. Именно по этой причине эта модель и была названа интегральной. Создание интегральной модели — отнюдь непростая задача, ведь цены и квоты цензурируют наблюдения, а границы периодов продаж из-за квот также будут испытывать отклонения. Однако благодаря вероятностному программированию можно создать дифференциальную модель спроса — она показывает, как вероятность покупки билета отдельным потенциальным клиентом зависит от цены и времени.

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

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

    f, ax = plt.subplots(1, 2, figsize=(12, 3.5)) x = np.linspace(0, 10) y = gamma.pdf(x, a=0.9, scale=3) ax[0].plot(x, y) ax[0].set_ylabel(r'$P(d|t)
    ) ax[0].set_xlabel('t (day)') ax[0].set_title(r'$P(d|t) sim Gamma (0.9, 3)
    ) t = gamma.rvs(a=0.9, scale=3, size=100) t = t[t < 10] sns.histplot(t, ax=ax[1], bins=np.r_[:10]) ax[1].set_xlabel('t (day)') ax[1].set_title('Dependence of the number of potential buyersnon the time interval');

    Первый график показывает, что вероятность появления покупателя за 10 дней до вылета гораздо меньше, чем за 5, и крайне высока за несколько часов до него. Второй график показывает, что количество потенциальных покупателей в разные интервалы времени может выглядеть совершенно по-разному. В данном случае показано распределение потенциальных покупателей для рейса, по которому желает лететь в среднем 100 человек.

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

    f, ax = plt.subplots(1, 2, figsize=(12, 3.5)) x = np.arange(5) n, a, b = 4, 1, 0.99*9 + 0.1 ax[0].plot(x, betabinom.pmf(x, n, a, b), 'bo', ms=8, label='betabinom pmf') ax[0].vlines(x, 0, betabinom.pmf(x, n, a, b), colors='b', lw=5, alpha=0.5) ax[0].set_xticks(x, map(str, price)) ax[0].set_xlabel('Price') ax[0].set_ylabel('Probability') ax[0].set_title('Dependence of the probability of purchasing a ticketnon the price 9 days before departure') n, a, b = 4, 1, 0.99*1 + 0.1 ax[1].plot(x, betabinom.pmf(x, n, a, b), 'bo', ms=8, label='betabinom pmf') ax[1].vlines(x, 0, betabinom.pmf(x, n, a, b), colors='b', lw=5, alpha=0.5) ax[1].set_xticks(x, map(str, price)) ax[1].set_xlabel('Price') ax[1].set_ylabel('Probability') ax[1].set_title('Dependence of the probability of purchasing a ticketnon the price 1 day before departure');

    График показывает, что за 9 дней до вылета вероятность покупки при большой цене практически равна 0, в то время как за один день до вылета — достаточно велика. Однако, поскольку один из параметров в этом распределении зависит от времени, то приведенная интегральная модель покажет, что за все 10 дней продаж распределение количества покупателей, готовых купить по определенной цене, будет выглядеть несколько неожиданным образом:

    t = np.sort(gamma.rvs(a=0.9, scale=3, size=10000))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] sns.histplot(demand, bins=np.r_[0.95:1.5:0.1], stat='probability') plt.xlabel('price') plt.title('pmf of purchasing a ticket at a certain price for all 10 days');

    Данную модель в дальнейшем будем использовать для моделирования "реальности", поскольку она действительно является порождающей интегральную. Пусть первый период будет занимать интервал времени от 10 до 3-х дней до вылета, в котором проверим, какие могут быть отклонения спроса при разных ценах:

    fig, ax = plt.subplots(1, 5, figsize=(15, 3)) mu_t1 = [] for i in range(5): dem1 = [] for _ in range(1000): t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] end_t1 = np.where(t >3)[0][-1] dem1.append(np.cumsum(demand[: end_t1 + 1] >= price[i])[-1]) mu_t1.append(np.mean(dem1)) xx = np.arange(poisson.ppf(0.001, mu_t1[-1]), poisson.ppf(0.999, mu_t1[-1])) yy = poisson.pmf(xx, mu_t1[-1]) ax[i].set_title(f'price = ') ax[i].plot(xx, yy, 'bo', ms=3, label=rf'$mu$ = ') ax[i].legend() ax[i].vlines(xx, 0, yy, colors='b', lw=1, alpha=0.5) sns.histplot(dem1, discrete=True, stat='density', ax=ax[i]) fig.suptitle(r'Period $T_=[10, 3)
    ) plt.tight_layout();

    На гистограммы нанесены точки, соответствующие пуассоновскому распределению с (средней интенсивностью), равной среднему значению всех возможных значений спроса. Отклонения хорошо аппроксимируются Пуасоновским распределением, что неудивительно, так как каждая гистограмма показывает распределение возможного числа покупок билетов за фиксированный период времени .

    Проделаем то же самое для второго периода, который будет занимать время с 3-го по 1-й день до вылета:

    fig, ax = plt.subplots(1, 5, figsize=(15, 3)) mu_t2 = [] for i in range(5): dem2 = [] for _ in range(1000): t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] start_t2 = np.where(t >3)[0][-1] end_t2 = np.where(t > 1)[0][-1] dem2.append(np.sum(demand[start_t2 : end_t2 + 1] >= price[i])) mu_t2.append(np.mean(dem2)) xx = np.arange(poisson.ppf(0.001, mu_t2[-1]), poisson.ppf(0.999, mu_t2[-1])) yy = poisson.pmf(xx, mu_t2[-1]) ax[i].set_title(f'price = ') ax[i].plot(xx, yy, 'bo', ms=3, label=rf'$mu$ = ') ax[i].legend() ax[i].vlines(xx, 0, yy, colors='b', lw=1, alpha=0.5) sns.histplot(dem2, discrete=True, stat='density', ax=ax[i]) #print(mu_t2) fig.suptitle(r'Period $T_=[3, 1)
    ) plt.tight_layout();

    Чем меньше цена (и больше ), тем хуже аппроксимация. Это связано с зависимостью вероятности покупки билета от времени. В случае, если предполагается использование только очень низких цен, то это проблема. Однако крайне трудно представить такую ситуацию, поэтому можно вполне уверенно полагаться на распределение Пуассона.

    Для третьего периода, который займет оставшееся время, имеем следующую картину:

    fig, ax = plt.subplots(1, 5, figsize=(15, 3)) mu_t3 = [] for i in range(5): dem3 = [] for _ in range(1000): t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] start_t3 = np.where(t >1)[0][-1] dem3.append(np.sum(demand[start_t3 + 1 : ] >= price[i])) mu_t3.append(np.mean(dem3)) xx = np.arange(poisson.ppf(0.001, mu_t3[-1]), poisson.ppf(0.999, mu_t3[-1])) yy = poisson.pmf(xx, mu_t3[-1]) ax[i].set_title(f'price = ') ax[i].plot(xx, yy, 'bo', ms=3, label=rf'$mu$ = ') ax[i].legend() ax[i].vlines(xx, 0, yy, colors='b', lw=1, alpha=0.5) sns.histplot(dem3, discrete=True, stat='density', ax=ax[i]) #print(mu_t3) fig.suptitle(r'Period $T_
    ) plt.tight_layout();

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

    plt.plot(price, mu_t1, 'ro-', label=r'$T_
    ) plt.plot(price, mu_t2, 'go-', label=r'$T_
    ) plt.plot(price, mu_t3, 'bo-', label=r'$T_
    ) plt.legend() plt.xlabel('Price') plt.ylabel('Mean demand') plt.title('Demand curves derived from a differential demand model');

    Теперь предположим, что некоторая авиакомпания обладает только что полученной интегральной моделью и решила провести оптимизацию продаж на ее основе. Пусть будет всего три подкласса бронирования: J, C и D, где J — самый дорогой подкласс, D — самый дешевый, а в самолете всего 40 мест.

    def profit_poisson(x): q1 = poisson.rvs(mu=mu_t1[x[0]]) if q1 > 40: q1 = 40 q2 = 0 q3 = 0 else: q2 = poisson.rvs(mu=mu_t2[x[1]]) if q1 + q2 > 40: q2 = 40 - q1 q3 = 0 else: q3 = poisson.rvs(mu=mu_t3[x[2]]) if q1 + q2 + q3 > 40: q3 = 40 - q1 - q2 profit = q1 * price[x[0]] + q2 * price[x[1]] + q3 * price[x[2]] return profit def e_profit_poisson(x): return np.mean([profit_poisson(x) for _ in range(300)]) def hist_res_x(res_vals): bins = np.r_[0:6] h = np.array([np.histogram(res_vals.T[col], bins=bins, density=True)[0] for col in range(3)]) return h def rand_vec_x(h): indices = np.r_[0:5] vec = [np.random.choice(indices, size=1 , p=h_i)[0] for h_i in h] return vec def opt_price_vecs(n_iter): h_x = np.array([[0.2]*5]*3) c_iter = 0 while h_x.max(axis=1).sum() != 3: V_X = np.array([rand_vec_x(h_x) for _ in range(n_iter)]) R = np.array([e_profit_poisson(V_X[i]) for i in range(n_iter)]) idx = np.argsort(R)[::-1][:30] h_x = hist_res_x(V_X[idx]) print(np.argmax(h_x, axis=1)) print('-'*30) c_iter += 1 if c_iter == 7: break opt_price = price[np.nonzero(h_x)[1]] print('opt_price =', opt_price) x = np.argmax(h_x, axis=1) return e_profit_poisson(x), x opt_price_vecs(100)

    Результат

    [1 2 3] ------------------------------ [1 2 2] ------------------------------ [1 2 3] ------------------------------ [1 2 3] ------------------------------ opt_price = [1.1 1.2 1.3] (47.53566666666667, array([1, 2, 3], dtype=int64))

    Наилучшими ценами для каждого из периодов оказались значения 1.1, 1.2, 1.3.

    Благодаря дифференциальной модели можно выяснить, к чему приведет использование данных цен в "реальности":

    def profit_real(x): t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] end_t1 = np.where(t >3)[0][-1] q1_cumsum = np.cumsum(demand[: end_t1 + 1] >= price[x[0]]) idx_u1 = np.where(q1_cumsum == 40)[0] if idx_u1.size != 0: end_t1 = idx_u1[0] q1 = q1_cumsum[end_t1] q2 = 0 q3 = 0 end_t2 = end_t1 end_t3 = end_t1 else: q1 = q1_cumsum[end_t1] end_t2 = np.where(t > 1)[0][-1] q2_cumsum = np.cumsum(demand[end_t1 + 1 : end_t2 + 1] >= price[x[1]]) idx_u2 = np.where(q2_cumsum == (40 - q1))[0] + end_t1 + 1 if idx_u2.size != 0: end_t2 = idx_u2[0] q2 = q2_cumsum[end_t2 - end_t1 - 1] q3 = 0 end_t3 = end_t2 else: q2 = q2_cumsum[end_t2 - end_t1 - 1] q3_cumsum = np.cumsum(demand[np.where(t < t[end_t2])[0]] >= price[x[2]]) idx_u3 = np.where(q3_cumsum == (40 - q1 - q2))[0] + end_t2 + 1 if idx_u3.size != 0: end_t3 = idx_u3[0] q3 = q3_cumsum[end_t3 - end_t2 - 1] else: end_t3 = t.size - 1 q3 = q3_cumsum[end_t3 - end_t2 - 1] profit = q1 * price[x[0]] + q2 * price[x[1]] + q3 * price[x[2]] return profit , q1, q2, q3, q1 + q2 + q3, t[end_t1], t[end_t2], t[end_t3] x = [1, 2, 3] #profit_real(x) sales_real = [profit_real(x) for _ in range(3000)] sales_real_df = pd.DataFrame(sales_real, columns=['profit', 'D', 'C', 'J', 'All', 'end_t1', 'end_t2', 'end_t3'] ) #sales_real_df.head()

    Распределение средней прибыли будет выглядеть следующим образом:

    sns.histplot(sales_real_df.profit) E_p = sales_real_df.profit.mean() plt.axvline(E_p, color='r', label=f'E(Profit) = ') plt.legend() plt.title('Average profit distribution');

    Так будет выглядеть заполняемость соответствующих подклассов:

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_real_df.D, discrete=True, ax=ax[0]) ax[0].set_title('D (price = 1.1)') ax[0].set_xlabel('Occupancy') sns.histplot(sales_real_df.C, discrete=True, ax=ax[1]) ax[1].set_title('C (price = 1.2)') ax[1].set_xlabel('Occupancy') sns.histplot(sales_real_df.J, discrete=True, ax=ax[2]) ax[2].set_title('J (price = 1.3)') ax[2].set_xlabel('Occupancy') plt.tight_layout();

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

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_real_df.end_t1, ax=ax[0]) e_t1 = sales_real_df.end_t1.mean() ax[0].axvline(e_t1, color='r', label=f'E(end_t1) = ') ax[0].legend() ax[0].set_title('End of period T1 (D sales)') ax[0].set_xlabel('Day') sns.histplot(sales_real_df.end_t2, ax=ax[1]) e_t2 = sales_real_df.end_t2.mean() ax[1].axvline(e_t2, color='r', label=f'E(end_t2) = ') ax[1].legend() ax[1].set_title('End of period T2') ax[1].set_xlabel('Day') sns.histplot(sales_real_df.end_t3, ax=ax[2]) e_t3 = sales_real_df.end_t3.mean() ax[2].axvline(e_t3, color='r', label=f'E(end_t3) = ') ax[2].legend() ax[2].set_title('End of period T3') ax[2].set_xlabel('Day') plt.tight_layout();

    В идеале время окончания периода (т.е. завершение продаж авиабилетов) должно располагаться как можно ближе к времени вылета. Однако, судя по графику, окончание продаж за 12 часов до вылета — вполне обычная ситуация, что очень плохо.

    Использование квот оказывает положительный эффект, позволяя ограничить слишком большие отклонения спроса и не продавать слишком много билетов по низким ценам. Теперь задумаемся над тем, что использование квот для каждого из подклассов предполагает наличие остатков: если мы ввели для подкласса D квоту в 12 мест, а продали всего 10, то останется 2. И вот этот остаток нужно куда-то деть — прибавить его либо к квоте подкласса C, либо к квоте подкласса J. Именно тут и необходимо применить нестинг.

    Нестинг позволяет установить иерархию подклассов, а вместе с ней правила перерасчета мест в каждом из них. Можно установить квоты для подклассов D и С, а также задать правило, согласно которому нераспроданные билеты в подклассе D будут прибавлены к квоте подкласса C. А подкласс J будет равен оставшемуся количеству мест, включая остаток подкласса C. Описанный тип нестинга называется линейным.

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

    # Функция моделирующая продажи на основе время-зависимой # модели спроса, иерархии подклассов и правила переасчета мест: def profit_lin(X, Up): U = np.copy(Up) t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] end_t1 = np.where(t >3)[0][-1] q1_cumsum = np.cumsum(demand[: end_t1 + 1] >= X[0]) idx_u1 = np.where(q1_cumsum == U[0])[0] if idx_u1.size != 0: end_t1 = idx_u1[0] q1 = q1_cumsum[end_t1] else: q1 = q1_cumsum[end_t1] U[1] = U[1] + U[0] - q1 end_t2 = np.where(t > 1)[0][-1] q2_cumsum = np.cumsum(demand[end_t1 + 1 : end_t2 + 1] >= X[1]) idx_u2 = np.where(q2_cumsum == U[1])[0] + end_t1 + 1 if idx_u2.size != 0: end_t2 = idx_u2[0] q2 = q2_cumsum[end_t2 - end_t1 - 1] else: q2 = q2_cumsum[end_t2 - end_t1 - 1] U[2] = 40 - q1 - q2 q3_cumsum = np.cumsum(demand[np.where(t < t[end_t2])[0]] >= X[2]) idx_u3 = np.where(q3_cumsum == U[2])[0] + end_t2 + 1 if idx_u3.size != 0: end_t3 = idx_u3[0] q3 = q3_cumsum[end_t3 - end_t2 - 1] else: end_t3 = t.size - 1 q3 = q3_cumsum[end_t3 - end_t2 - 1] profit = q1 * X[0] + q2 * X[1] + q3 * X[2] return profit, q1, q2, q3, q1 + q2 + q3, t[end_t1], t[end_t2], t[end_t3] #X = np.array([1.1, 1.2, 1.3]) #Up = np.array([9, 9, 40]) #profit_lin(X, Up) # Функция, вычисляющая среднюю прибыль: def e_profit_lin(X, Up, n_iter): return np.mean([profit_lin(X, Up)[0] for _ in range(n_iter)]) #e_profit_lin(X, Up, 10000) # ряд вспомогательных функций для оптимизации методом кросс-энтропии: def hist_res_x(res_vals): bins = np.r_[0:6] h = np.array([np.histogram(res_vals.T[col], bins=bins, density=True)[0] for col in range(3)]) return h def rand_vec_x(h): indices = np.r_[0:5] vec = [np.random.choice(indices, size=1 , p=h_i)[0] for h_i in h] return vec def hist_res_u(res_vals): bins = np.r_[1:22] h = np.array([np.histogram(res_vals.T[col], bins=bins, density=True)[0] for col in range(2)]) return h def rand_vec_u(h): indices = np.r_[1:21] vec = [np.random.choice(indices, size=1 , p=h_i)[0] for h_i in h] return vec + [40] # Функция выполняющая оптимизацию: def opt_price_vecs(n_iter): h_x = np.array([[0.2]*5]*3) h_u = np.array([[0.05]*20]*2) c_iter = 0 while h_u.max(axis=1).sum() != 2: V_X = np.array([rand_vec_x(h_x) for _ in range(n_iter)]) V_U = np.array([rand_vec_u(h_u) for _ in range(n_iter)]) R = np.array([e_profit_lin(price[V_X[i_vec]], V_U[i_vec], 200) for i_vec in range(n_iter)]) idx = np.argsort(R)[::-1][:20] h_x = hist_res_x(V_X[idx]) h_u = hist_res_u(V_U[idx]) print(np.argmax(h_x, axis=1)) print(np.argmax(h_u, axis=1)) print('-'*30) c_iter += 1 if c_iter == 7: break opt_vec_x = price[np.nonzero(h_x)[1]] opt_vec_u = np.hstack((np.argmax(h_u, axis=1), [40])) print(opt_vec_x, opt_vec_u) return e_profit_lin(opt_vec_x, opt_vec_u, 200) opt_price_vecs(200)

    Python

    [1 2 3] [18 8] ------------------------------ [1 2 3] [17 8] ------------------------------ [1 2 3] [10 10] ------------------------------ [1 2 3] [10 10] ------------------------------ [1 2 3] [ 7 10] ------------------------------ [1 2 3] [ 7 10] ------------------------------ [1 2 3] [ 7 10] ------------------------------ [1.1 1.2 1.3] [ 7 10 40] 48.27900000000002

    Оптимальные цены получились теми же самыми, но теперь есть оптимальные значения для квот:

    • D — 7 мест;
    • C — 10 мест;
    • J — забирает все оставшиеся места.

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

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

    epm = [] for u1 in range(1, 20): p_row = [] #print(u1, end=', ') for u2 in range(1, 20): p_row.append(e_profit_lin([1.1, 1.2, 1.3], [u1, u2, 40], 1000)) epm.append(p_row) epm = pd.DataFrame(epm, columns=range(1, 20), index=range(1, 20)) plt.figure(figsize=(12, 7)) sns.heatmap(epm, annot=True, fmt=".2f", cmap='nipy_spectral') plt.ylabel('Subclass D quota') plt.xlabel('Subclass C quota') plt.title('Dependence of average profitnon quotas of subclasses D and C');

    Светлая область на графике соответствует всем субоптимальным решениям, которые и находит используемый метод оптимизации. Для оптимизации выполняется многократное моделирование процесса продаж и затем вычисляется его среднее значение, которое может испытывать отклонение от истинного матожидания. Почему бы просто сразу не "считать вероятности"? К ответу на этот вопрос мы вернемся ниже, а пока продолжим с построением графика.

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

    Распределение возможной прибыли по сравнению с предыдущим результатом кардинально поменялось, а ее среднее значение выросло на 1.5%:

    X = np.array([1.1, 1.2, 1.3]) Up = np.array([7, 15, 40]) sales_lin = [profit_lin(X, Up) for _ in range(3000)] sales_lin_df = pd.DataFrame(sales_lin, columns=['profit', 'D', 'C', 'J', 'All', 'end_t1', 'end_t2', 'end_t3'] ) #sales_lin_df.head() sns.histplot(sales_lin_df.profit, binwidth=1) E_p = sales_lin_df.profit.mean() plt.axvline(E_p, color='r', label=f'E(Profit) = ') plt.legend() plt.title('Average profit distribution');

    Введение квот также сильно повлияло на распределение количества продаваемых билетов в каждом подклассе (заполняемость):

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_lin_df.D, discrete=True, ax=ax[0]) ax[0].set_title('D (price = 1.1)') ax[0].set_xlabel('Occupancy') sns.histplot(sales_lin_df.C, discrete=True, ax=ax[1]) ax[1].set_title('C (price = 1.2)') ax[1].set_xlabel('Occupancy') sns.histplot(sales_lin_df.J, discrete=True, ax=ax[2]) ax[2].set_title('J (price = 1.3)') ax[2].set_xlabel('Occupancy') plt.tight_layout();

    Как видим, введение квот резко ограничивает вариативность, а постепенное уменьшение квоты C до вычисленного выше значения, равного 11, приведет к сдвигу распределения заполняемости подкласса J вправо и уменьшению его правого хвоста.

    Самое интересное теперь — это распределение времени окончания продаж авиабилетов:

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_lin_df.end_t1, ax=ax[0]) e_t1 = sales_lin_df.end_t1.mean() ax[0].axvline(e_t1, color='r', label=f'E(end_t1) = ') ax[0].legend() ax[0].set_title('End of period T1 (D sales)') ax[0].set_xlabel('Day') sns.histplot(sales_lin_df.end_t2, ax=ax[1]) e_t2 = sales_lin_df.end_t2.mean() ax[1].axvline(e_t2, color='r', label=f'E(end_t2) = ') ax[1].legend() ax[1].set_title('End of period T2 (C sales)') ax[1].set_xlabel('Day') sns.histplot(sales_lin_df.end_t3, ax=ax[2]) e_t3 = sales_lin_df.end_t3.mean() ax[2].axvline(e_t3, color='r', label=f'E(end_t3) = ') ax[2].legend() ax[2].set_title('End of period T3 (J sales)') ax[2].set_xlabel('Day') plt.tight_layout();

    Во-первых, следует обратить внимание на время завершения периода — из-за введения квоты он теперь часто заканчивается намного раньше чем за 3 дня до вылета. То же самое можно сказать и о периоде . Важно отметить, что время завершения продаж теперь сильно сместилось к времени вылета, а билеты присутствуют в продаже гораздо дольше.

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

    def profit_par(X, Up): U = np.copy(Up) t = np.sort(gamma.rvs(a=0.9, scale=3, size=100))[::-1] t = t[t < 10] price_idx = betabinom.rvs(n, 1, 0.99 * t + 0.1) demand = price[price_idx] end_t1 = np.where(t >3)[0][-1] q1_cumsum = np.cumsum(demand[: end_t1 + 1] >= X[0]) idx_u1 = np.where(q1_cumsum == U[0])[0] if idx_u1.size != 0: end_t1 = idx_u1[0] q1 = q1_cumsum[end_t1] else: q1 = q1_cumsum[end_t1] end_t2 = np.where(t > 1)[0][-1] q2_cumsum = np.cumsum(demand[end_t1 + 1 : end_t2 + 1] >= X[1]) idx_u2 = np.where(q2_cumsum == U[1])[0] + end_t1 + 1 if idx_u2.size != 0: end_t2 = idx_u2[0] q2 = q2_cumsum[end_t2 - end_t1 - 1] else: q2 = q2_cumsum[end_t2 - end_t1 - 1] U[2] = 40 - q1 - q2 q3_cumsum = np.cumsum(demand[np.where(t < t[end_t2])[0]] >= X[2]) idx_u3 = np.where(q3_cumsum == U[2])[0] + end_t2 + 1 if idx_u3.size != 0: end_t3 = idx_u3[0] q3 = q3_cumsum[end_t3 - end_t2 - 1] else: end_t3 = t.size - 1 q3 = q3_cumsum[end_t3 - end_t2 - 1] profit = q1 * X[0] + q2 * X[1] + q3 * X[2] return profit, q1, q2, q3, q1 + q2 + q3, t[end_t1], t[end_t2], t[end_t3] #X = np.array([1.1, 1.2, 1.3]) #Up = np.array([9, 9, 40]) #profit_par(X, Up) def e_profit_par(X, Up, n_iter): return np.mean([profit_par(X, Up)[0] for _ in range(n_iter)]) def opt_price_vecs(n_iter): h_x = np.array([[0.2]*5]*3) h_u = np.array([[0.05]*20]*2) c_iter = 0 while h_u.max(axis=1).sum() != 2: V_X = np.array([rand_vec_x(h_x) for _ in range(n_iter)]) V_U = np.array([rand_vec_u(h_u) for _ in range(n_iter)]) R = np.array([e_profit_par(price[V_X[i_vec]], V_U[i_vec], 200) for i_vec in range(n_iter)]) idx = np.argsort(R)[::-1][:20] h_x = hist_res_x(V_X[idx]) h_u = hist_res_u(V_U[idx]) print(np.argmax(h_x, axis=1)) print(np.argmax(h_u, axis=1)) print('-'*30) c_iter += 1 if c_iter == 7: break opt_vec_x = price[np.nonzero(h_x)[1]] opt_vec_u = np.hstack((np.argmax(h_u, axis=1), [40])) print(opt_vec_x, opt_vec_u) return e_profit_par(opt_vec_x, opt_vec_u, 200) opt_price_vecs(200)

    Python

    [1 3 3] [10 11] ------------------------------ [1 2 3] [17 9] ------------------------------ [1 2 3] [10 9] ------------------------------ [1 2 3] [10 9] ------------------------------ [1 2 3] [10 9] ------------------------------ [1 2 3] [10 9] ------------------------------ [1 2 3] [10 9] ------------------------------ [1.1 1.2 1.3] [10 9 40] 48.53200000000001

    Оптимальные цены не изменились, но оптимальные квоты приняли другие значения:

    • D — 10 мест;
    • C — 9 мест;
    • J — забирает все оставшиеся места.

    Изначально можно ожидать, что значение средней прибыли должно измениться, пускай и не сильно. Тем не менее, этого не произошло — даже форма распределения осталась практически прежней:

    X = np.array([1.1, 1.2, 1.3]) Up = np.array([10, 9, 40]) sales_par = [profit_par(X, Up) for _ in range(3000)] sales_par_df = pd.DataFrame(sales_par, columns=['profit', 'D', 'C', 'J', 'All', 'end_t1', 'end_t2', 'end_t3'] ) #sales_par_df.head() sns.histplot(sales_par_df.profit, binwidth=1) E_p = sales_par_df.profit.mean() plt.axvline(E_p, color='r', label=f'E(Profit) = ') plt.legend() plt.title('Average profit distribution');

    На первый взгляд, в заполняемости подклассов нет никаких изменений:

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_par_df.D, discrete=True, ax=ax[0]) ax[0].set_title('D (price = 1.1)') ax[0].set_xlabel('Occupancy') sns.histplot(sales_par_df.C, discrete=True, ax=ax[1]) ax[1].set_title('C (price = 1.2)') ax[1].set_xlabel('Occupancy') sns.histplot(sales_par_df.J, discrete=True, ax=ax[2]) ax[2].set_title('J (price = 1.3)') ax[2].set_xlabel('Occupancy') plt.tight_layout();

    Если взглянуть повнимательнее и сравнить полученные данные с предыдущими, то можно заметить, что у распределения заполняемости подкласса C исчез маленький правый хвостик. Связано это с тем, что подкласс D теперь отдает остатки в подкласс J, а не C. Это небольшое, но все же примечательное отличие от предыдущего типа нестинга.

    Наиболее важное отличие можно заметить в распределении времени окончания периодов продаж:

    f, ax = plt.subplots(1, 3, figsize=(12, 4)) sns.histplot(sales_par_df.end_t1, ax=ax[0]) e_t1 = sales_par_df.end_t1.mean() ax[0].axvline(e_t1, color='r', label=f'E(end_t1) = ') ax[0].legend() ax[0].set_title('End of period T1 (D sales)') ax[0].set_xlabel('Day') sns.histplot(sales_par_df.end_t2, ax=ax[1]) e_t2 = sales_par_df.end_t2.mean() ax[1].axvline(e_t2, color='r', label=f'E(end_t2) = ') ax[1].legend() ax[1].set_title('End of period T2 (C sales)') ax[1].set_xlabel('Day') sns.histplot(sales_par_df.end_t3, ax=ax[2]) e_t3 = sales_par_df.end_t3.mean() ax[2].axvline(e_t3, color='r', label=f'E(end_t3) = ') ax[2].legend() ax[2].set_title('End of period T3 (J sales)') ax[2].set_xlabel('Day') plt.tight_layout();

    Время окончания второго периода продаж сильно отдалилось от даты вылета, что объясняется наличием жесткой квоты для подкласса C.

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

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

    Нестинг

    Подбор станка 8-800-1000-111 Корзина

    Вернуться назад Закрыть меню

    Закрыть меню

    1 $ = 95.03 ₽ 1 ¥ = 13.48 ₽

    Корзина Корзина СКЛАД БЕЗ ВЫХОДНЫХ с 9 - 18:00 Москва

    Выбор направления

    • Металлообработка
    • Деревообработка
    • Мебельное
    • Все направления

    [email protected] Подбор станка

    Время работы: Пн.-Сб.: 9:00-18:00

    • Металлообработка Металлообрабатывающее оборудование
    • Деревообработка Деревообрабатывающее оборудование
    • Мебельное Оборудование для производства мебели
    • Тяжелая металлообработка
    • Инструмент Инструмент и заточное оборудование
    • Стеклообрабатывающее оборудование
    • Камнеобрабатывающее оборудование
    • Заточное Оборудование для заточки инструмента
    • Компрессоры
    • Запчасти для станков
    • Специализированное оборудование
    • Фрезерные станки и центры с ЧПУ
    • Акции
    • Обучение

    Cannot find 'equipment' template with page 'element'

    Оборудование

    • Металлообрабатывающее оборудование
    • Деревообрабатывающее оборудование
    • Оборудование для производства мебели
    • Фрезерное оборудование с ЧПУ
    • Инструменты и заточное оборудование
    • Стеклообрабатывающее оборудование
    • Камнеобрабатывающее оборудование
    • Запчасти для станков
    • Продажа БУ

    Услуги

    • Сервис
    • Лизинг и кредит
    • Комплексное проектирование производств
    • Доставка
    • Обучение

    О компании

    • Складские комплексы и запись
    • Технологии
    • Производители
    • Отзывы
    • Незаконное использование информации
    • Новости
    • Видео энциклопедия
    • Служба контроля качества
    • Связь с руководством
    • Возврат товаров
    • Вакансии

    Контакты

    Время работы: Пн.-Сб.: 9:00-18:00

    1 $ = 95.03 ₽

    1 ¥ = 13.48 ₽

    Выберите страну:

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

    © Copyright 2024. Все права защищены.

    Источники:

    https://rosi.bg/aes-flexa-pro-cnc-nesting-mashina-5338.html&rut=1f25accf75628348fdf19f285bcdcb7a90dcf0f75955f3f36808b366ec593e81
    https://www.youtube.com/watch?v=1GbInYgXk7I&rut=e291ad3cb841a3b3d4c0ebff660d1b53993ab44da01070953c87814babe32ffa
    https://habr.com/ru/articles/786628/&rut=618537b5ec46c77f2a413661c81f0c5f52abd21ad4e87823d7c1537e7f74bcc9
    https://www.youtube.com/watch?v=0Aim4OuQKrM&rut=440c31eef19a882418652ab8b9f777eee2649ec61efa9cce2b6ad7f80876177c
    https://arsenalmeb.ru/baza-znanij/tekhnologii/tekhnologiya-nesting&rut=59f86c776449a2f7eb381213887fcc533c2614178d574e1e663e5089a6482447
    https://deepnest.io/&rut=4db02eff2525feed6e5712f69e8a5ee8d45f6b23ec642ad80a27d558b682c109
    https://www.youtube.com/watch?v=fbYU7P48MBc&rut=caa72c2f0fdce4b756d240370c5fd254a0c04bde2e466994235a75bf248b37f8
    https://stankoteam.ru/info/articles/tekhnologiya-nesting-nesting/&rut=08a639114371be5430a9ad118090fb73ef6f0d26e43b136f97fb1da781fa8ebc
    https://www.stanki.ru/catalog/obrabatyvayushchie_tsentry_nesting/&rut=317a8259cfd6b1e31b658bd211522336e7b0516f84906571a47feee3817bedad